523 lines
12 KiB
TypeScript
523 lines
12 KiB
TypeScript
import { capitalize } from "lodash";
|
|
import { DateTime, Info } from "luxon";
|
|
|
|
import {
|
|
Options,
|
|
PeriodOptionType,
|
|
PeriodType,
|
|
TFunction,
|
|
Unit,
|
|
} from "./Cron.types";
|
|
|
|
export const parseNumber = (value: unknown) => {
|
|
if (typeof value === "string") {
|
|
const str: string = value.trim();
|
|
if (/^\d+$/.test(str)) {
|
|
const num = Number(str);
|
|
if (!Number.isNaN(num) && Number.isFinite(num)) {
|
|
return num;
|
|
}
|
|
}
|
|
} else if (typeof value === "number") {
|
|
if (
|
|
!Number.isNaN(value) &&
|
|
Number.isFinite(value) &&
|
|
value === Math.floor(value)
|
|
) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
export const assertValidArray = (arr: unknown): void | never => {
|
|
if (
|
|
arr === undefined ||
|
|
!Array.isArray(arr) ||
|
|
arr.length !== 5 ||
|
|
arr.some((element) => !Array.isArray(element))
|
|
) {
|
|
throw new Error("Invalid cron array");
|
|
}
|
|
};
|
|
|
|
export const getRange = (start: number, end: number): number[] => {
|
|
const array: number[] = [];
|
|
for (let i = start; i <= end; i += 1) {
|
|
array.push(i);
|
|
}
|
|
return array;
|
|
};
|
|
|
|
export const sort = (array: number[]) => [...array].sort((a, b) => a - b);
|
|
|
|
export const flatten = (arrays: number[][]) =>
|
|
([] as number[]).concat.apply([], arrays);
|
|
|
|
export const dedup = (array: number[]) => {
|
|
const result: number[] = [];
|
|
array.forEach((i) => {
|
|
if (result.indexOf(i) < 0) {
|
|
result.push(i);
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
export const getPeriodFromCronParts = (cronParts: number[][]): PeriodType => {
|
|
if (cronParts[3].length > 0) {
|
|
return "Year";
|
|
}
|
|
if (cronParts[2].length > 0) {
|
|
return "Month";
|
|
}
|
|
if (cronParts[4].length > 0) {
|
|
return "Week";
|
|
}
|
|
if (cronParts[1].length > 0) {
|
|
return "Day";
|
|
}
|
|
if (cronParts[0].length > 0) {
|
|
return "Hour";
|
|
}
|
|
// return "minute";
|
|
return "Hour";
|
|
};
|
|
|
|
export const fixFormatValue = (value: number, local: string) => {
|
|
const result = value.toLocaleString(local, {
|
|
minimumIntegerDigits: 2,
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
useGrouping: false,
|
|
});
|
|
return result;
|
|
};
|
|
|
|
const replaceAlternatives = (str: string, unit: Unit) => {
|
|
if (unit.alt) {
|
|
str = str.toUpperCase();
|
|
for (let i = 0; i < unit.alt.length; i += 1) {
|
|
str = str.replace(unit.alt[i], String(i + unit.min));
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
|
|
const getError = (error: string, unit: Unit) =>
|
|
new Error(`${error} for ${unit.name}`);
|
|
|
|
const fixSunday = (values: number[], unit: Unit) => {
|
|
if (unit.name === "weekday") {
|
|
values = values.map((value) => {
|
|
if (value === 7) {
|
|
return 0;
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
return values;
|
|
};
|
|
|
|
const assertInRange = (values: number[], unit: Unit) => {
|
|
const first = values[0];
|
|
const last = values[values.length - 1];
|
|
if (first < unit.min) {
|
|
throw getError(`Value "${first}" out of range`, unit);
|
|
} else if (last > unit.max) {
|
|
throw getError(`Value "${last}" out of range`, unit);
|
|
}
|
|
};
|
|
|
|
const isInterval = (values: number[], step: number) => {
|
|
for (let i = 1; i < values.length; i += 1) {
|
|
const prev = values[i - 1];
|
|
const value = values[i];
|
|
if (value - prev !== step) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const isFull = (values: number[], unit: Unit) => {
|
|
return values.length === unit.max - unit.min + 1;
|
|
};
|
|
|
|
const getStep = (values: number[]) => {
|
|
if (values.length > 2) {
|
|
const step = values[1] - values[0];
|
|
if (step > 1) {
|
|
return step;
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const isFullInterval = (values: number[], unit: Unit, step: number) => {
|
|
const min = values[0];
|
|
const max = values[values.length - 1];
|
|
const haveAllValues = values.length === (max - min) / step + 1;
|
|
if (min === unit.min && max + step > unit.max && haveAllValues) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const formatValue = (value: number, unit: Unit, options: Options) => {
|
|
if (
|
|
(options.outputWeekdayNames && unit.name === "weekday") ||
|
|
(options.outputMonthNames && unit.name === "month")
|
|
) {
|
|
if (unit.alt) {
|
|
return unit.alt[value - unit.min];
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const toRanges = (values: number[]) => {
|
|
const retval: number[][] = [];
|
|
let startPart: number | undefined;
|
|
values.forEach((value, index, self) => {
|
|
if (value !== self[index + 1] - 1) {
|
|
if (startPart !== undefined) {
|
|
retval.push([startPart, value]);
|
|
startPart = undefined;
|
|
} else {
|
|
retval.push([value]);
|
|
}
|
|
} else if (startPart === undefined) {
|
|
startPart = value;
|
|
}
|
|
});
|
|
return retval;
|
|
};
|
|
|
|
const parseRange = (rangeString: string, context: string, unit: Unit) => {
|
|
const subparts = rangeString.split("-");
|
|
if (subparts.length === 1) {
|
|
const value = parseNumber(subparts[0]);
|
|
if (value === undefined) {
|
|
throw getError(`Invalid value "${context}"`, unit);
|
|
}
|
|
return [value];
|
|
}
|
|
|
|
if (subparts.length === 2) {
|
|
const minValue = parseNumber(subparts[0]);
|
|
const maxValue = parseNumber(subparts[1]);
|
|
if (minValue === undefined || maxValue === undefined) {
|
|
throw getError(`Invalid value "${context}"`, unit);
|
|
}
|
|
if (maxValue < minValue) {
|
|
throw getError(
|
|
`Max range is less than min range in "${rangeString}"`,
|
|
unit,
|
|
);
|
|
}
|
|
return getRange(minValue, maxValue);
|
|
}
|
|
|
|
throw getError(`Invalid value "${rangeString}"`, unit);
|
|
};
|
|
|
|
const parseStep = (step: string, unit: Unit) => {
|
|
if (step !== undefined) {
|
|
const parsedStep = parseNumber(step);
|
|
if (parsedStep === undefined) {
|
|
throw getError(`Invalid interval step value "${step}"`, unit);
|
|
}
|
|
return parsedStep;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const applyInterval = (values: number[], step: number) => {
|
|
if (step) {
|
|
const minVal = values[0];
|
|
values = values.filter(
|
|
(value) => value % step === minVal % step || value === minVal,
|
|
);
|
|
}
|
|
return values;
|
|
};
|
|
const toString = (values: number[], unit: Unit, options: Options) => {
|
|
let retval = "";
|
|
if (isFull(values, unit) || values.length === 0) {
|
|
if (options.outputHashes) {
|
|
retval = "H";
|
|
} else {
|
|
retval = "*";
|
|
}
|
|
} else {
|
|
const step = getStep(values);
|
|
if (step && isInterval(values, step)) {
|
|
if (isFullInterval(values, unit, step)) {
|
|
if (options.outputHashes) {
|
|
retval = `H/${step}`;
|
|
} else {
|
|
retval = `*/${step}`;
|
|
}
|
|
} else {
|
|
const min = values[0];
|
|
const max = values[values.length - 1];
|
|
const range = `${formatValue(min, unit, options)}-${formatValue(
|
|
max,
|
|
unit,
|
|
options,
|
|
)}`;
|
|
if (options.outputHashes) {
|
|
retval = `H(${range})/${step}`;
|
|
} else {
|
|
retval = `${range}/${step}`;
|
|
}
|
|
}
|
|
} else {
|
|
retval = toRanges(values)
|
|
.map((range) => {
|
|
if (range.length === 1) {
|
|
return formatValue(range[0], unit, options);
|
|
}
|
|
return `${formatValue(range[0], unit, options)}-${formatValue(
|
|
range[1],
|
|
unit,
|
|
options,
|
|
)}`;
|
|
})
|
|
.join(",");
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
};
|
|
|
|
export const arrayToStringPart = (
|
|
arr: number[],
|
|
unit: Unit,
|
|
options: Options,
|
|
) => {
|
|
const values = sort(
|
|
dedup(
|
|
fixSunday(
|
|
arr.map((value) => {
|
|
const parsedValue = parseNumber(value);
|
|
if (parsedValue === undefined) {
|
|
throw getError(`Invalid value "${value}"`, unit);
|
|
}
|
|
return parsedValue;
|
|
}),
|
|
unit,
|
|
),
|
|
),
|
|
);
|
|
assertInRange(values, unit);
|
|
return toString(values, unit, options);
|
|
};
|
|
|
|
export const stringToArrayPart = (str: string, unit: Unit, full = false) => {
|
|
if ((str === "*" || str === "*/1") && !full) {
|
|
return [];
|
|
}
|
|
|
|
const values = sort(
|
|
dedup(
|
|
fixSunday(
|
|
flatten(
|
|
replaceAlternatives(str, unit)
|
|
.split(",")
|
|
.map((value: string) => {
|
|
const valueParts = value.split("/");
|
|
if (valueParts.length > 2) {
|
|
throw getError(`Invalid value "${str}"`, unit);
|
|
}
|
|
let parsedValues: number[];
|
|
const left = valueParts[0];
|
|
const right = valueParts[1];
|
|
if (left === "*") {
|
|
parsedValues = getRange(unit.min, unit.max);
|
|
} else {
|
|
parsedValues = parseRange(left, str, unit);
|
|
}
|
|
const step = parseStep(right, unit);
|
|
return applyInterval(parsedValues, step);
|
|
}),
|
|
),
|
|
unit,
|
|
),
|
|
),
|
|
);
|
|
assertInRange(values, unit);
|
|
return values;
|
|
};
|
|
|
|
const shiftMonth = (arr: number[][], date: DateTime) => {
|
|
while (arr[3].indexOf(date.month) === -1) {
|
|
date = date.plus({ months: 1 }).startOf("month");
|
|
}
|
|
return date;
|
|
};
|
|
|
|
const shiftDay = (arr: number[][], date: DateTime): [DateTime, boolean] => {
|
|
const currentMonth = date.month;
|
|
while (
|
|
arr[2].indexOf(date.day) === -1 ||
|
|
// luxon uses 1-7 for weekdays, but we use 0-6
|
|
arr[4].indexOf(date.weekday === 7 ? 0 : date.weekday) === -1
|
|
) {
|
|
date = date.plus({ days: 1 }).startOf("day");
|
|
if (currentMonth !== date.month) {
|
|
return [date, true];
|
|
}
|
|
}
|
|
return [date, false];
|
|
};
|
|
|
|
const shiftHour = (arr: number[][], date: DateTime): [DateTime, boolean] => {
|
|
const currentDay = date.day;
|
|
while (arr[1].indexOf(date.hour) === -1) {
|
|
date = date.plus({ hours: 1 }).startOf("hour");
|
|
if (currentDay !== date.day) {
|
|
return [date, true];
|
|
}
|
|
}
|
|
return [date, false];
|
|
};
|
|
|
|
const shiftMinute = (arr: number[][], date: DateTime): [DateTime, boolean] => {
|
|
const currentHour = date.hour;
|
|
while (arr[0].indexOf(date.minute) === -1) {
|
|
date = date.plus({ minutes: 1 }).startOf("minute");
|
|
if (currentHour !== date.hour) {
|
|
return [date, true];
|
|
}
|
|
}
|
|
return [date, false];
|
|
};
|
|
|
|
export const findDate = (arr: number[][], date: DateTime) => {
|
|
let retry = 24;
|
|
let monthChanged: boolean;
|
|
let dayChanged: boolean;
|
|
let hourChanged: boolean;
|
|
|
|
// eslint-disable-next-line no-plusplus
|
|
while (--retry) {
|
|
date = shiftMonth(arr, date);
|
|
[date, monthChanged] = shiftDay(arr, date);
|
|
if (!monthChanged) {
|
|
[date, dayChanged] = shiftHour(arr, date);
|
|
if (!dayChanged) {
|
|
[date, hourChanged] = shiftMinute(arr, date);
|
|
if (!hourChanged) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!retry) {
|
|
throw new Error("Unable to find execution time for schedule");
|
|
}
|
|
return date.set({ second: 0, millisecond: 0 });
|
|
};
|
|
|
|
export const getUnits = (locale?: string) => {
|
|
const units: ReadonlyArray<Unit> = Object.freeze([
|
|
{
|
|
name: "minute",
|
|
min: 0,
|
|
max: 59,
|
|
total: 60,
|
|
},
|
|
{
|
|
name: "hour",
|
|
min: 0,
|
|
max: 23,
|
|
total: 24,
|
|
},
|
|
{
|
|
name: "day",
|
|
min: 1,
|
|
max: 31,
|
|
total: 31,
|
|
},
|
|
{
|
|
name: "month",
|
|
min: 1,
|
|
max: 12,
|
|
total: 12,
|
|
alt: [
|
|
"JAN",
|
|
"FEB",
|
|
"MAR",
|
|
"APR",
|
|
"MAY",
|
|
"JUN",
|
|
"JUL",
|
|
"AUG",
|
|
"SEP",
|
|
"OCT",
|
|
"NOV",
|
|
"DEC",
|
|
],
|
|
altWithTranslation: locale
|
|
? Info.months("long", { locale }).map(capitalize)
|
|
: undefined,
|
|
},
|
|
{
|
|
name: "weekday",
|
|
min: 0,
|
|
max: 6,
|
|
total: 7,
|
|
alt: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"],
|
|
altWithTranslation: locale
|
|
? Info.weekdays("long", { locale }).map((_, index, arr) =>
|
|
capitalize(arr[(index + arr.length - 1) % arr.length]),
|
|
)
|
|
: undefined,
|
|
},
|
|
]);
|
|
|
|
return units;
|
|
};
|
|
|
|
export const getLabel = (period: PeriodType, t: TFunction) => {
|
|
switch (period) {
|
|
case "Year":
|
|
return t("EveryYear");
|
|
case "Month":
|
|
return t("EveryMonth");
|
|
case "Week":
|
|
return t("EveryWeek");
|
|
case "Day":
|
|
return t("EveryDay");
|
|
case "Hour":
|
|
return t("EveryHour");
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
export const getOptions = (t: TFunction): PeriodOptionType[] => [
|
|
{
|
|
key: "Year",
|
|
label: getLabel("Year", t),
|
|
},
|
|
{
|
|
key: "Month",
|
|
label: getLabel("Month", t),
|
|
},
|
|
{
|
|
key: "Week",
|
|
label: getLabel("Week", t),
|
|
},
|
|
{
|
|
key: "Day",
|
|
label: getLabel("Day", t),
|
|
},
|
|
{
|
|
key: "Hour",
|
|
label: getLabel("Hour", t),
|
|
},
|
|
];
|