diff --git a/__tests__/nextDate/index.ts b/__tests__/nextDate/index.ts index 77075fd..92dd32c 100644 --- a/__tests__/nextDate/index.ts +++ b/__tests__/nextDate/index.ts @@ -9,11 +9,8 @@ type Case = { output: string; }; -const envSuite = process.env.TEST_SUITE; -let suites = readdirSync(path.join(__dirname, './suites')); -if (envSuite && envSuite !== 'undefined') { - suites = [envSuite + '.txt']; -} +// No flag filtering since only smoke tests exist +const suites = readdirSync(path.join(__dirname, './suites')); suites.forEach(name => { describe('nextDate test suite: ' + name, () => { const content = readFileSync( diff --git a/__tests__/parseText/index.ts b/__tests__/parseText/index.ts index 6a1697f..7dc7d86 100644 --- a/__tests__/parseText/index.ts +++ b/__tests__/parseText/index.ts @@ -1,5 +1,5 @@ import { readFileSync, readdirSync } from 'fs'; -import { parseText } from '../../src/index'; +import { parseText, crontext } from '../../src/index'; const currentDate = new Date('2023-3-4'); @@ -16,7 +16,7 @@ type Case = { const envSuite = process.env.TEST_SUITE; let suites = readdirSync(path.join(__dirname, './suites')); if (envSuite && envSuite !== 'undefined') { - suites = [envSuite + '.txt']; + suites = envSuite.split(',').map(suite => `${suite}.txt`); } suites.forEach(name => { describe('Crontext test suite: ' + name, () => { @@ -46,6 +46,11 @@ suites.forEach(name => { cases.forEach(({ input, output }) => { test(input, () => { expect(parseText(input)).toEqual(output); + if (name === 'repeat.txt') { + expect(crontext(input)).toEqual( + expect.objectContaining({ repeat: true }), + ); + } }); }); }); diff --git a/__tests__/parseText/suites/compound.txt b/__tests__/parseText/suites/compound.txt index 55d311e..de570e1 100644 --- a/__tests__/parseText/suites/compound.txt +++ b/__tests__/parseText/suites/compound.txt @@ -1,2 +1,14 @@ Every 5 minutes, every other hour, on every day excluding Friday, and also on the 13th of the month, in April and August. -*/5 */2 13 4,8 0,1,2,3,4,6 \ No newline at end of file +*/5 */2 13 4,8 0,1,2,3,4,6 + +At 04:00 on every day-of-month from 8 through 14. +0 4 8-14 * * + +At 9am every 10th and 15th of the month +0 9 10,15 * * + +At 9AM on the first day of every second month +0 9 1 */2 * + +Every Monday, Tuesday and Thursday at noon +0 12 * * 1,2,4 diff --git a/__tests__/parseText/suites/repeat.txt b/__tests__/parseText/suites/repeat.txt index 0108062..667f432 100644 --- a/__tests__/parseText/suites/repeat.txt +++ b/__tests__/parseText/suites/repeat.txt @@ -1,22 +1,19 @@ -At 04:00 on every day-of-month from 8 through 14. -0 4 8-14 * * +At every 9am +0 9 * * * -Hourly at 45 minutes past the hour -45 * * * * +On Fridays +0 9 * * 5 -At 9am every 10th and 15th of the month -0 9 10,15 * * +On Wednesdays +0 9 * * 3 -At every minute in April -* * * 4 * - -At 9AM on the first day of every second month -0 9 1 */2 * +On weekdays +0 9 * * 1-5 -Twice a day at 9am and 9pm -0 9,21 * * * +On Weekends +0 9 * * 0,6 -Every 15th minute +Every 15 minutes */15 * * * * Every hour @@ -32,10 +29,7 @@ Every weekday at 9am 0 9 * * 1-5 Every weekend at 9am -0 9 * * 6,0 +0 9 * * 0,6 Every Saturday -0 0 * * SAT - -Every Monday, Tuesday and Thursday at noon -0 12 * * 1,2,4 \ No newline at end of file +0 9 * * 6 \ No newline at end of file diff --git a/__tests__/parseText/suites/smoke.txt b/__tests__/parseText/suites/smoke.txt index b3d0020..bdc5b56 100644 --- a/__tests__/parseText/suites/smoke.txt +++ b/__tests__/parseText/suites/smoke.txt @@ -29,7 +29,7 @@ At 9am 0 9 * * * At midnight -0 24 * * * +0 0 * * * At noon 0 12 * * * @@ -40,15 +40,6 @@ At every minute on monday On Friday 0 9 * * 5 -On Wednesdays -0 9 * * 3 - -On weekdays -0 9 * * 1-5 - -On Weekends -0 9 * * 0,6 - On Wednesday at 2:13pm 13 14 * * 3 diff --git a/package.json b/package.json index bdb3d12..86dc662 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crontext", - "version": "0.2.9", + "version": "0.2.10", "description": "Simple utility for parsing human text into a cron schedule.", "files": [ "lib" @@ -9,9 +9,9 @@ "types": "lib/index.d.ts", "scripts": { "test": "node ./scripts/jest-runner.js --config jest.config.js --watch", - "test:ci": "node ./scripts/jest-runner.js --config jest.config.js src __tests__ --suite=smoke", + "test:ci": "node ./scripts/jest-runner.js --config jest.config.js src __tests__ --suite=smoke,repeat", "coverage": "yarn test:ci -- --coverage", - "test:smoke": "node ./scripts/jest-runner.js --config jest.config.js --suite=smoke __tests__", + "test:smoke": "node ./scripts/jest-runner.js --config jest.config.js --suite=smoke,repeat __tests__", "build": "rm -rf lib && tsc", "lint": "eslint --ignore-path .eslintignore --ext .js,.ts", "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"", diff --git a/src/generate.ts b/src/generate.ts index a23e7e9..268ce31 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,4 +1,4 @@ -import { Parsed, INIT, DEFAULT } from './parser'; +import { type Crontext, INIT, DEFAULT } from './parser'; /** * CRON FORMAT: @@ -19,7 +19,7 @@ const getValue = (str: string): string => { return str; }; -export const generate = (parsed: Parsed): string => { +export const generate = (parsed: Crontext): string => { return `${getValue(parsed.minutes)} ${getValue(parsed.hour)} ${getValue( parsed.dayOfMonth, )} ${getValue(parsed.month)} ${getValue(parsed.dayOfWeek)}`; diff --git a/src/index.ts b/src/index.ts index 2a3ec91..e62be1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import tokenize from './tokenize'; -import parse from './parser'; +import parse, { type Crontext } from './parser'; import { generate } from './generate'; import * as next from './next'; import { parseOptions } from './options'; @@ -14,6 +14,12 @@ export const parseText = (input: string, options?: InputOptions): string => { return cron; }; +export const crontext = (input: string, options?: InputOptions): Crontext => { + const tokens = tokenize(input); + const parsed = parse(tokens, parseOptions(options)); + return parsed; +}; + export const nextDate = next.nextDate; export const version = pJson.version; @@ -25,4 +31,4 @@ export const version = pJson.version; * - repeat: bool */ -export default parseText; +export default crontext; diff --git a/src/parser.ts b/src/parser.ts index 79c1b9f..f9065c8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,16 +1,18 @@ import type { Token } from './tokenize'; import { TokenType } from './tokens'; +import { testRepeat as freqTestRepeat } from './tokens/frequency'; import { getNumber } from './tokens/number'; import { getTime } from './tokens/clock'; -import { getDayOfWeek } from './tokens/day'; +import { getDayOfWeek, pluralDayRegexOptions } from './tokens/day'; import type { Options } from './options'; -export type Parsed = { +export type Crontext = { minutes: string; hour: string; dayOfMonth: string; dayOfWeek: string; month: string; + repeat: boolean; }; export const DEFAULT = '*'; @@ -30,32 +32,42 @@ const { RELATIVE_DAY, } = TokenType; -const defaultParsed: Parsed = { +const defaultParsed: Crontext = { minutes: INIT, hour: INIT, dayOfMonth: INIT, dayOfWeek: INIT, month: INIT, + repeat: false, }; export const updateDay = ( - crontext: Parsed, + crontext: Crontext, tokens: Token[], options: Options, -): Parsed => { +): Crontext => { // If there are no minutes or hour set we use defaults // 'On monday' -> 9am Monday if (crontext.minutes === INIT) crontext.minutes = options.defaultMinute; if (crontext.hour === INIT) crontext.hour = options.defaultHour; + const re = new RegExp(pluralDayRegexOptions); + // Plural 'on mondays' means its repeating + if ( + tokens.length >= 2 && + tokens[0].value === 'on' && + re.test(tokens[1].value) + ) { + crontext.repeat = true; + } const dayOfWeek = getDayOfWeek(tokens[1].value); return { ...crontext, dayOfWeek }; }; export const updateDays = ( - crontext: Parsed, + crontext: Crontext, tokens: Token[], options: Options, -): Parsed => { +): Crontext => { // Default like 'next week' let delta = 1; let setDate = true; @@ -104,16 +116,26 @@ export const updateDays = ( // The grammar export const rules = [ + { + match: [FREQUENCY], + update: (crontext: Crontext, tokens: Token[]): Crontext => { + const re = new RegExp(freqTestRepeat); + if (re.test(tokens[0].value)) { + crontext.repeat = true; + } + return crontext; + }, + }, { match: [FREQUENCY, NUMBER, MINUTE], - update: (crontext: Parsed, tokens: Token[]): Parsed => { + update: (crontext: Crontext, tokens: Token[]): Crontext => { crontext.minutes = '*/' + getNumber(tokens[1].value); return crontext; }, }, { match: [FREQUENCY, MINUTE], - update: (crontext: Parsed): Parsed => { + update: (crontext: Crontext): Crontext => { crontext.minutes = DEFAULT; crontext.hour = DEFAULT; return crontext; @@ -121,7 +143,7 @@ export const rules = [ }, { match: [FREQUENCY, NUMBER, HOUR], - update: (crontext: Parsed, tokens: Token[]): Parsed => { + update: (crontext: Crontext, tokens: Token[]): Crontext => { if (crontext.minutes === INIT) crontext.minutes = '0'; crontext.hour = '*/' + getNumber(tokens[1].value); return crontext; @@ -129,7 +151,7 @@ export const rules = [ }, { match: [FREQUENCY, HOUR], - update: (crontext: Parsed): Parsed => { + update: (crontext: Crontext): Crontext => { crontext.minutes = '0'; crontext.hour = DEFAULT; return crontext; @@ -153,7 +175,11 @@ export const rules = [ }, { match: [FREQUENCY, DAYS], - update: (crontext: Parsed, tokens: Token[], options: Options): Parsed => { + update: ( + crontext: Crontext, + tokens: Token[], + options: Options, + ): Crontext => { if (crontext.minutes === INIT) crontext.minutes = options.defaultMinute; if (crontext.hour === INIT) crontext.hour = options.defaultHour; if (tokens[1].value.indexOf('month') > -1) { @@ -167,7 +193,11 @@ export const rules = [ }, { match: [RELATIVE_DAY], - update: (crontext: Parsed, tokens: Token[], options: Options): Parsed => { + update: ( + crontext: Crontext, + tokens: Token[], + options: Options, + ): Crontext => { if (tokens[0].value === 'tomorrow') { const { startDate } = options; const tomorrow = new Date(startDate.getTime()); @@ -181,7 +211,7 @@ export const rules = [ }, { match: [FREQUENCY, CLOCK], - update: (crontext: Parsed, tokens: Token[]): Parsed => { + update: (crontext: Crontext, tokens: Token[]): Crontext => { const [hour, minute] = getTime(tokens[1].value); crontext.minutes = minute.toString(); crontext.hour = hour.toString(); @@ -190,7 +220,7 @@ export const rules = [ }, ]; -export const parser = (tokens: Token[], options: Options): Parsed => { +export const parser = (tokens: Token[], options: Options): Crontext => { let crontext = { ...defaultParsed }; // Iterate all tokens for (let t = 0; t < tokens.length; t++) { diff --git a/src/tokens/clock.test.ts b/src/tokens/clock.test.ts index 07a1757..2245ead 100644 --- a/src/tokens/clock.test.ts +++ b/src/tokens/clock.test.ts @@ -11,7 +11,7 @@ describe('Clock token getTime() should', () => { expect(getTime('17:00')).toEqual([17, 0]); }); test('return the correct clock time for special names', () => { - expect(getTime('midnight')).toEqual([24, 0]); + expect(getTime('midnight')).toEqual([0, 0]); expect(getTime('noon')).toEqual([12, 0]); }); }); diff --git a/src/tokens/clock.ts b/src/tokens/clock.ts index aab4233..69c390e 100644 --- a/src/tokens/clock.ts +++ b/src/tokens/clock.ts @@ -9,7 +9,7 @@ export type Time = { * @returns [hour, minute] */ export const getTime = (str: string): [number, number] => { - if (str === 'midnight') return [24, 0]; + if (str === 'midnight') return [0, 0]; if (str === 'noon') return [12, 0]; const re = new RegExp('([0-9]+)[:]?([0-9]+)?'); const match = re.exec(str); diff --git a/src/tokens/day.ts b/src/tokens/day.ts index 7372f43..12ccec1 100644 --- a/src/tokens/day.ts +++ b/src/tokens/day.ts @@ -1,5 +1,8 @@ export const dayRegexOptions = - '^(mon|tues|tue|wed|wednes|thurs|thur|fri|sat|sun)(day|days)?$|weekday|weekend|weekdays|weekends$'; + '^(mon|tues|tue|wed|wednes|thurs|thur|fri|sat|satur|sun)(day|days)?$|weekday|weekend|weekdays|weekends$'; + +export const pluralDayRegexOptions = + '^(mon|tues|tue|wed|wednes|thurs|thur|fri|sat|satur|sun)(days)?$|weekdays|weekends$'; const weekMap: Record = { sun: '0', diff --git a/src/tokens/frequency.ts b/src/tokens/frequency.ts new file mode 100644 index 0000000..0fbdf83 --- /dev/null +++ b/src/tokens/frequency.ts @@ -0,0 +1 @@ +export const testRepeat = '^(every|each|every other)$';