From e829685b591d3167eae64711afb0b1a70563e031 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 26 Feb 2020 16:51:02 +0100 Subject: [PATCH 01/23] Insert byminute function to totext to display minutes in daily case --- src/nlp/index.ts | 81 ++++---- src/nlp/totext.ts | 510 +++++++++++++++++++++++++--------------------- test/nlp.test.ts | 175 +++++++++------- 3 files changed, 415 insertions(+), 351 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index f18da328..e1729230 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,17 +1,17 @@ -import ToText, { DateFormatter, GetText } from './totext' -import parseText from './parsetext' -import RRule from '../index' -import ENGLISH, { Language } from './i18n' +import ToText, { DateFormatter, GetText } from "./totext"; +import parseText from "./parsetext"; +import RRule from "../index"; +import ENGLISH, { Language } from "./i18n"; /*! -* rrule.js - Library for working with recurrence rules for calendar dates. -* https://github.com/jakubroztocil/rrule -* -* Copyright 2010, Jakub Roztocil and Lars Schoning -* Licenced under the BSD licence. -* https://github.com/jakubroztocil/rrule/blob/master/LICENCE -* -*/ + * rrule.js - Library for working with recurrence rules for calendar dates. + * https://github.com/jakubroztocil/rrule + * + * Copyright 2010, Jakub Roztocil and Lars Schoning + * Licenced under the BSD licence. + * https://github.com/jakubroztocil/rrule/blob/master/LICENCE + * + */ /** * @@ -94,42 +94,47 @@ import ENGLISH, { Language } from './i18n' * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function (text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined) -} +const fromText = function(text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined); +}; const common = [ - 'count', - 'until', - 'interval', - 'byweekday', - 'bymonthday', - 'bymonth' -] + "count", + "until", + "interval", + "byweekday", + "bymonthday", + "bymonth" +]; -ToText.IMPLEMENTED = [] -ToText.IMPLEMENTED[RRule.HOURLY] = common -ToText.IMPLEMENTED[RRule.MINUTELY] = common -ToText.IMPLEMENTED[RRule.DAILY] = ['byhour'].concat(common) -ToText.IMPLEMENTED[RRule.WEEKLY] = common -ToText.IMPLEMENTED[RRule.MONTHLY] = common -ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) +ToText.IMPLEMENTED = []; +ToText.IMPLEMENTED[RRule.HOURLY] = common; +ToText.IMPLEMENTED[RRule.MINUTELY] = common; +ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.WEEKLY] = common; +ToText.IMPLEMENTED[RRule.MONTHLY] = common; +ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); // ============================================================================= // Export // ============================================================================= -const toText = function (rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter) { - return new ToText(rrule, gettext, language, dateFormatter).toString() -} +const toText = function( + rrule: RRule, + gettext?: GetText, + language?: Language, + dateFormatter?: DateFormatter +) { + return new ToText(rrule, gettext, language, dateFormatter).toString(); +}; -const { isFullyConvertible } = ToText +const { isFullyConvertible } = ToText; export interface Nlp { - fromText: typeof fromText - parseText: typeof parseText - isFullyConvertible: typeof isFullyConvertible - toText: typeof toText + fromText: typeof fromText; + parseText: typeof parseText; + isFullyConvertible: typeof isFullyConvertible; + toText: typeof toText; } -export { fromText, parseText, isFullyConvertible, toText } +export { fromText, parseText, isFullyConvertible, toText }; diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index fa73b838..89870c5b 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options, ByWeekday } from '../types' -import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent, padStart } from '../helpers' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options, ByWeekday } from "../types"; +import { Weekday } from "../weekday"; +import { isArray, isNumber, isPresent, padStart } from "../helpers"; // ============================================================================= // Helper functions @@ -11,21 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from '../helpers' /** * Return true if a value is in an array */ -const contains = function (arr: string[], val: string) { - return arr.indexOf(val) !== -1 -} +const contains = function(arr: string[], val: string) { + return arr.indexOf(val) !== -1; +}; // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string +export type GetText = (id: string | number | Weekday) => string; -const defaultGetText: GetText = id => id.toString() +const defaultGetText: GetText = id => id.toString(); -export type DateFormatter = (year: number, month: string, day: number) => string +export type DateFormatter = ( + year: number, + month: string, + day: number +) => string; -const defaultDateFormatter: DateFormatter = (year: number, month: string, day: number) => `${month} ${day}, ${year}` +const defaultDateFormatter: DateFormatter = ( + year: number, + month: string, + day: number +) => `${month} ${day}, ${year}`; /** * @@ -36,84 +44,89 @@ const defaultDateFormatter: DateFormatter = (year: number, month: string, day: n * @constructor */ export default class ToText { - static IMPLEMENTED: string[][] - private rrule: RRule - private text: string[] - private gettext: GetText - private dateFormatter: DateFormatter - private language: Language - private options: Partial - private origOptions: Partial - private bymonthday: Options['bymonthday'] | null + static IMPLEMENTED: string[][]; + private rrule: RRule; + private text: string[]; + private gettext: GetText; + private dateFormatter: DateFormatter; + private language: Language; + private options: Partial; + private origOptions: Partial; + private bymonthday: Options["bymonthday"] | null; private byweekday: { - allWeeks: ByWeekday[] | null - someWeeks: ByWeekday[] | null - isWeekdays: boolean - isEveryDay: boolean - } | null - - constructor (rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter) { - this.text = [] - this.language = language || ENGLISH - this.gettext = gettext - this.dateFormatter = dateFormatter - this.rrule = rrule - this.options = rrule.options - this.origOptions = rrule.origOptions + allWeeks: ByWeekday[] | null; + someWeeks: ByWeekday[] | null; + isWeekdays: boolean; + isEveryDay: boolean; + } | null; + + constructor( + rrule: RRule, + gettext: GetText = defaultGetText, + language: Language = ENGLISH, + dateFormatter: DateFormatter = defaultDateFormatter + ) { + this.text = []; + this.language = language || ENGLISH; + this.gettext = gettext; + this.dateFormatter = dateFormatter; + this.rrule = rrule; + this.options = rrule.options; + this.origOptions = rrule.origOptions; if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!) - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) + const bymonthday = ([] as number[]).concat(this.options.bymonthday!); + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); - bymonthday.sort((a, b) => a - b) - bynmonthday.sort((a, b) => b - a) + bymonthday.sort((a, b) => a - b); + bynmonthday.sort((a, b) => b - a); // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday) - if (!this.bymonthday.length) this.bymonthday = null + this.bymonthday = bymonthday.concat(bynmonthday); + if (!this.bymonthday.length) this.bymonthday = null; } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday - const days = String(byweekday) + : this.origOptions.byweekday; + const days = String(byweekday); this.byweekday = { - allWeeks: byweekday.filter(function (weekday: Weekday) { - return !weekday.n + allWeeks: byweekday.filter(function(weekday: Weekday) { + return !weekday.n; }), - someWeeks: byweekday.filter(function (weekday: Weekday) { - return Boolean(weekday.n) + someWeeks: byweekday.filter(function(weekday: Weekday) { + return Boolean(weekday.n); }), isWeekdays: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') === -1 && - days.indexOf('SU') === -1, + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") === -1 && + days.indexOf("SU") === -1, isEveryDay: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') !== -1 && - days.indexOf('SU') !== -1 - } - - const sortWeekDays = function (a: Weekday, b: Weekday) { - return a.weekday - b.weekday - } - - this.byweekday.allWeeks!.sort(sortWeekDays) - this.byweekday.someWeeks!.sort(sortWeekDays) - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") !== -1 && + days.indexOf("SU") !== -1 + }; + + const sortWeekDays = function(a: Weekday, b: Weekday) { + return a.weekday - b.weekday; + }; + + this.byweekday.allWeeks!.sort(sortWeekDays); + this.byweekday.someWeeks!.sort(sortWeekDays); + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; } else { - this.byweekday = null + this.byweekday = null; } } @@ -122,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible (rrule: RRule) { - let canConvert = true + static isFullyConvertible(rrule: RRule) { + let canConvert = true; - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false - if (rrule.origOptions.until && rrule.origOptions.count) return false + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; + if (rrule.origOptions.until && rrule.origOptions.count) return false; for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false + if (contains(["dtstart", "wkst", "freq"], key)) return true; + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; } - return canConvert + return canConvert; } - isFullyConvertible () { - return ToText.isFullyConvertible(this.rrule) + isFullyConvertible() { + return ToText.isFullyConvertible(this.rrule); } /** @@ -146,334 +159,359 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString () { - const gettext = this.gettext + toString() { + const gettext = this.gettext; if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext('RRule error: Unable to fully convert this rrule to text') + return gettext("RRule error: Unable to fully convert this rrule to text"); } - this.text = [gettext('every')] + this.text = [gettext("every")]; // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]]() + this[RRule.FREQUENCIES[this.options.freq]](); if (this.options.until) { - this.add(gettext('until')) - const until = this.options.until - this.add(this.dateFormatter(until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate())) + this.add(gettext("until")); + const until = this.options.until; + this.add( + this.dateFormatter( + until.getUTCFullYear(), + this.language.monthNames[until.getUTCMonth()], + until.getUTCDate() + ) + ); } else if (this.options.count) { - this.add(gettext('for')) + this.add(gettext("for")) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext('times') : gettext('time') - ) + this.plural(this.options.count) ? gettext("times") : gettext("time") + ); } - if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) + if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); - return this.text.join('') + return this.text.join(""); } - HOURLY () { - const gettext = this.gettext + HOURLY() { + const gettext = this.gettext; - if (this.options.interval !== 1) this.add(this.options.interval!.toString()) + if (this.options.interval !== 1) + this.add(this.options.interval!.toString()); this.add( - this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') - ) + this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") + ); } - MINUTELY () { - const gettext = this.gettext + MINUTELY() { + const gettext = this.gettext; - if (this.options.interval !== 1) this.add(this.options.interval!.toString()) + if (this.options.interval !== 1) + this.add(this.options.interval!.toString()); this.add( this.plural(this.options.interval!) - ? gettext('minutes') - : gettext('minutes') - ) + ? gettext("minutes") + : gettext("minutes") + ); } - DAILY () { - const gettext = this.gettext + DAILY() { + const gettext = this.gettext; - if (this.options.interval !== 1) this.add(this.options.interval!.toString()) + if (this.options.interval !== 1) + this.add(this.options.interval!.toString()); if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) - + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() - } else if (this.origOptions.byhour) { - this._byhour() + this._byweekday(); + } else if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); } } - WEEKLY () { - const gettext = this.gettext + WEEKLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') - ) + this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") + ); } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } else { - if (this.options.interval === 1) this.add(gettext('week')) + if (this.options.interval === 1) this.add(gettext("week")); if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } } } - MONTHLY () { - const gettext = this.gettext + MONTHLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext('months')) - if (this.plural(this.options.interval!)) this.add(gettext('in')) + this.add(this.options.interval!.toString()).add(gettext("months")); + if (this.plural(this.options.interval!)) this.add(gettext("in")); } else { // this.add(gettext('MONTH')) } - this._bymonth() + this._bymonth(); } else { - if (this.options.interval !== 1) this.add(this.options.interval!.toString()) + if (this.options.interval !== 1) + this.add(this.options.interval!.toString()); this.add( this.plural(this.options.interval!) - ? gettext('months') - : gettext('month') - ) + ? gettext("months") + : gettext("month") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } } - YEARLY () { - const gettext = this.gettext + YEARLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) - this.add(gettext('years')) + this.add(this.options.interval!.toString()); + this.add(gettext("years")); } else { // this.add(gettext('YEAR')) } - this._bymonth() + this._bymonth(); } else { - if (this.options.interval !== 1) this.add(this.options.interval!.toString()) + if (this.options.interval !== 1) + this.add(this.options.interval!.toString()); this.add( - this.plural(this.options.interval!) ? gettext('years') : gettext('year') - ) + this.plural(this.options.interval!) ? gettext("years") : gettext("year") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } if (this.options.byyearday) { - this.add(gettext('on the')) - .add(this.list(this.options.byyearday, this.nth, gettext('and'))) - .add(gettext('day')) + this.add(gettext("on the")) + .add(this.list(this.options.byyearday, this.nth, gettext("and"))) + .add(gettext("day")); } if (this.options.byweekno) { - this.add(gettext('in')) + this.add(gettext("in")) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext('weeks') - : gettext('week') + ? gettext("weeks") + : gettext("week") ) - .add(this.list(this.options.byweekno, undefined, gettext('and'))) + .add(this.list(this.options.byweekno, undefined, gettext("and"))); } } - private _bymonthday () { - const gettext = this.gettext + private _bymonthday() { + const gettext = this.gettext; if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext('on')) + this.add(gettext("on")) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) ) - .add(gettext('the')) - .add(this.list(this.bymonthday!, this.nth, gettext('or'))) + .add(gettext("the")) + .add(this.list(this.bymonthday!, this.nth, gettext("or"))); } else { - this.add(gettext('on the')).add( - this.list(this.bymonthday!, this.nth, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.bymonthday!, this.nth, gettext("and")) + ); } // this.add(gettext('DAY')) } - private _byweekday () { - const gettext = this.gettext + private _byweekday() { + const gettext = this.gettext; if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext('on')).add( + this.add(gettext("on")).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ) + ); } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext('and')) + if (this.byweekday!.allWeeks) this.add(gettext("and")); - this.add(gettext('on the')).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) + ); } } - private _byhour () { - const gettext = this.gettext + private _byhour() { + const gettext = this.gettext; + + this.add(gettext("at")).add( + this.list(this.origOptions.byhour!, undefined, gettext("and")) + ); + } + + private _byminute() { + const gettext = this.gettext; - this.add(gettext('at')).add( - this.list(this.origOptions.byhour!, undefined, gettext('and')) - ) + this.add(gettext("at")).add( + `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` + ); } - private _bymonth () { + private _bymonth() { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) - ) + this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) + ); } - nth (n: number | string) { - n = parseInt(n.toString(), 10) - let nth: string - let npos: number - const gettext = this.gettext + nth(n: number | string) { + n = parseInt(n.toString(), 10); + let nth: string; + let npos: number; + const gettext = this.gettext; - if (n === -1) return gettext('last') + if (n === -1) return gettext("last"); - npos = Math.abs(n) + npos = Math.abs(n); switch (npos) { case 1: case 21: case 31: - nth = npos + gettext('st') - break + nth = npos + gettext("st"); + break; case 2: case 22: - nth = npos + gettext('nd') - break + nth = npos + gettext("nd"); + break; case 3: case 23: - nth = npos + gettext('rd') - break + nth = npos + gettext("rd"); + break; default: - nth = npos + gettext('th') + nth = npos + gettext("th"); } - return n < 0 ? nth + ' ' + gettext('last') : nth + return n < 0 ? nth + " " + gettext("last") : nth; } - monthtext (m: number) { - return this.language.monthNames[m - 1] + monthtext(m: number) { + return this.language.monthNames[m - 1]; } - weekdaytext (wday: Weekday | number) { - const weekday = - isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() + weekdaytext(wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ) + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + this.language.dayNames[weekday] + ); } - plural (n: number) { - return n % 100 !== 1 + plural(n: number) { + return n % 100 !== 1; } - add (s: string) { - this.text.push(' ') - this.text.push(s) - return this + add(s: string) { + this.text.push(" "); + this.text.push(s); + return this; } - list (arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, delim: string = ',') { + list( + arr: ByWeekday | ByWeekday[], + callback?: GetText, + finalDelim?: string, + delim: string = "," + ) { if (!isArray(arr)) { - arr = [arr] + arr = [arr]; } - const delimJoin = function ( + const delimJoin = function( array: string[], delimiter: string, finalDelimiter: string ) { - let list = '' + let list = ""; for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += ' ' + finalDelimiter + ' ' + list += " " + finalDelimiter + " "; } else { - list += delimiter + ' ' + list += delimiter + " "; } } - list += array[i] + list += array[i]; } - return list - } + return list; + }; callback = callback || - function (o) { - return o.toString() - } - const self = this - const realCallback = function (arg: ByWeekday) { - return callback && callback.call(self, arg) - } + function(o) { + return o.toString(); + }; + const self = this; + const realCallback = function(arg: ByWeekday) { + return callback && callback.call(self, arg); + }; if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim) + return delimJoin(arr.map(realCallback), delim, finalDelim); } else { - return arr.map(realCallback).join(delim + ' ') + return arr.map(realCallback).join(delim + " "); } } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 540912cd..2bd32bdd 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -1,101 +1,122 @@ -import { expect } from 'chai' -import { DateTime } from 'luxon' -import RRule from '../src'; -import { optionsToString } from '../src/optionstostring'; -import {DateFormatter} from '../src/nlp/totext' +import { expect } from "chai"; +import { DateTime } from "luxon"; +import RRule from "../src"; +import { optionsToString } from "../src/optionstostring"; +import { DateFormatter } from "../src/nlp/totext"; const texts = [ - ['Every day', 'RRULE:FREQ=DAILY'], - ['Every day at 10, 12 and 17', 'RRULE:FREQ=DAILY;BYHOUR=10,12,17'], - ['Every week', 'RRULE:FREQ=WEEKLY'], - ['Every hour', 'RRULE:FREQ=HOURLY'], - ['Every 4 hours', 'RRULE:INTERVAL=4;FREQ=HOURLY'], - ['Every week on Tuesday', 'RRULE:FREQ=WEEKLY;BYDAY=TU'], - ['Every week on Monday, Wednesday', 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE'], - ['Every weekday', 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'], - ['Every 2 weeks', 'RRULE:INTERVAL=2;FREQ=WEEKLY'], - ['Every month', 'RRULE:FREQ=MONTHLY'], - ['Every 6 months', 'RRULE:INTERVAL=6;FREQ=MONTHLY'], - ['Every year', 'RRULE:FREQ=YEARLY'], - ['Every year on the 1st Friday', 'RRULE:FREQ=YEARLY;BYDAY=+1FR'], - ['Every year on the 13th Friday', 'RRULE:FREQ=YEARLY;BYDAY=+13FR'], - ['Every month on the 4th', 'RRULE:FREQ=MONTHLY;BYMONTHDAY=4'], - ['Every month on the 4th last', 'RRULE:FREQ=MONTHLY;BYMONTHDAY=-4'], - ['Every month on the 3rd Tuesday', 'RRULE:FREQ=MONTHLY;BYDAY=+3TU'], - ['Every month on the 3rd last Tuesday', 'RRULE:FREQ=MONTHLY;BYDAY=-3TU'], - ['Every month on the last Monday', 'RRULE:FREQ=MONTHLY;BYDAY=-1MO'], - ['Every month on the 2nd last Friday', 'RRULE:FREQ=MONTHLY;BYDAY=-2FR'], + ["Every day", "RRULE:FREQ=DAILY"], + ["Every day at 10, 12 and 17", "RRULE:FREQ=DAILY;BYHOUR=10,12,17"], + ["Every day at 10:30", "RRULE:FREQ=DAILY;BYHOUR=10;BYMINUTE=30"], + ["Every week", "RRULE:FREQ=WEEKLY"], + ["Every hour", "RRULE:FREQ=HOURLY"], + ["Every 4 hours", "RRULE:INTERVAL=4;FREQ=HOURLY"], + ["Every week on Tuesday", "RRULE:FREQ=WEEKLY;BYDAY=TU"], + ["Every week on Monday, Wednesday", "RRULE:FREQ=WEEKLY;BYDAY=MO,WE"], + ["Every weekday", "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"], + ["Every 2 weeks", "RRULE:INTERVAL=2;FREQ=WEEKLY"], + ["Every month", "RRULE:FREQ=MONTHLY"], + ["Every 6 months", "RRULE:INTERVAL=6;FREQ=MONTHLY"], + ["Every year", "RRULE:FREQ=YEARLY"], + ["Every year on the 1st Friday", "RRULE:FREQ=YEARLY;BYDAY=+1FR"], + ["Every year on the 13th Friday", "RRULE:FREQ=YEARLY;BYDAY=+13FR"], + ["Every month on the 4th", "RRULE:FREQ=MONTHLY;BYMONTHDAY=4"], + ["Every month on the 4th last", "RRULE:FREQ=MONTHLY;BYMONTHDAY=-4"], + ["Every month on the 3rd Tuesday", "RRULE:FREQ=MONTHLY;BYDAY=+3TU"], + ["Every month on the 3rd last Tuesday", "RRULE:FREQ=MONTHLY;BYDAY=-3TU"], + ["Every month on the last Monday", "RRULE:FREQ=MONTHLY;BYDAY=-1MO"], + ["Every month on the 2nd last Friday", "RRULE:FREQ=MONTHLY;BYDAY=-2FR"], // ['Every week until January 1, 2007', 'RRULE:FREQ=WEEKLY;UNTIL=20070101T080000Z'], - ['Every week for 20 times', 'RRULE:FREQ=WEEKLY;COUNT=20'] -] + ["Every week for 20 times", "RRULE:FREQ=WEEKLY;COUNT=20"] +]; -describe('NLP', () => { - it('fromText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromText(text).toString()).equals(str, text + ' => ' + str) - }) - }) +describe("NLP", () => { + it("fromText()", function() { + texts.forEach(function(item) { + const text = item[0]; + const str = item[1]; + expect(RRule.fromText(text).toString()).equals(str, text + " => " + str); + }); + }); - it('toText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromString(str).toText().toLowerCase()).equals(text.toLowerCase(), - str + ' => ' + text) - }) - }) + it("toText()", function() { + texts.forEach(function(item) { + const text = item[0]; + const str = item[1]; + expect( + RRule.fromString(str) + .toText() + .toLowerCase() + ).equals(text.toLowerCase(), str + " => " + text); + }); + }); - it('parseText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(optionsToString(RRule.parseText(text))).equals(str, text + ' => ' + str) - }) - }) + it("parseText()", function() { + texts.forEach(function(item) { + const text = item[0]; + const str = item[1]; + expect(optionsToString(RRule.parseText(text))).equals( + str, + text + " => " + str + ); + }); + }); - it('permits integers in byweekday (#153)', () => { + it("permits integers in byweekday (#153)", () => { const rrule = new RRule({ freq: RRule.WEEKLY, byweekday: 0 - }) + }); - expect(rrule.toText()).to.equal('every week on Monday') - expect(rrule.toString()).to.equal('RRULE:FREQ=WEEKLY;BYDAY=MO') - }) + expect(rrule.toText()).to.equal("every week on Monday"); + expect(rrule.toString()).to.equal("RRULE:FREQ=WEEKLY;BYDAY=MO"); + }); - it('sorts monthdays correctly (#101)', () => { - const options = { "freq": 2, "bymonthday": [3, 10, 17, 24] } - const rule = new RRule(options) - expect(rule.toText()).to.equal('every week on the 3rd, 10th, 17th and 24th') - }) + it("sorts monthdays correctly (#101)", () => { + const options = { freq: 2, bymonthday: [3, 10, 17, 24] }; + const rule = new RRule(options); + expect(rule.toText()).to.equal( + "every week on the 3rd, 10th, 17th and 24th" + ); + }); - it('shows correct text for every day', () => { - const options = { "freq": RRule.WEEKLY, byweekday: [ - RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR, RRule.SA, RRule.SU - ]} - const rule = new RRule(options) - expect(rule.toText()).to.equal('every day') - }) + it("shows correct text for every day", () => { + const options = { + freq: RRule.WEEKLY, + byweekday: [ + RRule.MO, + RRule.TU, + RRule.WE, + RRule.TH, + RRule.FR, + RRule.SA, + RRule.SU + ] + }; + const rule = new RRule(options); + expect(rule.toText()).to.equal("every day"); + }); - it('by default formats \'until\' correctly', () => { + it("by default formats 'until' correctly", () => { const rrule = new RRule({ freq: RRule.WEEKLY, until: DateTime.utc(2012, 11, 10).toJSDate() - }) + }); - expect(rrule.toText()).to.equal('every week until November 10, 2012') - }) + expect(rrule.toText()).to.equal("every week until November 10, 2012"); + }); - it('formats \'until\' as desired if asked', () => { + it("formats 'until' as desired if asked", () => { const rrule = new RRule({ freq: RRule.WEEKLY, until: DateTime.utc(2012, 11, 10).toJSDate() - }) + }); - const dateFormatter: DateFormatter = (year, month, day) => `${day}. ${month}, ${year}` + const dateFormatter: DateFormatter = (year, month, day) => + `${day}. ${month}, ${year}`; - expect(rrule.toText(undefined, undefined, dateFormatter)).to.equal('every week until 10. November, 2012') - }) -}) \ No newline at end of file + expect(rrule.toText(undefined, undefined, dateFormatter)).to.equal( + "every week until 10. November, 2012" + ); + }); +}); From 77b1251f6824e63d6c89f3f91fc9facb641e0d46 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 26 Feb 2020 16:52:18 +0100 Subject: [PATCH 02/23] Insert byminute function to totext to display minute --- src/nlp/index.ts | 60 +++--- src/nlp/totext.ts | 483 +++++++++++++++++++++++----------------------- 2 files changed, 274 insertions(+), 269 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index e1729230..1d04fd45 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,7 +1,7 @@ -import ToText, { DateFormatter, GetText } from "./totext"; -import parseText from "./parsetext"; -import RRule from "../index"; -import ENGLISH, { Language } from "./i18n"; +import ToText, { DateFormatter, GetText } from './totext' +import parseText from './parsetext' +import RRule from '../index' +import ENGLISH, { Language } from './i18n' /*! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -94,47 +94,47 @@ import ENGLISH, { Language } from "./i18n"; * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function(text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined); -}; +const fromText = function (text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined) +} const common = [ - "count", - "until", - "interval", - "byweekday", - "bymonthday", - "bymonth" -]; + 'count', + 'until', + 'interval', + 'byweekday', + 'bymonthday', + 'bymonth' +] -ToText.IMPLEMENTED = []; -ToText.IMPLEMENTED[RRule.HOURLY] = common; -ToText.IMPLEMENTED[RRule.MINUTELY] = common; -ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.WEEKLY] = common; -ToText.IMPLEMENTED[RRule.MONTHLY] = common; -ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); +ToText.IMPLEMENTED = [] +ToText.IMPLEMENTED[RRule.HOURLY] = common +ToText.IMPLEMENTED[RRule.MINUTELY] = common +ToText.IMPLEMENTED[RRule.DAILY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.WEEKLY] = common +ToText.IMPLEMENTED[RRule.MONTHLY] = common +ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) // ============================================================================= // Export // ============================================================================= -const toText = function( +const toText = function ( rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter ) { - return new ToText(rrule, gettext, language, dateFormatter).toString(); -}; + return new ToText(rrule, gettext, language, dateFormatter).toString() +} -const { isFullyConvertible } = ToText; +const { isFullyConvertible } = ToText export interface Nlp { - fromText: typeof fromText; - parseText: typeof parseText; - isFullyConvertible: typeof isFullyConvertible; - toText: typeof toText; + fromText: typeof fromText + parseText: typeof parseText + isFullyConvertible: typeof isFullyConvertible + toText: typeof toText } -export { fromText, parseText, isFullyConvertible, toText }; +export { fromText, parseText, isFullyConvertible, toText } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 89870c5b..942490ac 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options, ByWeekday } from "../types"; -import { Weekday } from "../weekday"; -import { isArray, isNumber, isPresent, padStart } from "../helpers"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options, ByWeekday } from '../types' +import { Weekday } from '../weekday' +import { isArray, isNumber, isPresent, padStart } from '../helpers' // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from "../helpers"; /** * Return true if a value is in an array */ -const contains = function(arr: string[], val: string) { - return arr.indexOf(val) !== -1; -}; +const contains = function (arr: string[], val: string) { + return arr.indexOf(val) !== -1 +} // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string; +export type GetText = (id: string | number | Weekday) => string -const defaultGetText: GetText = id => id.toString(); +const defaultGetText: GetText = id => id.toString() export type DateFormatter = ( year: number, month: string, day: number -) => string; +) => string const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}`; +) => `${month} ${day}, ${year}` /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][]; - private rrule: RRule; - private text: string[]; - private gettext: GetText; - private dateFormatter: DateFormatter; - private language: Language; - private options: Partial; - private origOptions: Partial; - private bymonthday: Options["bymonthday"] | null; + static IMPLEMENTED: string[][] + private rrule: RRule + private text: string[] + private gettext: GetText + private dateFormatter: DateFormatter + private language: Language + private options: Partial + private origOptions: Partial + private bymonthday: Options['bymonthday'] | null private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null; + } | null - constructor( + constructor ( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = []; - this.language = language || ENGLISH; - this.gettext = gettext; - this.dateFormatter = dateFormatter; - this.rrule = rrule; - this.options = rrule.options; - this.origOptions = rrule.origOptions; + this.text = [] + this.language = language || ENGLISH + this.gettext = gettext + this.dateFormatter = dateFormatter + this.rrule = rrule + this.options = rrule.options + this.origOptions = rrule.origOptions if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!); - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); + const bymonthday = ([] as number[]).concat(this.options.bymonthday!) + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) - bymonthday.sort((a, b) => a - b); - bynmonthday.sort((a, b) => b - a); + bymonthday.sort((a, b) => a - b) + bynmonthday.sort((a, b) => b - a) // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday); - if (!this.bymonthday.length) this.bymonthday = null; + this.bymonthday = bymonthday.concat(bynmonthday) + if (!this.bymonthday.length) this.bymonthday = null } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday; - const days = String(byweekday); + : this.origOptions.byweekday + const days = String(byweekday) this.byweekday = { - allWeeks: byweekday.filter(function(weekday: Weekday) { - return !weekday.n; + allWeeks: byweekday.filter(function (weekday: Weekday) { + return !weekday.n }), - someWeeks: byweekday.filter(function(weekday: Weekday) { - return Boolean(weekday.n); + someWeeks: byweekday.filter(function (weekday: Weekday) { + return Boolean(weekday.n) }), isWeekdays: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") === -1 && - days.indexOf("SU") === -1, + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') === -1 && + days.indexOf('SU') === -1, isEveryDay: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") !== -1 && - days.indexOf("SU") !== -1 - }; - - const sortWeekDays = function(a: Weekday, b: Weekday) { - return a.weekday - b.weekday; - }; - - this.byweekday.allWeeks!.sort(sortWeekDays); - this.byweekday.someWeeks!.sort(sortWeekDays); - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') !== -1 && + days.indexOf('SU') !== -1 + } + + const sortWeekDays = function (a: Weekday, b: Weekday) { + return a.weekday - b.weekday + } + + this.byweekday.allWeeks!.sort(sortWeekDays) + this.byweekday.someWeeks!.sort(sortWeekDays) + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null } else { - this.byweekday = null; + this.byweekday = null } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible(rrule: RRule) { - let canConvert = true; + static isFullyConvertible (rrule: RRule) { + let canConvert = true - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; - if (rrule.origOptions.until && rrule.origOptions.count) return false; + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false + if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(["dtstart", "wkst", "freq"], key)) return true; - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; + if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } - return canConvert; + return canConvert } - isFullyConvertible() { - return ToText.isFullyConvertible(this.rrule); + isFullyConvertible () { + return ToText.isFullyConvertible(this.rrule) } /** @@ -159,359 +159,364 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString() { - const gettext = this.gettext; + toString () { + const gettext = this.gettext if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext("RRule error: Unable to fully convert this rrule to text"); + return gettext('RRule error: Unable to fully convert this rrule to text') } - this.text = [gettext("every")]; + this.text = [gettext('every')] // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]](); + this[RRule.FREQUENCIES[this.options.freq]]() if (this.options.until) { - this.add(gettext("until")); - const until = this.options.until; + this.add(gettext('until')) + const until = this.options.until this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ); + ) } else if (this.options.count) { - this.add(gettext("for")) + this.add(gettext('for')) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext("times") : gettext("time") - ); + this.plural(this.options.count) ? gettext('times') : gettext('time') + ) } - if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); + if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) - return this.text.join(""); + return this.text.join('') } - HOURLY() { - const gettext = this.gettext; + HOURLY () { + const gettext = this.gettext - if (this.options.interval !== 1) - this.add(this.options.interval!.toString()); + if (this.options.interval !== 1) { + this.add(this.options.interval!.toString()) + } this.add( - this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") - ); + this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') + ) } - MINUTELY() { - const gettext = this.gettext; + MINUTELY () { + const gettext = this.gettext - if (this.options.interval !== 1) - this.add(this.options.interval!.toString()); + if (this.options.interval !== 1) { + this.add(this.options.interval!.toString()) + } this.add( this.plural(this.options.interval!) - ? gettext("minutes") - : gettext("minutes") - ); + ? gettext('minutes') + : gettext('minutes') + ) } - DAILY() { - const gettext = this.gettext; + DAILY () { + const gettext = this.gettext - if (this.options.interval !== 1) - this.add(this.options.interval!.toString()); + if (this.options.interval !== 1) { + this.add(this.options.interval!.toString()) + } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - WEEKLY() { - const gettext = this.gettext; + WEEKLY () { + const gettext = this.gettext if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") - ); + this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') + ) } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } else { - if (this.options.interval === 1) this.add(gettext("week")); + if (this.options.interval === 1) this.add(gettext('week')) if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } } } - MONTHLY() { - const gettext = this.gettext; + MONTHLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext("months")); - if (this.plural(this.options.interval!)) this.add(gettext("in")); + this.add(this.options.interval!.toString()).add(gettext('months')) + if (this.plural(this.options.interval!)) this.add(gettext('in')) } else { // this.add(gettext('MONTH')) } - this._bymonth(); + this._bymonth() } else { - if (this.options.interval !== 1) - this.add(this.options.interval!.toString()); + if (this.options.interval !== 1) { + this.add(this.options.interval!.toString()) + } this.add( this.plural(this.options.interval!) - ? gettext("months") - : gettext("month") - ); + ? gettext('months') + : gettext('month') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } } - YEARLY() { - const gettext = this.gettext; + YEARLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); - this.add(gettext("years")); + this.add(this.options.interval!.toString()) + this.add(gettext('years')) } else { // this.add(gettext('YEAR')) } - this._bymonth(); + this._bymonth() } else { - if (this.options.interval !== 1) - this.add(this.options.interval!.toString()); + if (this.options.interval !== 1) { + this.add(this.options.interval!.toString()) + } this.add( - this.plural(this.options.interval!) ? gettext("years") : gettext("year") - ); + this.plural(this.options.interval!) ? gettext('years') : gettext('year') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } if (this.options.byyearday) { - this.add(gettext("on the")) - .add(this.list(this.options.byyearday, this.nth, gettext("and"))) - .add(gettext("day")); + this.add(gettext('on the')) + .add(this.list(this.options.byyearday, this.nth, gettext('and'))) + .add(gettext('day')) } if (this.options.byweekno) { - this.add(gettext("in")) + this.add(gettext('in')) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext("weeks") - : gettext("week") + ? gettext('weeks') + : gettext('week') ) - .add(this.list(this.options.byweekno, undefined, gettext("and"))); + .add(this.list(this.options.byweekno, undefined, gettext('and'))) } } - private _bymonthday() { - const gettext = this.gettext; + private _bymonthday () { + const gettext = this.gettext if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext("on")) + this.add(gettext('on')) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) ) - .add(gettext("the")) - .add(this.list(this.bymonthday!, this.nth, gettext("or"))); + .add(gettext('the')) + .add(this.list(this.bymonthday!, this.nth, gettext('or'))) } else { - this.add(gettext("on the")).add( - this.list(this.bymonthday!, this.nth, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.bymonthday!, this.nth, gettext('and')) + ) } // this.add(gettext('DAY')) } - private _byweekday() { - const gettext = this.gettext; + private _byweekday () { + const gettext = this.gettext if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext("on")).add( + this.add(gettext('on')).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ); + ) } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext("and")); + if (this.byweekday!.allWeeks) this.add(gettext('and')) - this.add(gettext("on the")).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) + ) } } - private _byhour() { - const gettext = this.gettext; + private _byhour () { + const gettext = this.gettext - this.add(gettext("at")).add( - this.list(this.origOptions.byhour!, undefined, gettext("and")) - ); + this.add(gettext('at')).add( + this.list(this.origOptions.byhour!, undefined, gettext('and')) + ) } - private _byminute() { - const gettext = this.gettext; + private _byminute () { + const gettext = this.gettext - this.add(gettext("at")).add( + this.add(gettext('at')).add( `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ); + ) } - private _bymonth() { + private _bymonth () { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) - ); + this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) + ) } - nth(n: number | string) { - n = parseInt(n.toString(), 10); - let nth: string; - let npos: number; - const gettext = this.gettext; + nth (n: number | string) { + n = parseInt(n.toString(), 10) + let nth: string + let npos: number + const gettext = this.gettext - if (n === -1) return gettext("last"); + if (n === -1) return gettext('last') - npos = Math.abs(n); + npos = Math.abs(n) switch (npos) { case 1: case 21: case 31: - nth = npos + gettext("st"); - break; + nth = npos + gettext('st') + break case 2: case 22: - nth = npos + gettext("nd"); - break; + nth = npos + gettext('nd') + break case 3: case 23: - nth = npos + gettext("rd"); - break; + nth = npos + gettext('rd') + break default: - nth = npos + gettext("th"); + nth = npos + gettext('th') } - return n < 0 ? nth + " " + gettext("last") : nth; + return n < 0 ? nth + ' ' + gettext('last') : nth } - monthtext(m: number) { - return this.language.monthNames[m - 1]; + monthtext (m: number) { + return this.language.monthNames[m - 1] } - weekdaytext(wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); + weekdaytext (wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ); + ) } - plural(n: number) { - return n % 100 !== 1; + plural (n: number) { + return n % 100 !== 1 } - add(s: string) { - this.text.push(" "); - this.text.push(s); - return this; + add (s: string) { + this.text.push(' ') + this.text.push(s) + return this } - list( + list ( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = "," + delim: string = ',' ) { if (!isArray(arr)) { - arr = [arr]; + arr = [arr] } - const delimJoin = function( + const delimJoin = function ( array: string[], delimiter: string, finalDelimiter: string ) { - let list = ""; + let list = '' for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += " " + finalDelimiter + " "; + list += ' ' + finalDelimiter + ' ' } else { - list += delimiter + " "; + list += delimiter + ' ' } } - list += array[i]; + list += array[i] } - return list; - }; + return list + } callback = callback || - function(o) { - return o.toString(); - }; - const self = this; - const realCallback = function(arg: ByWeekday) { - return callback && callback.call(self, arg); - }; + function (o) { + return o.toString() + } + const self = this + const realCallback = function (arg: ByWeekday) { + return callback && callback.call(self, arg) + } if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim); + return delimJoin(arr.map(realCallback), delim, finalDelim) } else { - return arr.map(realCallback).join(delim + " "); + return arr.map(realCallback).join(delim + ' ') } } } From 72ccadcb55cfc5b012259eded89b88369552432b Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:39:52 +0100 Subject: [PATCH 03/23] Add function C to evaluate colon in daily case --- src/nlp/i18n.ts | 121 +++++---- src/nlp/parsetext.ts | 578 ++++++++++++++++++++++--------------------- 2 files changed, 369 insertions(+), 330 deletions(-) diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index 63941b3b..a4eed2fd 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -3,67 +3,82 @@ // ============================================================================= export interface Language { - dayNames: string[] - monthNames: string[] + dayNames: string[]; + monthNames: string[]; tokens: { - [k: string]: RegExp - } + [k: string]: RegExp; + }; } const ENGLISH: Language = { dayNames: [ - 'Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday' + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" ], monthNames: [ - 'January', 'February', 'March', 'April', 'May', - 'June', 'July', 'August', 'September', 'October', - 'November', 'December' + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" ], tokens: { - 'SKIP': /^[ \r\n\t]+|^\.$/, - 'number': /^[1-9][0-9]*/, - 'numberAsText': /^(one|two|three)/i, - 'every': /^every/i, - 'day(s)': /^days?/i, - 'weekday(s)': /^weekdays?/i, - 'week(s)': /^weeks?/i, - 'hour(s)': /^hours?/i, - 'minute(s)': /^minutes?/i, - 'month(s)': /^months?/i, - 'year(s)': /^years?/i, - 'on': /^(on|in)/i, - 'at': /^(at)/i, - 'the': /^the/i, - 'first': /^first/i, - 'second': /^second/i, - 'third': /^third/i, - 'nth': /^([1-9][0-9]*)(\.|th|nd|rd|st)/i, - 'last': /^last/i, - 'for': /^for/i, - 'time(s)': /^times?/i, - 'until': /^(un)?til/i, - 'monday': /^mo(n(day)?)?/i, - 'tuesday': /^tu(e(s(day)?)?)?/i, - 'wednesday': /^we(d(n(esday)?)?)?/i, - 'thursday': /^th(u(r(sday)?)?)?/i, - 'friday': /^fr(i(day)?)?/i, - 'saturday': /^sa(t(urday)?)?/i, - 'sunday': /^su(n(day)?)?/i, - 'january': /^jan(uary)?/i, - 'february': /^feb(ruary)?/i, - 'march': /^mar(ch)?/i, - 'april': /^apr(il)?/i, - 'may': /^may/i, - 'june': /^june?/i, - 'july': /^july?/i, - 'august': /^aug(ust)?/i, - 'september': /^sep(t(ember)?)?/i, - 'october': /^oct(ober)?/i, - 'november': /^nov(ember)?/i, - 'december': /^dec(ember)?/i, - 'comma': /^(,\s*|(and|or)\s*)+/i + SKIP: /^[ \r\n\t]+|^\.$/, + number: /^[1-9][0-9]*/, + numberAsText: /^(one|two|three)/i, + every: /^every/i, + "day(s)": /^days?/i, + "weekday(s)": /^weekdays?/i, + "week(s)": /^weeks?/i, + "hour(s)": /^hours?/i, + "minute(s)": /^minutes?/i, + "month(s)": /^months?/i, + "year(s)": /^years?/i, + on: /^(on|in)/i, + at: /^(at)/i, + the: /^the/i, + first: /^first/i, + second: /^second/i, + third: /^third/i, + nth: /^([1-9][0-9]*)(\.|th|nd|rd|st)/i, + last: /^last/i, + for: /^for/i, + "time(s)": /^times?/i, + until: /^(un)?til/i, + monday: /^mo(n(day)?)?/i, + tuesday: /^tu(e(s(day)?)?)?/i, + wednesday: /^we(d(n(esday)?)?)?/i, + thursday: /^th(u(r(sday)?)?)?/i, + friday: /^fr(i(day)?)?/i, + saturday: /^sa(t(urday)?)?/i, + sunday: /^su(n(day)?)?/i, + january: /^jan(uary)?/i, + february: /^feb(ruary)?/i, + march: /^mar(ch)?/i, + april: /^apr(il)?/i, + may: /^may/i, + june: /^june?/i, + july: /^july?/i, + august: /^aug(ust)?/i, + september: /^sep(t(ember)?)?/i, + october: /^oct(ober)?/i, + november: /^nov(ember)?/i, + december: /^dec(ember)?/i, + comma: /^(,\s*|(and|or)\s*)+/i, + colon: /^(\s*:\s*)/i } -} +}; -export default ENGLISH +export default ENGLISH; diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 089de0d0..fee46847 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,286 +1,287 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options } from '../types' -import { WeekdayStr } from '../weekday' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options } from "../types"; +import { WeekdayStr } from "../weekday"; // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp } - public text: string - public symbol: string | null - public value: RegExpExecArray | null - private done = true - - constructor (rules: { [k: string]: RegExp }) { - this.rules = rules + private readonly rules: { [k: string]: RegExp }; + public text: string; + public symbol: string | null; + public value: RegExpExecArray | null; + private done = true; + + constructor(rules: { [k: string]: RegExp }) { + this.rules = rules; } - start (text: string) { - this.text = text - this.done = false - return this.nextSymbol() + start(text: string) { + this.text = text; + this.done = false; + return this.nextSymbol(); } - isDone () { - return this.done && this.symbol === null + isDone() { + return this.done && this.symbol === null; } - nextSymbol () { - let best: RegExpExecArray | null - let bestSymbol: string - const p = this + nextSymbol() { + let best: RegExpExecArray | null; + let bestSymbol: string; + const p = this; - this.symbol = null - this.value = null + this.symbol = null; + this.value = null; do { - if (this.done) return false + if (this.done) return false; - let rule: RegExp - best = null + let rule: RegExp; + best = null; for (let name in this.rules) { - rule = this.rules[name] - const match = rule.exec(p.text) + rule = this.rules[name]; + const match = rule.exec(p.text); if (match) { if (best === null || match[0].length > best[0].length) { - best = match - bestSymbol = name + best = match; + bestSymbol = name; } } } if (best != null) { - this.text = this.text.substr(best[0].length) + this.text = this.text.substr(best[0].length); - if (this.text === '') this.done = true + if (this.text === "") this.done = true; } if (best == null) { - this.done = true - this.symbol = null - this.value = null - return + this.done = true; + this.symbol = null; + this.value = null; + return; } - // @ts-ignore - } while (bestSymbol === 'SKIP') + // @ts-ignore + } while (bestSymbol === "SKIP"); // @ts-ignore - this.symbol = bestSymbol - this.value = best - return true + this.symbol = bestSymbol; + this.value = best; + return true; } - accept (name: string) { + accept(name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value - this.nextSymbol() - return v + const v = this.value; + this.nextSymbol(); + return v; } - this.nextSymbol() - return true + this.nextSymbol(); + return true; } - return false + return false; } - acceptNumber () { - return this.accept('number') as RegExpExecArray + acceptNumber() { + return this.accept("number") as RegExpExecArray; } - expect (name: string) { - if (this.accept(name)) return true + expect(name: string) { + if (this.accept(name)) return true; - throw new Error('expected ' + name + ' but found ' + this.symbol) + throw new Error("expected " + name + " but found " + this.symbol); } } -export default function parseText (text: string, language: Language = ENGLISH) { - const options: Partial = {} - const ttr = new Parser(language.tokens) +export default function parseText(text: string, language: Language = ENGLISH) { + const options: Partial = {}; + const ttr = new Parser(language.tokens); - if (!ttr.start(text)) return null + if (!ttr.start(text)) return null; - S() - return options + S(); + return options; - function S () { + function S() { // every [n] - ttr.expect('every') - let n = ttr.acceptNumber() - if (n) options.interval = parseInt(n[0], 10) - if (ttr.isDone()) throw new Error('Unexpected end') + ttr.expect("every"); + let n = ttr.acceptNumber(); + if (n) options.interval = parseInt(n[0], 10); + if (ttr.isDone()) throw new Error("Unexpected end"); switch (ttr.symbol) { - case 'day(s)': - options.freq = RRule.DAILY + case "day(s)": + options.freq = RRule.DAILY; if (ttr.nextSymbol()) { - AT() - F() + AT(); + C(); + F(); } - break + break; // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case 'weekday(s)': - options.freq = RRule.WEEKLY - options.byweekday = [ - RRule.MO, - RRule.TU, - RRule.WE, - RRule.TH, - RRule.FR - ] - ttr.nextSymbol() - F() - break - - case 'week(s)': - options.freq = RRule.WEEKLY + case "weekday(s)": + options.freq = RRule.WEEKLY; + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; + ttr.nextSymbol(); + F(); + break; + + case "week(s)": + options.freq = RRule.WEEKLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'hour(s)': - options.freq = RRule.HOURLY + case "hour(s)": + options.freq = RRule.HOURLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'minute(s)': - options.freq = RRule.MINUTELY + case "minute(s)": + options.freq = RRule.MINUTELY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'month(s)': - options.freq = RRule.MONTHLY + case "month(s)": + options.freq = RRule.MONTHLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'year(s)': - options.freq = RRule.YEARLY + case "year(s)": + options.freq = RRule.YEARLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break - - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - options.freq = RRule.WEEKLY - const key: WeekdayStr = ttr.symbol.substr(0, 2).toUpperCase() as WeekdayStr - options.byweekday = [RRule[key]] - - if (!ttr.nextSymbol()) return + break; + + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + options.freq = RRule.WEEKLY; + const key: WeekdayStr = ttr.symbol + .substr(0, 2) + .toUpperCase() as WeekdayStr; + options.byweekday = [RRule[key]]; + + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let wkd = decodeWKD() as keyof typeof RRule + let wkd = decodeWKD() as keyof typeof RRule; if (!wkd) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected weekday') + throw new Error( + "Unexpected symbol " + ttr.symbol + ", expected weekday" + ); } // @ts-ignore - options.byweekday.push(RRule[wkd]) - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + ttr.nextSymbol(); } - MDAYs() - F() - break - - case 'january': - case 'february': - case 'march': - case 'april': - case 'may': - case 'june': - case 'july': - case 'august': - case 'september': - case 'october': - case 'november': - case 'december': - options.freq = RRule.YEARLY - options.bymonth = [decodeM() as number] - - if (!ttr.nextSymbol()) return + MDAYs(); + F(); + break; + + case "january": + case "february": + case "march": + case "april": + case "may": + case "june": + case "july": + case "august": + case "september": + case "october": + case "november": + case "december": + options.freq = RRule.YEARLY; + options.bymonth = [decodeM() as number]; + + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let m = decodeM() + let m = decodeM(); if (!m) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected month') + throw new Error( + "Unexpected symbol " + ttr.symbol + ", expected month" + ); } - options.bymonth.push(m) - ttr.nextSymbol() + options.bymonth.push(m); + ttr.nextSymbol(); } - ON() - F() - break + ON(); + F(); + break; default: - throw new Error('Unknown symbol') + throw new Error("Unknown symbol"); } } - function ON () { - const on = ttr.accept('on') - const the = ttr.accept('the') - if (!(on || the)) return + function ON() { + const on = ttr.accept("on"); + const the = ttr.accept("the"); + if (!(on || the)) return; do { - let nth = decodeNTH() - let wkd = decodeWKD() - let m = decodeM() + let nth = decodeNTH(); + let wkd = decodeWKD(); + let m = decodeM(); // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)) + options.byweekday.push(RRule[wkd].nth(nth)); } else { - if (!options.bymonthday) options.bymonthday = [] + if (!options.bymonthday) options.bymonthday = []; // @ts-ignore - options.bymonthday.push(nth) - ttr.accept('day(s)') + options.bymonthday.push(nth); + ttr.accept("day(s)"); } // } else if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd]) - } else if (ttr.symbol === 'weekday(s)') { - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + } else if (ttr.symbol === "weekday(s)") { + ttr.nextSymbol(); if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -288,156 +289,179 @@ export default function parseText (text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ] + ]; } - } else if (ttr.symbol === 'week(s)') { - ttr.nextSymbol() - let n = ttr.acceptNumber() + } else if (ttr.symbol === "week(s)") { + ttr.nextSymbol(); + let n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected week number') + throw new Error( + "Unexpected symbol " + ttr.symbol + ", expected week number" + ); } - options.byweekno = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byweekno = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + '; expected monthday') + throw new Error( + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.byweekno.push(parseInt(n[0], 10)) + options.byweekno.push(parseInt(n[0], 10)); } } else if (m) { - ttr.nextSymbol() - if (!options.bymonth) options.bymonth = [] + ttr.nextSymbol(); + if (!options.bymonth) options.bymonth = []; // @ts-ignore - options.bymonth.push(m) + options.bymonth.push(m); } else { - return + return; } - } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) + } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); } - function AT () { - const at = ttr.accept('at') - if (!at) return + function AT() { + const at = ttr.accept("at"); + if (!at) return; do { - let n = ttr.acceptNumber() + let n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') + throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); } - options.byhour = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byhour = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + '; expected hour') + throw new Error( + "Unexpected symbol " + ttr.symbol + "; expected hour" + ); } - options.byhour.push(parseInt(n[0], 10)) + options.byhour.push(parseInt(n[0], 10)); + } + } while (ttr.accept("comma") || ttr.accept("at")); + } + + function C() { + const colon = ttr.accept("colon"); + if (!colon) return; + + do { + let m = ttr.acceptNumber(); + if (!m) { + throw new Error( + "Unexpected symbol " + ttr.symbol + ", expected minutes" + ); } - } while (ttr.accept('comma') || ttr.accept('at')) + options.byminute = parseInt(m[0], 10); + } while (ttr.accept("colon")); } - function decodeM () { + function decodeM() { switch (ttr.symbol) { - case 'january': - return 1 - case 'february': - return 2 - case 'march': - return 3 - case 'april': - return 4 - case 'may': - return 5 - case 'june': - return 6 - case 'july': - return 7 - case 'august': - return 8 - case 'september': - return 9 - case 'october': - return 10 - case 'november': - return 11 - case 'december': - return 12 + case "january": + return 1; + case "february": + return 2; + case "march": + return 3; + case "april": + return 4; + case "may": + return 5; + case "june": + return 6; + case "july": + return 7; + case "august": + return 8; + case "september": + return 9; + case "october": + return 10; + case "november": + return 11; + case "december": + return 12; default: - return false + return false; } } - function decodeWKD () { + function decodeWKD() { switch (ttr.symbol) { - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - return ttr.symbol.substr(0, 2).toUpperCase() + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + return ttr.symbol.substr(0, 2).toUpperCase(); default: - return false + return false; } } - function decodeNTH () { + function decodeNTH() { switch (ttr.symbol) { - case 'last': - ttr.nextSymbol() - return -1 - case 'first': - ttr.nextSymbol() - return 1 - case 'second': - ttr.nextSymbol() - return ttr.accept('last') ? -2 : 2 - case 'third': - ttr.nextSymbol() - return ttr.accept('last') ? -3 : 3 - case 'nth': - const v = parseInt(ttr.value![1], 10) - if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) - - ttr.nextSymbol() - return ttr.accept('last') ? -v : v + case "last": + ttr.nextSymbol(); + return -1; + case "first": + ttr.nextSymbol(); + return 1; + case "second": + ttr.nextSymbol(); + return ttr.accept("last") ? -2 : 2; + case "third": + ttr.nextSymbol(); + return ttr.accept("last") ? -3 : 3; + case "nth": + const v = parseInt(ttr.value![1], 10); + if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); + + ttr.nextSymbol(); + return ttr.accept("last") ? -v : v; default: - return false + return false; } } - function MDAYs () { - ttr.accept('on') - ttr.accept('the') + function MDAYs() { + ttr.accept("on"); + ttr.accept("the"); - let nth = decodeNTH() - if (!nth) return + let nth = decodeNTH(); + if (!nth) return; - options.bymonthday = [nth] - ttr.nextSymbol() + options.bymonthday = [nth]; + ttr.nextSymbol(); - while (ttr.accept('comma')) { - nth = decodeNTH() + while (ttr.accept("comma")) { + nth = decodeNTH(); if (!nth) { - throw new Error('Unexpected symbol ' + ttr.symbol + '; expected monthday') + throw new Error( + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.bymonthday.push(nth) - ttr.nextSymbol() + options.bymonthday.push(nth); + ttr.nextSymbol(); } } - function F () { - if (ttr.symbol === 'until') { - const date = Date.parse(ttr.text) + function F() { + if (ttr.symbol === "until") { + const date = Date.parse(ttr.text); - if (!date) throw new Error('Cannot parse until date:' + ttr.text) - options.until = new Date(date) - } else if (ttr.accept('for')) { - options.count = parseInt(ttr.value![0], 10) - ttr.expect('number') + if (!date) throw new Error("Cannot parse until date:" + ttr.text); + options.until = new Date(date); + } else if (ttr.accept("for")) { + options.count = parseInt(ttr.value![0], 10); + ttr.expect("number"); // ttr.expect('times') } } From d14b99361a8e15a1b955a6011db8d1b4229ad888 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:40:44 +0100 Subject: [PATCH 04/23] Add function C to display time in daily case --- src/nlp/i18n.ts | 64 ++--- src/nlp/parsetext.ts | 568 +++++++++++++++++++++---------------------- 2 files changed, 316 insertions(+), 316 deletions(-) diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index a4eed2fd..b9237eae 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -3,49 +3,49 @@ // ============================================================================= export interface Language { - dayNames: string[]; - monthNames: string[]; + dayNames: string[] + monthNames: string[] tokens: { [k: string]: RegExp; - }; + } } const ENGLISH: Language = { dayNames: [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' ], monthNames: [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' ], tokens: { SKIP: /^[ \r\n\t]+|^\.$/, number: /^[1-9][0-9]*/, numberAsText: /^(one|two|three)/i, every: /^every/i, - "day(s)": /^days?/i, - "weekday(s)": /^weekdays?/i, - "week(s)": /^weeks?/i, - "hour(s)": /^hours?/i, - "minute(s)": /^minutes?/i, - "month(s)": /^months?/i, - "year(s)": /^years?/i, + 'day(s)': /^days?/i, + 'weekday(s)': /^weekdays?/i, + 'week(s)': /^weeks?/i, + 'hour(s)': /^hours?/i, + 'minute(s)': /^minutes?/i, + 'month(s)': /^months?/i, + 'year(s)': /^years?/i, on: /^(on|in)/i, at: /^(at)/i, the: /^the/i, @@ -55,7 +55,7 @@ const ENGLISH: Language = { nth: /^([1-9][0-9]*)(\.|th|nd|rd|st)/i, last: /^last/i, for: /^for/i, - "time(s)": /^times?/i, + 'time(s)': /^times?/i, until: /^(un)?til/i, monday: /^mo(n(day)?)?/i, tuesday: /^tu(e(s(day)?)?)?/i, @@ -79,6 +79,6 @@ const ENGLISH: Language = { comma: /^(,\s*|(and|or)\s*)+/i, colon: /^(\s*:\s*)/i } -}; +} -export default ENGLISH; +export default ENGLISH diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index fee46847..74b9bd0d 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,287 +1,287 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options } from "../types"; -import { WeekdayStr } from "../weekday"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options } from '../types' +import { WeekdayStr } from '../weekday' // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp }; - public text: string; - public symbol: string | null; - public value: RegExpExecArray | null; - private done = true; - - constructor(rules: { [k: string]: RegExp }) { - this.rules = rules; + private readonly rules: { [k: string]: RegExp } + public text: string + public symbol: string | null + public value: RegExpExecArray | null + private done = true + + constructor (rules: { [k: string]: RegExp }) { + this.rules = rules } - start(text: string) { - this.text = text; - this.done = false; - return this.nextSymbol(); + start (text: string) { + this.text = text + this.done = false + return this.nextSymbol() } - isDone() { - return this.done && this.symbol === null; + isDone () { + return this.done && this.symbol === null } - nextSymbol() { - let best: RegExpExecArray | null; - let bestSymbol: string; - const p = this; + nextSymbol () { + let best: RegExpExecArray | null + let bestSymbol: string + const p = this - this.symbol = null; - this.value = null; + this.symbol = null + this.value = null do { - if (this.done) return false; + if (this.done) return false - let rule: RegExp; - best = null; + let rule: RegExp + best = null for (let name in this.rules) { - rule = this.rules[name]; - const match = rule.exec(p.text); + rule = this.rules[name] + const match = rule.exec(p.text) if (match) { if (best === null || match[0].length > best[0].length) { - best = match; - bestSymbol = name; + best = match + bestSymbol = name } } } if (best != null) { - this.text = this.text.substr(best[0].length); + this.text = this.text.substr(best[0].length) - if (this.text === "") this.done = true; + if (this.text === '') this.done = true } if (best == null) { - this.done = true; - this.symbol = null; - this.value = null; - return; + this.done = true + this.symbol = null + this.value = null + return } // @ts-ignore - } while (bestSymbol === "SKIP"); + } while (bestSymbol === 'SKIP') // @ts-ignore - this.symbol = bestSymbol; - this.value = best; - return true; + this.symbol = bestSymbol + this.value = best + return true } - accept(name: string) { + accept (name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value; - this.nextSymbol(); - return v; + const v = this.value + this.nextSymbol() + return v } - this.nextSymbol(); - return true; + this.nextSymbol() + return true } - return false; + return false } - acceptNumber() { - return this.accept("number") as RegExpExecArray; + acceptNumber () { + return this.accept('number') as RegExpExecArray } - expect(name: string) { - if (this.accept(name)) return true; + expect (name: string) { + if (this.accept(name)) return true - throw new Error("expected " + name + " but found " + this.symbol); + throw new Error('expected ' + name + ' but found ' + this.symbol) } } -export default function parseText(text: string, language: Language = ENGLISH) { - const options: Partial = {}; - const ttr = new Parser(language.tokens); +export default function parseText (text: string, language: Language = ENGLISH) { + const options: Partial = {} + const ttr = new Parser(language.tokens) - if (!ttr.start(text)) return null; + if (!ttr.start(text)) return null - S(); - return options; + S() + return options - function S() { + function S () { // every [n] - ttr.expect("every"); - let n = ttr.acceptNumber(); - if (n) options.interval = parseInt(n[0], 10); - if (ttr.isDone()) throw new Error("Unexpected end"); + ttr.expect('every') + let n = ttr.acceptNumber() + if (n) options.interval = parseInt(n[0], 10) + if (ttr.isDone()) throw new Error('Unexpected end') switch (ttr.symbol) { - case "day(s)": - options.freq = RRule.DAILY; + case 'day(s)': + options.freq = RRule.DAILY if (ttr.nextSymbol()) { - AT(); - C(); - F(); + AT() + C() + F() } - break; + break // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case "weekday(s)": - options.freq = RRule.WEEKLY; - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; - ttr.nextSymbol(); - F(); - break; - - case "week(s)": - options.freq = RRule.WEEKLY; + case 'weekday(s)': + options.freq = RRule.WEEKLY + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] + ttr.nextSymbol() + F() + break + + case 'week(s)': + options.freq = RRule.WEEKLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "hour(s)": - options.freq = RRule.HOURLY; + case 'hour(s)': + options.freq = RRule.HOURLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "minute(s)": - options.freq = RRule.MINUTELY; + case 'minute(s)': + options.freq = RRule.MINUTELY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "month(s)": - options.freq = RRule.MONTHLY; + case 'month(s)': + options.freq = RRule.MONTHLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "year(s)": - options.freq = RRule.YEARLY; + case 'year(s)': + options.freq = RRule.YEARLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; - - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - options.freq = RRule.WEEKLY; + break + + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + options.freq = RRule.WEEKLY const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr; - options.byweekday = [RRule[key]]; + .toUpperCase() as WeekdayStr + options.byweekday = [RRule[key]] - if (!ttr.nextSymbol()) return; + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let wkd = decodeWKD() as keyof typeof RRule; + let wkd = decodeWKD() as keyof typeof RRule if (!wkd) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected weekday" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected weekday' + ) } // @ts-ignore - options.byweekday.push(RRule[wkd]); - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + ttr.nextSymbol() } - MDAYs(); - F(); - break; - - case "january": - case "february": - case "march": - case "april": - case "may": - case "june": - case "july": - case "august": - case "september": - case "october": - case "november": - case "december": - options.freq = RRule.YEARLY; - options.bymonth = [decodeM() as number]; - - if (!ttr.nextSymbol()) return; + MDAYs() + F() + break + + case 'january': + case 'february': + case 'march': + case 'april': + case 'may': + case 'june': + case 'july': + case 'august': + case 'september': + case 'october': + case 'november': + case 'december': + options.freq = RRule.YEARLY + options.bymonth = [decodeM() as number] + + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let m = decodeM(); + let m = decodeM() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected month" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected month' + ) } - options.bymonth.push(m); - ttr.nextSymbol(); + options.bymonth.push(m) + ttr.nextSymbol() } - ON(); - F(); - break; + ON() + F() + break default: - throw new Error("Unknown symbol"); + throw new Error('Unknown symbol') } } - function ON() { - const on = ttr.accept("on"); - const the = ttr.accept("the"); - if (!(on || the)) return; + function ON () { + const on = ttr.accept('on') + const the = ttr.accept('the') + if (!(on || the)) return do { - let nth = decodeNTH(); - let wkd = decodeWKD(); - let m = decodeM(); + let nth = decodeNTH() + let wkd = decodeWKD() + let m = decodeM() // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)); + options.byweekday.push(RRule[wkd].nth(nth)) } else { - if (!options.bymonthday) options.bymonthday = []; + if (!options.bymonthday) options.bymonthday = [] // @ts-ignore - options.bymonthday.push(nth); - ttr.accept("day(s)"); + options.bymonthday.push(nth) + ttr.accept('day(s)') } // } else if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd]); - } else if (ttr.symbol === "weekday(s)") { - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + } else if (ttr.symbol === 'weekday(s)') { + ttr.nextSymbol() if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -289,179 +289,179 @@ export default function parseText(text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ]; + ] } - } else if (ttr.symbol === "week(s)") { - ttr.nextSymbol(); - let n = ttr.acceptNumber(); + } else if (ttr.symbol === 'week(s)') { + ttr.nextSymbol() + let n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected week number" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected week number' + ) } - options.byweekno = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byweekno = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.byweekno.push(parseInt(n[0], 10)); + options.byweekno.push(parseInt(n[0], 10)) } } else if (m) { - ttr.nextSymbol(); - if (!options.bymonth) options.bymonth = []; + ttr.nextSymbol() + if (!options.bymonth) options.bymonth = [] // @ts-ignore - options.bymonth.push(m); + options.bymonth.push(m) } else { - return; + return } - } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); + } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) } - function AT() { - const at = ttr.accept("at"); - if (!at) return; + function AT () { + const at = ttr.accept('at') + if (!at) return do { - let n = ttr.acceptNumber(); + let n = ttr.acceptNumber() if (!n) { - throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); + throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') } - options.byhour = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byhour = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected hour" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected hour' + ) } - options.byhour.push(parseInt(n[0], 10)); + options.byhour.push(parseInt(n[0], 10)) } - } while (ttr.accept("comma") || ttr.accept("at")); + } while (ttr.accept('comma') || ttr.accept('at')) } - function C() { - const colon = ttr.accept("colon"); - if (!colon) return; + function C () { + const colon = ttr.accept('colon') + if (!colon) return do { - let m = ttr.acceptNumber(); + let m = ttr.acceptNumber() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected minutes" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected minutes' + ) } - options.byminute = parseInt(m[0], 10); - } while (ttr.accept("colon")); + options.byminute = parseInt(m[0], 10) + } while (ttr.accept('colon')) } - function decodeM() { + function decodeM () { switch (ttr.symbol) { - case "january": - return 1; - case "february": - return 2; - case "march": - return 3; - case "april": - return 4; - case "may": - return 5; - case "june": - return 6; - case "july": - return 7; - case "august": - return 8; - case "september": - return 9; - case "october": - return 10; - case "november": - return 11; - case "december": - return 12; + case 'january': + return 1 + case 'february': + return 2 + case 'march': + return 3 + case 'april': + return 4 + case 'may': + return 5 + case 'june': + return 6 + case 'july': + return 7 + case 'august': + return 8 + case 'september': + return 9 + case 'october': + return 10 + case 'november': + return 11 + case 'december': + return 12 default: - return false; + return false } } - function decodeWKD() { + function decodeWKD () { switch (ttr.symbol) { - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - return ttr.symbol.substr(0, 2).toUpperCase(); + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + return ttr.symbol.substr(0, 2).toUpperCase() default: - return false; + return false } } - function decodeNTH() { + function decodeNTH () { switch (ttr.symbol) { - case "last": - ttr.nextSymbol(); - return -1; - case "first": - ttr.nextSymbol(); - return 1; - case "second": - ttr.nextSymbol(); - return ttr.accept("last") ? -2 : 2; - case "third": - ttr.nextSymbol(); - return ttr.accept("last") ? -3 : 3; - case "nth": - const v = parseInt(ttr.value![1], 10); - if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); - - ttr.nextSymbol(); - return ttr.accept("last") ? -v : v; + case 'last': + ttr.nextSymbol() + return -1 + case 'first': + ttr.nextSymbol() + return 1 + case 'second': + ttr.nextSymbol() + return ttr.accept('last') ? -2 : 2 + case 'third': + ttr.nextSymbol() + return ttr.accept('last') ? -3 : 3 + case 'nth': + const v = parseInt(ttr.value![1], 10) + if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) + + ttr.nextSymbol() + return ttr.accept('last') ? -v : v default: - return false; + return false } } - function MDAYs() { - ttr.accept("on"); - ttr.accept("the"); + function MDAYs () { + ttr.accept('on') + ttr.accept('the') - let nth = decodeNTH(); - if (!nth) return; + let nth = decodeNTH() + if (!nth) return - options.bymonthday = [nth]; - ttr.nextSymbol(); + options.bymonthday = [nth] + ttr.nextSymbol() - while (ttr.accept("comma")) { - nth = decodeNTH(); + while (ttr.accept('comma')) { + nth = decodeNTH() if (!nth) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.bymonthday.push(nth); - ttr.nextSymbol(); + options.bymonthday.push(nth) + ttr.nextSymbol() } } - function F() { - if (ttr.symbol === "until") { - const date = Date.parse(ttr.text); + function F () { + if (ttr.symbol === 'until') { + const date = Date.parse(ttr.text) - if (!date) throw new Error("Cannot parse until date:" + ttr.text); - options.until = new Date(date); - } else if (ttr.accept("for")) { - options.count = parseInt(ttr.value![0], 10); - ttr.expect("number"); + if (!date) throw new Error('Cannot parse until date:' + ttr.text) + options.until = new Date(date) + } else if (ttr.accept('for')) { + options.count = parseInt(ttr.value![0], 10) + ttr.expect('number') // ttr.expect('times') } } From 37129c25b942cb1a4d1300ca3b33c658b76d461b Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:49:07 +0100 Subject: [PATCH 05/23] Add time to weekly case --- src/nlp/index.ts | 60 ++--- src/nlp/parsetext.ts | 570 ++++++++++++++++++++++--------------------- src/nlp/totext.ts | 472 +++++++++++++++++------------------ test/nlp.test.ts | 1 + 4 files changed, 555 insertions(+), 548 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index 1d04fd45..cf8642a1 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,7 +1,7 @@ -import ToText, { DateFormatter, GetText } from './totext' -import parseText from './parsetext' -import RRule from '../index' -import ENGLISH, { Language } from './i18n' +import ToText, { DateFormatter, GetText } from "./totext"; +import parseText from "./parsetext"; +import RRule from "../index"; +import ENGLISH, { Language } from "./i18n"; /*! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -94,47 +94,47 @@ import ENGLISH, { Language } from './i18n' * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function (text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined) -} +const fromText = function(text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined); +}; const common = [ - 'count', - 'until', - 'interval', - 'byweekday', - 'bymonthday', - 'bymonth' -] + "count", + "until", + "interval", + "byweekday", + "bymonthday", + "bymonth" +]; -ToText.IMPLEMENTED = [] -ToText.IMPLEMENTED[RRule.HOURLY] = common -ToText.IMPLEMENTED[RRule.MINUTELY] = common -ToText.IMPLEMENTED[RRule.DAILY] = ['byhour', 'byminute'].concat(common) -ToText.IMPLEMENTED[RRule.WEEKLY] = common -ToText.IMPLEMENTED[RRule.MONTHLY] = common -ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) +ToText.IMPLEMENTED = []; +ToText.IMPLEMENTED[RRule.HOURLY] = common; +ToText.IMPLEMENTED[RRule.MINUTELY] = common; +ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.WEEKLY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.MONTHLY] = common; +ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); // ============================================================================= // Export // ============================================================================= -const toText = function ( +const toText = function( rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter ) { - return new ToText(rrule, gettext, language, dateFormatter).toString() -} + return new ToText(rrule, gettext, language, dateFormatter).toString(); +}; -const { isFullyConvertible } = ToText +const { isFullyConvertible } = ToText; export interface Nlp { - fromText: typeof fromText - parseText: typeof parseText - isFullyConvertible: typeof isFullyConvertible - toText: typeof toText + fromText: typeof fromText; + parseText: typeof parseText; + isFullyConvertible: typeof isFullyConvertible; + toText: typeof toText; } -export { fromText, parseText, isFullyConvertible, toText } +export { fromText, parseText, isFullyConvertible, toText }; diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 74b9bd0d..459eecec 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,287 +1,289 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options } from '../types' -import { WeekdayStr } from '../weekday' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options } from "../types"; +import { WeekdayStr } from "../weekday"; // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp } - public text: string - public symbol: string | null - public value: RegExpExecArray | null - private done = true - - constructor (rules: { [k: string]: RegExp }) { - this.rules = rules + private readonly rules: { [k: string]: RegExp }; + public text: string; + public symbol: string | null; + public value: RegExpExecArray | null; + private done = true; + + constructor(rules: { [k: string]: RegExp }) { + this.rules = rules; } - start (text: string) { - this.text = text - this.done = false - return this.nextSymbol() + start(text: string) { + this.text = text; + this.done = false; + return this.nextSymbol(); } - isDone () { - return this.done && this.symbol === null + isDone() { + return this.done && this.symbol === null; } - nextSymbol () { - let best: RegExpExecArray | null - let bestSymbol: string - const p = this + nextSymbol() { + let best: RegExpExecArray | null; + let bestSymbol: string; + const p = this; - this.symbol = null - this.value = null + this.symbol = null; + this.value = null; do { - if (this.done) return false + if (this.done) return false; - let rule: RegExp - best = null + let rule: RegExp; + best = null; for (let name in this.rules) { - rule = this.rules[name] - const match = rule.exec(p.text) + rule = this.rules[name]; + const match = rule.exec(p.text); if (match) { if (best === null || match[0].length > best[0].length) { - best = match - bestSymbol = name + best = match; + bestSymbol = name; } } } if (best != null) { - this.text = this.text.substr(best[0].length) + this.text = this.text.substr(best[0].length); - if (this.text === '') this.done = true + if (this.text === "") this.done = true; } if (best == null) { - this.done = true - this.symbol = null - this.value = null - return + this.done = true; + this.symbol = null; + this.value = null; + return; } // @ts-ignore - } while (bestSymbol === 'SKIP') + } while (bestSymbol === "SKIP"); // @ts-ignore - this.symbol = bestSymbol - this.value = best - return true + this.symbol = bestSymbol; + this.value = best; + return true; } - accept (name: string) { + accept(name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value - this.nextSymbol() - return v + const v = this.value; + this.nextSymbol(); + return v; } - this.nextSymbol() - return true + this.nextSymbol(); + return true; } - return false + return false; } - acceptNumber () { - return this.accept('number') as RegExpExecArray + acceptNumber() { + return this.accept("number") as RegExpExecArray; } - expect (name: string) { - if (this.accept(name)) return true + expect(name: string) { + if (this.accept(name)) return true; - throw new Error('expected ' + name + ' but found ' + this.symbol) + throw new Error("expected " + name + " but found " + this.symbol); } } -export default function parseText (text: string, language: Language = ENGLISH) { - const options: Partial = {} - const ttr = new Parser(language.tokens) +export default function parseText(text: string, language: Language = ENGLISH) { + const options: Partial = {}; + const ttr = new Parser(language.tokens); - if (!ttr.start(text)) return null + if (!ttr.start(text)) return null; - S() - return options + S(); + return options; - function S () { + function S() { // every [n] - ttr.expect('every') - let n = ttr.acceptNumber() - if (n) options.interval = parseInt(n[0], 10) - if (ttr.isDone()) throw new Error('Unexpected end') + ttr.expect("every"); + let n = ttr.acceptNumber(); + if (n) options.interval = parseInt(n[0], 10); + if (ttr.isDone()) throw new Error("Unexpected end"); switch (ttr.symbol) { - case 'day(s)': - options.freq = RRule.DAILY + case "day(s)": + options.freq = RRule.DAILY; if (ttr.nextSymbol()) { - AT() - C() - F() + AT(); + C(); + F(); } - break + break; // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case 'weekday(s)': - options.freq = RRule.WEEKLY - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] - ttr.nextSymbol() - F() - break - - case 'week(s)': - options.freq = RRule.WEEKLY + case "weekday(s)": + options.freq = RRule.WEEKLY; + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; + ttr.nextSymbol(); + F(); + break; + + case "week(s)": + options.freq = RRule.WEEKLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + AT(); + C(); + F(); } - break + break; - case 'hour(s)': - options.freq = RRule.HOURLY + case "hour(s)": + options.freq = RRule.HOURLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'minute(s)': - options.freq = RRule.MINUTELY + case "minute(s)": + options.freq = RRule.MINUTELY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'month(s)': - options.freq = RRule.MONTHLY + case "month(s)": + options.freq = RRule.MONTHLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'year(s)': - options.freq = RRule.YEARLY + case "year(s)": + options.freq = RRule.YEARLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break - - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - options.freq = RRule.WEEKLY + break; + + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + options.freq = RRule.WEEKLY; const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr - options.byweekday = [RRule[key]] + .toUpperCase() as WeekdayStr; + options.byweekday = [RRule[key]]; - if (!ttr.nextSymbol()) return + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let wkd = decodeWKD() as keyof typeof RRule + let wkd = decodeWKD() as keyof typeof RRule; if (!wkd) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected weekday' - ) + "Unexpected symbol " + ttr.symbol + ", expected weekday" + ); } // @ts-ignore - options.byweekday.push(RRule[wkd]) - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + ttr.nextSymbol(); } - MDAYs() - F() - break - - case 'january': - case 'february': - case 'march': - case 'april': - case 'may': - case 'june': - case 'july': - case 'august': - case 'september': - case 'october': - case 'november': - case 'december': - options.freq = RRule.YEARLY - options.bymonth = [decodeM() as number] - - if (!ttr.nextSymbol()) return + MDAYs(); + F(); + break; + + case "january": + case "february": + case "march": + case "april": + case "may": + case "june": + case "july": + case "august": + case "september": + case "october": + case "november": + case "december": + options.freq = RRule.YEARLY; + options.bymonth = [decodeM() as number]; + + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let m = decodeM() + let m = decodeM(); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected month' - ) + "Unexpected symbol " + ttr.symbol + ", expected month" + ); } - options.bymonth.push(m) - ttr.nextSymbol() + options.bymonth.push(m); + ttr.nextSymbol(); } - ON() - F() - break + ON(); + F(); + break; default: - throw new Error('Unknown symbol') + throw new Error("Unknown symbol"); } } - function ON () { - const on = ttr.accept('on') - const the = ttr.accept('the') - if (!(on || the)) return + function ON() { + const on = ttr.accept("on"); + const the = ttr.accept("the"); + if (!(on || the)) return; do { - let nth = decodeNTH() - let wkd = decodeWKD() - let m = decodeM() + let nth = decodeNTH(); + let wkd = decodeWKD(); + let m = decodeM(); // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)) + options.byweekday.push(RRule[wkd].nth(nth)); } else { - if (!options.bymonthday) options.bymonthday = [] + if (!options.bymonthday) options.bymonthday = []; // @ts-ignore - options.bymonthday.push(nth) - ttr.accept('day(s)') + options.bymonthday.push(nth); + ttr.accept("day(s)"); } // } else if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd]) - } else if (ttr.symbol === 'weekday(s)') { - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + } else if (ttr.symbol === "weekday(s)") { + ttr.nextSymbol(); if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -289,179 +291,179 @@ export default function parseText (text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ] + ]; } - } else if (ttr.symbol === 'week(s)') { - ttr.nextSymbol() - let n = ttr.acceptNumber() + } else if (ttr.symbol === "week(s)") { + ttr.nextSymbol(); + let n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected week number' - ) + "Unexpected symbol " + ttr.symbol + ", expected week number" + ); } - options.byweekno = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byweekno = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.byweekno.push(parseInt(n[0], 10)) + options.byweekno.push(parseInt(n[0], 10)); } } else if (m) { - ttr.nextSymbol() - if (!options.bymonth) options.bymonth = [] + ttr.nextSymbol(); + if (!options.bymonth) options.bymonth = []; // @ts-ignore - options.bymonth.push(m) + options.bymonth.push(m); } else { - return + return; } - } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) + } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); } - function AT () { - const at = ttr.accept('at') - if (!at) return + function AT() { + const at = ttr.accept("at"); + if (!at) return; do { - let n = ttr.acceptNumber() + let n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') + throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); } - options.byhour = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byhour = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected hour' - ) + "Unexpected symbol " + ttr.symbol + "; expected hour" + ); } - options.byhour.push(parseInt(n[0], 10)) + options.byhour.push(parseInt(n[0], 10)); } - } while (ttr.accept('comma') || ttr.accept('at')) + } while (ttr.accept("comma") || ttr.accept("at")); } - function C () { - const colon = ttr.accept('colon') - if (!colon) return + function C() { + const colon = ttr.accept("colon"); + if (!colon) return; do { - let m = ttr.acceptNumber() + let m = ttr.acceptNumber(); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected minutes' - ) + "Unexpected symbol " + ttr.symbol + ", expected minutes" + ); } - options.byminute = parseInt(m[0], 10) - } while (ttr.accept('colon')) + options.byminute = parseInt(m[0], 10); + } while (ttr.accept("colon")); } - function decodeM () { + function decodeM() { switch (ttr.symbol) { - case 'january': - return 1 - case 'february': - return 2 - case 'march': - return 3 - case 'april': - return 4 - case 'may': - return 5 - case 'june': - return 6 - case 'july': - return 7 - case 'august': - return 8 - case 'september': - return 9 - case 'october': - return 10 - case 'november': - return 11 - case 'december': - return 12 + case "january": + return 1; + case "february": + return 2; + case "march": + return 3; + case "april": + return 4; + case "may": + return 5; + case "june": + return 6; + case "july": + return 7; + case "august": + return 8; + case "september": + return 9; + case "october": + return 10; + case "november": + return 11; + case "december": + return 12; default: - return false + return false; } } - function decodeWKD () { + function decodeWKD() { switch (ttr.symbol) { - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - return ttr.symbol.substr(0, 2).toUpperCase() + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + return ttr.symbol.substr(0, 2).toUpperCase(); default: - return false + return false; } } - function decodeNTH () { + function decodeNTH() { switch (ttr.symbol) { - case 'last': - ttr.nextSymbol() - return -1 - case 'first': - ttr.nextSymbol() - return 1 - case 'second': - ttr.nextSymbol() - return ttr.accept('last') ? -2 : 2 - case 'third': - ttr.nextSymbol() - return ttr.accept('last') ? -3 : 3 - case 'nth': - const v = parseInt(ttr.value![1], 10) - if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) - - ttr.nextSymbol() - return ttr.accept('last') ? -v : v + case "last": + ttr.nextSymbol(); + return -1; + case "first": + ttr.nextSymbol(); + return 1; + case "second": + ttr.nextSymbol(); + return ttr.accept("last") ? -2 : 2; + case "third": + ttr.nextSymbol(); + return ttr.accept("last") ? -3 : 3; + case "nth": + const v = parseInt(ttr.value![1], 10); + if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); + + ttr.nextSymbol(); + return ttr.accept("last") ? -v : v; default: - return false + return false; } } - function MDAYs () { - ttr.accept('on') - ttr.accept('the') + function MDAYs() { + ttr.accept("on"); + ttr.accept("the"); - let nth = decodeNTH() - if (!nth) return + let nth = decodeNTH(); + if (!nth) return; - options.bymonthday = [nth] - ttr.nextSymbol() + options.bymonthday = [nth]; + ttr.nextSymbol(); - while (ttr.accept('comma')) { - nth = decodeNTH() + while (ttr.accept("comma")) { + nth = decodeNTH(); if (!nth) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.bymonthday.push(nth) - ttr.nextSymbol() + options.bymonthday.push(nth); + ttr.nextSymbol(); } } - function F () { - if (ttr.symbol === 'until') { - const date = Date.parse(ttr.text) + function F() { + if (ttr.symbol === "until") { + const date = Date.parse(ttr.text); - if (!date) throw new Error('Cannot parse until date:' + ttr.text) - options.until = new Date(date) - } else if (ttr.accept('for')) { - options.count = parseInt(ttr.value![0], 10) - ttr.expect('number') + if (!date) throw new Error("Cannot parse until date:" + ttr.text); + options.until = new Date(date); + } else if (ttr.accept("for")) { + options.count = parseInt(ttr.value![0], 10); + ttr.expect("number"); // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 942490ac..82b8c78e 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options, ByWeekday } from '../types' -import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent, padStart } from '../helpers' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options, ByWeekday } from "../types"; +import { Weekday } from "../weekday"; +import { isArray, isNumber, isPresent, padStart } from "../helpers"; // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from '../helpers' /** * Return true if a value is in an array */ -const contains = function (arr: string[], val: string) { - return arr.indexOf(val) !== -1 -} +const contains = function(arr: string[], val: string) { + return arr.indexOf(val) !== -1; +}; // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string +export type GetText = (id: string | number | Weekday) => string; -const defaultGetText: GetText = id => id.toString() +const defaultGetText: GetText = id => id.toString(); export type DateFormatter = ( year: number, month: string, day: number -) => string +) => string; const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}` +) => `${month} ${day}, ${year}`; /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][] - private rrule: RRule - private text: string[] - private gettext: GetText - private dateFormatter: DateFormatter - private language: Language - private options: Partial - private origOptions: Partial - private bymonthday: Options['bymonthday'] | null + static IMPLEMENTED: string[][]; + private rrule: RRule; + private text: string[]; + private gettext: GetText; + private dateFormatter: DateFormatter; + private language: Language; + private options: Partial; + private origOptions: Partial; + private bymonthday: Options["bymonthday"] | null; private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null + } | null; - constructor ( + constructor( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = [] - this.language = language || ENGLISH - this.gettext = gettext - this.dateFormatter = dateFormatter - this.rrule = rrule - this.options = rrule.options - this.origOptions = rrule.origOptions + this.text = []; + this.language = language || ENGLISH; + this.gettext = gettext; + this.dateFormatter = dateFormatter; + this.rrule = rrule; + this.options = rrule.options; + this.origOptions = rrule.origOptions; if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!) - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) + const bymonthday = ([] as number[]).concat(this.options.bymonthday!); + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); - bymonthday.sort((a, b) => a - b) - bynmonthday.sort((a, b) => b - a) + bymonthday.sort((a, b) => a - b); + bynmonthday.sort((a, b) => b - a); // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday) - if (!this.bymonthday.length) this.bymonthday = null + this.bymonthday = bymonthday.concat(bynmonthday); + if (!this.bymonthday.length) this.bymonthday = null; } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday - const days = String(byweekday) + : this.origOptions.byweekday; + const days = String(byweekday); this.byweekday = { - allWeeks: byweekday.filter(function (weekday: Weekday) { - return !weekday.n + allWeeks: byweekday.filter(function(weekday: Weekday) { + return !weekday.n; }), - someWeeks: byweekday.filter(function (weekday: Weekday) { - return Boolean(weekday.n) + someWeeks: byweekday.filter(function(weekday: Weekday) { + return Boolean(weekday.n); }), isWeekdays: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') === -1 && - days.indexOf('SU') === -1, + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") === -1 && + days.indexOf("SU") === -1, isEveryDay: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') !== -1 && - days.indexOf('SU') !== -1 - } - - const sortWeekDays = function (a: Weekday, b: Weekday) { - return a.weekday - b.weekday - } - - this.byweekday.allWeeks!.sort(sortWeekDays) - this.byweekday.someWeeks!.sort(sortWeekDays) - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") !== -1 && + days.indexOf("SU") !== -1 + }; + + const sortWeekDays = function(a: Weekday, b: Weekday) { + return a.weekday - b.weekday; + }; + + this.byweekday.allWeeks!.sort(sortWeekDays); + this.byweekday.someWeeks!.sort(sortWeekDays); + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; } else { - this.byweekday = null + this.byweekday = null; } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible (rrule: RRule) { - let canConvert = true + static isFullyConvertible(rrule: RRule) { + let canConvert = true; - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false - if (rrule.origOptions.until && rrule.origOptions.count) return false + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; + if (rrule.origOptions.until && rrule.origOptions.count) return false; for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false + if (contains(["dtstart", "wkst", "freq"], key)) return true; + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; } - return canConvert + return canConvert; } - isFullyConvertible () { - return ToText.isFullyConvertible(this.rrule) + isFullyConvertible() { + return ToText.isFullyConvertible(this.rrule); } /** @@ -159,364 +159,368 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString () { - const gettext = this.gettext + toString() { + const gettext = this.gettext; if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext('RRule error: Unable to fully convert this rrule to text') + return gettext("RRule error: Unable to fully convert this rrule to text"); } - this.text = [gettext('every')] + this.text = [gettext("every")]; // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]]() + this[RRule.FREQUENCIES[this.options.freq]](); if (this.options.until) { - this.add(gettext('until')) - const until = this.options.until + this.add(gettext("until")); + const until = this.options.until; this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ) + ); } else if (this.options.count) { - this.add(gettext('for')) + this.add(gettext("for")) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext('times') : gettext('time') - ) + this.plural(this.options.count) ? gettext("times") : gettext("time") + ); } - if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) + if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); - return this.text.join('') + return this.text.join(""); } - HOURLY () { - const gettext = this.gettext + HOURLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') - ) + this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") + ); } - MINUTELY () { - const gettext = this.gettext + MINUTELY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('minutes') - : gettext('minutes') - ) + ? gettext("minutes") + : gettext("minutes") + ); } - DAILY () { - const gettext = this.gettext + DAILY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - WEEKLY () { - const gettext = this.gettext + WEEKLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') - ) + this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") + ); } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } else { - if (this.options.interval === 1) this.add(gettext('week')) + if (this.options.interval === 1) this.add(gettext("week")); if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); + } else if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); } } } - MONTHLY () { - const gettext = this.gettext + MONTHLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext('months')) - if (this.plural(this.options.interval!)) this.add(gettext('in')) + this.add(this.options.interval!.toString()).add(gettext("months")); + if (this.plural(this.options.interval!)) this.add(gettext("in")); } else { // this.add(gettext('MONTH')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('months') - : gettext('month') - ) + ? gettext("months") + : gettext("month") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } } - YEARLY () { - const gettext = this.gettext + YEARLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) - this.add(gettext('years')) + this.add(this.options.interval!.toString()); + this.add(gettext("years")); } else { // this.add(gettext('YEAR')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('years') : gettext('year') - ) + this.plural(this.options.interval!) ? gettext("years") : gettext("year") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } if (this.options.byyearday) { - this.add(gettext('on the')) - .add(this.list(this.options.byyearday, this.nth, gettext('and'))) - .add(gettext('day')) + this.add(gettext("on the")) + .add(this.list(this.options.byyearday, this.nth, gettext("and"))) + .add(gettext("day")); } if (this.options.byweekno) { - this.add(gettext('in')) + this.add(gettext("in")) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext('weeks') - : gettext('week') + ? gettext("weeks") + : gettext("week") ) - .add(this.list(this.options.byweekno, undefined, gettext('and'))) + .add(this.list(this.options.byweekno, undefined, gettext("and"))); } } - private _bymonthday () { - const gettext = this.gettext + private _bymonthday() { + const gettext = this.gettext; if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext('on')) + this.add(gettext("on")) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) ) - .add(gettext('the')) - .add(this.list(this.bymonthday!, this.nth, gettext('or'))) + .add(gettext("the")) + .add(this.list(this.bymonthday!, this.nth, gettext("or"))); } else { - this.add(gettext('on the')).add( - this.list(this.bymonthday!, this.nth, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.bymonthday!, this.nth, gettext("and")) + ); } // this.add(gettext('DAY')) } - private _byweekday () { - const gettext = this.gettext + private _byweekday() { + const gettext = this.gettext; if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext('on')).add( + this.add(gettext("on")).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ) + ); } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext('and')) + if (this.byweekday!.allWeeks) this.add(gettext("and")); - this.add(gettext('on the')).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) + ); } } - private _byhour () { - const gettext = this.gettext + private _byhour() { + const gettext = this.gettext; - this.add(gettext('at')).add( - this.list(this.origOptions.byhour!, undefined, gettext('and')) - ) + this.add(gettext("at")).add( + this.list(this.origOptions.byhour!, undefined, gettext("and")) + ); } - private _byminute () { - const gettext = this.gettext + private _byminute() { + const gettext = this.gettext; - this.add(gettext('at')).add( + this.add(gettext("at")).add( `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ) + ); } - private _bymonth () { + private _bymonth() { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) - ) + this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) + ); } - nth (n: number | string) { - n = parseInt(n.toString(), 10) - let nth: string - let npos: number - const gettext = this.gettext + nth(n: number | string) { + n = parseInt(n.toString(), 10); + let nth: string; + let npos: number; + const gettext = this.gettext; - if (n === -1) return gettext('last') + if (n === -1) return gettext("last"); - npos = Math.abs(n) + npos = Math.abs(n); switch (npos) { case 1: case 21: case 31: - nth = npos + gettext('st') - break + nth = npos + gettext("st"); + break; case 2: case 22: - nth = npos + gettext('nd') - break + nth = npos + gettext("nd"); + break; case 3: case 23: - nth = npos + gettext('rd') - break + nth = npos + gettext("rd"); + break; default: - nth = npos + gettext('th') + nth = npos + gettext("th"); } - return n < 0 ? nth + ' ' + gettext('last') : nth + return n < 0 ? nth + " " + gettext("last") : nth; } - monthtext (m: number) { - return this.language.monthNames[m - 1] + monthtext(m: number) { + return this.language.monthNames[m - 1]; } - weekdaytext (wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() + weekdaytext(wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + this.language.dayNames[weekday] - ) + ); } - plural (n: number) { - return n % 100 !== 1 + plural(n: number) { + return n % 100 !== 1; } - add (s: string) { - this.text.push(' ') - this.text.push(s) - return this + add(s: string) { + this.text.push(" "); + this.text.push(s); + return this; } - list ( + list( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = ',' + delim: string = "," ) { if (!isArray(arr)) { - arr = [arr] + arr = [arr]; } - const delimJoin = function ( + const delimJoin = function( array: string[], delimiter: string, finalDelimiter: string ) { - let list = '' + let list = ""; for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += ' ' + finalDelimiter + ' ' + list += " " + finalDelimiter + " "; } else { - list += delimiter + ' ' + list += delimiter + " "; } } - list += array[i] + list += array[i]; } - return list - } + return list; + }; callback = callback || - function (o) { - return o.toString() - } - const self = this - const realCallback = function (arg: ByWeekday) { - return callback && callback.call(self, arg) - } + function(o) { + return o.toString(); + }; + const self = this; + const realCallback = function(arg: ByWeekday) { + return callback && callback.call(self, arg); + }; if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim) + return delimJoin(arr.map(realCallback), delim, finalDelim); } else { - return arr.map(realCallback).join(delim + ' ') + return arr.map(realCallback).join(delim + " "); } } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 2bd32bdd..6e3ebbd2 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -9,6 +9,7 @@ const texts = [ ["Every day at 10, 12 and 17", "RRULE:FREQ=DAILY;BYHOUR=10,12,17"], ["Every day at 10:30", "RRULE:FREQ=DAILY;BYHOUR=10;BYMINUTE=30"], ["Every week", "RRULE:FREQ=WEEKLY"], + ["Every week at 10:30", "RRULE:FREQ=WEEKLY;BYHOUR=10;BYMINUTE=30"], ["Every hour", "RRULE:FREQ=HOURLY"], ["Every 4 hours", "RRULE:INTERVAL=4;FREQ=HOURLY"], ["Every week on Tuesday", "RRULE:FREQ=WEEKLY;BYDAY=TU"], From be2e24626f6391ff543416bccd5337cba7fb7ba4 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:49:51 +0100 Subject: [PATCH 06/23] Add time in weekly case --- src/nlp/index.ts | 60 ++--- src/nlp/parsetext.ts | 572 +++++++++++++++++++++---------------------- src/nlp/totext.ts | 472 +++++++++++++++++------------------ 3 files changed, 552 insertions(+), 552 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index cf8642a1..9e4b0431 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,7 +1,7 @@ -import ToText, { DateFormatter, GetText } from "./totext"; -import parseText from "./parsetext"; -import RRule from "../index"; -import ENGLISH, { Language } from "./i18n"; +import ToText, { DateFormatter, GetText } from './totext' +import parseText from './parsetext' +import RRule from '../index' +import ENGLISH, { Language } from './i18n' /*! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -94,47 +94,47 @@ import ENGLISH, { Language } from "./i18n"; * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function(text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined); -}; +const fromText = function (text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined) +} const common = [ - "count", - "until", - "interval", - "byweekday", - "bymonthday", - "bymonth" -]; + 'count', + 'until', + 'interval', + 'byweekday', + 'bymonthday', + 'bymonth' +] -ToText.IMPLEMENTED = []; -ToText.IMPLEMENTED[RRule.HOURLY] = common; -ToText.IMPLEMENTED[RRule.MINUTELY] = common; -ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.WEEKLY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.MONTHLY] = common; -ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); +ToText.IMPLEMENTED = [] +ToText.IMPLEMENTED[RRule.HOURLY] = common +ToText.IMPLEMENTED[RRule.MINUTELY] = common +ToText.IMPLEMENTED[RRule.DAILY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.WEEKLY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.MONTHLY] = common +ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) // ============================================================================= // Export // ============================================================================= -const toText = function( +const toText = function ( rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter ) { - return new ToText(rrule, gettext, language, dateFormatter).toString(); -}; + return new ToText(rrule, gettext, language, dateFormatter).toString() +} -const { isFullyConvertible } = ToText; +const { isFullyConvertible } = ToText export interface Nlp { - fromText: typeof fromText; - parseText: typeof parseText; - isFullyConvertible: typeof isFullyConvertible; - toText: typeof toText; + fromText: typeof fromText + parseText: typeof parseText + isFullyConvertible: typeof isFullyConvertible + toText: typeof toText } -export { fromText, parseText, isFullyConvertible, toText }; +export { fromText, parseText, isFullyConvertible, toText } diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 459eecec..b54407ce 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,289 +1,289 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options } from "../types"; -import { WeekdayStr } from "../weekday"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options } from '../types' +import { WeekdayStr } from '../weekday' // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp }; - public text: string; - public symbol: string | null; - public value: RegExpExecArray | null; - private done = true; - - constructor(rules: { [k: string]: RegExp }) { - this.rules = rules; + private readonly rules: { [k: string]: RegExp } + public text: string + public symbol: string | null + public value: RegExpExecArray | null + private done = true + + constructor (rules: { [k: string]: RegExp }) { + this.rules = rules } - start(text: string) { - this.text = text; - this.done = false; - return this.nextSymbol(); + start (text: string) { + this.text = text + this.done = false + return this.nextSymbol() } - isDone() { - return this.done && this.symbol === null; + isDone () { + return this.done && this.symbol === null } - nextSymbol() { - let best: RegExpExecArray | null; - let bestSymbol: string; - const p = this; + nextSymbol () { + let best: RegExpExecArray | null + let bestSymbol: string + const p = this - this.symbol = null; - this.value = null; + this.symbol = null + this.value = null do { - if (this.done) return false; + if (this.done) return false - let rule: RegExp; - best = null; + let rule: RegExp + best = null for (let name in this.rules) { - rule = this.rules[name]; - const match = rule.exec(p.text); + rule = this.rules[name] + const match = rule.exec(p.text) if (match) { if (best === null || match[0].length > best[0].length) { - best = match; - bestSymbol = name; + best = match + bestSymbol = name } } } if (best != null) { - this.text = this.text.substr(best[0].length); + this.text = this.text.substr(best[0].length) - if (this.text === "") this.done = true; + if (this.text === '') this.done = true } if (best == null) { - this.done = true; - this.symbol = null; - this.value = null; - return; + this.done = true + this.symbol = null + this.value = null + return } // @ts-ignore - } while (bestSymbol === "SKIP"); + } while (bestSymbol === 'SKIP') // @ts-ignore - this.symbol = bestSymbol; - this.value = best; - return true; + this.symbol = bestSymbol + this.value = best + return true } - accept(name: string) { + accept (name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value; - this.nextSymbol(); - return v; + const v = this.value + this.nextSymbol() + return v } - this.nextSymbol(); - return true; + this.nextSymbol() + return true } - return false; + return false } - acceptNumber() { - return this.accept("number") as RegExpExecArray; + acceptNumber () { + return this.accept('number') as RegExpExecArray } - expect(name: string) { - if (this.accept(name)) return true; + expect (name: string) { + if (this.accept(name)) return true - throw new Error("expected " + name + " but found " + this.symbol); + throw new Error('expected ' + name + ' but found ' + this.symbol) } } -export default function parseText(text: string, language: Language = ENGLISH) { - const options: Partial = {}; - const ttr = new Parser(language.tokens); +export default function parseText (text: string, language: Language = ENGLISH) { + const options: Partial = {} + const ttr = new Parser(language.tokens) - if (!ttr.start(text)) return null; + if (!ttr.start(text)) return null - S(); - return options; + S() + return options - function S() { + function S () { // every [n] - ttr.expect("every"); - let n = ttr.acceptNumber(); - if (n) options.interval = parseInt(n[0], 10); - if (ttr.isDone()) throw new Error("Unexpected end"); + ttr.expect('every') + let n = ttr.acceptNumber() + if (n) options.interval = parseInt(n[0], 10) + if (ttr.isDone()) throw new Error('Unexpected end') switch (ttr.symbol) { - case "day(s)": - options.freq = RRule.DAILY; + case 'day(s)': + options.freq = RRule.DAILY if (ttr.nextSymbol()) { - AT(); - C(); - F(); + AT() + C() + F() } - break; + break // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case "weekday(s)": - options.freq = RRule.WEEKLY; - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; - ttr.nextSymbol(); - F(); - break; - - case "week(s)": - options.freq = RRule.WEEKLY; + case 'weekday(s)': + options.freq = RRule.WEEKLY + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] + ttr.nextSymbol() + F() + break + + case 'week(s)': + options.freq = RRule.WEEKLY if (ttr.nextSymbol()) { - ON(); - AT(); - C(); - F(); + ON() + AT() + C() + F() } - break; + break - case "hour(s)": - options.freq = RRule.HOURLY; + case 'hour(s)': + options.freq = RRule.HOURLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "minute(s)": - options.freq = RRule.MINUTELY; + case 'minute(s)': + options.freq = RRule.MINUTELY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "month(s)": - options.freq = RRule.MONTHLY; + case 'month(s)': + options.freq = RRule.MONTHLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "year(s)": - options.freq = RRule.YEARLY; + case 'year(s)': + options.freq = RRule.YEARLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; - - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - options.freq = RRule.WEEKLY; + break + + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + options.freq = RRule.WEEKLY const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr; - options.byweekday = [RRule[key]]; + .toUpperCase() as WeekdayStr + options.byweekday = [RRule[key]] - if (!ttr.nextSymbol()) return; + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let wkd = decodeWKD() as keyof typeof RRule; + let wkd = decodeWKD() as keyof typeof RRule if (!wkd) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected weekday" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected weekday' + ) } // @ts-ignore - options.byweekday.push(RRule[wkd]); - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + ttr.nextSymbol() } - MDAYs(); - F(); - break; - - case "january": - case "february": - case "march": - case "april": - case "may": - case "june": - case "july": - case "august": - case "september": - case "october": - case "november": - case "december": - options.freq = RRule.YEARLY; - options.bymonth = [decodeM() as number]; - - if (!ttr.nextSymbol()) return; + MDAYs() + F() + break + + case 'january': + case 'february': + case 'march': + case 'april': + case 'may': + case 'june': + case 'july': + case 'august': + case 'september': + case 'october': + case 'november': + case 'december': + options.freq = RRule.YEARLY + options.bymonth = [decodeM() as number] + + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let m = decodeM(); + let m = decodeM() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected month" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected month' + ) } - options.bymonth.push(m); - ttr.nextSymbol(); + options.bymonth.push(m) + ttr.nextSymbol() } - ON(); - F(); - break; + ON() + F() + break default: - throw new Error("Unknown symbol"); + throw new Error('Unknown symbol') } } - function ON() { - const on = ttr.accept("on"); - const the = ttr.accept("the"); - if (!(on || the)) return; + function ON () { + const on = ttr.accept('on') + const the = ttr.accept('the') + if (!(on || the)) return do { - let nth = decodeNTH(); - let wkd = decodeWKD(); - let m = decodeM(); + let nth = decodeNTH() + let wkd = decodeWKD() + let m = decodeM() // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)); + options.byweekday.push(RRule[wkd].nth(nth)) } else { - if (!options.bymonthday) options.bymonthday = []; + if (!options.bymonthday) options.bymonthday = [] // @ts-ignore - options.bymonthday.push(nth); - ttr.accept("day(s)"); + options.bymonthday.push(nth) + ttr.accept('day(s)') } // } else if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd]); - } else if (ttr.symbol === "weekday(s)") { - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + } else if (ttr.symbol === 'weekday(s)') { + ttr.nextSymbol() if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -291,179 +291,179 @@ export default function parseText(text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ]; + ] } - } else if (ttr.symbol === "week(s)") { - ttr.nextSymbol(); - let n = ttr.acceptNumber(); + } else if (ttr.symbol === 'week(s)') { + ttr.nextSymbol() + let n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected week number" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected week number' + ) } - options.byweekno = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byweekno = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.byweekno.push(parseInt(n[0], 10)); + options.byweekno.push(parseInt(n[0], 10)) } } else if (m) { - ttr.nextSymbol(); - if (!options.bymonth) options.bymonth = []; + ttr.nextSymbol() + if (!options.bymonth) options.bymonth = [] // @ts-ignore - options.bymonth.push(m); + options.bymonth.push(m) } else { - return; + return } - } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); + } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) } - function AT() { - const at = ttr.accept("at"); - if (!at) return; + function AT () { + const at = ttr.accept('at') + if (!at) return do { - let n = ttr.acceptNumber(); + let n = ttr.acceptNumber() if (!n) { - throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); + throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') } - options.byhour = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byhour = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected hour" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected hour' + ) } - options.byhour.push(parseInt(n[0], 10)); + options.byhour.push(parseInt(n[0], 10)) } - } while (ttr.accept("comma") || ttr.accept("at")); + } while (ttr.accept('comma') || ttr.accept('at')) } - function C() { - const colon = ttr.accept("colon"); - if (!colon) return; + function C () { + const colon = ttr.accept('colon') + if (!colon) return do { - let m = ttr.acceptNumber(); + let m = ttr.acceptNumber() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected minutes" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected minutes' + ) } - options.byminute = parseInt(m[0], 10); - } while (ttr.accept("colon")); + options.byminute = parseInt(m[0], 10) + } while (ttr.accept('colon')) } - function decodeM() { + function decodeM () { switch (ttr.symbol) { - case "january": - return 1; - case "february": - return 2; - case "march": - return 3; - case "april": - return 4; - case "may": - return 5; - case "june": - return 6; - case "july": - return 7; - case "august": - return 8; - case "september": - return 9; - case "october": - return 10; - case "november": - return 11; - case "december": - return 12; + case 'january': + return 1 + case 'february': + return 2 + case 'march': + return 3 + case 'april': + return 4 + case 'may': + return 5 + case 'june': + return 6 + case 'july': + return 7 + case 'august': + return 8 + case 'september': + return 9 + case 'october': + return 10 + case 'november': + return 11 + case 'december': + return 12 default: - return false; + return false } } - function decodeWKD() { + function decodeWKD () { switch (ttr.symbol) { - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - return ttr.symbol.substr(0, 2).toUpperCase(); + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + return ttr.symbol.substr(0, 2).toUpperCase() default: - return false; + return false } } - function decodeNTH() { + function decodeNTH () { switch (ttr.symbol) { - case "last": - ttr.nextSymbol(); - return -1; - case "first": - ttr.nextSymbol(); - return 1; - case "second": - ttr.nextSymbol(); - return ttr.accept("last") ? -2 : 2; - case "third": - ttr.nextSymbol(); - return ttr.accept("last") ? -3 : 3; - case "nth": - const v = parseInt(ttr.value![1], 10); - if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); - - ttr.nextSymbol(); - return ttr.accept("last") ? -v : v; + case 'last': + ttr.nextSymbol() + return -1 + case 'first': + ttr.nextSymbol() + return 1 + case 'second': + ttr.nextSymbol() + return ttr.accept('last') ? -2 : 2 + case 'third': + ttr.nextSymbol() + return ttr.accept('last') ? -3 : 3 + case 'nth': + const v = parseInt(ttr.value![1], 10) + if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) + + ttr.nextSymbol() + return ttr.accept('last') ? -v : v default: - return false; + return false } } - function MDAYs() { - ttr.accept("on"); - ttr.accept("the"); + function MDAYs () { + ttr.accept('on') + ttr.accept('the') - let nth = decodeNTH(); - if (!nth) return; + let nth = decodeNTH() + if (!nth) return - options.bymonthday = [nth]; - ttr.nextSymbol(); + options.bymonthday = [nth] + ttr.nextSymbol() - while (ttr.accept("comma")) { - nth = decodeNTH(); + while (ttr.accept('comma')) { + nth = decodeNTH() if (!nth) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.bymonthday.push(nth); - ttr.nextSymbol(); + options.bymonthday.push(nth) + ttr.nextSymbol() } } - function F() { - if (ttr.symbol === "until") { - const date = Date.parse(ttr.text); + function F () { + if (ttr.symbol === 'until') { + const date = Date.parse(ttr.text) - if (!date) throw new Error("Cannot parse until date:" + ttr.text); - options.until = new Date(date); - } else if (ttr.accept("for")) { - options.count = parseInt(ttr.value![0], 10); - ttr.expect("number"); + if (!date) throw new Error('Cannot parse until date:' + ttr.text) + options.until = new Date(date) + } else if (ttr.accept('for')) { + options.count = parseInt(ttr.value![0], 10) + ttr.expect('number') // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 82b8c78e..74de08fd 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options, ByWeekday } from "../types"; -import { Weekday } from "../weekday"; -import { isArray, isNumber, isPresent, padStart } from "../helpers"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options, ByWeekday } from '../types' +import { Weekday } from '../weekday' +import { isArray, isNumber, isPresent, padStart } from '../helpers' // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from "../helpers"; /** * Return true if a value is in an array */ -const contains = function(arr: string[], val: string) { - return arr.indexOf(val) !== -1; -}; +const contains = function (arr: string[], val: string) { + return arr.indexOf(val) !== -1 +} // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string; +export type GetText = (id: string | number | Weekday) => string -const defaultGetText: GetText = id => id.toString(); +const defaultGetText: GetText = id => id.toString() export type DateFormatter = ( year: number, month: string, day: number -) => string; +) => string const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}`; +) => `${month} ${day}, ${year}` /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][]; - private rrule: RRule; - private text: string[]; - private gettext: GetText; - private dateFormatter: DateFormatter; - private language: Language; - private options: Partial; - private origOptions: Partial; - private bymonthday: Options["bymonthday"] | null; + static IMPLEMENTED: string[][] + private rrule: RRule + private text: string[] + private gettext: GetText + private dateFormatter: DateFormatter + private language: Language + private options: Partial + private origOptions: Partial + private bymonthday: Options['bymonthday'] | null private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null; + } | null - constructor( + constructor ( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = []; - this.language = language || ENGLISH; - this.gettext = gettext; - this.dateFormatter = dateFormatter; - this.rrule = rrule; - this.options = rrule.options; - this.origOptions = rrule.origOptions; + this.text = [] + this.language = language || ENGLISH + this.gettext = gettext + this.dateFormatter = dateFormatter + this.rrule = rrule + this.options = rrule.options + this.origOptions = rrule.origOptions if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!); - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); + const bymonthday = ([] as number[]).concat(this.options.bymonthday!) + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) - bymonthday.sort((a, b) => a - b); - bynmonthday.sort((a, b) => b - a); + bymonthday.sort((a, b) => a - b) + bynmonthday.sort((a, b) => b - a) // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday); - if (!this.bymonthday.length) this.bymonthday = null; + this.bymonthday = bymonthday.concat(bynmonthday) + if (!this.bymonthday.length) this.bymonthday = null } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday; - const days = String(byweekday); + : this.origOptions.byweekday + const days = String(byweekday) this.byweekday = { - allWeeks: byweekday.filter(function(weekday: Weekday) { - return !weekday.n; + allWeeks: byweekday.filter(function (weekday: Weekday) { + return !weekday.n }), - someWeeks: byweekday.filter(function(weekday: Weekday) { - return Boolean(weekday.n); + someWeeks: byweekday.filter(function (weekday: Weekday) { + return Boolean(weekday.n) }), isWeekdays: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") === -1 && - days.indexOf("SU") === -1, + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') === -1 && + days.indexOf('SU') === -1, isEveryDay: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") !== -1 && - days.indexOf("SU") !== -1 - }; - - const sortWeekDays = function(a: Weekday, b: Weekday) { - return a.weekday - b.weekday; - }; - - this.byweekday.allWeeks!.sort(sortWeekDays); - this.byweekday.someWeeks!.sort(sortWeekDays); - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') !== -1 && + days.indexOf('SU') !== -1 + } + + const sortWeekDays = function (a: Weekday, b: Weekday) { + return a.weekday - b.weekday + } + + this.byweekday.allWeeks!.sort(sortWeekDays) + this.byweekday.someWeeks!.sort(sortWeekDays) + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null } else { - this.byweekday = null; + this.byweekday = null } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible(rrule: RRule) { - let canConvert = true; + static isFullyConvertible (rrule: RRule) { + let canConvert = true - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; - if (rrule.origOptions.until && rrule.origOptions.count) return false; + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false + if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(["dtstart", "wkst", "freq"], key)) return true; - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; + if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } - return canConvert; + return canConvert } - isFullyConvertible() { - return ToText.isFullyConvertible(this.rrule); + isFullyConvertible () { + return ToText.isFullyConvertible(this.rrule) } /** @@ -159,368 +159,368 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString() { - const gettext = this.gettext; + toString () { + const gettext = this.gettext if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext("RRule error: Unable to fully convert this rrule to text"); + return gettext('RRule error: Unable to fully convert this rrule to text') } - this.text = [gettext("every")]; + this.text = [gettext('every')] // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]](); + this[RRule.FREQUENCIES[this.options.freq]]() if (this.options.until) { - this.add(gettext("until")); - const until = this.options.until; + this.add(gettext('until')) + const until = this.options.until this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ); + ) } else if (this.options.count) { - this.add(gettext("for")) + this.add(gettext('for')) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext("times") : gettext("time") - ); + this.plural(this.options.count) ? gettext('times') : gettext('time') + ) } - if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); + if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) - return this.text.join(""); + return this.text.join('') } - HOURLY() { - const gettext = this.gettext; + HOURLY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") - ); + this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') + ) } - MINUTELY() { - const gettext = this.gettext; + MINUTELY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("minutes") - : gettext("minutes") - ); + ? gettext('minutes') + : gettext('minutes') + ) } - DAILY() { - const gettext = this.gettext; + DAILY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - WEEKLY() { - const gettext = this.gettext; + WEEKLY () { + const gettext = this.gettext if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") - ); + this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') + ) } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } else { - if (this.options.interval === 1) this.add(gettext("week")); + if (this.options.interval === 1) this.add(gettext('week')) if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } } - MONTHLY() { - const gettext = this.gettext; + MONTHLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext("months")); - if (this.plural(this.options.interval!)) this.add(gettext("in")); + this.add(this.options.interval!.toString()).add(gettext('months')) + if (this.plural(this.options.interval!)) this.add(gettext('in')) } else { // this.add(gettext('MONTH')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("months") - : gettext("month") - ); + ? gettext('months') + : gettext('month') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } } - YEARLY() { - const gettext = this.gettext; + YEARLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); - this.add(gettext("years")); + this.add(this.options.interval!.toString()) + this.add(gettext('years')) } else { // this.add(gettext('YEAR')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("years") : gettext("year") - ); + this.plural(this.options.interval!) ? gettext('years') : gettext('year') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } if (this.options.byyearday) { - this.add(gettext("on the")) - .add(this.list(this.options.byyearday, this.nth, gettext("and"))) - .add(gettext("day")); + this.add(gettext('on the')) + .add(this.list(this.options.byyearday, this.nth, gettext('and'))) + .add(gettext('day')) } if (this.options.byweekno) { - this.add(gettext("in")) + this.add(gettext('in')) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext("weeks") - : gettext("week") + ? gettext('weeks') + : gettext('week') ) - .add(this.list(this.options.byweekno, undefined, gettext("and"))); + .add(this.list(this.options.byweekno, undefined, gettext('and'))) } } - private _bymonthday() { - const gettext = this.gettext; + private _bymonthday () { + const gettext = this.gettext if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext("on")) + this.add(gettext('on')) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) ) - .add(gettext("the")) - .add(this.list(this.bymonthday!, this.nth, gettext("or"))); + .add(gettext('the')) + .add(this.list(this.bymonthday!, this.nth, gettext('or'))) } else { - this.add(gettext("on the")).add( - this.list(this.bymonthday!, this.nth, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.bymonthday!, this.nth, gettext('and')) + ) } // this.add(gettext('DAY')) } - private _byweekday() { - const gettext = this.gettext; + private _byweekday () { + const gettext = this.gettext if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext("on")).add( + this.add(gettext('on')).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ); + ) } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext("and")); + if (this.byweekday!.allWeeks) this.add(gettext('and')) - this.add(gettext("on the")).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) + ) } } - private _byhour() { - const gettext = this.gettext; + private _byhour () { + const gettext = this.gettext - this.add(gettext("at")).add( - this.list(this.origOptions.byhour!, undefined, gettext("and")) - ); + this.add(gettext('at')).add( + this.list(this.origOptions.byhour!, undefined, gettext('and')) + ) } - private _byminute() { - const gettext = this.gettext; + private _byminute () { + const gettext = this.gettext - this.add(gettext("at")).add( + this.add(gettext('at')).add( `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ); + ) } - private _bymonth() { + private _bymonth () { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) - ); + this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) + ) } - nth(n: number | string) { - n = parseInt(n.toString(), 10); - let nth: string; - let npos: number; - const gettext = this.gettext; + nth (n: number | string) { + n = parseInt(n.toString(), 10) + let nth: string + let npos: number + const gettext = this.gettext - if (n === -1) return gettext("last"); + if (n === -1) return gettext('last') - npos = Math.abs(n); + npos = Math.abs(n) switch (npos) { case 1: case 21: case 31: - nth = npos + gettext("st"); - break; + nth = npos + gettext('st') + break case 2: case 22: - nth = npos + gettext("nd"); - break; + nth = npos + gettext('nd') + break case 3: case 23: - nth = npos + gettext("rd"); - break; + nth = npos + gettext('rd') + break default: - nth = npos + gettext("th"); + nth = npos + gettext('th') } - return n < 0 ? nth + " " + gettext("last") : nth; + return n < 0 ? nth + ' ' + gettext('last') : nth } - monthtext(m: number) { - return this.language.monthNames[m - 1]; + monthtext (m: number) { + return this.language.monthNames[m - 1] } - weekdaytext(wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); + weekdaytext (wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ); + ) } - plural(n: number) { - return n % 100 !== 1; + plural (n: number) { + return n % 100 !== 1 } - add(s: string) { - this.text.push(" "); - this.text.push(s); - return this; + add (s: string) { + this.text.push(' ') + this.text.push(s) + return this } - list( + list ( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = "," + delim: string = ',' ) { if (!isArray(arr)) { - arr = [arr]; + arr = [arr] } - const delimJoin = function( + const delimJoin = function ( array: string[], delimiter: string, finalDelimiter: string ) { - let list = ""; + let list = '' for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += " " + finalDelimiter + " "; + list += ' ' + finalDelimiter + ' ' } else { - list += delimiter + " "; + list += delimiter + ' ' } } - list += array[i]; + list += array[i] } - return list; - }; + return list + } callback = callback || - function(o) { - return o.toString(); - }; - const self = this; - const realCallback = function(arg: ByWeekday) { - return callback && callback.call(self, arg); - }; + function (o) { + return o.toString() + } + const self = this + const realCallback = function (arg: ByWeekday) { + return callback && callback.call(self, arg) + } if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim); + return delimJoin(arr.map(realCallback), delim, finalDelim) } else { - return arr.map(realCallback).join(delim + " "); + return arr.map(realCallback).join(delim + ' ') } } } From cb7dea89b395852b5e07801f174459752113471d Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:54:14 +0100 Subject: [PATCH 07/23] add time in monthly case --- src/nlp/index.ts | 60 ++--- src/nlp/parsetext.ts | 574 ++++++++++++++++++++++--------------------- src/nlp/totext.ts | 476 +++++++++++++++++------------------ test/nlp.test.ts | 1 + 4 files changed, 559 insertions(+), 552 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index 9e4b0431..893678fc 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,7 +1,7 @@ -import ToText, { DateFormatter, GetText } from './totext' -import parseText from './parsetext' -import RRule from '../index' -import ENGLISH, { Language } from './i18n' +import ToText, { DateFormatter, GetText } from "./totext"; +import parseText from "./parsetext"; +import RRule from "../index"; +import ENGLISH, { Language } from "./i18n"; /*! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -94,47 +94,47 @@ import ENGLISH, { Language } from './i18n' * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function (text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined) -} +const fromText = function(text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined); +}; const common = [ - 'count', - 'until', - 'interval', - 'byweekday', - 'bymonthday', - 'bymonth' -] + "count", + "until", + "interval", + "byweekday", + "bymonthday", + "bymonth" +]; -ToText.IMPLEMENTED = [] -ToText.IMPLEMENTED[RRule.HOURLY] = common -ToText.IMPLEMENTED[RRule.MINUTELY] = common -ToText.IMPLEMENTED[RRule.DAILY] = ['byhour', 'byminute'].concat(common) -ToText.IMPLEMENTED[RRule.WEEKLY] = ['byhour', 'byminute'].concat(common) -ToText.IMPLEMENTED[RRule.MONTHLY] = common -ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) +ToText.IMPLEMENTED = []; +ToText.IMPLEMENTED[RRule.HOURLY] = common; +ToText.IMPLEMENTED[RRule.MINUTELY] = common; +ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.WEEKLY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.MONTHLY] = ["byhour", "byminute"].concat(common); +ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); // ============================================================================= // Export // ============================================================================= -const toText = function ( +const toText = function( rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter ) { - return new ToText(rrule, gettext, language, dateFormatter).toString() -} + return new ToText(rrule, gettext, language, dateFormatter).toString(); +}; -const { isFullyConvertible } = ToText +const { isFullyConvertible } = ToText; export interface Nlp { - fromText: typeof fromText - parseText: typeof parseText - isFullyConvertible: typeof isFullyConvertible - toText: typeof toText + fromText: typeof fromText; + parseText: typeof parseText; + isFullyConvertible: typeof isFullyConvertible; + toText: typeof toText; } -export { fromText, parseText, isFullyConvertible, toText } +export { fromText, parseText, isFullyConvertible, toText }; diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index b54407ce..777ff069 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,289 +1,291 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options } from '../types' -import { WeekdayStr } from '../weekday' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options } from "../types"; +import { WeekdayStr } from "../weekday"; // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp } - public text: string - public symbol: string | null - public value: RegExpExecArray | null - private done = true - - constructor (rules: { [k: string]: RegExp }) { - this.rules = rules + private readonly rules: { [k: string]: RegExp }; + public text: string; + public symbol: string | null; + public value: RegExpExecArray | null; + private done = true; + + constructor(rules: { [k: string]: RegExp }) { + this.rules = rules; } - start (text: string) { - this.text = text - this.done = false - return this.nextSymbol() + start(text: string) { + this.text = text; + this.done = false; + return this.nextSymbol(); } - isDone () { - return this.done && this.symbol === null + isDone() { + return this.done && this.symbol === null; } - nextSymbol () { - let best: RegExpExecArray | null - let bestSymbol: string - const p = this + nextSymbol() { + let best: RegExpExecArray | null; + let bestSymbol: string; + const p = this; - this.symbol = null - this.value = null + this.symbol = null; + this.value = null; do { - if (this.done) return false + if (this.done) return false; - let rule: RegExp - best = null + let rule: RegExp; + best = null; for (let name in this.rules) { - rule = this.rules[name] - const match = rule.exec(p.text) + rule = this.rules[name]; + const match = rule.exec(p.text); if (match) { if (best === null || match[0].length > best[0].length) { - best = match - bestSymbol = name + best = match; + bestSymbol = name; } } } if (best != null) { - this.text = this.text.substr(best[0].length) + this.text = this.text.substr(best[0].length); - if (this.text === '') this.done = true + if (this.text === "") this.done = true; } if (best == null) { - this.done = true - this.symbol = null - this.value = null - return + this.done = true; + this.symbol = null; + this.value = null; + return; } // @ts-ignore - } while (bestSymbol === 'SKIP') + } while (bestSymbol === "SKIP"); // @ts-ignore - this.symbol = bestSymbol - this.value = best - return true + this.symbol = bestSymbol; + this.value = best; + return true; } - accept (name: string) { + accept(name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value - this.nextSymbol() - return v + const v = this.value; + this.nextSymbol(); + return v; } - this.nextSymbol() - return true + this.nextSymbol(); + return true; } - return false + return false; } - acceptNumber () { - return this.accept('number') as RegExpExecArray + acceptNumber() { + return this.accept("number") as RegExpExecArray; } - expect (name: string) { - if (this.accept(name)) return true + expect(name: string) { + if (this.accept(name)) return true; - throw new Error('expected ' + name + ' but found ' + this.symbol) + throw new Error("expected " + name + " but found " + this.symbol); } } -export default function parseText (text: string, language: Language = ENGLISH) { - const options: Partial = {} - const ttr = new Parser(language.tokens) +export default function parseText(text: string, language: Language = ENGLISH) { + const options: Partial = {}; + const ttr = new Parser(language.tokens); - if (!ttr.start(text)) return null + if (!ttr.start(text)) return null; - S() - return options + S(); + return options; - function S () { + function S() { // every [n] - ttr.expect('every') - let n = ttr.acceptNumber() - if (n) options.interval = parseInt(n[0], 10) - if (ttr.isDone()) throw new Error('Unexpected end') + ttr.expect("every"); + let n = ttr.acceptNumber(); + if (n) options.interval = parseInt(n[0], 10); + if (ttr.isDone()) throw new Error("Unexpected end"); switch (ttr.symbol) { - case 'day(s)': - options.freq = RRule.DAILY + case "day(s)": + options.freq = RRule.DAILY; if (ttr.nextSymbol()) { - AT() - C() - F() + AT(); + C(); + F(); } - break + break; // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case 'weekday(s)': - options.freq = RRule.WEEKLY - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] - ttr.nextSymbol() - F() - break - - case 'week(s)': - options.freq = RRule.WEEKLY + case "weekday(s)": + options.freq = RRule.WEEKLY; + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; + ttr.nextSymbol(); + F(); + break; + + case "week(s)": + options.freq = RRule.WEEKLY; if (ttr.nextSymbol()) { - ON() - AT() - C() - F() + ON(); + AT(); + C(); + F(); } - break + break; - case 'hour(s)': - options.freq = RRule.HOURLY + case "hour(s)": + options.freq = RRule.HOURLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'minute(s)': - options.freq = RRule.MINUTELY + case "minute(s)": + options.freq = RRule.MINUTELY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'month(s)': - options.freq = RRule.MONTHLY + case "month(s)": + options.freq = RRule.MONTHLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + AT(); + C(); + F(); } - break + break; - case 'year(s)': - options.freq = RRule.YEARLY + case "year(s)": + options.freq = RRule.YEARLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break - - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - options.freq = RRule.WEEKLY + break; + + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + options.freq = RRule.WEEKLY; const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr - options.byweekday = [RRule[key]] + .toUpperCase() as WeekdayStr; + options.byweekday = [RRule[key]]; - if (!ttr.nextSymbol()) return + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let wkd = decodeWKD() as keyof typeof RRule + let wkd = decodeWKD() as keyof typeof RRule; if (!wkd) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected weekday' - ) + "Unexpected symbol " + ttr.symbol + ", expected weekday" + ); } // @ts-ignore - options.byweekday.push(RRule[wkd]) - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + ttr.nextSymbol(); } - MDAYs() - F() - break - - case 'january': - case 'february': - case 'march': - case 'april': - case 'may': - case 'june': - case 'july': - case 'august': - case 'september': - case 'october': - case 'november': - case 'december': - options.freq = RRule.YEARLY - options.bymonth = [decodeM() as number] - - if (!ttr.nextSymbol()) return + MDAYs(); + F(); + break; + + case "january": + case "february": + case "march": + case "april": + case "may": + case "june": + case "july": + case "august": + case "september": + case "october": + case "november": + case "december": + options.freq = RRule.YEARLY; + options.bymonth = [decodeM() as number]; + + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let m = decodeM() + let m = decodeM(); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected month' - ) + "Unexpected symbol " + ttr.symbol + ", expected month" + ); } - options.bymonth.push(m) - ttr.nextSymbol() + options.bymonth.push(m); + ttr.nextSymbol(); } - ON() - F() - break + ON(); + F(); + break; default: - throw new Error('Unknown symbol') + throw new Error("Unknown symbol"); } } - function ON () { - const on = ttr.accept('on') - const the = ttr.accept('the') - if (!(on || the)) return + function ON() { + const on = ttr.accept("on"); + const the = ttr.accept("the"); + if (!(on || the)) return; do { - let nth = decodeNTH() - let wkd = decodeWKD() - let m = decodeM() + let nth = decodeNTH(); + let wkd = decodeWKD(); + let m = decodeM(); // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)) + options.byweekday.push(RRule[wkd].nth(nth)); } else { - if (!options.bymonthday) options.bymonthday = [] + if (!options.bymonthday) options.bymonthday = []; // @ts-ignore - options.bymonthday.push(nth) - ttr.accept('day(s)') + options.bymonthday.push(nth); + ttr.accept("day(s)"); } // } else if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd]) - } else if (ttr.symbol === 'weekday(s)') { - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + } else if (ttr.symbol === "weekday(s)") { + ttr.nextSymbol(); if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -291,179 +293,179 @@ export default function parseText (text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ] + ]; } - } else if (ttr.symbol === 'week(s)') { - ttr.nextSymbol() - let n = ttr.acceptNumber() + } else if (ttr.symbol === "week(s)") { + ttr.nextSymbol(); + let n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected week number' - ) + "Unexpected symbol " + ttr.symbol + ", expected week number" + ); } - options.byweekno = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byweekno = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.byweekno.push(parseInt(n[0], 10)) + options.byweekno.push(parseInt(n[0], 10)); } } else if (m) { - ttr.nextSymbol() - if (!options.bymonth) options.bymonth = [] + ttr.nextSymbol(); + if (!options.bymonth) options.bymonth = []; // @ts-ignore - options.bymonth.push(m) + options.bymonth.push(m); } else { - return + return; } - } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) + } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); } - function AT () { - const at = ttr.accept('at') - if (!at) return + function AT() { + const at = ttr.accept("at"); + if (!at) return; do { - let n = ttr.acceptNumber() + let n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') + throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); } - options.byhour = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byhour = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected hour' - ) + "Unexpected symbol " + ttr.symbol + "; expected hour" + ); } - options.byhour.push(parseInt(n[0], 10)) + options.byhour.push(parseInt(n[0], 10)); } - } while (ttr.accept('comma') || ttr.accept('at')) + } while (ttr.accept("comma") || ttr.accept("at")); } - function C () { - const colon = ttr.accept('colon') - if (!colon) return + function C() { + const colon = ttr.accept("colon"); + if (!colon) return; do { - let m = ttr.acceptNumber() + let m = ttr.acceptNumber(); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected minutes' - ) + "Unexpected symbol " + ttr.symbol + ", expected minutes" + ); } - options.byminute = parseInt(m[0], 10) - } while (ttr.accept('colon')) + options.byminute = parseInt(m[0], 10); + } while (ttr.accept("colon")); } - function decodeM () { + function decodeM() { switch (ttr.symbol) { - case 'january': - return 1 - case 'february': - return 2 - case 'march': - return 3 - case 'april': - return 4 - case 'may': - return 5 - case 'june': - return 6 - case 'july': - return 7 - case 'august': - return 8 - case 'september': - return 9 - case 'october': - return 10 - case 'november': - return 11 - case 'december': - return 12 + case "january": + return 1; + case "february": + return 2; + case "march": + return 3; + case "april": + return 4; + case "may": + return 5; + case "june": + return 6; + case "july": + return 7; + case "august": + return 8; + case "september": + return 9; + case "october": + return 10; + case "november": + return 11; + case "december": + return 12; default: - return false + return false; } } - function decodeWKD () { + function decodeWKD() { switch (ttr.symbol) { - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - return ttr.symbol.substr(0, 2).toUpperCase() + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + return ttr.symbol.substr(0, 2).toUpperCase(); default: - return false + return false; } } - function decodeNTH () { + function decodeNTH() { switch (ttr.symbol) { - case 'last': - ttr.nextSymbol() - return -1 - case 'first': - ttr.nextSymbol() - return 1 - case 'second': - ttr.nextSymbol() - return ttr.accept('last') ? -2 : 2 - case 'third': - ttr.nextSymbol() - return ttr.accept('last') ? -3 : 3 - case 'nth': - const v = parseInt(ttr.value![1], 10) - if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) - - ttr.nextSymbol() - return ttr.accept('last') ? -v : v + case "last": + ttr.nextSymbol(); + return -1; + case "first": + ttr.nextSymbol(); + return 1; + case "second": + ttr.nextSymbol(); + return ttr.accept("last") ? -2 : 2; + case "third": + ttr.nextSymbol(); + return ttr.accept("last") ? -3 : 3; + case "nth": + const v = parseInt(ttr.value![1], 10); + if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); + + ttr.nextSymbol(); + return ttr.accept("last") ? -v : v; default: - return false + return false; } } - function MDAYs () { - ttr.accept('on') - ttr.accept('the') + function MDAYs() { + ttr.accept("on"); + ttr.accept("the"); - let nth = decodeNTH() - if (!nth) return + let nth = decodeNTH(); + if (!nth) return; - options.bymonthday = [nth] - ttr.nextSymbol() + options.bymonthday = [nth]; + ttr.nextSymbol(); - while (ttr.accept('comma')) { - nth = decodeNTH() + while (ttr.accept("comma")) { + nth = decodeNTH(); if (!nth) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.bymonthday.push(nth) - ttr.nextSymbol() + options.bymonthday.push(nth); + ttr.nextSymbol(); } } - function F () { - if (ttr.symbol === 'until') { - const date = Date.parse(ttr.text) + function F() { + if (ttr.symbol === "until") { + const date = Date.parse(ttr.text); - if (!date) throw new Error('Cannot parse until date:' + ttr.text) - options.until = new Date(date) - } else if (ttr.accept('for')) { - options.count = parseInt(ttr.value![0], 10) - ttr.expect('number') + if (!date) throw new Error("Cannot parse until date:" + ttr.text); + options.until = new Date(date); + } else if (ttr.accept("for")) { + options.count = parseInt(ttr.value![0], 10); + ttr.expect("number"); // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 74de08fd..24cd525c 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options, ByWeekday } from '../types' -import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent, padStart } from '../helpers' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options, ByWeekday } from "../types"; +import { Weekday } from "../weekday"; +import { isArray, isNumber, isPresent, padStart } from "../helpers"; // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from '../helpers' /** * Return true if a value is in an array */ -const contains = function (arr: string[], val: string) { - return arr.indexOf(val) !== -1 -} +const contains = function(arr: string[], val: string) { + return arr.indexOf(val) !== -1; +}; // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string +export type GetText = (id: string | number | Weekday) => string; -const defaultGetText: GetText = id => id.toString() +const defaultGetText: GetText = id => id.toString(); export type DateFormatter = ( year: number, month: string, day: number -) => string +) => string; const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}` +) => `${month} ${day}, ${year}`; /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][] - private rrule: RRule - private text: string[] - private gettext: GetText - private dateFormatter: DateFormatter - private language: Language - private options: Partial - private origOptions: Partial - private bymonthday: Options['bymonthday'] | null + static IMPLEMENTED: string[][]; + private rrule: RRule; + private text: string[]; + private gettext: GetText; + private dateFormatter: DateFormatter; + private language: Language; + private options: Partial; + private origOptions: Partial; + private bymonthday: Options["bymonthday"] | null; private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null + } | null; - constructor ( + constructor( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = [] - this.language = language || ENGLISH - this.gettext = gettext - this.dateFormatter = dateFormatter - this.rrule = rrule - this.options = rrule.options - this.origOptions = rrule.origOptions + this.text = []; + this.language = language || ENGLISH; + this.gettext = gettext; + this.dateFormatter = dateFormatter; + this.rrule = rrule; + this.options = rrule.options; + this.origOptions = rrule.origOptions; if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!) - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) + const bymonthday = ([] as number[]).concat(this.options.bymonthday!); + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); - bymonthday.sort((a, b) => a - b) - bynmonthday.sort((a, b) => b - a) + bymonthday.sort((a, b) => a - b); + bynmonthday.sort((a, b) => b - a); // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday) - if (!this.bymonthday.length) this.bymonthday = null + this.bymonthday = bymonthday.concat(bynmonthday); + if (!this.bymonthday.length) this.bymonthday = null; } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday - const days = String(byweekday) + : this.origOptions.byweekday; + const days = String(byweekday); this.byweekday = { - allWeeks: byweekday.filter(function (weekday: Weekday) { - return !weekday.n + allWeeks: byweekday.filter(function(weekday: Weekday) { + return !weekday.n; }), - someWeeks: byweekday.filter(function (weekday: Weekday) { - return Boolean(weekday.n) + someWeeks: byweekday.filter(function(weekday: Weekday) { + return Boolean(weekday.n); }), isWeekdays: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') === -1 && - days.indexOf('SU') === -1, + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") === -1 && + days.indexOf("SU") === -1, isEveryDay: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') !== -1 && - days.indexOf('SU') !== -1 - } - - const sortWeekDays = function (a: Weekday, b: Weekday) { - return a.weekday - b.weekday - } - - this.byweekday.allWeeks!.sort(sortWeekDays) - this.byweekday.someWeeks!.sort(sortWeekDays) - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") !== -1 && + days.indexOf("SU") !== -1 + }; + + const sortWeekDays = function(a: Weekday, b: Weekday) { + return a.weekday - b.weekday; + }; + + this.byweekday.allWeeks!.sort(sortWeekDays); + this.byweekday.someWeeks!.sort(sortWeekDays); + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; } else { - this.byweekday = null + this.byweekday = null; } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible (rrule: RRule) { - let canConvert = true + static isFullyConvertible(rrule: RRule) { + let canConvert = true; - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false - if (rrule.origOptions.until && rrule.origOptions.count) return false + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; + if (rrule.origOptions.until && rrule.origOptions.count) return false; for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false + if (contains(["dtstart", "wkst", "freq"], key)) return true; + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; } - return canConvert + return canConvert; } - isFullyConvertible () { - return ToText.isFullyConvertible(this.rrule) + isFullyConvertible() { + return ToText.isFullyConvertible(this.rrule); } /** @@ -159,368 +159,372 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString () { - const gettext = this.gettext + toString() { + const gettext = this.gettext; if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext('RRule error: Unable to fully convert this rrule to text') + return gettext("RRule error: Unable to fully convert this rrule to text"); } - this.text = [gettext('every')] + this.text = [gettext("every")]; // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]]() + this[RRule.FREQUENCIES[this.options.freq]](); if (this.options.until) { - this.add(gettext('until')) - const until = this.options.until + this.add(gettext("until")); + const until = this.options.until; this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ) + ); } else if (this.options.count) { - this.add(gettext('for')) + this.add(gettext("for")) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext('times') : gettext('time') - ) + this.plural(this.options.count) ? gettext("times") : gettext("time") + ); } - if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) + if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); - return this.text.join('') + return this.text.join(""); } - HOURLY () { - const gettext = this.gettext + HOURLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') - ) + this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") + ); } - MINUTELY () { - const gettext = this.gettext + MINUTELY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('minutes') - : gettext('minutes') - ) + ? gettext("minutes") + : gettext("minutes") + ); } - DAILY () { - const gettext = this.gettext + DAILY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - WEEKLY () { - const gettext = this.gettext + WEEKLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') - ) + this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") + ); } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } else { - if (this.options.interval === 1) this.add(gettext('week')) + if (this.options.interval === 1) this.add(gettext("week")); if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } } - MONTHLY () { - const gettext = this.gettext + MONTHLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext('months')) - if (this.plural(this.options.interval!)) this.add(gettext('in')) + this.add(this.options.interval!.toString()).add(gettext("months")); + if (this.plural(this.options.interval!)) this.add(gettext("in")); } else { // this.add(gettext('MONTH')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('months') - : gettext('month') - ) + ? gettext("months") + : gettext("month") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); + } else if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); } } - YEARLY () { - const gettext = this.gettext + YEARLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) - this.add(gettext('years')) + this.add(this.options.interval!.toString()); + this.add(gettext("years")); } else { // this.add(gettext('YEAR')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('years') : gettext('year') - ) + this.plural(this.options.interval!) ? gettext("years") : gettext("year") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } if (this.options.byyearday) { - this.add(gettext('on the')) - .add(this.list(this.options.byyearday, this.nth, gettext('and'))) - .add(gettext('day')) + this.add(gettext("on the")) + .add(this.list(this.options.byyearday, this.nth, gettext("and"))) + .add(gettext("day")); } if (this.options.byweekno) { - this.add(gettext('in')) + this.add(gettext("in")) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext('weeks') - : gettext('week') + ? gettext("weeks") + : gettext("week") ) - .add(this.list(this.options.byweekno, undefined, gettext('and'))) + .add(this.list(this.options.byweekno, undefined, gettext("and"))); } } - private _bymonthday () { - const gettext = this.gettext + private _bymonthday() { + const gettext = this.gettext; if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext('on')) + this.add(gettext("on")) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) ) - .add(gettext('the')) - .add(this.list(this.bymonthday!, this.nth, gettext('or'))) + .add(gettext("the")) + .add(this.list(this.bymonthday!, this.nth, gettext("or"))); } else { - this.add(gettext('on the')).add( - this.list(this.bymonthday!, this.nth, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.bymonthday!, this.nth, gettext("and")) + ); } // this.add(gettext('DAY')) } - private _byweekday () { - const gettext = this.gettext + private _byweekday() { + const gettext = this.gettext; if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext('on')).add( + this.add(gettext("on")).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ) + ); } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext('and')) + if (this.byweekday!.allWeeks) this.add(gettext("and")); - this.add(gettext('on the')).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) + ); } } - private _byhour () { - const gettext = this.gettext + private _byhour() { + const gettext = this.gettext; - this.add(gettext('at')).add( - this.list(this.origOptions.byhour!, undefined, gettext('and')) - ) + this.add(gettext("at")).add( + this.list(this.origOptions.byhour!, undefined, gettext("and")) + ); } - private _byminute () { - const gettext = this.gettext + private _byminute() { + const gettext = this.gettext; - this.add(gettext('at')).add( + this.add(gettext("at")).add( `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ) + ); } - private _bymonth () { + private _bymonth() { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) - ) + this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) + ); } - nth (n: number | string) { - n = parseInt(n.toString(), 10) - let nth: string - let npos: number - const gettext = this.gettext + nth(n: number | string) { + n = parseInt(n.toString(), 10); + let nth: string; + let npos: number; + const gettext = this.gettext; - if (n === -1) return gettext('last') + if (n === -1) return gettext("last"); - npos = Math.abs(n) + npos = Math.abs(n); switch (npos) { case 1: case 21: case 31: - nth = npos + gettext('st') - break + nth = npos + gettext("st"); + break; case 2: case 22: - nth = npos + gettext('nd') - break + nth = npos + gettext("nd"); + break; case 3: case 23: - nth = npos + gettext('rd') - break + nth = npos + gettext("rd"); + break; default: - nth = npos + gettext('th') + nth = npos + gettext("th"); } - return n < 0 ? nth + ' ' + gettext('last') : nth + return n < 0 ? nth + " " + gettext("last") : nth; } - monthtext (m: number) { - return this.language.monthNames[m - 1] + monthtext(m: number) { + return this.language.monthNames[m - 1]; } - weekdaytext (wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() + weekdaytext(wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + this.language.dayNames[weekday] - ) + ); } - plural (n: number) { - return n % 100 !== 1 + plural(n: number) { + return n % 100 !== 1; } - add (s: string) { - this.text.push(' ') - this.text.push(s) - return this + add(s: string) { + this.text.push(" "); + this.text.push(s); + return this; } - list ( + list( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = ',' + delim: string = "," ) { if (!isArray(arr)) { - arr = [arr] + arr = [arr]; } - const delimJoin = function ( + const delimJoin = function( array: string[], delimiter: string, finalDelimiter: string ) { - let list = '' + let list = ""; for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += ' ' + finalDelimiter + ' ' + list += " " + finalDelimiter + " "; } else { - list += delimiter + ' ' + list += delimiter + " "; } } - list += array[i] + list += array[i]; } - return list - } + return list; + }; callback = callback || - function (o) { - return o.toString() - } - const self = this - const realCallback = function (arg: ByWeekday) { - return callback && callback.call(self, arg) - } + function(o) { + return o.toString(); + }; + const self = this; + const realCallback = function(arg: ByWeekday) { + return callback && callback.call(self, arg); + }; if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim) + return delimJoin(arr.map(realCallback), delim, finalDelim); } else { - return arr.map(realCallback).join(delim + ' ') + return arr.map(realCallback).join(delim + " "); } } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 6e3ebbd2..3b9a9ee0 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -17,6 +17,7 @@ const texts = [ ["Every weekday", "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"], ["Every 2 weeks", "RRULE:INTERVAL=2;FREQ=WEEKLY"], ["Every month", "RRULE:FREQ=MONTHLY"], + ["Every month at 10:30", "RRULE:FREQ=MONTHLY;BYHOUR=10;BYMINUTE=30"], ["Every 6 months", "RRULE:INTERVAL=6;FREQ=MONTHLY"], ["Every year", "RRULE:FREQ=YEARLY"], ["Every year on the 1st Friday", "RRULE:FREQ=YEARLY;BYDAY=+1FR"], From e394e508f8e5a31bff42302d642b3041d2d72eea Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 09:54:48 +0100 Subject: [PATCH 08/23] add time in monthly case --- src/nlp/index.ts | 60 ++--- src/nlp/parsetext.ts | 576 +++++++++++++++++++++---------------------- src/nlp/totext.ts | 476 +++++++++++++++++------------------ 3 files changed, 556 insertions(+), 556 deletions(-) diff --git a/src/nlp/index.ts b/src/nlp/index.ts index 893678fc..1a5dcd5e 100644 --- a/src/nlp/index.ts +++ b/src/nlp/index.ts @@ -1,7 +1,7 @@ -import ToText, { DateFormatter, GetText } from "./totext"; -import parseText from "./parsetext"; -import RRule from "../index"; -import ENGLISH, { Language } from "./i18n"; +import ToText, { DateFormatter, GetText } from './totext' +import parseText from './parsetext' +import RRule from '../index' +import ENGLISH, { Language } from './i18n' /*! * rrule.js - Library for working with recurrence rules for calendar dates. @@ -94,47 +94,47 @@ import ENGLISH, { Language } from "./i18n"; * @param {String} text * @return {Object, Boolean} the rule, or null. */ -const fromText = function(text: string, language: Language = ENGLISH) { - return new RRule(parseText(text, language) || undefined); -}; +const fromText = function (text: string, language: Language = ENGLISH) { + return new RRule(parseText(text, language) || undefined) +} const common = [ - "count", - "until", - "interval", - "byweekday", - "bymonthday", - "bymonth" -]; + 'count', + 'until', + 'interval', + 'byweekday', + 'bymonthday', + 'bymonth' +] -ToText.IMPLEMENTED = []; -ToText.IMPLEMENTED[RRule.HOURLY] = common; -ToText.IMPLEMENTED[RRule.MINUTELY] = common; -ToText.IMPLEMENTED[RRule.DAILY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.WEEKLY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.MONTHLY] = ["byhour", "byminute"].concat(common); -ToText.IMPLEMENTED[RRule.YEARLY] = ["byweekno", "byyearday"].concat(common); +ToText.IMPLEMENTED = [] +ToText.IMPLEMENTED[RRule.HOURLY] = common +ToText.IMPLEMENTED[RRule.MINUTELY] = common +ToText.IMPLEMENTED[RRule.DAILY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.WEEKLY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.MONTHLY] = ['byhour', 'byminute'].concat(common) +ToText.IMPLEMENTED[RRule.YEARLY] = ['byweekno', 'byyearday'].concat(common) // ============================================================================= // Export // ============================================================================= -const toText = function( +const toText = function ( rrule: RRule, gettext?: GetText, language?: Language, dateFormatter?: DateFormatter ) { - return new ToText(rrule, gettext, language, dateFormatter).toString(); -}; + return new ToText(rrule, gettext, language, dateFormatter).toString() +} -const { isFullyConvertible } = ToText; +const { isFullyConvertible } = ToText export interface Nlp { - fromText: typeof fromText; - parseText: typeof parseText; - isFullyConvertible: typeof isFullyConvertible; - toText: typeof toText; + fromText: typeof fromText + parseText: typeof parseText + isFullyConvertible: typeof isFullyConvertible + toText: typeof toText } -export { fromText, parseText, isFullyConvertible, toText }; +export { fromText, parseText, isFullyConvertible, toText } diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 777ff069..3fe0972f 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,291 +1,291 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options } from "../types"; -import { WeekdayStr } from "../weekday"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options } from '../types' +import { WeekdayStr } from '../weekday' // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp }; - public text: string; - public symbol: string | null; - public value: RegExpExecArray | null; - private done = true; - - constructor(rules: { [k: string]: RegExp }) { - this.rules = rules; + private readonly rules: { [k: string]: RegExp } + public text: string + public symbol: string | null + public value: RegExpExecArray | null + private done = true + + constructor (rules: { [k: string]: RegExp }) { + this.rules = rules } - start(text: string) { - this.text = text; - this.done = false; - return this.nextSymbol(); + start (text: string) { + this.text = text + this.done = false + return this.nextSymbol() } - isDone() { - return this.done && this.symbol === null; + isDone () { + return this.done && this.symbol === null } - nextSymbol() { - let best: RegExpExecArray | null; - let bestSymbol: string; - const p = this; + nextSymbol () { + let best: RegExpExecArray | null + let bestSymbol: string + const p = this - this.symbol = null; - this.value = null; + this.symbol = null + this.value = null do { - if (this.done) return false; + if (this.done) return false - let rule: RegExp; - best = null; + let rule: RegExp + best = null for (let name in this.rules) { - rule = this.rules[name]; - const match = rule.exec(p.text); + rule = this.rules[name] + const match = rule.exec(p.text) if (match) { if (best === null || match[0].length > best[0].length) { - best = match; - bestSymbol = name; + best = match + bestSymbol = name } } } if (best != null) { - this.text = this.text.substr(best[0].length); + this.text = this.text.substr(best[0].length) - if (this.text === "") this.done = true; + if (this.text === '') this.done = true } if (best == null) { - this.done = true; - this.symbol = null; - this.value = null; - return; + this.done = true + this.symbol = null + this.value = null + return } // @ts-ignore - } while (bestSymbol === "SKIP"); + } while (bestSymbol === 'SKIP') // @ts-ignore - this.symbol = bestSymbol; - this.value = best; - return true; + this.symbol = bestSymbol + this.value = best + return true } - accept(name: string) { + accept (name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value; - this.nextSymbol(); - return v; + const v = this.value + this.nextSymbol() + return v } - this.nextSymbol(); - return true; + this.nextSymbol() + return true } - return false; + return false } - acceptNumber() { - return this.accept("number") as RegExpExecArray; + acceptNumber () { + return this.accept('number') as RegExpExecArray } - expect(name: string) { - if (this.accept(name)) return true; + expect (name: string) { + if (this.accept(name)) return true - throw new Error("expected " + name + " but found " + this.symbol); + throw new Error('expected ' + name + ' but found ' + this.symbol) } } -export default function parseText(text: string, language: Language = ENGLISH) { - const options: Partial = {}; - const ttr = new Parser(language.tokens); +export default function parseText (text: string, language: Language = ENGLISH) { + const options: Partial = {} + const ttr = new Parser(language.tokens) - if (!ttr.start(text)) return null; + if (!ttr.start(text)) return null - S(); - return options; + S() + return options - function S() { + function S () { // every [n] - ttr.expect("every"); - let n = ttr.acceptNumber(); - if (n) options.interval = parseInt(n[0], 10); - if (ttr.isDone()) throw new Error("Unexpected end"); + ttr.expect('every') + let n = ttr.acceptNumber() + if (n) options.interval = parseInt(n[0], 10) + if (ttr.isDone()) throw new Error('Unexpected end') switch (ttr.symbol) { - case "day(s)": - options.freq = RRule.DAILY; + case 'day(s)': + options.freq = RRule.DAILY if (ttr.nextSymbol()) { - AT(); - C(); - F(); + AT() + C() + F() } - break; + break // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case "weekday(s)": - options.freq = RRule.WEEKLY; - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; - ttr.nextSymbol(); - F(); - break; - - case "week(s)": - options.freq = RRule.WEEKLY; + case 'weekday(s)': + options.freq = RRule.WEEKLY + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] + ttr.nextSymbol() + F() + break + + case 'week(s)': + options.freq = RRule.WEEKLY if (ttr.nextSymbol()) { - ON(); - AT(); - C(); - F(); + ON() + AT() + C() + F() } - break; + break - case "hour(s)": - options.freq = RRule.HOURLY; + case 'hour(s)': + options.freq = RRule.HOURLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "minute(s)": - options.freq = RRule.MINUTELY; + case 'minute(s)': + options.freq = RRule.MINUTELY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "month(s)": - options.freq = RRule.MONTHLY; + case 'month(s)': + options.freq = RRule.MONTHLY if (ttr.nextSymbol()) { - ON(); - AT(); - C(); - F(); + ON() + AT() + C() + F() } - break; + break - case "year(s)": - options.freq = RRule.YEARLY; + case 'year(s)': + options.freq = RRule.YEARLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; - - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - options.freq = RRule.WEEKLY; + break + + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + options.freq = RRule.WEEKLY const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr; - options.byweekday = [RRule[key]]; + .toUpperCase() as WeekdayStr + options.byweekday = [RRule[key]] - if (!ttr.nextSymbol()) return; + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let wkd = decodeWKD() as keyof typeof RRule; + let wkd = decodeWKD() as keyof typeof RRule if (!wkd) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected weekday" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected weekday' + ) } // @ts-ignore - options.byweekday.push(RRule[wkd]); - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + ttr.nextSymbol() } - MDAYs(); - F(); - break; - - case "january": - case "february": - case "march": - case "april": - case "may": - case "june": - case "july": - case "august": - case "september": - case "october": - case "november": - case "december": - options.freq = RRule.YEARLY; - options.bymonth = [decodeM() as number]; - - if (!ttr.nextSymbol()) return; + MDAYs() + F() + break + + case 'january': + case 'february': + case 'march': + case 'april': + case 'may': + case 'june': + case 'july': + case 'august': + case 'september': + case 'october': + case 'november': + case 'december': + options.freq = RRule.YEARLY + options.bymonth = [decodeM() as number] + + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let m = decodeM(); + let m = decodeM() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected month" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected month' + ) } - options.bymonth.push(m); - ttr.nextSymbol(); + options.bymonth.push(m) + ttr.nextSymbol() } - ON(); - F(); - break; + ON() + F() + break default: - throw new Error("Unknown symbol"); + throw new Error('Unknown symbol') } } - function ON() { - const on = ttr.accept("on"); - const the = ttr.accept("the"); - if (!(on || the)) return; + function ON () { + const on = ttr.accept('on') + const the = ttr.accept('the') + if (!(on || the)) return do { - let nth = decodeNTH(); - let wkd = decodeWKD(); - let m = decodeM(); + let nth = decodeNTH() + let wkd = decodeWKD() + let m = decodeM() // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)); + options.byweekday.push(RRule[wkd].nth(nth)) } else { - if (!options.bymonthday) options.bymonthday = []; + if (!options.bymonthday) options.bymonthday = [] // @ts-ignore - options.bymonthday.push(nth); - ttr.accept("day(s)"); + options.bymonthday.push(nth) + ttr.accept('day(s)') } // } else if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd]); - } else if (ttr.symbol === "weekday(s)") { - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + } else if (ttr.symbol === 'weekday(s)') { + ttr.nextSymbol() if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -293,179 +293,179 @@ export default function parseText(text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ]; + ] } - } else if (ttr.symbol === "week(s)") { - ttr.nextSymbol(); - let n = ttr.acceptNumber(); + } else if (ttr.symbol === 'week(s)') { + ttr.nextSymbol() + let n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected week number" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected week number' + ) } - options.byweekno = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byweekno = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.byweekno.push(parseInt(n[0], 10)); + options.byweekno.push(parseInt(n[0], 10)) } } else if (m) { - ttr.nextSymbol(); - if (!options.bymonth) options.bymonth = []; + ttr.nextSymbol() + if (!options.bymonth) options.bymonth = [] // @ts-ignore - options.bymonth.push(m); + options.bymonth.push(m) } else { - return; + return } - } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); + } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) } - function AT() { - const at = ttr.accept("at"); - if (!at) return; + function AT () { + const at = ttr.accept('at') + if (!at) return do { - let n = ttr.acceptNumber(); + let n = ttr.acceptNumber() if (!n) { - throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); + throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') } - options.byhour = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byhour = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected hour" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected hour' + ) } - options.byhour.push(parseInt(n[0], 10)); + options.byhour.push(parseInt(n[0], 10)) } - } while (ttr.accept("comma") || ttr.accept("at")); + } while (ttr.accept('comma') || ttr.accept('at')) } - function C() { - const colon = ttr.accept("colon"); - if (!colon) return; + function C () { + const colon = ttr.accept('colon') + if (!colon) return do { - let m = ttr.acceptNumber(); + let m = ttr.acceptNumber() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected minutes" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected minutes' + ) } - options.byminute = parseInt(m[0], 10); - } while (ttr.accept("colon")); + options.byminute = parseInt(m[0], 10) + } while (ttr.accept('colon')) } - function decodeM() { + function decodeM () { switch (ttr.symbol) { - case "january": - return 1; - case "february": - return 2; - case "march": - return 3; - case "april": - return 4; - case "may": - return 5; - case "june": - return 6; - case "july": - return 7; - case "august": - return 8; - case "september": - return 9; - case "october": - return 10; - case "november": - return 11; - case "december": - return 12; + case 'january': + return 1 + case 'february': + return 2 + case 'march': + return 3 + case 'april': + return 4 + case 'may': + return 5 + case 'june': + return 6 + case 'july': + return 7 + case 'august': + return 8 + case 'september': + return 9 + case 'october': + return 10 + case 'november': + return 11 + case 'december': + return 12 default: - return false; + return false } } - function decodeWKD() { + function decodeWKD () { switch (ttr.symbol) { - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - return ttr.symbol.substr(0, 2).toUpperCase(); + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + return ttr.symbol.substr(0, 2).toUpperCase() default: - return false; + return false } } - function decodeNTH() { + function decodeNTH () { switch (ttr.symbol) { - case "last": - ttr.nextSymbol(); - return -1; - case "first": - ttr.nextSymbol(); - return 1; - case "second": - ttr.nextSymbol(); - return ttr.accept("last") ? -2 : 2; - case "third": - ttr.nextSymbol(); - return ttr.accept("last") ? -3 : 3; - case "nth": - const v = parseInt(ttr.value![1], 10); - if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); - - ttr.nextSymbol(); - return ttr.accept("last") ? -v : v; + case 'last': + ttr.nextSymbol() + return -1 + case 'first': + ttr.nextSymbol() + return 1 + case 'second': + ttr.nextSymbol() + return ttr.accept('last') ? -2 : 2 + case 'third': + ttr.nextSymbol() + return ttr.accept('last') ? -3 : 3 + case 'nth': + const v = parseInt(ttr.value![1], 10) + if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) + + ttr.nextSymbol() + return ttr.accept('last') ? -v : v default: - return false; + return false } } - function MDAYs() { - ttr.accept("on"); - ttr.accept("the"); + function MDAYs () { + ttr.accept('on') + ttr.accept('the') - let nth = decodeNTH(); - if (!nth) return; + let nth = decodeNTH() + if (!nth) return - options.bymonthday = [nth]; - ttr.nextSymbol(); + options.bymonthday = [nth] + ttr.nextSymbol() - while (ttr.accept("comma")) { - nth = decodeNTH(); + while (ttr.accept('comma')) { + nth = decodeNTH() if (!nth) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.bymonthday.push(nth); - ttr.nextSymbol(); + options.bymonthday.push(nth) + ttr.nextSymbol() } } - function F() { - if (ttr.symbol === "until") { - const date = Date.parse(ttr.text); + function F () { + if (ttr.symbol === 'until') { + const date = Date.parse(ttr.text) - if (!date) throw new Error("Cannot parse until date:" + ttr.text); - options.until = new Date(date); - } else if (ttr.accept("for")) { - options.count = parseInt(ttr.value![0], 10); - ttr.expect("number"); + if (!date) throw new Error('Cannot parse until date:' + ttr.text) + options.until = new Date(date) + } else if (ttr.accept('for')) { + options.count = parseInt(ttr.value![0], 10) + ttr.expect('number') // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 24cd525c..44065be0 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options, ByWeekday } from "../types"; -import { Weekday } from "../weekday"; -import { isArray, isNumber, isPresent, padStart } from "../helpers"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options, ByWeekday } from '../types' +import { Weekday } from '../weekday' +import { isArray, isNumber, isPresent, padStart } from '../helpers' // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from "../helpers"; /** * Return true if a value is in an array */ -const contains = function(arr: string[], val: string) { - return arr.indexOf(val) !== -1; -}; +const contains = function (arr: string[], val: string) { + return arr.indexOf(val) !== -1 +} // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string; +export type GetText = (id: string | number | Weekday) => string -const defaultGetText: GetText = id => id.toString(); +const defaultGetText: GetText = id => id.toString() export type DateFormatter = ( year: number, month: string, day: number -) => string; +) => string const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}`; +) => `${month} ${day}, ${year}` /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][]; - private rrule: RRule; - private text: string[]; - private gettext: GetText; - private dateFormatter: DateFormatter; - private language: Language; - private options: Partial; - private origOptions: Partial; - private bymonthday: Options["bymonthday"] | null; + static IMPLEMENTED: string[][] + private rrule: RRule + private text: string[] + private gettext: GetText + private dateFormatter: DateFormatter + private language: Language + private options: Partial + private origOptions: Partial + private bymonthday: Options['bymonthday'] | null private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null; + } | null - constructor( + constructor ( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = []; - this.language = language || ENGLISH; - this.gettext = gettext; - this.dateFormatter = dateFormatter; - this.rrule = rrule; - this.options = rrule.options; - this.origOptions = rrule.origOptions; + this.text = [] + this.language = language || ENGLISH + this.gettext = gettext + this.dateFormatter = dateFormatter + this.rrule = rrule + this.options = rrule.options + this.origOptions = rrule.origOptions if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!); - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); + const bymonthday = ([] as number[]).concat(this.options.bymonthday!) + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) - bymonthday.sort((a, b) => a - b); - bynmonthday.sort((a, b) => b - a); + bymonthday.sort((a, b) => a - b) + bynmonthday.sort((a, b) => b - a) // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday); - if (!this.bymonthday.length) this.bymonthday = null; + this.bymonthday = bymonthday.concat(bynmonthday) + if (!this.bymonthday.length) this.bymonthday = null } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday; - const days = String(byweekday); + : this.origOptions.byweekday + const days = String(byweekday) this.byweekday = { - allWeeks: byweekday.filter(function(weekday: Weekday) { - return !weekday.n; + allWeeks: byweekday.filter(function (weekday: Weekday) { + return !weekday.n }), - someWeeks: byweekday.filter(function(weekday: Weekday) { - return Boolean(weekday.n); + someWeeks: byweekday.filter(function (weekday: Weekday) { + return Boolean(weekday.n) }), isWeekdays: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") === -1 && - days.indexOf("SU") === -1, + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') === -1 && + days.indexOf('SU') === -1, isEveryDay: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") !== -1 && - days.indexOf("SU") !== -1 - }; - - const sortWeekDays = function(a: Weekday, b: Weekday) { - return a.weekday - b.weekday; - }; - - this.byweekday.allWeeks!.sort(sortWeekDays); - this.byweekday.someWeeks!.sort(sortWeekDays); - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') !== -1 && + days.indexOf('SU') !== -1 + } + + const sortWeekDays = function (a: Weekday, b: Weekday) { + return a.weekday - b.weekday + } + + this.byweekday.allWeeks!.sort(sortWeekDays) + this.byweekday.someWeeks!.sort(sortWeekDays) + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null } else { - this.byweekday = null; + this.byweekday = null } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible(rrule: RRule) { - let canConvert = true; + static isFullyConvertible (rrule: RRule) { + let canConvert = true - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; - if (rrule.origOptions.until && rrule.origOptions.count) return false; + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false + if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(["dtstart", "wkst", "freq"], key)) return true; - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; + if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } - return canConvert; + return canConvert } - isFullyConvertible() { - return ToText.isFullyConvertible(this.rrule); + isFullyConvertible () { + return ToText.isFullyConvertible(this.rrule) } /** @@ -159,372 +159,372 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString() { - const gettext = this.gettext; + toString () { + const gettext = this.gettext if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext("RRule error: Unable to fully convert this rrule to text"); + return gettext('RRule error: Unable to fully convert this rrule to text') } - this.text = [gettext("every")]; + this.text = [gettext('every')] // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]](); + this[RRule.FREQUENCIES[this.options.freq]]() if (this.options.until) { - this.add(gettext("until")); - const until = this.options.until; + this.add(gettext('until')) + const until = this.options.until this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ); + ) } else if (this.options.count) { - this.add(gettext("for")) + this.add(gettext('for')) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext("times") : gettext("time") - ); + this.plural(this.options.count) ? gettext('times') : gettext('time') + ) } - if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); + if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) - return this.text.join(""); + return this.text.join('') } - HOURLY() { - const gettext = this.gettext; + HOURLY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") - ); + this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') + ) } - MINUTELY() { - const gettext = this.gettext; + MINUTELY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("minutes") - : gettext("minutes") - ); + ? gettext('minutes') + : gettext('minutes') + ) } - DAILY() { - const gettext = this.gettext; + DAILY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - WEEKLY() { - const gettext = this.gettext; + WEEKLY () { + const gettext = this.gettext if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") - ); + this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') + ) } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } else { - if (this.options.interval === 1) this.add(gettext("week")); + if (this.options.interval === 1) this.add(gettext('week')) if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } } - MONTHLY() { - const gettext = this.gettext; + MONTHLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext("months")); - if (this.plural(this.options.interval!)) this.add(gettext("in")); + this.add(this.options.interval!.toString()).add(gettext('months')) + if (this.plural(this.options.interval!)) this.add(gettext('in')) } else { // this.add(gettext('MONTH')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("months") - : gettext("month") - ); + ? gettext('months') + : gettext('month') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - YEARLY() { - const gettext = this.gettext; + YEARLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); - this.add(gettext("years")); + this.add(this.options.interval!.toString()) + this.add(gettext('years')) } else { // this.add(gettext('YEAR')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("years") : gettext("year") - ); + this.plural(this.options.interval!) ? gettext('years') : gettext('year') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } if (this.options.byyearday) { - this.add(gettext("on the")) - .add(this.list(this.options.byyearday, this.nth, gettext("and"))) - .add(gettext("day")); + this.add(gettext('on the')) + .add(this.list(this.options.byyearday, this.nth, gettext('and'))) + .add(gettext('day')) } if (this.options.byweekno) { - this.add(gettext("in")) + this.add(gettext('in')) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext("weeks") - : gettext("week") + ? gettext('weeks') + : gettext('week') ) - .add(this.list(this.options.byweekno, undefined, gettext("and"))); + .add(this.list(this.options.byweekno, undefined, gettext('and'))) } } - private _bymonthday() { - const gettext = this.gettext; + private _bymonthday () { + const gettext = this.gettext if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext("on")) + this.add(gettext('on')) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) ) - .add(gettext("the")) - .add(this.list(this.bymonthday!, this.nth, gettext("or"))); + .add(gettext('the')) + .add(this.list(this.bymonthday!, this.nth, gettext('or'))) } else { - this.add(gettext("on the")).add( - this.list(this.bymonthday!, this.nth, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.bymonthday!, this.nth, gettext('and')) + ) } // this.add(gettext('DAY')) } - private _byweekday() { - const gettext = this.gettext; + private _byweekday () { + const gettext = this.gettext if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext("on")).add( + this.add(gettext('on')).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ); + ) } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext("and")); + if (this.byweekday!.allWeeks) this.add(gettext('and')) - this.add(gettext("on the")).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) + ) } } - private _byhour() { - const gettext = this.gettext; + private _byhour () { + const gettext = this.gettext - this.add(gettext("at")).add( - this.list(this.origOptions.byhour!, undefined, gettext("and")) - ); + this.add(gettext('at')).add( + this.list(this.origOptions.byhour!, undefined, gettext('and')) + ) } - private _byminute() { - const gettext = this.gettext; + private _byminute () { + const gettext = this.gettext - this.add(gettext("at")).add( + this.add(gettext('at')).add( `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ); + ) } - private _bymonth() { + private _bymonth () { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) - ); + this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) + ) } - nth(n: number | string) { - n = parseInt(n.toString(), 10); - let nth: string; - let npos: number; - const gettext = this.gettext; + nth (n: number | string) { + n = parseInt(n.toString(), 10) + let nth: string + let npos: number + const gettext = this.gettext - if (n === -1) return gettext("last"); + if (n === -1) return gettext('last') - npos = Math.abs(n); + npos = Math.abs(n) switch (npos) { case 1: case 21: case 31: - nth = npos + gettext("st"); - break; + nth = npos + gettext('st') + break case 2: case 22: - nth = npos + gettext("nd"); - break; + nth = npos + gettext('nd') + break case 3: case 23: - nth = npos + gettext("rd"); - break; + nth = npos + gettext('rd') + break default: - nth = npos + gettext("th"); + nth = npos + gettext('th') } - return n < 0 ? nth + " " + gettext("last") : nth; + return n < 0 ? nth + ' ' + gettext('last') : nth } - monthtext(m: number) { - return this.language.monthNames[m - 1]; + monthtext (m: number) { + return this.language.monthNames[m - 1] } - weekdaytext(wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); + weekdaytext (wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ); + ) } - plural(n: number) { - return n % 100 !== 1; + plural (n: number) { + return n % 100 !== 1 } - add(s: string) { - this.text.push(" "); - this.text.push(s); - return this; + add (s: string) { + this.text.push(' ') + this.text.push(s) + return this } - list( + list ( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = "," + delim: string = ',' ) { if (!isArray(arr)) { - arr = [arr]; + arr = [arr] } - const delimJoin = function( + const delimJoin = function ( array: string[], delimiter: string, finalDelimiter: string ) { - let list = ""; + let list = '' for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += " " + finalDelimiter + " "; + list += ' ' + finalDelimiter + ' ' } else { - list += delimiter + " "; + list += delimiter + ' ' } } - list += array[i]; + list += array[i] } - return list; - }; + return list + } callback = callback || - function(o) { - return o.toString(); - }; - const self = this; - const realCallback = function(arg: ByWeekday) { - return callback && callback.call(self, arg); - }; + function (o) { + return o.toString() + } + const self = this + const realCallback = function (arg: ByWeekday) { + return callback && callback.call(self, arg) + } if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim); + return delimJoin(arr.map(realCallback), delim, finalDelim) } else { - return arr.map(realCallback).join(delim + " "); + return arr.map(realCallback).join(delim + ' ') } } } From f371f58af191c641e88dffd573fd003ea678639a Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 12:11:45 +0100 Subject: [PATCH 09/23] Upgrade weekly case --- src/nlp/i18n.ts | 66 ++--- src/nlp/parsetext.ts | 578 ++++++++++++++++++++++--------------------- src/nlp/totext.ts | 500 +++++++++++++++++++------------------ test/nlp.test.ts | 23 +- 4 files changed, 606 insertions(+), 561 deletions(-) diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index b9237eae..f0cc749c 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -3,49 +3,49 @@ // ============================================================================= export interface Language { - dayNames: string[] - monthNames: string[] + dayNames: string[]; + monthNames: string[]; tokens: { [k: string]: RegExp; - } + }; } const ENGLISH: Language = { dayNames: [ - 'Sunday', - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday' + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" ], monthNames: [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" ], tokens: { SKIP: /^[ \r\n\t]+|^\.$/, - number: /^[1-9][0-9]*/, + number: /^[0-9][0-9]*/, numberAsText: /^(one|two|three)/i, every: /^every/i, - 'day(s)': /^days?/i, - 'weekday(s)': /^weekdays?/i, - 'week(s)': /^weeks?/i, - 'hour(s)': /^hours?/i, - 'minute(s)': /^minutes?/i, - 'month(s)': /^months?/i, - 'year(s)': /^years?/i, + "day(s)": /^days?/i, + "weekday(s)": /^weekdays?/i, + "week(s)": /^weeks?/i, + "hour(s)": /^hours?/i, + "minute(s)": /^minutes?/i, + "month(s)": /^months?/i, + "year(s)": /^years?/i, on: /^(on|in)/i, at: /^(at)/i, the: /^the/i, @@ -55,7 +55,7 @@ const ENGLISH: Language = { nth: /^([1-9][0-9]*)(\.|th|nd|rd|st)/i, last: /^last/i, for: /^for/i, - 'time(s)': /^times?/i, + "time(s)": /^times?/i, until: /^(un)?til/i, monday: /^mo(n(day)?)?/i, tuesday: /^tu(e(s(day)?)?)?/i, @@ -79,6 +79,6 @@ const ENGLISH: Language = { comma: /^(,\s*|(and|or)\s*)+/i, colon: /^(\s*:\s*)/i } -} +}; -export default ENGLISH +export default ENGLISH; diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 3fe0972f..8f060168 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,291 +1,293 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options } from '../types' -import { WeekdayStr } from '../weekday' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options } from "../types"; +import { WeekdayStr } from "../weekday"; // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp } - public text: string - public symbol: string | null - public value: RegExpExecArray | null - private done = true - - constructor (rules: { [k: string]: RegExp }) { - this.rules = rules + private readonly rules: { [k: string]: RegExp }; + public text: string; + public symbol: string | null; + public value: RegExpExecArray | null; + private done = true; + + constructor(rules: { [k: string]: RegExp }) { + this.rules = rules; } - start (text: string) { - this.text = text - this.done = false - return this.nextSymbol() + start(text: string) { + this.text = text; + this.done = false; + return this.nextSymbol(); } - isDone () { - return this.done && this.symbol === null + isDone() { + return this.done && this.symbol === null; } - nextSymbol () { - let best: RegExpExecArray | null - let bestSymbol: string - const p = this + nextSymbol() { + let best: RegExpExecArray | null; + let bestSymbol: string; + const p = this; - this.symbol = null - this.value = null + this.symbol = null; + this.value = null; do { - if (this.done) return false + if (this.done) return false; - let rule: RegExp - best = null + let rule: RegExp; + best = null; for (let name in this.rules) { - rule = this.rules[name] - const match = rule.exec(p.text) + rule = this.rules[name]; + const match = rule.exec(p.text); if (match) { if (best === null || match[0].length > best[0].length) { - best = match - bestSymbol = name + best = match; + bestSymbol = name; } } } if (best != null) { - this.text = this.text.substr(best[0].length) + this.text = this.text.substr(best[0].length); - if (this.text === '') this.done = true + if (this.text === "") this.done = true; } if (best == null) { - this.done = true - this.symbol = null - this.value = null - return + this.done = true; + this.symbol = null; + this.value = null; + return; } // @ts-ignore - } while (bestSymbol === 'SKIP') + } while (bestSymbol === "SKIP"); // @ts-ignore - this.symbol = bestSymbol - this.value = best - return true + this.symbol = bestSymbol; + this.value = best; + return true; } - accept (name: string) { + accept(name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value - this.nextSymbol() - return v + const v = this.value; + this.nextSymbol(); + return v; } - this.nextSymbol() - return true + this.nextSymbol(); + return true; } - return false + return false; } - acceptNumber () { - return this.accept('number') as RegExpExecArray + acceptNumber() { + return this.accept("number") as RegExpExecArray; } - expect (name: string) { - if (this.accept(name)) return true + expect(name: string) { + if (this.accept(name)) return true; - throw new Error('expected ' + name + ' but found ' + this.symbol) + throw new Error("expected " + name + " but found " + this.symbol); } } -export default function parseText (text: string, language: Language = ENGLISH) { - const options: Partial = {} - const ttr = new Parser(language.tokens) +export default function parseText(text: string, language: Language = ENGLISH) { + const options: Partial = {}; + const ttr = new Parser(language.tokens); - if (!ttr.start(text)) return null + if (!ttr.start(text)) return null; - S() - return options + S(); + return options; - function S () { + function S() { // every [n] - ttr.expect('every') - let n = ttr.acceptNumber() - if (n) options.interval = parseInt(n[0], 10) - if (ttr.isDone()) throw new Error('Unexpected end') + ttr.expect("every"); + let n = ttr.acceptNumber(); + if (n) options.interval = parseInt(n[0], 10); + if (ttr.isDone()) throw new Error("Unexpected end"); switch (ttr.symbol) { - case 'day(s)': - options.freq = RRule.DAILY + case "day(s)": + options.freq = RRule.DAILY; if (ttr.nextSymbol()) { - AT() - C() - F() + AT(); + COLON(); + F(); } - break + break; // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case 'weekday(s)': - options.freq = RRule.WEEKLY - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] - ttr.nextSymbol() - F() - break - - case 'week(s)': - options.freq = RRule.WEEKLY + case "weekday(s)": + options.freq = RRule.WEEKLY; + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; + ttr.nextSymbol(); + AT(); + COLON(); + F(); + break; + + case "week(s)": + options.freq = RRule.WEEKLY; if (ttr.nextSymbol()) { - ON() - AT() - C() - F() + ON(); + AT(); + COLON(); + F(); } - break + break; - case 'hour(s)': - options.freq = RRule.HOURLY + case "hour(s)": + options.freq = RRule.HOURLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'minute(s)': - options.freq = RRule.MINUTELY + case "minute(s)": + options.freq = RRule.MINUTELY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break + break; - case 'month(s)': - options.freq = RRule.MONTHLY + case "month(s)": + options.freq = RRule.MONTHLY; if (ttr.nextSymbol()) { - ON() - AT() - C() - F() + ON(); + AT(); + COLON(); + F(); } - break + break; - case 'year(s)': - options.freq = RRule.YEARLY + case "year(s)": + options.freq = RRule.YEARLY; if (ttr.nextSymbol()) { - ON() - F() + ON(); + F(); } - break - - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - options.freq = RRule.WEEKLY + break; + + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + options.freq = RRule.WEEKLY; const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr - options.byweekday = [RRule[key]] + .toUpperCase() as WeekdayStr; + options.byweekday = [RRule[key]]; - if (!ttr.nextSymbol()) return + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let wkd = decodeWKD() as keyof typeof RRule + let wkd = decodeWKD() as keyof typeof RRule; if (!wkd) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected weekday' - ) + "Unexpected symbol " + ttr.symbol + ", expected weekday" + ); } // @ts-ignore - options.byweekday.push(RRule[wkd]) - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + ttr.nextSymbol(); } - MDAYs() - F() - break - - case 'january': - case 'february': - case 'march': - case 'april': - case 'may': - case 'june': - case 'july': - case 'august': - case 'september': - case 'october': - case 'november': - case 'december': - options.freq = RRule.YEARLY - options.bymonth = [decodeM() as number] - - if (!ttr.nextSymbol()) return + MDAYs(); + F(); + break; + + case "january": + case "february": + case "march": + case "april": + case "may": + case "june": + case "july": + case "august": + case "september": + case "october": + case "november": + case "december": + options.freq = RRule.YEARLY; + options.bymonth = [decodeM() as number]; + + if (!ttr.nextSymbol()) return; // TODO check for duplicates - while (ttr.accept('comma')) { - if (ttr.isDone()) throw new Error('Unexpected end') + while (ttr.accept("comma")) { + if (ttr.isDone()) throw new Error("Unexpected end"); - let m = decodeM() + let m = decodeM(); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected month' - ) + "Unexpected symbol " + ttr.symbol + ", expected month" + ); } - options.bymonth.push(m) - ttr.nextSymbol() + options.bymonth.push(m); + ttr.nextSymbol(); } - ON() - F() - break + ON(); + F(); + break; default: - throw new Error('Unknown symbol') + throw new Error("Unknown symbol"); } } - function ON () { - const on = ttr.accept('on') - const the = ttr.accept('the') - if (!(on || the)) return + function ON() { + const on = ttr.accept("on"); + const the = ttr.accept("the"); + if (!(on || the)) return; do { - let nth = decodeNTH() - let wkd = decodeWKD() - let m = decodeM() + let nth = decodeNTH(); + let wkd = decodeWKD(); + let m = decodeM(); // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)) + options.byweekday.push(RRule[wkd].nth(nth)); } else { - if (!options.bymonthday) options.bymonthday = [] + if (!options.bymonthday) options.bymonthday = []; // @ts-ignore - options.bymonthday.push(nth) - ttr.accept('day(s)') + options.bymonthday.push(nth); + ttr.accept("day(s)"); } // } else if (wkd) { - ttr.nextSymbol() - if (!options.byweekday) options.byweekday = [] + ttr.nextSymbol(); + if (!options.byweekday) options.byweekday = []; // @ts-ignore - options.byweekday.push(RRule[wkd]) - } else if (ttr.symbol === 'weekday(s)') { - ttr.nextSymbol() + options.byweekday.push(RRule[wkd]); + } else if (ttr.symbol === "weekday(s)") { + ttr.nextSymbol(); if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -293,179 +295,179 @@ export default function parseText (text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ] + ]; } - } else if (ttr.symbol === 'week(s)') { - ttr.nextSymbol() - let n = ttr.acceptNumber() + } else if (ttr.symbol === "week(s)") { + ttr.nextSymbol(); + let n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected week number' - ) + "Unexpected symbol " + ttr.symbol + ", expected week number" + ); } - options.byweekno = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byweekno = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.byweekno.push(parseInt(n[0], 10)) + options.byweekno.push(parseInt(n[0], 10)); } } else if (m) { - ttr.nextSymbol() - if (!options.bymonth) options.bymonth = [] + ttr.nextSymbol(); + if (!options.bymonth) options.bymonth = []; // @ts-ignore - options.bymonth.push(m) + options.bymonth.push(m); } else { - return + return; } - } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) + } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); } - function AT () { - const at = ttr.accept('at') - if (!at) return + function AT() { + const at = ttr.accept("at"); + if (!at) return; do { - let n = ttr.acceptNumber() + let n = ttr.acceptNumber(); if (!n) { - throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') + throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); } - options.byhour = [parseInt(n[0], 10)] - while (ttr.accept('comma')) { - n = ttr.acceptNumber() + options.byhour = [parseInt(n[0], 10)]; + while (ttr.accept("comma")) { + n = ttr.acceptNumber(); if (!n) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected hour' - ) + "Unexpected symbol " + ttr.symbol + "; expected hour" + ); } - options.byhour.push(parseInt(n[0], 10)) + options.byhour.push(parseInt(n[0], 10)); } - } while (ttr.accept('comma') || ttr.accept('at')) + } while (ttr.accept("comma") || ttr.accept("at")); } - function C () { - const colon = ttr.accept('colon') - if (!colon) return + function COLON() { + const colon = ttr.accept("colon"); + if (!colon) return; do { - let m = ttr.acceptNumber() + let m = parseInt(ttr.acceptNumber()![0], 10); if (!m) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + ', expected minutes' - ) + "Unexpected symbol " + ttr.symbol + ", expected minutes" + ); } - options.byminute = parseInt(m[0], 10) - } while (ttr.accept('colon')) + options.byminute = m; + } while (ttr.accept("colon")); } - function decodeM () { + function decodeM() { switch (ttr.symbol) { - case 'january': - return 1 - case 'february': - return 2 - case 'march': - return 3 - case 'april': - return 4 - case 'may': - return 5 - case 'june': - return 6 - case 'july': - return 7 - case 'august': - return 8 - case 'september': - return 9 - case 'october': - return 10 - case 'november': - return 11 - case 'december': - return 12 + case "january": + return 1; + case "february": + return 2; + case "march": + return 3; + case "april": + return 4; + case "may": + return 5; + case "june": + return 6; + case "july": + return 7; + case "august": + return 8; + case "september": + return 9; + case "october": + return 10; + case "november": + return 11; + case "december": + return 12; default: - return false + return false; } } - function decodeWKD () { + function decodeWKD() { switch (ttr.symbol) { - case 'monday': - case 'tuesday': - case 'wednesday': - case 'thursday': - case 'friday': - case 'saturday': - case 'sunday': - return ttr.symbol.substr(0, 2).toUpperCase() + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + return ttr.symbol.substr(0, 2).toUpperCase(); default: - return false + return false; } } - function decodeNTH () { + function decodeNTH() { switch (ttr.symbol) { - case 'last': - ttr.nextSymbol() - return -1 - case 'first': - ttr.nextSymbol() - return 1 - case 'second': - ttr.nextSymbol() - return ttr.accept('last') ? -2 : 2 - case 'third': - ttr.nextSymbol() - return ttr.accept('last') ? -3 : 3 - case 'nth': - const v = parseInt(ttr.value![1], 10) - if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) - - ttr.nextSymbol() - return ttr.accept('last') ? -v : v + case "last": + ttr.nextSymbol(); + return -1; + case "first": + ttr.nextSymbol(); + return 1; + case "second": + ttr.nextSymbol(); + return ttr.accept("last") ? -2 : 2; + case "third": + ttr.nextSymbol(); + return ttr.accept("last") ? -3 : 3; + case "nth": + const v = parseInt(ttr.value![1], 10); + if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); + + ttr.nextSymbol(); + return ttr.accept("last") ? -v : v; default: - return false + return false; } } - function MDAYs () { - ttr.accept('on') - ttr.accept('the') + function MDAYs() { + ttr.accept("on"); + ttr.accept("the"); - let nth = decodeNTH() - if (!nth) return + let nth = decodeNTH(); + if (!nth) return; - options.bymonthday = [nth] - ttr.nextSymbol() + options.bymonthday = [nth]; + ttr.nextSymbol(); - while (ttr.accept('comma')) { - nth = decodeNTH() + while (ttr.accept("comma")) { + nth = decodeNTH(); if (!nth) { throw new Error( - 'Unexpected symbol ' + ttr.symbol + '; expected monthday' - ) + "Unexpected symbol " + ttr.symbol + "; expected monthday" + ); } - options.bymonthday.push(nth) - ttr.nextSymbol() + options.bymonthday.push(nth); + ttr.nextSymbol(); } } - function F () { - if (ttr.symbol === 'until') { - const date = Date.parse(ttr.text) + function F() { + if (ttr.symbol === "until") { + const date = Date.parse(ttr.text); - if (!date) throw new Error('Cannot parse until date:' + ttr.text) - options.until = new Date(date) - } else if (ttr.accept('for')) { - options.count = parseInt(ttr.value![0], 10) - ttr.expect('number') + if (!date) throw new Error("Cannot parse until date:" + ttr.text); + options.until = new Date(date); + } else if (ttr.accept("for")) { + options.count = parseInt(ttr.value![0], 10); + ttr.expect("number"); // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 44065be0..64f7ded6 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options, ByWeekday } from '../types' -import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent, padStart } from '../helpers' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options, ByWeekday } from "../types"; +import { Weekday } from "../weekday"; +import { isArray, isNumber, isPresent, padStart } from "../helpers"; // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from '../helpers' /** * Return true if a value is in an array */ -const contains = function (arr: string[], val: string) { - return arr.indexOf(val) !== -1 -} +const contains = function(arr: string[], val: string) { + return arr.indexOf(val) !== -1; +}; // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string +export type GetText = (id: string | number | Weekday) => string; -const defaultGetText: GetText = id => id.toString() +const defaultGetText: GetText = id => id.toString(); export type DateFormatter = ( year: number, month: string, day: number -) => string +) => string; const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}` +) => `${month} ${day}, ${year}`; /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][] - private rrule: RRule - private text: string[] - private gettext: GetText - private dateFormatter: DateFormatter - private language: Language - private options: Partial - private origOptions: Partial - private bymonthday: Options['bymonthday'] | null + static IMPLEMENTED: string[][]; + private rrule: RRule; + private text: string[]; + private gettext: GetText; + private dateFormatter: DateFormatter; + private language: Language; + private options: Partial; + private origOptions: Partial; + private bymonthday: Options["bymonthday"] | null; private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null + } | null; - constructor ( + constructor( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = [] - this.language = language || ENGLISH - this.gettext = gettext - this.dateFormatter = dateFormatter - this.rrule = rrule - this.options = rrule.options - this.origOptions = rrule.origOptions + this.text = []; + this.language = language || ENGLISH; + this.gettext = gettext; + this.dateFormatter = dateFormatter; + this.rrule = rrule; + this.options = rrule.options; + this.origOptions = rrule.origOptions; if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!) - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) + const bymonthday = ([] as number[]).concat(this.options.bymonthday!); + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); - bymonthday.sort((a, b) => a - b) - bynmonthday.sort((a, b) => b - a) + bymonthday.sort((a, b) => a - b); + bynmonthday.sort((a, b) => b - a); // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday) - if (!this.bymonthday.length) this.bymonthday = null + this.bymonthday = bymonthday.concat(bynmonthday); + if (!this.bymonthday.length) this.bymonthday = null; } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday - const days = String(byweekday) + : this.origOptions.byweekday; + const days = String(byweekday); this.byweekday = { - allWeeks: byweekday.filter(function (weekday: Weekday) { - return !weekday.n + allWeeks: byweekday.filter(function(weekday: Weekday) { + return !weekday.n; }), - someWeeks: byweekday.filter(function (weekday: Weekday) { - return Boolean(weekday.n) + someWeeks: byweekday.filter(function(weekday: Weekday) { + return Boolean(weekday.n); }), isWeekdays: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') === -1 && - days.indexOf('SU') === -1, + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") === -1 && + days.indexOf("SU") === -1, isEveryDay: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') !== -1 && - days.indexOf('SU') !== -1 - } - - const sortWeekDays = function (a: Weekday, b: Weekday) { - return a.weekday - b.weekday - } - - this.byweekday.allWeeks!.sort(sortWeekDays) - this.byweekday.someWeeks!.sort(sortWeekDays) - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") !== -1 && + days.indexOf("SU") !== -1 + }; + + const sortWeekDays = function(a: Weekday, b: Weekday) { + return a.weekday - b.weekday; + }; + + this.byweekday.allWeeks!.sort(sortWeekDays); + this.byweekday.someWeeks!.sort(sortWeekDays); + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; } else { - this.byweekday = null + this.byweekday = null; } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible (rrule: RRule) { - let canConvert = true + static isFullyConvertible(rrule: RRule) { + let canConvert = true; - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false - if (rrule.origOptions.until && rrule.origOptions.count) return false + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; + if (rrule.origOptions.until && rrule.origOptions.count) return false; for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false + if (contains(["dtstart", "wkst", "freq"], key)) return true; + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; } - return canConvert + return canConvert; } - isFullyConvertible () { - return ToText.isFullyConvertible(this.rrule) + isFullyConvertible() { + return ToText.isFullyConvertible(this.rrule); } /** @@ -159,372 +159,394 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString () { - const gettext = this.gettext + toString() { + const gettext = this.gettext; if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext('RRule error: Unable to fully convert this rrule to text') + return gettext("RRule error: Unable to fully convert this rrule to text"); } - this.text = [gettext('every')] + this.text = [gettext("every")]; // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]]() + this[RRule.FREQUENCIES[this.options.freq]](); if (this.options.until) { - this.add(gettext('until')) - const until = this.options.until + this.add(gettext("until")); + const until = this.options.until; this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ) + ); } else if (this.options.count) { - this.add(gettext('for')) + this.add(gettext("for")) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext('times') : gettext('time') - ) + this.plural(this.options.count) ? gettext("times") : gettext("time") + ); } - if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) + if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); - return this.text.join('') + return this.text.join(""); } - HOURLY () { - const gettext = this.gettext + HOURLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') - ) + this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") + ); } - MINUTELY () { - const gettext = this.gettext + MINUTELY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('minutes') - : gettext('minutes') - ) + ? gettext("minutes") + : gettext("minutes") + ); } - DAILY () { - const gettext = this.gettext + DAILY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - WEEKLY () { - const gettext = this.gettext + WEEKLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') - ) + this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") + ); } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); + if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); + } } else { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); + if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); + } } else { - if (this.options.interval === 1) this.add(gettext('week')) + if (this.options.interval === 1) this.add(gettext("week")); if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); + if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); + } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } } - MONTHLY () { - const gettext = this.gettext + MONTHLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext('months')) - if (this.plural(this.options.interval!)) this.add(gettext('in')) + this.add(this.options.interval!.toString()).add(gettext("months")); + if (this.plural(this.options.interval!)) this.add(gettext("in")); } else { // this.add(gettext('MONTH')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('months') - : gettext('month') - ) + ? gettext("months") + : gettext("month") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - YEARLY () { - const gettext = this.gettext + YEARLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) - this.add(gettext('years')) + this.add(this.options.interval!.toString()); + this.add(gettext("years")); } else { // this.add(gettext('YEAR')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('years') : gettext('year') - ) + this.plural(this.options.interval!) ? gettext("years") : gettext("year") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } if (this.options.byyearday) { - this.add(gettext('on the')) - .add(this.list(this.options.byyearday, this.nth, gettext('and'))) - .add(gettext('day')) + this.add(gettext("on the")) + .add(this.list(this.options.byyearday, this.nth, gettext("and"))) + .add(gettext("day")); } if (this.options.byweekno) { - this.add(gettext('in')) + this.add(gettext("in")) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext('weeks') - : gettext('week') + ? gettext("weeks") + : gettext("week") ) - .add(this.list(this.options.byweekno, undefined, gettext('and'))) + .add(this.list(this.options.byweekno, undefined, gettext("and"))); } } - private _bymonthday () { - const gettext = this.gettext + private _bymonthday() { + const gettext = this.gettext; if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext('on')) + this.add(gettext("on")) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) ) - .add(gettext('the')) - .add(this.list(this.bymonthday!, this.nth, gettext('or'))) + .add(gettext("the")) + .add(this.list(this.bymonthday!, this.nth, gettext("or"))); } else { - this.add(gettext('on the')).add( - this.list(this.bymonthday!, this.nth, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.bymonthday!, this.nth, gettext("and")) + ); } // this.add(gettext('DAY')) } - private _byweekday () { - const gettext = this.gettext + private _byweekday() { + const gettext = this.gettext; if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext('on')).add( + this.add(gettext("on")).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ) + ); } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext('and')) + if (this.byweekday!.allWeeks) this.add(gettext("and")); - this.add(gettext('on the')).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) + ); } } - private _byhour () { - const gettext = this.gettext + private _byhour() { + const gettext = this.gettext; - this.add(gettext('at')).add( - this.list(this.origOptions.byhour!, undefined, gettext('and')) - ) + this.add(gettext("at")).add( + this.list(this.origOptions.byhour!, undefined, gettext("and")) + ); } - private _byminute () { - const gettext = this.gettext + private _byminute() { + const gettext = this.gettext; + + function returnminutes(minutes: string): string { + if (minutes.length > 1) return minutes; + return `0${minutes}`; + } - this.add(gettext('at')).add( - `${this.origOptions.byhour!.toString()}:${this.origOptions.byminute!.toString()}` - ) + this.add(gettext("at")).add( + `${this.origOptions.byhour!.toString()}:${returnminutes( + this.origOptions.byminute!.toString() + )}` + ); } - private _bymonth () { + private _bymonth() { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) - ) + this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) + ); } - nth (n: number | string) { - n = parseInt(n.toString(), 10) - let nth: string - let npos: number - const gettext = this.gettext + nth(n: number | string) { + n = parseInt(n.toString(), 10); + let nth: string; + let npos: number; + const gettext = this.gettext; - if (n === -1) return gettext('last') + if (n === -1) return gettext("last"); - npos = Math.abs(n) + npos = Math.abs(n); switch (npos) { case 1: case 21: case 31: - nth = npos + gettext('st') - break + nth = npos + gettext("st"); + break; case 2: case 22: - nth = npos + gettext('nd') - break + nth = npos + gettext("nd"); + break; case 3: case 23: - nth = npos + gettext('rd') - break + nth = npos + gettext("rd"); + break; default: - nth = npos + gettext('th') + nth = npos + gettext("th"); } - return n < 0 ? nth + ' ' + gettext('last') : nth + return n < 0 ? nth + " " + gettext("last") : nth; } - monthtext (m: number) { - return this.language.monthNames[m - 1] + monthtext(m: number) { + return this.language.monthNames[m - 1]; } - weekdaytext (wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() + weekdaytext(wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + this.language.dayNames[weekday] - ) + ); } - plural (n: number) { - return n % 100 !== 1 + plural(n: number) { + return n % 100 !== 1; } - add (s: string) { - this.text.push(' ') - this.text.push(s) - return this + add(s: string) { + this.text.push(" "); + this.text.push(s); + return this; } - list ( + list( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = ',' + delim: string = "," ) { if (!isArray(arr)) { - arr = [arr] + arr = [arr]; } - const delimJoin = function ( + const delimJoin = function( array: string[], delimiter: string, finalDelimiter: string ) { - let list = '' + let list = ""; for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += ' ' + finalDelimiter + ' ' + list += " " + finalDelimiter + " "; } else { - list += delimiter + ' ' + list += delimiter + " "; } } - list += array[i] + list += array[i]; } - return list - } + return list; + }; callback = callback || - function (o) { - return o.toString() - } - const self = this - const realCallback = function (arg: ByWeekday) { - return callback && callback.call(self, arg) - } + function(o) { + return o.toString(); + }; + const self = this; + const realCallback = function(arg: ByWeekday) { + return callback && callback.call(self, arg); + }; if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim) + return delimJoin(arr.map(realCallback), delim, finalDelim); } else { - return arr.map(realCallback).join(delim + ' ') + return arr.map(realCallback).join(delim + " "); } } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 3b9a9ee0..998b79f7 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -7,15 +7,36 @@ import { DateFormatter } from "../src/nlp/totext"; const texts = [ ["Every day", "RRULE:FREQ=DAILY"], ["Every day at 10, 12 and 17", "RRULE:FREQ=DAILY;BYHOUR=10,12,17"], - ["Every day at 10:30", "RRULE:FREQ=DAILY;BYHOUR=10;BYMINUTE=30"], + ["Every day at 10:05", "RRULE:FREQ=DAILY;BYHOUR=10;BYMINUTE=5"], + ["Every day at 10:27", "RRULE:FREQ=DAILY;BYHOUR=10;BYMINUTE=27"], ["Every week", "RRULE:FREQ=WEEKLY"], ["Every week at 10:30", "RRULE:FREQ=WEEKLY;BYHOUR=10;BYMINUTE=30"], ["Every hour", "RRULE:FREQ=HOURLY"], ["Every 4 hours", "RRULE:INTERVAL=4;FREQ=HOURLY"], ["Every week on Tuesday", "RRULE:FREQ=WEEKLY;BYDAY=TU"], + [ + "Every week on Tuesday at 10:42", + "RRULE:FREQ=WEEKLY;BYDAY=TU;BYHOUR=10;BYMINUTE=42" + ], + [ + "Every week on Tuesday at 10:07", + "RRULE:FREQ=WEEKLY;BYDAY=TU;BYHOUR=10;BYMINUTE=7" + ], ["Every week on Monday, Wednesday", "RRULE:FREQ=WEEKLY;BYDAY=MO,WE"], + [ + "Every week on Monday, Wednesday at 10:07", + "RRULE:FREQ=WEEKLY;BYDAY=MO,WE;BYHOUR=10;BYMINUTE=7" + ], ["Every weekday", "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"], + [ + "Every weekday at 10:07", + "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=10;BYMINUTE=7" + ], ["Every 2 weeks", "RRULE:INTERVAL=2;FREQ=WEEKLY"], + [ + "Every 2 weeks at 10:07", + "RRULE:INTERVAL=2;FREQ=WEEKLY;BYHOUR=10;BYMINUTE=7" + ], ["Every month", "RRULE:FREQ=MONTHLY"], ["Every month at 10:30", "RRULE:FREQ=MONTHLY;BYHOUR=10;BYMINUTE=30"], ["Every 6 months", "RRULE:INTERVAL=6;FREQ=MONTHLY"], From d078c0216677af20cb881e0cb794a22e66e6f9fd Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 12:12:04 +0100 Subject: [PATCH 10/23] Upgrade weekly case --- src/nlp/i18n.ts | 64 ++--- src/nlp/parsetext.ts | 580 +++++++++++++++++++++---------------------- src/nlp/totext.ts | 494 ++++++++++++++++++------------------ 3 files changed, 569 insertions(+), 569 deletions(-) diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index f0cc749c..738eedb0 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -3,49 +3,49 @@ // ============================================================================= export interface Language { - dayNames: string[]; - monthNames: string[]; + dayNames: string[] + monthNames: string[] tokens: { [k: string]: RegExp; - }; + } } const ENGLISH: Language = { dayNames: [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' ], monthNames: [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' ], tokens: { SKIP: /^[ \r\n\t]+|^\.$/, number: /^[0-9][0-9]*/, numberAsText: /^(one|two|three)/i, every: /^every/i, - "day(s)": /^days?/i, - "weekday(s)": /^weekdays?/i, - "week(s)": /^weeks?/i, - "hour(s)": /^hours?/i, - "minute(s)": /^minutes?/i, - "month(s)": /^months?/i, - "year(s)": /^years?/i, + 'day(s)': /^days?/i, + 'weekday(s)': /^weekdays?/i, + 'week(s)': /^weeks?/i, + 'hour(s)': /^hours?/i, + 'minute(s)': /^minutes?/i, + 'month(s)': /^months?/i, + 'year(s)': /^years?/i, on: /^(on|in)/i, at: /^(at)/i, the: /^the/i, @@ -55,7 +55,7 @@ const ENGLISH: Language = { nth: /^([1-9][0-9]*)(\.|th|nd|rd|st)/i, last: /^last/i, for: /^for/i, - "time(s)": /^times?/i, + 'time(s)': /^times?/i, until: /^(un)?til/i, monday: /^mo(n(day)?)?/i, tuesday: /^tu(e(s(day)?)?)?/i, @@ -79,6 +79,6 @@ const ENGLISH: Language = { comma: /^(,\s*|(and|or)\s*)+/i, colon: /^(\s*:\s*)/i } -}; +} -export default ENGLISH; +export default ENGLISH diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index 8f060168..bd7a155b 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -1,293 +1,293 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options } from "../types"; -import { WeekdayStr } from "../weekday"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options } from '../types' +import { WeekdayStr } from '../weekday' // ============================================================================= // Parser // ============================================================================= class Parser { - private readonly rules: { [k: string]: RegExp }; - public text: string; - public symbol: string | null; - public value: RegExpExecArray | null; - private done = true; - - constructor(rules: { [k: string]: RegExp }) { - this.rules = rules; + private readonly rules: { [k: string]: RegExp } + public text: string + public symbol: string | null + public value: RegExpExecArray | null + private done = true + + constructor (rules: { [k: string]: RegExp }) { + this.rules = rules } - start(text: string) { - this.text = text; - this.done = false; - return this.nextSymbol(); + start (text: string) { + this.text = text + this.done = false + return this.nextSymbol() } - isDone() { - return this.done && this.symbol === null; + isDone () { + return this.done && this.symbol === null } - nextSymbol() { - let best: RegExpExecArray | null; - let bestSymbol: string; - const p = this; + nextSymbol () { + let best: RegExpExecArray | null + let bestSymbol: string + const p = this - this.symbol = null; - this.value = null; + this.symbol = null + this.value = null do { - if (this.done) return false; + if (this.done) return false - let rule: RegExp; - best = null; + let rule: RegExp + best = null for (let name in this.rules) { - rule = this.rules[name]; - const match = rule.exec(p.text); + rule = this.rules[name] + const match = rule.exec(p.text) if (match) { if (best === null || match[0].length > best[0].length) { - best = match; - bestSymbol = name; + best = match + bestSymbol = name } } } if (best != null) { - this.text = this.text.substr(best[0].length); + this.text = this.text.substr(best[0].length) - if (this.text === "") this.done = true; + if (this.text === '') this.done = true } if (best == null) { - this.done = true; - this.symbol = null; - this.value = null; - return; + this.done = true + this.symbol = null + this.value = null + return } // @ts-ignore - } while (bestSymbol === "SKIP"); + } while (bestSymbol === 'SKIP') // @ts-ignore - this.symbol = bestSymbol; - this.value = best; - return true; + this.symbol = bestSymbol + this.value = best + return true } - accept(name: string) { + accept (name: string) { if (this.symbol === name) { if (this.value) { - const v = this.value; - this.nextSymbol(); - return v; + const v = this.value + this.nextSymbol() + return v } - this.nextSymbol(); - return true; + this.nextSymbol() + return true } - return false; + return false } - acceptNumber() { - return this.accept("number") as RegExpExecArray; + acceptNumber () { + return this.accept('number') as RegExpExecArray } - expect(name: string) { - if (this.accept(name)) return true; + expect (name: string) { + if (this.accept(name)) return true - throw new Error("expected " + name + " but found " + this.symbol); + throw new Error('expected ' + name + ' but found ' + this.symbol) } } -export default function parseText(text: string, language: Language = ENGLISH) { - const options: Partial = {}; - const ttr = new Parser(language.tokens); +export default function parseText (text: string, language: Language = ENGLISH) { + const options: Partial = {} + const ttr = new Parser(language.tokens) - if (!ttr.start(text)) return null; + if (!ttr.start(text)) return null - S(); - return options; + S() + return options - function S() { + function S () { // every [n] - ttr.expect("every"); - let n = ttr.acceptNumber(); - if (n) options.interval = parseInt(n[0], 10); - if (ttr.isDone()) throw new Error("Unexpected end"); + ttr.expect('every') + let n = ttr.acceptNumber() + if (n) options.interval = parseInt(n[0], 10) + if (ttr.isDone()) throw new Error('Unexpected end') switch (ttr.symbol) { - case "day(s)": - options.freq = RRule.DAILY; + case 'day(s)': + options.freq = RRule.DAILY if (ttr.nextSymbol()) { - AT(); - COLON(); - F(); + AT() + COLON() + F() } - break; + break // FIXME Note: every 2 weekdays != every two weeks on weekdays. // DAILY on weekdays is not a valid rule - case "weekday(s)": - options.freq = RRule.WEEKLY; - options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR]; - ttr.nextSymbol(); - AT(); - COLON(); - F(); - break; - - case "week(s)": - options.freq = RRule.WEEKLY; + case 'weekday(s)': + options.freq = RRule.WEEKLY + options.byweekday = [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] + ttr.nextSymbol() + AT() + COLON() + F() + break + + case 'week(s)': + options.freq = RRule.WEEKLY if (ttr.nextSymbol()) { - ON(); - AT(); - COLON(); - F(); + ON() + AT() + COLON() + F() } - break; + break - case "hour(s)": - options.freq = RRule.HOURLY; + case 'hour(s)': + options.freq = RRule.HOURLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "minute(s)": - options.freq = RRule.MINUTELY; + case 'minute(s)': + options.freq = RRule.MINUTELY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; + break - case "month(s)": - options.freq = RRule.MONTHLY; + case 'month(s)': + options.freq = RRule.MONTHLY if (ttr.nextSymbol()) { - ON(); - AT(); - COLON(); - F(); + ON() + AT() + COLON() + F() } - break; + break - case "year(s)": - options.freq = RRule.YEARLY; + case 'year(s)': + options.freq = RRule.YEARLY if (ttr.nextSymbol()) { - ON(); - F(); + ON() + F() } - break; - - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - options.freq = RRule.WEEKLY; + break + + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + options.freq = RRule.WEEKLY const key: WeekdayStr = ttr.symbol .substr(0, 2) - .toUpperCase() as WeekdayStr; - options.byweekday = [RRule[key]]; + .toUpperCase() as WeekdayStr + options.byweekday = [RRule[key]] - if (!ttr.nextSymbol()) return; + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let wkd = decodeWKD() as keyof typeof RRule; + let wkd = decodeWKD() as keyof typeof RRule if (!wkd) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected weekday" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected weekday' + ) } // @ts-ignore - options.byweekday.push(RRule[wkd]); - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + ttr.nextSymbol() } - MDAYs(); - F(); - break; - - case "january": - case "february": - case "march": - case "april": - case "may": - case "june": - case "july": - case "august": - case "september": - case "october": - case "november": - case "december": - options.freq = RRule.YEARLY; - options.bymonth = [decodeM() as number]; - - if (!ttr.nextSymbol()) return; + MDAYs() + F() + break + + case 'january': + case 'february': + case 'march': + case 'april': + case 'may': + case 'june': + case 'july': + case 'august': + case 'september': + case 'october': + case 'november': + case 'december': + options.freq = RRule.YEARLY + options.bymonth = [decodeM() as number] + + if (!ttr.nextSymbol()) return // TODO check for duplicates - while (ttr.accept("comma")) { - if (ttr.isDone()) throw new Error("Unexpected end"); + while (ttr.accept('comma')) { + if (ttr.isDone()) throw new Error('Unexpected end') - let m = decodeM(); + let m = decodeM() if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected month" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected month' + ) } - options.bymonth.push(m); - ttr.nextSymbol(); + options.bymonth.push(m) + ttr.nextSymbol() } - ON(); - F(); - break; + ON() + F() + break default: - throw new Error("Unknown symbol"); + throw new Error('Unknown symbol') } } - function ON() { - const on = ttr.accept("on"); - const the = ttr.accept("the"); - if (!(on || the)) return; + function ON () { + const on = ttr.accept('on') + const the = ttr.accept('the') + if (!(on || the)) return do { - let nth = decodeNTH(); - let wkd = decodeWKD(); - let m = decodeM(); + let nth = decodeNTH() + let wkd = decodeWKD() + let m = decodeM() // nth | if (nth) { // ttr.nextSymbol() if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd].nth(nth)); + options.byweekday.push(RRule[wkd].nth(nth)) } else { - if (!options.bymonthday) options.bymonthday = []; + if (!options.bymonthday) options.bymonthday = [] // @ts-ignore - options.bymonthday.push(nth); - ttr.accept("day(s)"); + options.bymonthday.push(nth) + ttr.accept('day(s)') } // } else if (wkd) { - ttr.nextSymbol(); - if (!options.byweekday) options.byweekday = []; + ttr.nextSymbol() + if (!options.byweekday) options.byweekday = [] // @ts-ignore - options.byweekday.push(RRule[wkd]); - } else if (ttr.symbol === "weekday(s)") { - ttr.nextSymbol(); + options.byweekday.push(RRule[wkd]) + } else if (ttr.symbol === 'weekday(s)') { + ttr.nextSymbol() if (!options.byweekday) { options.byweekday = [ RRule.MO, @@ -295,179 +295,179 @@ export default function parseText(text: string, language: Language = ENGLISH) { RRule.WE, RRule.TH, RRule.FR - ]; + ] } - } else if (ttr.symbol === "week(s)") { - ttr.nextSymbol(); - let n = ttr.acceptNumber(); + } else if (ttr.symbol === 'week(s)') { + ttr.nextSymbol() + let n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected week number" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected week number' + ) } - options.byweekno = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byweekno = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.byweekno.push(parseInt(n[0], 10)); + options.byweekno.push(parseInt(n[0], 10)) } } else if (m) { - ttr.nextSymbol(); - if (!options.bymonth) options.bymonth = []; + ttr.nextSymbol() + if (!options.bymonth) options.bymonth = [] // @ts-ignore - options.bymonth.push(m); + options.bymonth.push(m) } else { - return; + return } - } while (ttr.accept("comma") || ttr.accept("the") || ttr.accept("on")); + } while (ttr.accept('comma') || ttr.accept('the') || ttr.accept('on')) } - function AT() { - const at = ttr.accept("at"); - if (!at) return; + function AT () { + const at = ttr.accept('at') + if (!at) return do { - let n = ttr.acceptNumber(); + let n = ttr.acceptNumber() if (!n) { - throw new Error("Unexpected symbol " + ttr.symbol + ", expected hour"); + throw new Error('Unexpected symbol ' + ttr.symbol + ', expected hour') } - options.byhour = [parseInt(n[0], 10)]; - while (ttr.accept("comma")) { - n = ttr.acceptNumber(); + options.byhour = [parseInt(n[0], 10)] + while (ttr.accept('comma')) { + n = ttr.acceptNumber() if (!n) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected hour" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected hour' + ) } - options.byhour.push(parseInt(n[0], 10)); + options.byhour.push(parseInt(n[0], 10)) } - } while (ttr.accept("comma") || ttr.accept("at")); + } while (ttr.accept('comma') || ttr.accept('at')) } - function COLON() { - const colon = ttr.accept("colon"); - if (!colon) return; + function COLON () { + const colon = ttr.accept('colon') + if (!colon) return do { - let m = parseInt(ttr.acceptNumber()![0], 10); + let m = parseInt(ttr.acceptNumber()[0], 10) if (!m) { throw new Error( - "Unexpected symbol " + ttr.symbol + ", expected minutes" - ); + 'Unexpected symbol ' + ttr.symbol + ', expected minutes' + ) } - options.byminute = m; - } while (ttr.accept("colon")); + options.byminute = m + } while (ttr.accept('colon')) } - function decodeM() { + function decodeM () { switch (ttr.symbol) { - case "january": - return 1; - case "february": - return 2; - case "march": - return 3; - case "april": - return 4; - case "may": - return 5; - case "june": - return 6; - case "july": - return 7; - case "august": - return 8; - case "september": - return 9; - case "october": - return 10; - case "november": - return 11; - case "december": - return 12; + case 'january': + return 1 + case 'february': + return 2 + case 'march': + return 3 + case 'april': + return 4 + case 'may': + return 5 + case 'june': + return 6 + case 'july': + return 7 + case 'august': + return 8 + case 'september': + return 9 + case 'october': + return 10 + case 'november': + return 11 + case 'december': + return 12 default: - return false; + return false } } - function decodeWKD() { + function decodeWKD () { switch (ttr.symbol) { - case "monday": - case "tuesday": - case "wednesday": - case "thursday": - case "friday": - case "saturday": - case "sunday": - return ttr.symbol.substr(0, 2).toUpperCase(); + case 'monday': + case 'tuesday': + case 'wednesday': + case 'thursday': + case 'friday': + case 'saturday': + case 'sunday': + return ttr.symbol.substr(0, 2).toUpperCase() default: - return false; + return false } } - function decodeNTH() { + function decodeNTH () { switch (ttr.symbol) { - case "last": - ttr.nextSymbol(); - return -1; - case "first": - ttr.nextSymbol(); - return 1; - case "second": - ttr.nextSymbol(); - return ttr.accept("last") ? -2 : 2; - case "third": - ttr.nextSymbol(); - return ttr.accept("last") ? -3 : 3; - case "nth": - const v = parseInt(ttr.value![1], 10); - if (v < -366 || v > 366) throw new Error("Nth out of range: " + v); - - ttr.nextSymbol(); - return ttr.accept("last") ? -v : v; + case 'last': + ttr.nextSymbol() + return -1 + case 'first': + ttr.nextSymbol() + return 1 + case 'second': + ttr.nextSymbol() + return ttr.accept('last') ? -2 : 2 + case 'third': + ttr.nextSymbol() + return ttr.accept('last') ? -3 : 3 + case 'nth': + const v = parseInt(ttr.value![1], 10) + if (v < -366 || v > 366) throw new Error('Nth out of range: ' + v) + + ttr.nextSymbol() + return ttr.accept('last') ? -v : v default: - return false; + return false } } - function MDAYs() { - ttr.accept("on"); - ttr.accept("the"); + function MDAYs () { + ttr.accept('on') + ttr.accept('the') - let nth = decodeNTH(); - if (!nth) return; + let nth = decodeNTH() + if (!nth) return - options.bymonthday = [nth]; - ttr.nextSymbol(); + options.bymonthday = [nth] + ttr.nextSymbol() - while (ttr.accept("comma")) { - nth = decodeNTH(); + while (ttr.accept('comma')) { + nth = decodeNTH() if (!nth) { throw new Error( - "Unexpected symbol " + ttr.symbol + "; expected monthday" - ); + 'Unexpected symbol ' + ttr.symbol + '; expected monthday' + ) } - options.bymonthday.push(nth); - ttr.nextSymbol(); + options.bymonthday.push(nth) + ttr.nextSymbol() } } - function F() { - if (ttr.symbol === "until") { - const date = Date.parse(ttr.text); + function F () { + if (ttr.symbol === 'until') { + const date = Date.parse(ttr.text) - if (!date) throw new Error("Cannot parse until date:" + ttr.text); - options.until = new Date(date); - } else if (ttr.accept("for")) { - options.count = parseInt(ttr.value![0], 10); - ttr.expect("number"); + if (!date) throw new Error('Cannot parse until date:' + ttr.text) + options.until = new Date(date) + } else if (ttr.accept('for')) { + options.count = parseInt(ttr.value![0], 10) + ttr.expect('number') // ttr.expect('times') } } diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index 64f7ded6..d719e66b 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options, ByWeekday } from "../types"; -import { Weekday } from "../weekday"; -import { isArray, isNumber, isPresent, padStart } from "../helpers"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options, ByWeekday } from '../types' +import { Weekday } from '../weekday' +import { isArray, isNumber, isPresent, padStart } from '../helpers' // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from "../helpers"; /** * Return true if a value is in an array */ -const contains = function(arr: string[], val: string) { - return arr.indexOf(val) !== -1; -}; +const contains = function (arr: string[], val: string) { + return arr.indexOf(val) !== -1 +} // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string; +export type GetText = (id: string | number | Weekday) => string -const defaultGetText: GetText = id => id.toString(); +const defaultGetText: GetText = id => id.toString() export type DateFormatter = ( year: number, month: string, day: number -) => string; +) => string const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}`; +) => `${month} ${day}, ${year}` /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][]; - private rrule: RRule; - private text: string[]; - private gettext: GetText; - private dateFormatter: DateFormatter; - private language: Language; - private options: Partial; - private origOptions: Partial; - private bymonthday: Options["bymonthday"] | null; + static IMPLEMENTED: string[][] + private rrule: RRule + private text: string[] + private gettext: GetText + private dateFormatter: DateFormatter + private language: Language + private options: Partial + private origOptions: Partial + private bymonthday: Options['bymonthday'] | null private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null; + } | null - constructor( + constructor ( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = []; - this.language = language || ENGLISH; - this.gettext = gettext; - this.dateFormatter = dateFormatter; - this.rrule = rrule; - this.options = rrule.options; - this.origOptions = rrule.origOptions; + this.text = [] + this.language = language || ENGLISH + this.gettext = gettext + this.dateFormatter = dateFormatter + this.rrule = rrule + this.options = rrule.options + this.origOptions = rrule.origOptions if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!); - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); + const bymonthday = ([] as number[]).concat(this.options.bymonthday!) + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) - bymonthday.sort((a, b) => a - b); - bynmonthday.sort((a, b) => b - a); + bymonthday.sort((a, b) => a - b) + bynmonthday.sort((a, b) => b - a) // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday); - if (!this.bymonthday.length) this.bymonthday = null; + this.bymonthday = bymonthday.concat(bynmonthday) + if (!this.bymonthday.length) this.bymonthday = null } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday; - const days = String(byweekday); + : this.origOptions.byweekday + const days = String(byweekday) this.byweekday = { - allWeeks: byweekday.filter(function(weekday: Weekday) { - return !weekday.n; + allWeeks: byweekday.filter(function (weekday: Weekday) { + return !weekday.n }), - someWeeks: byweekday.filter(function(weekday: Weekday) { - return Boolean(weekday.n); + someWeeks: byweekday.filter(function (weekday: Weekday) { + return Boolean(weekday.n) }), isWeekdays: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") === -1 && - days.indexOf("SU") === -1, + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') === -1 && + days.indexOf('SU') === -1, isEveryDay: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") !== -1 && - days.indexOf("SU") !== -1 - }; - - const sortWeekDays = function(a: Weekday, b: Weekday) { - return a.weekday - b.weekday; - }; - - this.byweekday.allWeeks!.sort(sortWeekDays); - this.byweekday.someWeeks!.sort(sortWeekDays); - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') !== -1 && + days.indexOf('SU') !== -1 + } + + const sortWeekDays = function (a: Weekday, b: Weekday) { + return a.weekday - b.weekday + } + + this.byweekday.allWeeks!.sort(sortWeekDays) + this.byweekday.someWeeks!.sort(sortWeekDays) + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null } else { - this.byweekday = null; + this.byweekday = null } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible(rrule: RRule) { - let canConvert = true; + static isFullyConvertible (rrule: RRule) { + let canConvert = true - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; - if (rrule.origOptions.until && rrule.origOptions.count) return false; + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false + if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(["dtstart", "wkst", "freq"], key)) return true; - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; + if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } - return canConvert; + return canConvert } - isFullyConvertible() { - return ToText.isFullyConvertible(this.rrule); + isFullyConvertible () { + return ToText.isFullyConvertible(this.rrule) } /** @@ -159,394 +159,394 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString() { - const gettext = this.gettext; + toString () { + const gettext = this.gettext if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext("RRule error: Unable to fully convert this rrule to text"); + return gettext('RRule error: Unable to fully convert this rrule to text') } - this.text = [gettext("every")]; + this.text = [gettext('every')] // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]](); + this[RRule.FREQUENCIES[this.options.freq]]() if (this.options.until) { - this.add(gettext("until")); - const until = this.options.until; + this.add(gettext('until')) + const until = this.options.until this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ); + ) } else if (this.options.count) { - this.add(gettext("for")) + this.add(gettext('for')) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext("times") : gettext("time") - ); + this.plural(this.options.count) ? gettext('times') : gettext('time') + ) } - if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); + if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) - return this.text.join(""); + return this.text.join('') } - HOURLY() { - const gettext = this.gettext; + HOURLY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") - ); + this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') + ) } - MINUTELY() { - const gettext = this.gettext; + MINUTELY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("minutes") - : gettext("minutes") - ); + ? gettext('minutes') + : gettext('minutes') + ) } - DAILY() { - const gettext = this.gettext; + DAILY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - WEEKLY() { - const gettext = this.gettext; + WEEKLY () { + const gettext = this.gettext if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") - ); + this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') + ) } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else { - if (this.options.interval === 1) this.add(gettext("week")); + if (this.options.interval === 1) this.add(gettext('week')) if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } } - MONTHLY() { - const gettext = this.gettext; + MONTHLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext("months")); - if (this.plural(this.options.interval!)) this.add(gettext("in")); + this.add(this.options.interval!.toString()).add(gettext('months')) + if (this.plural(this.options.interval!)) this.add(gettext('in')) } else { // this.add(gettext('MONTH')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("months") - : gettext("month") - ); + ? gettext('months') + : gettext('month') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - YEARLY() { - const gettext = this.gettext; + YEARLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); - this.add(gettext("years")); + this.add(this.options.interval!.toString()) + this.add(gettext('years')) } else { // this.add(gettext('YEAR')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("years") : gettext("year") - ); + this.plural(this.options.interval!) ? gettext('years') : gettext('year') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } if (this.options.byyearday) { - this.add(gettext("on the")) - .add(this.list(this.options.byyearday, this.nth, gettext("and"))) - .add(gettext("day")); + this.add(gettext('on the')) + .add(this.list(this.options.byyearday, this.nth, gettext('and'))) + .add(gettext('day')) } if (this.options.byweekno) { - this.add(gettext("in")) + this.add(gettext('in')) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext("weeks") - : gettext("week") + ? gettext('weeks') + : gettext('week') ) - .add(this.list(this.options.byweekno, undefined, gettext("and"))); + .add(this.list(this.options.byweekno, undefined, gettext('and'))) } } - private _bymonthday() { - const gettext = this.gettext; + private _bymonthday () { + const gettext = this.gettext if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext("on")) + this.add(gettext('on')) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) ) - .add(gettext("the")) - .add(this.list(this.bymonthday!, this.nth, gettext("or"))); + .add(gettext('the')) + .add(this.list(this.bymonthday!, this.nth, gettext('or'))) } else { - this.add(gettext("on the")).add( - this.list(this.bymonthday!, this.nth, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.bymonthday!, this.nth, gettext('and')) + ) } // this.add(gettext('DAY')) } - private _byweekday() { - const gettext = this.gettext; + private _byweekday () { + const gettext = this.gettext if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext("on")).add( + this.add(gettext('on')).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ); + ) } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext("and")); + if (this.byweekday!.allWeeks) this.add(gettext('and')) - this.add(gettext("on the")).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) + ) } } - private _byhour() { - const gettext = this.gettext; + private _byhour () { + const gettext = this.gettext - this.add(gettext("at")).add( - this.list(this.origOptions.byhour!, undefined, gettext("and")) - ); + this.add(gettext('at')).add( + this.list(this.origOptions.byhour!, undefined, gettext('and')) + ) } - private _byminute() { - const gettext = this.gettext; + private _byminute () { + const gettext = this.gettext - function returnminutes(minutes: string): string { - if (minutes.length > 1) return minutes; - return `0${minutes}`; + function returnminutes (minutes: string): string { + if (minutes.length > 1) return minutes + return `0${minutes}` } - this.add(gettext("at")).add( + this.add(gettext('at')).add( `${this.origOptions.byhour!.toString()}:${returnminutes( this.origOptions.byminute!.toString() )}` - ); + ) } - private _bymonth() { + private _bymonth () { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) - ); + this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) + ) } - nth(n: number | string) { - n = parseInt(n.toString(), 10); - let nth: string; - let npos: number; - const gettext = this.gettext; + nth (n: number | string) { + n = parseInt(n.toString(), 10) + let nth: string + let npos: number + const gettext = this.gettext - if (n === -1) return gettext("last"); + if (n === -1) return gettext('last') - npos = Math.abs(n); + npos = Math.abs(n) switch (npos) { case 1: case 21: case 31: - nth = npos + gettext("st"); - break; + nth = npos + gettext('st') + break case 2: case 22: - nth = npos + gettext("nd"); - break; + nth = npos + gettext('nd') + break case 3: case 23: - nth = npos + gettext("rd"); - break; + nth = npos + gettext('rd') + break default: - nth = npos + gettext("th"); + nth = npos + gettext('th') } - return n < 0 ? nth + " " + gettext("last") : nth; + return n < 0 ? nth + ' ' + gettext('last') : nth } - monthtext(m: number) { - return this.language.monthNames[m - 1]; + monthtext (m: number) { + return this.language.monthNames[m - 1] } - weekdaytext(wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); + weekdaytext (wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ); + ) } - plural(n: number) { - return n % 100 !== 1; + plural (n: number) { + return n % 100 !== 1 } - add(s: string) { - this.text.push(" "); - this.text.push(s); - return this; + add (s: string) { + this.text.push(' ') + this.text.push(s) + return this } - list( + list ( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = "," + delim: string = ',' ) { if (!isArray(arr)) { - arr = [arr]; + arr = [arr] } - const delimJoin = function( + const delimJoin = function ( array: string[], delimiter: string, finalDelimiter: string ) { - let list = ""; + let list = '' for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += " " + finalDelimiter + " "; + list += ' ' + finalDelimiter + ' ' } else { - list += delimiter + " "; + list += delimiter + ' ' } } - list += array[i]; + list += array[i] } - return list; - }; + return list + } callback = callback || - function(o) { - return o.toString(); - }; - const self = this; - const realCallback = function(arg: ByWeekday) { - return callback && callback.call(self, arg); - }; + function (o) { + return o.toString() + } + const self = this + const realCallback = function (arg: ByWeekday) { + return callback && callback.call(self, arg) + } if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim); + return delimJoin(arr.map(realCallback), delim, finalDelim) } else { - return arr.map(realCallback).join(delim + " "); + return arr.map(realCallback).join(delim + ' ') } } } From b327eab1f08d258e764e50228595b4d46a410a80 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 12:22:21 +0100 Subject: [PATCH 11/23] Upgrade monthly case --- src/nlp/totext.ts | 504 +++++++++++++++++++++++----------------------- test/nlp.test.ts | 8 + 2 files changed, 265 insertions(+), 247 deletions(-) diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index d719e66b..d43d08f5 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from './i18n' -import RRule from '../index' -import { Options, ByWeekday } from '../types' -import { Weekday } from '../weekday' -import { isArray, isNumber, isPresent, padStart } from '../helpers' +import ENGLISH, { Language } from "./i18n"; +import RRule from "../index"; +import { Options, ByWeekday } from "../types"; +import { Weekday } from "../weekday"; +import { isArray, isNumber, isPresent, padStart } from "../helpers"; // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from '../helpers' /** * Return true if a value is in an array */ -const contains = function (arr: string[], val: string) { - return arr.indexOf(val) !== -1 -} +const contains = function(arr: string[], val: string) { + return arr.indexOf(val) !== -1; +}; // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string +export type GetText = (id: string | number | Weekday) => string; -const defaultGetText: GetText = id => id.toString() +const defaultGetText: GetText = id => id.toString(); export type DateFormatter = ( year: number, month: string, day: number -) => string +) => string; const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}` +) => `${month} ${day}, ${year}`; /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][] - private rrule: RRule - private text: string[] - private gettext: GetText - private dateFormatter: DateFormatter - private language: Language - private options: Partial - private origOptions: Partial - private bymonthday: Options['bymonthday'] | null + static IMPLEMENTED: string[][]; + private rrule: RRule; + private text: string[]; + private gettext: GetText; + private dateFormatter: DateFormatter; + private language: Language; + private options: Partial; + private origOptions: Partial; + private bymonthday: Options["bymonthday"] | null; private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null + } | null; - constructor ( + constructor( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = [] - this.language = language || ENGLISH - this.gettext = gettext - this.dateFormatter = dateFormatter - this.rrule = rrule - this.options = rrule.options - this.origOptions = rrule.origOptions + this.text = []; + this.language = language || ENGLISH; + this.gettext = gettext; + this.dateFormatter = dateFormatter; + this.rrule = rrule; + this.options = rrule.options; + this.origOptions = rrule.origOptions; if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!) - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) + const bymonthday = ([] as number[]).concat(this.options.bymonthday!); + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); - bymonthday.sort((a, b) => a - b) - bynmonthday.sort((a, b) => b - a) + bymonthday.sort((a, b) => a - b); + bynmonthday.sort((a, b) => b - a); // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday) - if (!this.bymonthday.length) this.bymonthday = null + this.bymonthday = bymonthday.concat(bynmonthday); + if (!this.bymonthday.length) this.bymonthday = null; } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday - const days = String(byweekday) + : this.origOptions.byweekday; + const days = String(byweekday); this.byweekday = { - allWeeks: byweekday.filter(function (weekday: Weekday) { - return !weekday.n + allWeeks: byweekday.filter(function(weekday: Weekday) { + return !weekday.n; }), - someWeeks: byweekday.filter(function (weekday: Weekday) { - return Boolean(weekday.n) + someWeeks: byweekday.filter(function(weekday: Weekday) { + return Boolean(weekday.n); }), isWeekdays: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') === -1 && - days.indexOf('SU') === -1, + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") === -1 && + days.indexOf("SU") === -1, isEveryDay: - days.indexOf('MO') !== -1 && - days.indexOf('TU') !== -1 && - days.indexOf('WE') !== -1 && - days.indexOf('TH') !== -1 && - days.indexOf('FR') !== -1 && - days.indexOf('SA') !== -1 && - days.indexOf('SU') !== -1 - } - - const sortWeekDays = function (a: Weekday, b: Weekday) { - return a.weekday - b.weekday - } - - this.byweekday.allWeeks!.sort(sortWeekDays) - this.byweekday.someWeeks!.sort(sortWeekDays) - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null + days.indexOf("MO") !== -1 && + days.indexOf("TU") !== -1 && + days.indexOf("WE") !== -1 && + days.indexOf("TH") !== -1 && + days.indexOf("FR") !== -1 && + days.indexOf("SA") !== -1 && + days.indexOf("SU") !== -1 + }; + + const sortWeekDays = function(a: Weekday, b: Weekday) { + return a.weekday - b.weekday; + }; + + this.byweekday.allWeeks!.sort(sortWeekDays); + this.byweekday.someWeeks!.sort(sortWeekDays); + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; } else { - this.byweekday = null + this.byweekday = null; } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible (rrule: RRule) { - let canConvert = true + static isFullyConvertible(rrule: RRule) { + let canConvert = true; - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false - if (rrule.origOptions.until && rrule.origOptions.count) return false + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; + if (rrule.origOptions.until && rrule.origOptions.count) return false; for (let key in rrule.origOptions) { - if (contains(['dtstart', 'wkst', 'freq'], key)) return true - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false + if (contains(["dtstart", "wkst", "freq"], key)) return true; + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; } - return canConvert + return canConvert; } - isFullyConvertible () { - return ToText.isFullyConvertible(this.rrule) + isFullyConvertible() { + return ToText.isFullyConvertible(this.rrule); } /** @@ -159,394 +159,404 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString () { - const gettext = this.gettext + toString() { + const gettext = this.gettext; if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext('RRule error: Unable to fully convert this rrule to text') + return gettext("RRule error: Unable to fully convert this rrule to text"); } - this.text = [gettext('every')] + this.text = [gettext("every")]; // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]]() + this[RRule.FREQUENCIES[this.options.freq]](); if (this.options.until) { - this.add(gettext('until')) - const until = this.options.until + this.add(gettext("until")); + const until = this.options.until; this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ) + ); } else if (this.options.count) { - this.add(gettext('for')) + this.add(gettext("for")) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext('times') : gettext('time') - ) + this.plural(this.options.count) ? gettext("times") : gettext("time") + ); } - if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) + if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); - return this.text.join('') + return this.text.join(""); } - HOURLY () { - const gettext = this.gettext + HOURLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') - ) + this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") + ); } - MINUTELY () { - const gettext = this.gettext + MINUTELY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('minutes') - : gettext('minutes') - ) + ? gettext("minutes") + : gettext("minutes") + ); } - DAILY () { - const gettext = this.gettext + DAILY() { + const gettext = this.gettext; if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); } else { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); } if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - WEEKLY () { - const gettext = this.gettext + WEEKLY() { + const gettext = this.gettext; if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') - ) + this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") + ); } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext('weekdays') - : gettext('weekday') - ) + ? gettext("weekdays") + : gettext("weekday") + ); if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } else { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext('days') : gettext('day') - ) + this.plural(this.options.interval!) ? gettext("days") : gettext("day") + ); if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } else { - if (this.options.interval === 1) this.add(gettext('week')) + if (this.options.interval === 1) this.add(gettext("week")); if (this.origOptions.bymonth) { - this.add(gettext('in')) - this._bymonth() + this.add(gettext("in")); + this._bymonth(); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } } - MONTHLY () { - const gettext = this.gettext + MONTHLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext('months')) - if (this.plural(this.options.interval!)) this.add(gettext('in')) + this.add(this.options.interval!.toString()).add(gettext("months")); + if (this.plural(this.options.interval!)) this.add(gettext("in")); } else { // this.add(gettext('MONTH')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( this.plural(this.options.interval!) - ? gettext('months') - : gettext('month') - ) + ? gettext("months") + : gettext("month") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); + if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); + } } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext('on')).add(gettext('weekdays')) + this.add(gettext("on")).add(gettext("weekdays")); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); + if (this.origOptions.byhour && !this.origOptions.byminute) { + this._byhour(); + } else if (this.origOptions.byhour && this.origOptions.byminute) { + this._byminute(); + } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour() + this._byhour(); } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute() + this._byminute(); } } - YEARLY () { - const gettext = this.gettext + YEARLY() { + const gettext = this.gettext; if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) - this.add(gettext('years')) + this.add(this.options.interval!.toString()); + this.add(gettext("years")); } else { // this.add(gettext('YEAR')) } - this._bymonth() + this._bymonth(); } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()) + this.add(this.options.interval!.toString()); } this.add( - this.plural(this.options.interval!) ? gettext('years') : gettext('year') - ) + this.plural(this.options.interval!) ? gettext("years") : gettext("year") + ); } if (this.bymonthday) { - this._bymonthday() + this._bymonthday(); } else if (this.byweekday) { - this._byweekday() + this._byweekday(); } if (this.options.byyearday) { - this.add(gettext('on the')) - .add(this.list(this.options.byyearday, this.nth, gettext('and'))) - .add(gettext('day')) + this.add(gettext("on the")) + .add(this.list(this.options.byyearday, this.nth, gettext("and"))) + .add(gettext("day")); } if (this.options.byweekno) { - this.add(gettext('in')) + this.add(gettext("in")) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext('weeks') - : gettext('week') + ? gettext("weeks") + : gettext("week") ) - .add(this.list(this.options.byweekno, undefined, gettext('and'))) + .add(this.list(this.options.byweekno, undefined, gettext("and"))); } } - private _bymonthday () { - const gettext = this.gettext + private _bymonthday() { + const gettext = this.gettext; if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext('on')) + this.add(gettext("on")) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) ) - .add(gettext('the')) - .add(this.list(this.bymonthday!, this.nth, gettext('or'))) + .add(gettext("the")) + .add(this.list(this.bymonthday!, this.nth, gettext("or"))); } else { - this.add(gettext('on the')).add( - this.list(this.bymonthday!, this.nth, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.bymonthday!, this.nth, gettext("and")) + ); } // this.add(gettext('DAY')) } - private _byweekday () { - const gettext = this.gettext + private _byweekday() { + const gettext = this.gettext; if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext('on')).add( + this.add(gettext("on")).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ) + ); } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext('and')) + if (this.byweekday!.allWeeks) this.add(gettext("and")); - this.add(gettext('on the')).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) - ) + this.add(gettext("on the")).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) + ); } } - private _byhour () { - const gettext = this.gettext + private _byhour() { + const gettext = this.gettext; - this.add(gettext('at')).add( - this.list(this.origOptions.byhour!, undefined, gettext('and')) - ) + this.add(gettext("at")).add( + this.list(this.origOptions.byhour!, undefined, gettext("and")) + ); } - private _byminute () { - const gettext = this.gettext + private _byminute() { + const gettext = this.gettext; - function returnminutes (minutes: string): string { - if (minutes.length > 1) return minutes - return `0${minutes}` + function returnminutes(minutes: string): string { + if (minutes.length > 1) return minutes; + return `0${minutes}`; } - this.add(gettext('at')).add( + this.add(gettext("at")).add( `${this.origOptions.byhour!.toString()}:${returnminutes( this.origOptions.byminute!.toString() )}` - ) + ); } - private _bymonth () { + private _bymonth() { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) - ) + this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) + ); } - nth (n: number | string) { - n = parseInt(n.toString(), 10) - let nth: string - let npos: number - const gettext = this.gettext + nth(n: number | string) { + n = parseInt(n.toString(), 10); + let nth: string; + let npos: number; + const gettext = this.gettext; - if (n === -1) return gettext('last') + if (n === -1) return gettext("last"); - npos = Math.abs(n) + npos = Math.abs(n); switch (npos) { case 1: case 21: case 31: - nth = npos + gettext('st') - break + nth = npos + gettext("st"); + break; case 2: case 22: - nth = npos + gettext('nd') - break + nth = npos + gettext("nd"); + break; case 3: case 23: - nth = npos + gettext('rd') - break + nth = npos + gettext("rd"); + break; default: - nth = npos + gettext('th') + nth = npos + gettext("th"); } - return n < 0 ? nth + ' ' + gettext('last') : nth + return n < 0 ? nth + " " + gettext("last") : nth; } - monthtext (m: number) { - return this.language.monthNames[m - 1] + monthtext(m: number) { + return this.language.monthNames[m - 1]; } - weekdaytext (wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() + weekdaytext(wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + this.language.dayNames[weekday] - ) + ); } - plural (n: number) { - return n % 100 !== 1 + plural(n: number) { + return n % 100 !== 1; } - add (s: string) { - this.text.push(' ') - this.text.push(s) - return this + add(s: string) { + this.text.push(" "); + this.text.push(s); + return this; } - list ( + list( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = ',' + delim: string = "," ) { if (!isArray(arr)) { - arr = [arr] + arr = [arr]; } - const delimJoin = function ( + const delimJoin = function( array: string[], delimiter: string, finalDelimiter: string ) { - let list = '' + let list = ""; for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += ' ' + finalDelimiter + ' ' + list += " " + finalDelimiter + " "; } else { - list += delimiter + ' ' + list += delimiter + " "; } } - list += array[i] + list += array[i]; } - return list - } + return list; + }; callback = callback || - function (o) { - return o.toString() - } - const self = this - const realCallback = function (arg: ByWeekday) { - return callback && callback.call(self, arg) - } + function(o) { + return o.toString(); + }; + const self = this; + const realCallback = function(arg: ByWeekday) { + return callback && callback.call(self, arg); + }; if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim) + return delimJoin(arr.map(realCallback), delim, finalDelim); } else { - return arr.map(realCallback).join(delim + ' ') + return arr.map(realCallback).join(delim + " "); } } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 998b79f7..0f80431f 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -40,10 +40,18 @@ const texts = [ ["Every month", "RRULE:FREQ=MONTHLY"], ["Every month at 10:30", "RRULE:FREQ=MONTHLY;BYHOUR=10;BYMINUTE=30"], ["Every 6 months", "RRULE:INTERVAL=6;FREQ=MONTHLY"], + [ + "Every 6 months at 10:30", + "RRULE:INTERVAL=6;FREQ=MONTHLY;BYHOUR=10;BYMINUTE=30" + ], ["Every year", "RRULE:FREQ=YEARLY"], ["Every year on the 1st Friday", "RRULE:FREQ=YEARLY;BYDAY=+1FR"], ["Every year on the 13th Friday", "RRULE:FREQ=YEARLY;BYDAY=+13FR"], ["Every month on the 4th", "RRULE:FREQ=MONTHLY;BYMONTHDAY=4"], + [ + "Every month on the 4th at 10:30", + "RRULE:FREQ=MONTHLY;BYMONTHDAY=4;BYHOUR=10;BYMINUTE=30" + ], ["Every month on the 4th last", "RRULE:FREQ=MONTHLY;BYMONTHDAY=-4"], ["Every month on the 3rd Tuesday", "RRULE:FREQ=MONTHLY;BYDAY=+3TU"], ["Every month on the 3rd last Tuesday", "RRULE:FREQ=MONTHLY;BYDAY=-3TU"], From dc5572d9665284795e81c78750d7c0a24da66e4a Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 12:22:38 +0100 Subject: [PATCH 12/23] Upgrade monthly case --- src/nlp/totext.ts | 502 +++++++++++++++++++++++----------------------- 1 file changed, 251 insertions(+), 251 deletions(-) diff --git a/src/nlp/totext.ts b/src/nlp/totext.ts index d43d08f5..c9ffd84f 100644 --- a/src/nlp/totext.ts +++ b/src/nlp/totext.ts @@ -1,8 +1,8 @@ -import ENGLISH, { Language } from "./i18n"; -import RRule from "../index"; -import { Options, ByWeekday } from "../types"; -import { Weekday } from "../weekday"; -import { isArray, isNumber, isPresent, padStart } from "../helpers"; +import ENGLISH, { Language } from './i18n' +import RRule from '../index' +import { Options, ByWeekday } from '../types' +import { Weekday } from '../weekday' +import { isArray, isNumber, isPresent, padStart } from '../helpers' // ============================================================================= // Helper functions @@ -11,29 +11,29 @@ import { isArray, isNumber, isPresent, padStart } from "../helpers"; /** * Return true if a value is in an array */ -const contains = function(arr: string[], val: string) { - return arr.indexOf(val) !== -1; -}; +const contains = function (arr: string[], val: string) { + return arr.indexOf(val) !== -1 +} // ============================================================================= // ToText // ============================================================================= -export type GetText = (id: string | number | Weekday) => string; +export type GetText = (id: string | number | Weekday) => string -const defaultGetText: GetText = id => id.toString(); +const defaultGetText: GetText = id => id.toString() export type DateFormatter = ( year: number, month: string, day: number -) => string; +) => string const defaultDateFormatter: DateFormatter = ( year: number, month: string, day: number -) => `${month} ${day}, ${year}`; +) => `${month} ${day}, ${year}` /** * @@ -44,89 +44,89 @@ const defaultDateFormatter: DateFormatter = ( * @constructor */ export default class ToText { - static IMPLEMENTED: string[][]; - private rrule: RRule; - private text: string[]; - private gettext: GetText; - private dateFormatter: DateFormatter; - private language: Language; - private options: Partial; - private origOptions: Partial; - private bymonthday: Options["bymonthday"] | null; + static IMPLEMENTED: string[][] + private rrule: RRule + private text: string[] + private gettext: GetText + private dateFormatter: DateFormatter + private language: Language + private options: Partial + private origOptions: Partial + private bymonthday: Options['bymonthday'] | null private byweekday: { allWeeks: ByWeekday[] | null; someWeeks: ByWeekday[] | null; isWeekdays: boolean; isEveryDay: boolean; - } | null; + } | null - constructor( + constructor ( rrule: RRule, gettext: GetText = defaultGetText, language: Language = ENGLISH, dateFormatter: DateFormatter = defaultDateFormatter ) { - this.text = []; - this.language = language || ENGLISH; - this.gettext = gettext; - this.dateFormatter = dateFormatter; - this.rrule = rrule; - this.options = rrule.options; - this.origOptions = rrule.origOptions; + this.text = [] + this.language = language || ENGLISH + this.gettext = gettext + this.dateFormatter = dateFormatter + this.rrule = rrule + this.options = rrule.options + this.origOptions = rrule.origOptions if (this.origOptions.bymonthday) { - const bymonthday = ([] as number[]).concat(this.options.bymonthday!); - const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!); + const bymonthday = ([] as number[]).concat(this.options.bymonthday!) + const bynmonthday = ([] as number[]).concat(this.options.bynmonthday!) - bymonthday.sort((a, b) => a - b); - bynmonthday.sort((a, b) => b - a); + bymonthday.sort((a, b) => a - b) + bynmonthday.sort((a, b) => b - a) // 1, 2, 3, .., -5, -4, -3, .. - this.bymonthday = bymonthday.concat(bynmonthday); - if (!this.bymonthday.length) this.bymonthday = null; + this.bymonthday = bymonthday.concat(bynmonthday) + if (!this.bymonthday.length) this.bymonthday = null } if (isPresent(this.origOptions.byweekday)) { const byweekday = !isArray(this.origOptions.byweekday) ? [this.origOptions.byweekday] - : this.origOptions.byweekday; - const days = String(byweekday); + : this.origOptions.byweekday + const days = String(byweekday) this.byweekday = { - allWeeks: byweekday.filter(function(weekday: Weekday) { - return !weekday.n; + allWeeks: byweekday.filter(function (weekday: Weekday) { + return !weekday.n }), - someWeeks: byweekday.filter(function(weekday: Weekday) { - return Boolean(weekday.n); + someWeeks: byweekday.filter(function (weekday: Weekday) { + return Boolean(weekday.n) }), isWeekdays: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") === -1 && - days.indexOf("SU") === -1, + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') === -1 && + days.indexOf('SU') === -1, isEveryDay: - days.indexOf("MO") !== -1 && - days.indexOf("TU") !== -1 && - days.indexOf("WE") !== -1 && - days.indexOf("TH") !== -1 && - days.indexOf("FR") !== -1 && - days.indexOf("SA") !== -1 && - days.indexOf("SU") !== -1 - }; - - const sortWeekDays = function(a: Weekday, b: Weekday) { - return a.weekday - b.weekday; - }; - - this.byweekday.allWeeks!.sort(sortWeekDays); - this.byweekday.someWeeks!.sort(sortWeekDays); - - if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null; - if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null; + days.indexOf('MO') !== -1 && + days.indexOf('TU') !== -1 && + days.indexOf('WE') !== -1 && + days.indexOf('TH') !== -1 && + days.indexOf('FR') !== -1 && + days.indexOf('SA') !== -1 && + days.indexOf('SU') !== -1 + } + + const sortWeekDays = function (a: Weekday, b: Weekday) { + return a.weekday - b.weekday + } + + this.byweekday.allWeeks!.sort(sortWeekDays) + this.byweekday.someWeeks!.sort(sortWeekDays) + + if (!this.byweekday.allWeeks!.length) this.byweekday.allWeeks = null + if (!this.byweekday.someWeeks!.length) this.byweekday.someWeeks = null } else { - this.byweekday = null; + this.byweekday = null } } @@ -135,22 +135,22 @@ export default class ToText { * @param {RRule} rrule * @return {Boolean} */ - static isFullyConvertible(rrule: RRule) { - let canConvert = true; + static isFullyConvertible (rrule: RRule) { + let canConvert = true - if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false; - if (rrule.origOptions.until && rrule.origOptions.count) return false; + if (!(rrule.options.freq in ToText.IMPLEMENTED)) return false + if (rrule.origOptions.until && rrule.origOptions.count) return false for (let key in rrule.origOptions) { - if (contains(["dtstart", "wkst", "freq"], key)) return true; - if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false; + if (contains(['dtstart', 'wkst', 'freq'], key)) return true + if (!contains(ToText.IMPLEMENTED[rrule.options.freq], key)) return false } - return canConvert; + return canConvert } - isFullyConvertible() { - return ToText.isFullyConvertible(this.rrule); + isFullyConvertible () { + return ToText.isFullyConvertible(this.rrule) } /** @@ -159,404 +159,404 @@ export default class ToText { * be omitted from the output an "(~ approximate)" will be appended. * @return {*} */ - toString() { - const gettext = this.gettext; + toString () { + const gettext = this.gettext if (!(this.options.freq! in ToText.IMPLEMENTED)) { - return gettext("RRule error: Unable to fully convert this rrule to text"); + return gettext('RRule error: Unable to fully convert this rrule to text') } - this.text = [gettext("every")]; + this.text = [gettext('every')] // @ts-ignore - this[RRule.FREQUENCIES[this.options.freq]](); + this[RRule.FREQUENCIES[this.options.freq]]() if (this.options.until) { - this.add(gettext("until")); - const until = this.options.until; + this.add(gettext('until')) + const until = this.options.until this.add( this.dateFormatter( until.getUTCFullYear(), this.language.monthNames[until.getUTCMonth()], until.getUTCDate() ) - ); + ) } else if (this.options.count) { - this.add(gettext("for")) + this.add(gettext('for')) .add(this.options.count.toString()) .add( - this.plural(this.options.count) ? gettext("times") : gettext("time") - ); + this.plural(this.options.count) ? gettext('times') : gettext('time') + ) } - if (!this.isFullyConvertible()) this.add(gettext("(~ approximate)")); + if (!this.isFullyConvertible()) this.add(gettext('(~ approximate)')) - return this.text.join(""); + return this.text.join('') } - HOURLY() { - const gettext = this.gettext; + HOURLY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("hours") : gettext("hour") - ); + this.plural(this.options.interval!) ? gettext('hours') : gettext('hour') + ) } - MINUTELY() { - const gettext = this.gettext; + MINUTELY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("minutes") - : gettext("minutes") - ); + ? gettext('minutes') + : gettext('minutes') + ) } - DAILY() { - const gettext = this.gettext; + DAILY () { + const gettext = this.gettext if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } if (this.byweekday && this.byweekday.isWeekdays) { this.add( this.plural(this.options.interval!) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) } else { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) } if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - WEEKLY() { - const gettext = this.gettext; + WEEKLY () { + const gettext = this.gettext if (this.options.interval !== 1) { this.add(this.options.interval!.toString()).add( - this.plural(this.options.interval!) ? gettext("weeks") : gettext("week") - ); + this.plural(this.options.interval!) ? gettext('weeks') : gettext('week') + ) } if (this.byweekday && this.byweekday.isWeekdays) { if (this.options.interval === 1) { this.add( this.plural(this.options.interval) - ? gettext("weekdays") - : gettext("weekday") - ); + ? gettext('weekdays') + : gettext('weekday') + ) if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } } else if (this.byweekday && this.byweekday.isEveryDay) { this.add( - this.plural(this.options.interval!) ? gettext("days") : gettext("day") - ); + this.plural(this.options.interval!) ? gettext('days') : gettext('day') + ) if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else { - if (this.options.interval === 1) this.add(gettext("week")); + if (this.options.interval === 1) this.add(gettext('week')) if (this.origOptions.bymonth) { - this.add(gettext("in")); - this._bymonth(); + this.add(gettext('in')) + this._bymonth() } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } } - MONTHLY() { - const gettext = this.gettext; + MONTHLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()).add(gettext("months")); - if (this.plural(this.options.interval!)) this.add(gettext("in")); + this.add(this.options.interval!.toString()).add(gettext('months')) + if (this.plural(this.options.interval!)) this.add(gettext('in')) } else { // this.add(gettext('MONTH')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( this.plural(this.options.interval!) - ? gettext("months") - : gettext("month") - ); + ? gettext('months') + : gettext('month') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else if (this.byweekday && this.byweekday.isWeekdays) { - this.add(gettext("on")).add(gettext("weekdays")); + this.add(gettext('on')).add(gettext('weekdays')) } else if (this.byweekday) { - this._byweekday(); + this._byweekday() if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } else if (this.origOptions.byhour && !this.origOptions.byminute) { - this._byhour(); + this._byhour() } else if (this.origOptions.byhour && this.origOptions.byminute) { - this._byminute(); + this._byminute() } } - YEARLY() { - const gettext = this.gettext; + YEARLY () { + const gettext = this.gettext if (this.origOptions.bymonth) { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); - this.add(gettext("years")); + this.add(this.options.interval!.toString()) + this.add(gettext('years')) } else { // this.add(gettext('YEAR')) } - this._bymonth(); + this._bymonth() } else { if (this.options.interval !== 1) { - this.add(this.options.interval!.toString()); + this.add(this.options.interval!.toString()) } this.add( - this.plural(this.options.interval!) ? gettext("years") : gettext("year") - ); + this.plural(this.options.interval!) ? gettext('years') : gettext('year') + ) } if (this.bymonthday) { - this._bymonthday(); + this._bymonthday() } else if (this.byweekday) { - this._byweekday(); + this._byweekday() } if (this.options.byyearday) { - this.add(gettext("on the")) - .add(this.list(this.options.byyearday, this.nth, gettext("and"))) - .add(gettext("day")); + this.add(gettext('on the')) + .add(this.list(this.options.byyearday, this.nth, gettext('and'))) + .add(gettext('day')) } if (this.options.byweekno) { - this.add(gettext("in")) + this.add(gettext('in')) .add( this.plural((this.options.byweekno as number[]).length) - ? gettext("weeks") - : gettext("week") + ? gettext('weeks') + : gettext('week') ) - .add(this.list(this.options.byweekno, undefined, gettext("and"))); + .add(this.list(this.options.byweekno, undefined, gettext('and'))) } } - private _bymonthday() { - const gettext = this.gettext; + private _bymonthday () { + const gettext = this.gettext if (this.byweekday && this.byweekday.allWeeks) { - this.add(gettext("on")) + this.add(gettext('on')) .add( - this.list(this.byweekday.allWeeks, this.weekdaytext, gettext("or")) + this.list(this.byweekday.allWeeks, this.weekdaytext, gettext('or')) ) - .add(gettext("the")) - .add(this.list(this.bymonthday!, this.nth, gettext("or"))); + .add(gettext('the')) + .add(this.list(this.bymonthday!, this.nth, gettext('or'))) } else { - this.add(gettext("on the")).add( - this.list(this.bymonthday!, this.nth, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.bymonthday!, this.nth, gettext('and')) + ) } // this.add(gettext('DAY')) } - private _byweekday() { - const gettext = this.gettext; + private _byweekday () { + const gettext = this.gettext if (this.byweekday!.allWeeks && !this.byweekday!.isWeekdays) { - this.add(gettext("on")).add( + this.add(gettext('on')).add( this.list(this.byweekday!.allWeeks, this.weekdaytext) - ); + ) } if (this.byweekday!.someWeeks) { - if (this.byweekday!.allWeeks) this.add(gettext("and")); + if (this.byweekday!.allWeeks) this.add(gettext('and')) - this.add(gettext("on the")).add( - this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext("and")) - ); + this.add(gettext('on the')).add( + this.list(this.byweekday!.someWeeks, this.weekdaytext, gettext('and')) + ) } } - private _byhour() { - const gettext = this.gettext; + private _byhour () { + const gettext = this.gettext - this.add(gettext("at")).add( - this.list(this.origOptions.byhour!, undefined, gettext("and")) - ); + this.add(gettext('at')).add( + this.list(this.origOptions.byhour!, undefined, gettext('and')) + ) } - private _byminute() { - const gettext = this.gettext; + private _byminute () { + const gettext = this.gettext - function returnminutes(minutes: string): string { - if (minutes.length > 1) return minutes; - return `0${minutes}`; + function returnminutes (minutes: string): string { + if (minutes.length > 1) return minutes + return `0${minutes}` } - this.add(gettext("at")).add( + this.add(gettext('at')).add( `${this.origOptions.byhour!.toString()}:${returnminutes( this.origOptions.byminute!.toString() )}` - ); + ) } - private _bymonth() { + private _bymonth () { this.add( - this.list(this.options.bymonth!, this.monthtext, this.gettext("and")) - ); + this.list(this.options.bymonth!, this.monthtext, this.gettext('and')) + ) } - nth(n: number | string) { - n = parseInt(n.toString(), 10); - let nth: string; - let npos: number; - const gettext = this.gettext; + nth (n: number | string) { + n = parseInt(n.toString(), 10) + let nth: string + let npos: number + const gettext = this.gettext - if (n === -1) return gettext("last"); + if (n === -1) return gettext('last') - npos = Math.abs(n); + npos = Math.abs(n) switch (npos) { case 1: case 21: case 31: - nth = npos + gettext("st"); - break; + nth = npos + gettext('st') + break case 2: case 22: - nth = npos + gettext("nd"); - break; + nth = npos + gettext('nd') + break case 3: case 23: - nth = npos + gettext("rd"); - break; + nth = npos + gettext('rd') + break default: - nth = npos + gettext("th"); + nth = npos + gettext('th') } - return n < 0 ? nth + " " + gettext("last") : nth; + return n < 0 ? nth + ' ' + gettext('last') : nth } - monthtext(m: number) { - return this.language.monthNames[m - 1]; + monthtext (m: number) { + return this.language.monthNames[m - 1] } - weekdaytext(wday: Weekday | number) { - const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday(); + weekdaytext (wday: Weekday | number) { + const weekday = isNumber(wday) ? (wday + 1) % 7 : wday.getJsWeekday() return ( - ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + " " : "") + + ((wday as Weekday).n ? this.nth((wday as Weekday).n!) + ' ' : '') + this.language.dayNames[weekday] - ); + ) } - plural(n: number) { - return n % 100 !== 1; + plural (n: number) { + return n % 100 !== 1 } - add(s: string) { - this.text.push(" "); - this.text.push(s); - return this; + add (s: string) { + this.text.push(' ') + this.text.push(s) + return this } - list( + list ( arr: ByWeekday | ByWeekday[], callback?: GetText, finalDelim?: string, - delim: string = "," + delim: string = ',' ) { if (!isArray(arr)) { - arr = [arr]; + arr = [arr] } - const delimJoin = function( + const delimJoin = function ( array: string[], delimiter: string, finalDelimiter: string ) { - let list = ""; + let list = '' for (let i = 0; i < array.length; i++) { if (i !== 0) { if (i === array.length - 1) { - list += " " + finalDelimiter + " "; + list += ' ' + finalDelimiter + ' ' } else { - list += delimiter + " "; + list += delimiter + ' ' } } - list += array[i]; + list += array[i] } - return list; - }; + return list + } callback = callback || - function(o) { - return o.toString(); - }; - const self = this; - const realCallback = function(arg: ByWeekday) { - return callback && callback.call(self, arg); - }; + function (o) { + return o.toString() + } + const self = this + const realCallback = function (arg: ByWeekday) { + return callback && callback.call(self, arg) + } if (finalDelim) { - return delimJoin(arr.map(realCallback), delim, finalDelim); + return delimJoin(arr.map(realCallback), delim, finalDelim) } else { - return arr.map(realCallback).join(delim + " "); + return arr.map(realCallback).join(delim + ' ') } } } From b1947f29cf1a9d79e617d1299291e28b0362fcb0 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 27 Feb 2020 13:07:05 +0100 Subject: [PATCH 13/23] v1.0.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8729ded2..d0fea91f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "rrule", - "version": "2.6.4", + "name": "@pulsarplatform/rrule", + "version": "1.0.2", "description": "JavaScript library for working with recurrence rules for calendar dates.", "homepage": "http://jakubroztocil.github.io/rrule/", "license": "BSD-3-Clause", From 5cc96f180da9e9f4c7771208c2e7cbc2b72139f9 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 19 Mar 2020 17:46:14 +0100 Subject: [PATCH 14/23] Upgrade scripts --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d0fea91f..6d9e05db 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,10 @@ "build": "yarn lint && tsc && webpack && tsc dist/esm/**/*.d.ts", "lint": "yarn tslint --project . --fix --config tslint.json", "test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts", - "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts" + "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts", + "no-can-do": "echo \"Cannot bump version automatically on branch master. Please run 'npm version [major|minor|patch]' manually.\"", + "build-version": "if [ $(git symbolic-ref --short HEAD) = master ]; then npm run --silent no-can-do; else npx version-from-git; fi", + "publish-version": "npm publish --tag $(if [ $(git symbolic-ref --short HEAD) = master ]; then echo latest; else echo $(git symbolic-ref --short HEAD); fi)" }, "nyc": { "extension": [ From 23207be9795bf010395b75166e7fe83529d04119 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 19 Mar 2020 17:48:06 +0100 Subject: [PATCH 15/23] Remove yarn from scripts --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6d9e05db..4802a243 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ }, "husky": { "hooks": { - "pre-commit": "yarn lint" + "pre-commit": "npm run lint" } }, "scripts": { - "build": "yarn lint && tsc && webpack && tsc dist/esm/**/*.d.ts", - "lint": "yarn tslint --project . --fix --config tslint.json", + "build": "npm run lint && tsc && webpack && tsc dist/esm/**/*.d.ts", + "lint": "tslint --project . --fix --config tslint.json", "test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts", "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts", "no-can-do": "echo \"Cannot bump version automatically on branch master. Please run 'npm version [major|minor|patch]' manually.\"", From 46f1f7f997ca956f751750d64c6a78687d8e4493 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 19 Mar 2020 17:48:48 +0100 Subject: [PATCH 16/23] 1.0.2-add-npmrc.23207be --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4802a243..404e8fd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pulsarplatform/rrule", - "version": "1.0.2", + "version": "1.0.2-add-npmrc.23207be", "description": "JavaScript library for working with recurrence rules for calendar dates.", "homepage": "http://jakubroztocil.github.io/rrule/", "license": "BSD-3-Clause", From c2b823d49fe2def85f9308f524ecdb730985ce75 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Thu, 19 Mar 2020 17:51:18 +0100 Subject: [PATCH 17/23] Remove homepage from package json and fix repository for pulsar --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 404e8fd9..3ead2929 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "@pulsarplatform/rrule", "version": "1.0.2-add-npmrc.23207be", "description": "JavaScript library for working with recurrence rules for calendar dates.", - "homepage": "http://jakubroztocil.github.io/rrule/", "license": "BSD-3-Clause", "keywords": [ "dates", @@ -17,7 +16,7 @@ "typings": "dist/esm/src/index.d.ts", "repository": { "type": "git", - "url": "git://github.com/jakubroztocil/rrule.git" + "url": "git://github.com/pulsarplatform/rrule.git" }, "husky": { "hooks": { From 5e341535c91292c4c8eeece5d01e936303e56ab8 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 8 Apr 2020 17:27:18 +0200 Subject: [PATCH 18/23] Add .npmrc file --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..20481df6 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://npm.pkg.github.com/pulsarplatform From a20a6edab543bfd780aaad1a493d2313dcf30ab0 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 8 Apr 2020 17:31:03 +0200 Subject: [PATCH 19/23] v1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ead2929..d89398eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pulsarplatform/rrule", - "version": "1.0.2-add-npmrc.23207be", + "version": "1.0.3", "description": "JavaScript library for working with recurrence rules for calendar dates.", "license": "BSD-3-Clause", "keywords": [ From 7167eb1e5948ab87cc58b87a92005f630f68796a Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 13 May 2020 09:11:57 +0200 Subject: [PATCH 20/23] Remove .npmrc file --- .npmrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 20481df6..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -registry=https://npm.pkg.github.com/pulsarplatform From b3c0dc8c44ea5c36811bb536a5e39f98daa4a248 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 13 May 2020 09:21:20 +0200 Subject: [PATCH 21/23] Revert bad modify in package json --- package.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d89398eb..40c7ad0d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { - "name": "@pulsarplatform/rrule", - "version": "1.0.3", + "name": "rrule", + "version": "2.6.4", "description": "JavaScript library for working with recurrence rules for calendar dates.", + "homepage": "http://jakubroztocil.github.io/rrule/", "license": "BSD-3-Clause", "keywords": [ "dates", @@ -16,7 +17,7 @@ "typings": "dist/esm/src/index.d.ts", "repository": { "type": "git", - "url": "git://github.com/pulsarplatform/rrule.git" + "url": "git://github.com/jakubroztocil/rrule.git" }, "husky": { "hooks": { @@ -27,10 +28,7 @@ "build": "npm run lint && tsc && webpack && tsc dist/esm/**/*.d.ts", "lint": "tslint --project . --fix --config tslint.json", "test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts", - "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts", - "no-can-do": "echo \"Cannot bump version automatically on branch master. Please run 'npm version [major|minor|patch]' manually.\"", - "build-version": "if [ $(git symbolic-ref --short HEAD) = master ]; then npm run --silent no-can-do; else npx version-from-git; fi", - "publish-version": "npm publish --tag $(if [ $(git symbolic-ref --short HEAD) = master ]; then echo latest; else echo $(git symbolic-ref --short HEAD); fi)" + "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts" }, "nyc": { "extension": [ From edc6474231cfe2f755ea6048df82d19eaae4a2f9 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 13 May 2020 09:23:22 +0200 Subject: [PATCH 22/23] Remove npm line command from package json --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 40c7ad0d..55a06051 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint" + "pre-commit": "yarn run lint" } }, "scripts": { - "build": "npm run lint && tsc && webpack && tsc dist/esm/**/*.d.ts", - "lint": "tslint --project . --fix --config tslint.json", + "build": "yarn lint && tsc && webpack && tsc dist/esm/**/*.d.ts", + "lint": "yarn tslint --project . --fix --config tslint.json", "test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts", "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts" }, From a5a3540d4341c0cbec7ff11fb3cc0c7dcf9da954 Mon Sep 17 00:00:00 2001 From: Leonardo Rosseti Date: Wed, 13 May 2020 09:24:29 +0200 Subject: [PATCH 23/23] Remove 'run' from pre-commit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55a06051..8729ded2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "husky": { "hooks": { - "pre-commit": "yarn run lint" + "pre-commit": "yarn lint" } }, "scripts": {