cron-humanize
Version:
Cron to human translator
580 lines (506 loc) • 23.1 kB
text/typescript
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;