diff --git a/src/nlp/i18n.ts b/src/nlp/i18n.ts index 1b2e3953..a2871730 100644 --- a/src/nlp/i18n.ts +++ b/src/nlp/i18n.ts @@ -40,14 +40,14 @@ const ENGLISH: Language = { 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, + 'weekday(s)': /^(weekdays?|business\s+days?|work\s+days?)/i, + 'week(s)': /^(weeks?|wks?|weekly)/i, + 'hour(s)': /^(hours?|hrs?|hourly)/i, + 'minute(s)': /^(minutes?|mins?)/i, + 'month(s)': /^(months?|monthly)/i, + 'year(s)': /^(years?|yearly|annually)/i, on: /^(on|in)/i, - at: /^(at)/i, + at: /^(at|@)/i, the: /^the/i, first: /^first/i, second: /^second/i, @@ -57,26 +57,26 @@ const ENGLISH: Language = { 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, + monday: /^(mo\.?(n\.?(days?)?)?)/i, + tuesday: /^(tu\.?(e\.?(s\.?(days?)?)?)?)/i, + wednesday: /^(we\.?(d\.?(n\.?(esdays?)?)?)?)/i, + thursday: /^(th\.?(u\.?(r\.?(s\.?(days?)?)?)?)?)/i, + friday: /^(fr\.?(i\.?(days?)?)?)/i, + saturday: /^(sa\.?(t\.?(urdays?)?)?)/i, + sunday: /^(su\.?(n\.?(days?)?)?)/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, + june: /^(june?|jun\.)/i, + july: /^(july?|jul\.)/i, + august: /^(aug(ust)?|aug\.)/i, + september: /^(sep(t(ember)?)?|sept\.|sep\.)/i, + october: /^(oct(ober)?|oct\.)/i, + november: /^(nov(ember)?|nov\.)/i, + december: /^(dec(ember)?|dec\.)/i, + comma: /^(,\s*|(and|or|&)\s*)+/i, }, } diff --git a/src/nlp/parsetext.ts b/src/nlp/parsetext.ts index fbaeccf9..1519beee 100644 --- a/src/nlp/parsetext.ts +++ b/src/nlp/parsetext.ts @@ -102,6 +102,14 @@ export default function parseText(text: string, language: Language = ENGLISH) { if (!ttr.start(text)) return null S() + // deduplicate arrays + if (options.byweekday && Array.isArray(options.byweekday)) { + options.byweekday = [...new Set(options.byweekday)] + } + if (options.bymonth && Array.isArray(options.bymonth)) { + options.bymonth = [...new Set(options.bymonth)] + } + return options function S() { @@ -114,7 +122,15 @@ export default function parseText(text: string, language: Language = ENGLISH) { switch (ttr.symbol) { case 'day(s)': options.freq = RRule.DAILY - if (ttr.nextSymbol()) { + ttr.nextSymbol() + if (!ttr.isDone()) { + if (ttr.accept('on')) { + const m = decodeM() + if (m) { + options.bymonth = [m] + ttr.nextSymbol() + } + } AT() F() } @@ -186,7 +202,6 @@ export default function parseText(text: string, language: Language = ENGLISH) { if (!ttr.nextSymbol()) return - // TODO check for duplicates while (ttr.accept('comma')) { if (ttr.isDone()) throw new Error('Unexpected end') @@ -222,7 +237,6 @@ export default function parseText(text: string, language: Language = ENGLISH) { if (!ttr.nextSymbol()) return - // TODO check for duplicates while (ttr.accept('comma')) { if (ttr.isDone()) throw new Error('Unexpected end') @@ -242,7 +256,7 @@ export default function parseText(text: string, language: Language = ENGLISH) { break default: - throw new Error('Unknown symbol') + throw new Error(`Unknown symbol: ${ttr.symbol}`) } } diff --git a/test/nlp.test.ts b/test/nlp.test.ts index 8948616c..c7440f46 100644 --- a/test/nlp.test.ts +++ b/test/nlp.test.ts @@ -32,6 +32,45 @@ const texts = [ ['Every week for 20 times', 'RRULE:FREQ=WEEKLY;COUNT=20'], ] +const fromTexts = [ + ...texts, + // Testing more flexible weekday formats + ['Every Mon & Wed', 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE'], + ['Every Mon and Wed', 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE'], + ['Every Tues & Thurs', 'RRULE:FREQ=WEEKLY;BYDAY=TU,TH'], + ['Every tu. and thurs. & wed.', 'RRULE:FREQ=WEEKLY;BYDAY=TU,TH,WE'], + + // Testing business/work day variations + ['Every business day', 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'], + ['Every work day', 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'], + + // Testing month abbreviations + ['Every month in Jan', 'RRULE:FREQ=MONTHLY;BYMONTH=1'], + ['Every week in Sept.', 'RRULE:FREQ=WEEKLY;BYMONTH=9'], + ['Every day in Dec.', 'RRULE:FREQ=DAILY;BYMONTH=12'], + + // Testing time unit variations + ['Every hr', 'RRULE:FREQ=HOURLY'], + ['Every 2 hrs', 'RRULE:INTERVAL=2;FREQ=HOURLY'], + ['Every min', 'RRULE:FREQ=MINUTELY'], + ['Every 5 mins', 'RRULE:INTERVAL=5;FREQ=MINUTELY'], + + // Testing @ symbol for at + ['Every day @ 10', 'RRULE:FREQ=DAILY;BYHOUR=10'], + ['Every week @ 9', 'RRULE:FREQ=WEEKLY;BYHOUR=9'], + + // Testing plural variations of days + ['Every Monday and Wednesdays', 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE'], + ['Every Tuesdays & Thursdays', 'RRULE:FREQ=WEEKLY;BYDAY=TU,TH'], + + // Deduplicates + [ + 'Every Monday, Tuesday, Wednesday, and Monday', + 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE', + ], + ['Every month in Jan & January', 'RRULE:FREQ=MONTHLY;BYMONTH=1'], +] + const toTexts = [ ...texts, [ @@ -41,30 +80,18 @@ const toTexts = [ ] describe('NLP', () => { - it('fromText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromText(text).toString()).toBe(str) - }) + it.each(fromTexts)('fromText() %s', (text, str) => { + expect(RRule.fromText(text).toString()).toBe(str) }) - it('toText()', function () { - toTexts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(RRule.fromString(str).toText().toLowerCase()).toBe( - text.toLowerCase() - ) - }) + it.each(toTexts)('toText() %s', (text, str) => { + expect(RRule.fromString(str).toText().toLowerCase()).toBe( + text.toLowerCase() + ) }) - it('parseText()', function () { - texts.forEach(function (item) { - const text = item[0] - const str = item[1] - expect(optionsToString(RRule.parseText(text))).toBe(str) - }) + it.each(texts)('parseText() %s', (text, str) => { + expect(optionsToString(RRule.parseText(text))).toBe(str) }) it('permits integers in byweekday (#153)', () => {