UNPKG

cron-humanize

Version:

Cron to human translator

580 lines (506 loc) 23.1 kB
interface CronConstants { SEPARATOR: string; COMA: string; DASH: string; WILDCARD: string; UNSPECIFIED: string; SLASH: string; SPECIFIC: string; LAST_DAY: string; LAST_WEEKDAY_MONTH: string; NEAREST_WEEKDAY: RegExp; LAST_WEEKDAY_SPECIFIC: RegExp; TYPE_MULTI: string; TYPE_RANGE: string; TYPE_SINGLE: string; TYPE_WILDCARD: string; TYPE_UNSPECIFIED: string; TYPE_INTERVAL: string; TYPE_SPECIFIC: string; TYPE_LAST_DAY: string; TYPE_LAST_WEEKDAY_MONTH: string; TYPE_NEAREST_WEEKDAY: string; TYPE_LAST_WEEKDAY_SPECIFIC: string; SHORT_DAYS: string[]; FULL_DAYS: string[]; SHORT_MONTHS: string[]; FULL_MONTHS: string[]; } interface CronDissection { seconds: string; minutes: string; hours: string; dayOfMonth: string; month: string; dayOfWeek: string; year: string; } interface MultiDef { values: number[], last: number } interface RangeDef { start: number, end: number } interface IntervalDef { start: number, step: number } interface SpecificDef { week: number, day: number } interface DaySpecificDef { day: number } export const CONSTANTS: CronConstants = { SEPARATOR: ' ', COMA: ',', DASH: '-', WILDCARD: '*', UNSPECIFIED: '?', SLASH: '/', SPECIFIC: '#', LAST_DAY: 'L', LAST_WEEKDAY_MONTH: 'LW', NEAREST_WEEKDAY: /([0-9]?[0-9])W/, LAST_WEEKDAY_SPECIFIC: /([0-9]?[0-9])L/, TYPE_MULTI: 'multi', TYPE_RANGE: 'range', TYPE_SINGLE: 'single', TYPE_WILDCARD: 'wildcard', TYPE_UNSPECIFIED: 'unspecified', TYPE_INTERVAL: 'interval', TYPE_SPECIFIC: 'specific', TYPE_LAST_DAY: 'last_day', TYPE_LAST_WEEKDAY_MONTH: 'last_weekday_month', TYPE_NEAREST_WEEKDAY: 'nearest_weekday', TYPE_LAST_WEEKDAY_SPECIFIC: 'last_weekday_specific', SHORT_DAYS: ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'], FULL_DAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], SHORT_MONTHS: ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'], FULL_MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], }; export class UnitDefinition { public type: string; public multi: MultiDef; public range: RangeDef; public interval: IntervalDef; public specific: SpecificDef; // TODO: Add L, LW, _L and _W public lastDay: string; public lastWeekdayMonth: string; public nearestWeekday: DaySpecificDef; public lastWeekdaySpecific: DaySpecificDef; public unspecified: string; public wildcard: string; public single: number; constructor(public rawData: string, public min: number, public max: number, public names?: string[], private indexBase?: number) { this.type = this.setType(rawData); switch(this.type) { case CONSTANTS.TYPE_MULTI: this.multi = this.getMultiDef(rawData); break; case CONSTANTS.TYPE_RANGE: this.range = this.getRangeDef(rawData); break; case CONSTANTS.TYPE_INTERVAL: this.interval = this.getIntervalDef(rawData); break; case CONSTANTS.TYPE_UNSPECIFIED: this.unspecified = this.getDef(rawData); break; case CONSTANTS.TYPE_WILDCARD: this.wildcard = this.getDef(rawData); break; case CONSTANTS.TYPE_SPECIFIC: this.specific = this.getSpecificDef(rawData); break; case CONSTANTS.TYPE_SINGLE: this.single = this.getSingleDef(rawData); break; case CONSTANTS.TYPE_LAST_DAY: this.lastDay = this.getDef(rawData); break; case CONSTANTS.TYPE_LAST_WEEKDAY_MONTH: this.lastWeekdayMonth = this.getDef(rawData); break; case CONSTANTS.TYPE_NEAREST_WEEKDAY: this.nearestWeekday = this.getDaySpecificDef(rawData); break; case CONSTANTS.TYPE_LAST_WEEKDAY_SPECIFIC: this.lastWeekdaySpecific = this.getDaySpecificDef(rawData); break; } } public setType(value: string): string { if (value.indexOf(CONSTANTS.COMA) > 0) { return CONSTANTS.TYPE_MULTI; } else if (value === CONSTANTS.WILDCARD) { return CONSTANTS.TYPE_WILDCARD; } else if (value === CONSTANTS.UNSPECIFIED) { return CONSTANTS.TYPE_UNSPECIFIED; } else if (value.indexOf(CONSTANTS.SLASH) > 0) { return CONSTANTS.TYPE_INTERVAL; } else if (value.indexOf(CONSTANTS.DASH) > 0) { return CONSTANTS.TYPE_RANGE; } else if (value.indexOf(CONSTANTS.SPECIFIC) > 0) { return CONSTANTS.TYPE_SPECIFIC; } else if (value === CONSTANTS.LAST_DAY) { return CONSTANTS.TYPE_LAST_DAY; } else if (value === CONSTANTS.LAST_WEEKDAY_MONTH) { return CONSTANTS.TYPE_LAST_WEEKDAY_MONTH; } else if (CONSTANTS.NEAREST_WEEKDAY.test(value)) { return CONSTANTS.TYPE_NEAREST_WEEKDAY; } else if (CONSTANTS.LAST_WEEKDAY_SPECIFIC.test(value)) { return CONSTANTS.TYPE_LAST_WEEKDAY_SPECIFIC; } else { return CONSTANTS.TYPE_SINGLE; } } private checkForNamed(value: string): number { return isNaN(parseInt(value)) ? this.getIndex(value) : parseInt(value); } private getMultiDef(value: string): MultiDef { let values = value.split(CONSTANTS.COMA).map(current => this.checkForNamed(current)), last = values.pop(); return { values, last }; } private getRangeDef(value: string): RangeDef { let range: number[] = value.split(CONSTANTS.DASH).map(current => this.checkForNamed(current)); return { start: range[0], end: range[1] }; } private getIntervalDef(value: string): IntervalDef { let interval = value.split(CONSTANTS.SLASH).map(current => this.checkForNamed(current)); return { start: interval[0], step: interval[1] }; } private getSpecificDef(value: string): SpecificDef { let specific = value.split(CONSTANTS.SPECIFIC).map(current => this.checkForNamed(current)); return { week: specific[1], day: specific[0] } } private getDaySpecificDef(value: string): DaySpecificDef { let regex = /([0-9]?[0-9])[WL]/; return { day: parseInt(value.match(regex)[1]) }; } private getDef(value: string): string { return value; } private getSingleDef(value: string): number { return this.checkForNamed(value); } private getIndex(value: string): number { return this.names.indexOf(value) + this.indexBase; } } export class CronExpression { private dissection: CronDissection; public seconds: UnitDefinition; public minutes: UnitDefinition; public hours: UnitDefinition; public dayOfMonth: UnitDefinition; public month: UnitDefinition; public dayOfWeek: UnitDefinition; public year: UnitDefinition; private cron: UnitDefinition[]; [key: string]: any; constructor(private expressionString: string) { this.setDissection(expressionString); this.seconds = new UnitDefinition(this.dissection.seconds, 0, 59); this.minutes = new UnitDefinition(this.dissection.minutes, 0, 59); this.hours = new UnitDefinition(this.dissection.hours, 0, 23); this.dayOfMonth = new UnitDefinition(this.dissection.dayOfMonth, 1, 31); this.month = new UnitDefinition(this.dissection.month, 1, 12, CONSTANTS.SHORT_MONTHS, 0); this.dayOfWeek = new UnitDefinition(this.dissection.dayOfWeek, 1, 7, CONSTANTS.SHORT_DAYS, 1); this.year = this.dissection.year ? new UnitDefinition(this.dissection.year, 1970, 2099) : null; this.cron = [this.seconds, this.minutes, this.hours, this.dayOfMonth, this.month, this.dayOfWeek, this.year]; } public isValid(): boolean { for (let unit of this.cron) { switch (unit.type) { case CONSTANTS.TYPE_SINGLE: return this.validateSingle(unit); case CONSTANTS.TYPE_UNSPECIFIED: return this.validateUnspecified(unit); case CONSTANTS.TYPE_WILDCARD: return this.validateWildcard(unit); case CONSTANTS.TYPE_MULTI: return this.validateMulti(unit); case CONSTANTS.TYPE_INTERVAL: return this.validateInterval(unit); case CONSTANTS.TYPE_RANGE: return this.validateRange(unit); case CONSTANTS.TYPE_SPECIFIC: return this.validateSpecific(unit); } } } private validateSingle(unit: UnitDefinition): boolean { return unit.single >= unit.min && unit.single <= unit.max; } private validateUnspecified(unit: UnitDefinition): boolean { return unit.unspecified === CONSTANTS.UNSPECIFIED; } private validateWildcard(unit: UnitDefinition): boolean { return unit.wildcard === CONSTANTS.WILDCARD; } private validateMulti(unit: UnitDefinition): boolean { return unit.multi.values.every(value => value >= unit.min && value <= unit.max); } private validateInterval(unit: UnitDefinition): boolean { return unit.interval.start >= unit.min && unit.interval.start <= unit.max; } private validateRange(unit: UnitDefinition): boolean { return unit.range.start < unit.range.end && unit.range.start >= unit.min && unit.range.end <= unit.max; } private validateSpecific(unit: UnitDefinition): boolean { return unit.specific.day >= 1 && unit.specific.day <= 7 && unit.specific.week >= 1 && unit.specific.week <= 5; } public getKey(key: string): any { return this[key]; } public setDissection(expression: string | CronDissection): void { if (!expression) { throw new Error('A valid cron expression or generated expression must be provided.'); } else if (typeof expression === 'string') { let exprArray = expression.split(CONSTANTS.SEPARATOR); if (exprArray.length > 7 || exprArray.length < 6) { throw new Error(`Invalid cron expression: ${expression}. Wrong length: ${exprArray.length}.`); } this.dissection = { seconds: exprArray[0], minutes: exprArray[1], hours: exprArray[2], dayOfMonth: exprArray[3], month: exprArray[4], dayOfWeek: exprArray[5], year: exprArray[6] }; } else { try { this.dissection = expression; } catch (error) { throw new Error(error); } } } public getDissection(): CronDissection { return this.dissection; } } class CronParser { public humanize(expression: string): string { let cron: CronExpression = new CronExpression(expression), order: string[] = ['month', 'dayOfWeek', 'year']; let result = `Fire ${this.getTimeString(cron.seconds, cron.minutes, cron.hours)}`, dayOfMonth = this.getDayOfMonthString(cron.dayOfMonth); if (dayOfMonth.length > 0) { result += `, ${dayOfMonth}`; } if (cron.dayOfMonth.type != CONSTANTS.TYPE_WILDCARD && cron.dayOfMonth.type != CONSTANTS.TYPE_UNSPECIFIED && (cron.month.type === CONSTANTS.TYPE_WILDCARD || cron.month.type === CONSTANTS.TYPE_UNSPECIFIED)) { result += ` of every month`; } for (let key of order) { if (cron[key].type != CONSTANTS.TYPE_WILDCARD && cron[key].type != CONSTANTS.TYPE_UNSPECIFIED || key === 'dayOfWeek') { result += `, ${this.getString(key, cron[key])}`; } } return result + '.'; } public getString(type: string, value: UnitDefinition) { switch(type) { case 'dayOfMonth': return this.getDayOfMonthString(value); case 'month': return this.getMonthString(value); case 'dayOfWeek': return this.getDayOfWeekString(value); case 'year': return this.getYearString(value); default: throw new Error('Something went wrong.'); } } public getTimeString(seconds: UnitDefinition, minutes: UnitDefinition, hours: UnitDefinition): string { if (seconds.type === CONSTANTS.TYPE_SINGLE && minutes.type === CONSTANTS.TYPE_SINGLE && hours.type === CONSTANTS.TYPE_SINGLE) { return `at ${this.padZero(hours.single)}:${this.padZero(minutes.single)}${seconds.single === 0 ? '' : ':' + this.padZero(seconds.single)}`; } else if (seconds.type === CONSTANTS.TYPE_WILDCARD && minutes.type === CONSTANTS.TYPE_WILDCARD && hours.type === CONSTANTS.TYPE_WILDCARD) { return 'every second'; } else if (seconds.type === CONSTANTS.TYPE_INTERVAL && minutes.type === CONSTANTS.TYPE_WILDCARD) { return `${this.getSecondsString(seconds)}, ${this.getHoursString(hours)}` } else { return `${this.getSecondsString(seconds)}, ${this.getMinutesString(minutes)}, ${this.getHoursString(hours)}`; } } public getSecondsString(seconds: UnitDefinition): string { switch(seconds.type) { case CONSTANTS.TYPE_WILDCARD: return 'every second'; case CONSTANTS.TYPE_UNSPECIFIED: return ''; case CONSTANTS.TYPE_RANGE: return `every second from ${seconds.range.start} through ${seconds.range.end}`; case CONSTANTS.TYPE_MULTI: return `at seconds ${seconds.multi.values.join(', ')} and ${seconds.multi.last}`; case CONSTANTS.TYPE_INTERVAL: let isOne = seconds.interval.step === 1; return `every ${isOne ? '' : seconds.interval.step + ' '}second${isOne ? '' : 's'}${seconds.interval.start === 0 ? '' : ' starting at second ' + seconds.interval.start}`; case CONSTANTS.TYPE_SINGLE: default: return `at second ${seconds.single}`; } } public getMinutesString(minutes: UnitDefinition) { switch(minutes.type) { case CONSTANTS.TYPE_WILDCARD: return 'every minute'; case CONSTANTS.TYPE_UNSPECIFIED: return ''; case CONSTANTS.TYPE_RANGE: return `every minute from ${minutes.range.start} through ${minutes.range.end}`; case CONSTANTS.TYPE_MULTI: return `at minutes ${minutes.multi.values.join(', ')} and ${minutes.multi.last}`; case CONSTANTS.TYPE_INTERVAL: let isOne = minutes.interval.step === 1; return `every ${isOne ? '' : minutes.interval.step + this.getOrdinal(minutes.interval.step) + ' '}minute${minutes.interval.start === 0 ? '' : ' starting at minute ' + minutes.interval.start}`; case CONSTANTS.TYPE_SINGLE: default: return `at minute ${minutes.single}`; } } public getHoursString(hours: UnitDefinition) { switch(hours.type) { case CONSTANTS.TYPE_WILDCARD: case CONSTANTS.TYPE_UNSPECIFIED: return 'every hour'; case CONSTANTS.TYPE_RANGE: return `during every hour from ${this.pad(hours.range.start)} through ${this.pad(hours.range.end)}`; case CONSTANTS.TYPE_MULTI: return `during the hours ${hours.multi.values.map(this.pad).join(', ')} and ${this.pad(hours.multi.last)}`; case CONSTANTS.TYPE_INTERVAL: let isOne = hours.interval.step === 1; return `past every ${isOne ? '' : hours.interval.step + this.getOrdinal(hours.interval.step) + ' '}hour starting at ${this.pad(hours.interval.start)}`; case CONSTANTS.TYPE_SINGLE: default: return `at ${this.pad(hours.single)}`; } } private getOrdinal(value: number): string { let numString = value.toString(); switch (parseInt(numString[numString.length - 1])) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } private pad(value: number | string): string { let converted = typeof value === 'number' ? value.toString() : value; return converted.length === 1 ? `0${value}:00` : `${value}:00`; } private padZero(value: number | string): string { let converted = typeof value === 'number' ? value.toString() : value; return converted.length === 1 ? `0${value}` : `${value}`; } public getDayOfMonthString(dayOfMonth: UnitDefinition) { switch(dayOfMonth.type) { case CONSTANTS.TYPE_WILDCARD: return ''; case CONSTANTS.TYPE_UNSPECIFIED: return ''; case CONSTANTS.TYPE_RANGE: return `between the ${dayOfMonth.range.start + this.getOrdinal(dayOfMonth.range.start)} and ${dayOfMonth.range.end + this.getOrdinal(dayOfMonth.range.end)}`; case CONSTANTS.TYPE_MULTI: return `during the ${dayOfMonth.multi.values.map(this.getOrdinal).join(', ')} and ${dayOfMonth.multi.last}`; case CONSTANTS.TYPE_INTERVAL: let isOne = dayOfMonth.interval.step === 1; return `every ${isOne ? '' : dayOfMonth.interval.step + ' '}day${isOne ? '' : 's'} starting on the ${dayOfMonth.interval.start + this.getOrdinal(dayOfMonth.interval.start)}`; case CONSTANTS.TYPE_NEAREST_WEEKDAY: return `on the weekday nearest to day ${dayOfMonth.nearestWeekday.day} of the month`; case CONSTANTS.TYPE_LAST_WEEKDAY_MONTH: return `on the last weekday of the month`; case CONSTANTS.TYPE_LAST_DAY: return `on the last day of the month`; case CONSTANTS.TYPE_SINGLE: default: return `on the ${dayOfMonth.single + this.getOrdinal(dayOfMonth.single)}` } } public getYearString(years: UnitDefinition) { switch(years.type) { case CONSTANTS.TYPE_WILDCARD: return 'every year'; case CONSTANTS.TYPE_UNSPECIFIED: return ''; case CONSTANTS.TYPE_RANGE: return `between ${years.range.start} and ${years.range.end}`; case CONSTANTS.TYPE_MULTI: return `during ${years.multi.values.join(', ')} and ${years.multi.last}`; case CONSTANTS.TYPE_INTERVAL: let isOne = years.interval.step === 1; return `every ${isOne ? '' : years.interval.step + ' '}year${isOne ? '' : 's'} starting on ${years.interval.start}`; case CONSTANTS.TYPE_SINGLE: default: return `during ${years.single}`; } } public getMonthString(months: UnitDefinition): string { switch(months.type) { case CONSTANTS.TYPE_WILDCARD: return 'every month'; case CONSTANTS.TYPE_UNSPECIFIED: return ''; case CONSTANTS.TYPE_RANGE: return `in the months of ${this.getMonthName(months.range.start)} through ${this.getMonthName(months.range.end)}`; case CONSTANTS.TYPE_MULTI: return `in the months of ${months.multi.values.map(this.getMonthName).join(', ')} and ${this.getMonthName(months.multi.last)}`; case CONSTANTS.TYPE_INTERVAL: let isOne = months.interval.step === 1; return `every ${isOne ? '' : months.interval.step + ' '}month${isOne ? '' : 's'} starting on ${this.getMonthName(months.interval.start)}`; case CONSTANTS.TYPE_SINGLE: default: return `in the month of ${this.getMonthName(months.single)}`; } } public getMonthName(month: number): string { return CONSTANTS.FULL_MONTHS[month]; } public getDayOfWeekString(days: UnitDefinition): string { console.log(days) switch(days.type) { case CONSTANTS.TYPE_WILDCARD: return 'every day'; case CONSTANTS.TYPE_UNSPECIFIED: return 'every day'; case CONSTANTS.TYPE_RANGE: return `only from ${this.getDayOfWeekName(days.range.start)} through ${this.getDayOfWeekName(days.range.end)}`; case CONSTANTS.TYPE_MULTI: return `only on ${days.multi.values.map(this.getDayOfWeekName).join(', ')} and ${this.getDayOfWeekName(days.multi.last)}`; case CONSTANTS.TYPE_INTERVAL: let isOne = days.interval.step === 1; return `every ${isOne ? '' : days.interval.step + ' '}day${isOne ? '' : 's'} starting on ${this.getDayOfWeekName(days.interval.start)}`; case CONSTANTS.TYPE_SPECIFIC: return `on the ${days.specific.week + this.getOrdinal(days.specific.week)} ${this.getDayOfWeekName(days.specific.day)} of the month`; case CONSTANTS.TYPE_LAST_WEEKDAY_SPECIFIC: return `on the last ${this.getDayOfWeekName(days.lastWeekdaySpecific.day)} of the month`; case CONSTANTS.TYPE_LAST_DAY: return `on the last Saturday of the month`; case CONSTANTS.TYPE_SINGLE: default: return `only on ${this.getDayOfWeekName(days.single)}s`; } } public getDayOfWeekName(day: number): string { return CONSTANTS.FULL_DAYS[day - 1]; } } const CronHumanize = new CronParser(); export default CronHumanize;