DocSpace-client/packages/shared/components/cron/Cron.utils.ts

549 lines
14 KiB
TypeScript

// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
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), // t("Hour"), - "Skip useless issue in UselessTranslationKeysTest"
},
];