react-js-cron-mui
Version:
A React cron editor with Material UI a forked repo from Xavier Rutayisire (https://github.com/xrutayisire/react-js-cron)
563 lines (479 loc) • 12.8 kB
text/typescript
import { MutableRefObject } from 'react'
import { UNITS, SUPPORTED_SHORTCUTS } from './constants'
import { range, sort, dedup, setError } from './utils'
import {
Unit,
PeriodType,
LeadingZero,
ClockFormat,
SetInternalError,
OnError,
AllowEmpty,
Locale,
Shortcuts,
SetValueNumbersOrUndefined,
SetValuePeriod,
} from './types'
/**
* Set values from cron string
*/
export function setValuesFromCronString(
cronString: string,
setInternalError: SetInternalError,
onError: OnError,
allowEmpty: AllowEmpty,
internalValueRef: MutableRefObject<string>,
firstRender: boolean,
locale: Locale,
shortcuts: Shortcuts,
setMinutes: SetValueNumbersOrUndefined,
setHours: SetValueNumbersOrUndefined,
setMonthDays: SetValueNumbersOrUndefined,
setMonths: SetValueNumbersOrUndefined,
setWeekDays: SetValueNumbersOrUndefined,
setPeriod: SetValuePeriod
) {
onError && onError(undefined)
setInternalError(false)
let error = false
// Handle empty cron string
if (!cronString) {
if (
allowEmpty === 'always' ||
(firstRender && allowEmpty === 'for-default-value')
) {
return
}
error = true
}
if (!error) {
// Shortcuts management
if (
shortcuts &&
(shortcuts === true || shortcuts.includes(cronString as any))
) {
if (cronString === '@reboot') {
setPeriod('reboot')
return
}
// Convert a shortcut to a valid cron string
const shortcutObject = SUPPORTED_SHORTCUTS.find(
(supportedShortcut) => supportedShortcut.name === cronString
)
if (shortcutObject) {
cronString = shortcutObject.value
}
}
try {
const cronParts = parseCronString(cronString)
const period = getPeriodFromCronparts(cronParts)
setPeriod(period)
setMinutes(cronParts[0])
setHours(cronParts[1])
setMonthDays(cronParts[2])
setMonths(cronParts[3])
setWeekDays(cronParts[4])
} catch (err) {
// Specific errors are not handle (yet)
error = true
}
}
if (error) {
internalValueRef.current = cronString
setInternalError(true)
setError(onError, locale)
}
}
/**
* Get cron string from values
*/
export function getCronStringFromValues(
period: PeriodType,
months: number[] | undefined,
monthDays: number[] | undefined,
weekDays: number[] | undefined,
hours: number[] | undefined,
minutes: number[] | undefined,
humanizeValue?: boolean,
useCronIntervals?: boolean,
) {
if (period === 'reboot') {
return '@reboot'
}
const newMonths = period === 'year' && months ? months : []
const newMonthDays =
(period === 'year' || period === 'month') && monthDays ? monthDays : []
const newWeekDays =
(period === 'year' || period === 'month' || period === 'week') && weekDays
? weekDays
: []
const newHours =
period !== 'minute' && period !== 'hour' && hours ? hours : []
const newMinutes = period !== 'minute' && minutes ? minutes : []
const parsedArray = parseCronArray(
[newMinutes, newHours, newMonthDays, newMonths, newWeekDays],
humanizeValue,
useCronIntervals,
)
return cronToString(parsedArray)
}
/**
* Returns the cron part array as a string.
*/
export function partToString(
cronPart: number[],
unit: Unit,
humanize?: boolean,
useCronIntervals?: boolean,
leadingZero?: LeadingZero,
clockFormat?: ClockFormat
) {
let retval = ''
if (isFull(cronPart, unit) || cronPart.length === 0) {
retval = '*'
} else {
const step = getStep(cronPart)
if (useCronIntervals && step && isInterval(cronPart, step)) {
if (isFullInterval(cronPart, unit, step)) {
retval = `*/${step}`
} else {
retval = `${formatValue(
getMin(cronPart),
unit,
humanize,
leadingZero,
clockFormat
)}-${formatValue(
getMax(cronPart),
unit,
humanize,
leadingZero,
clockFormat
)}/${step}`
}
} else {
retval = toRanges(cronPart)
.map((range: number | number[]) => {
if (Array.isArray(range)) {
return `${formatValue(
range[0],
unit,
humanize,
leadingZero,
clockFormat
)}-${formatValue(
range[1],
unit,
humanize,
leadingZero,
clockFormat
)}`
}
return formatValue(range, unit, humanize, leadingZero, clockFormat)
})
.join(',')
}
}
return retval
}
/**
* Format the value
*/
export function formatValue(
value: number,
unit: Unit,
humanize?: boolean,
leadingZero?: LeadingZero,
clockFormat?: ClockFormat
) {
let cronPartString = value.toString()
const { type, alt, min } = unit
const needLeadingZero =
leadingZero && (leadingZero === true || leadingZero.includes(type as any))
const need24HourClock =
clockFormat === '24-hour-clock' && (type === 'hours' || type === 'minutes')
if ((humanize && type === 'week-days') || (humanize && type === 'months')) {
cronPartString = alt![value - min]
} else if (value < 10 && (needLeadingZero || need24HourClock)) {
cronPartString = cronPartString.padStart(2, '0')
}
if (type === 'hours' && clockFormat === '12-hour-clock') {
const suffix = value >= 12 ? 'PM' : 'AM'
let hour: number | string = value % 12 || 12
if (hour < 10 && needLeadingZero) {
hour = hour.toString().padStart(2, '0')
}
cronPartString = `${hour}${suffix}`
}
return cronPartString
}
/**
* Parses a 2-dimentional array of integers as a cron schedule
*/
function parseCronArray(cronArr: number[][], humanizeValue?: boolean, useCronIntervals?: boolean) {
if (cronArr.length === 5) {
return cronArr.map((partArr, idx) => {
const unit = UNITS[idx]
const parsedArray = parsePartArray(partArr, unit)
return partToString(parsedArray, unit, humanizeValue, useCronIntervals)
})
}
throw new Error('Invalid cron array')
}
/**
* Returns the cron array as a string
*/
function cronToString(parts: string[]) {
return parts.join(' ')
}
/**
* Find the period from cron parts
*/
function getPeriodFromCronparts(cronParts: number[][]): PeriodType {
if (cronParts[3].length > 0) {
return 'year'
} else if (cronParts[2].length > 0) {
return 'month'
} else if (cronParts[4].length > 0) {
return 'week'
} else if (cronParts[1].length > 0) {
return 'day'
} else if (cronParts[0].length > 0) {
return 'hour'
}
return 'minute'
}
/**
* Parses a cron string to an array of parts
*/
function parseCronString(str: string) {
if (typeof str !== 'string') {
throw new Error('Invalid cron string')
}
const parts = str.replace(/\s+/g, ' ').trim().split(' ')
if (parts.length === 5) {
return parts.map((partStr, idx) => {
return parsePartString(partStr, UNITS[idx])
})
}
throw new Error('Invalid cron string format')
}
/**
* Parses a string as a range of positive integers
*/
function parsePartString(str: string, unit: Unit) {
if (str === '*' || str === '*/1') {
return []
}
const stringParts = str.split('/')
if (stringParts.length > 2) {
throw new Error(`Invalid value "${unit.type}"`)
}
const rangeString = replaceAlternatives(stringParts[0], unit.min, unit.alt)
let parsedValues: number[]
if (rangeString === '*') {
parsedValues = range(unit.min, unit.max)
} else {
parsedValues = sort(
dedup(
fixSunday(
rangeString
.split(',')
.map((range) => {
return parseRange(range, str, unit)
})
.flat(),
unit
)
)
)
const value = outOfRange(parsedValues, unit)
if (typeof value !== 'undefined') {
throw new Error(`Value "${value}" out of range for ${unit.type}`)
}
}
const step = parseStep(stringParts[1], unit)
const intervalValues = applyInterval(parsedValues, step)
if (intervalValues.length === unit.total) {
return []
} else if (intervalValues.length === 0) {
throw new Error(`Empty interval value "${str}" for ${unit.type}`)
}
return intervalValues
}
/**
* Replaces the alternative representations of numbers in a string
*/
function replaceAlternatives(str: string, min: number, alt?: string[]) {
if (alt) {
str = str.toUpperCase()
for (let i = 0; i < alt.length; i++) {
str = str.replace(alt[i], `${i + min}`)
}
}
return str
}
/**
* Replace all 7 with 0 as Sunday can be represented by both
*/
function fixSunday(values: number[], unit: Unit) {
if (unit.type === 'week-days') {
values = values.map(function (value) {
if (value === 7) {
return 0
}
return value
})
}
return values
}
/**
* Parses a range string
*/
function parseRange(rangeStr: string, context: string, unit: Unit) {
const subparts = rangeStr.split('-')
if (subparts.length === 1) {
const value = parseInt(subparts[0], 10)
if (isNaN(value)) {
throw new Error(`Invalid value "${context}" for ${unit.type}`)
}
return [value]
} else if (subparts.length === 2) {
const minValue = parseInt(subparts[0], 10)
const maxValue = parseInt(subparts[1], 10)
if (maxValue <= minValue) {
throw new Error(
`Max range is less than min range in "${rangeStr}" for ${unit.type}`
)
}
return range(minValue, maxValue)
} else {
throw new Error(`Invalid value "${rangeStr}" for ${unit.type}`)
}
}
/**
* Finds an element from values that is outside of the range of unit
*/
function outOfRange(values: number[], unit: Unit) {
const first = values[0]
const last = values[values.length - 1]
if (first < unit.min) {
return first
} else if (last > unit.max) {
return last
}
return
}
/**
* Parses the step from a part string
*/
function parseStep(step: string, unit: Unit) {
if (typeof step !== 'undefined') {
const parsedStep = parseInt(step, 10)
if (isNaN(parsedStep) || parsedStep < 1) {
throw new Error(`Invalid interval step value "${step}" for ${unit.type}`)
}
return parsedStep
}
}
/**
* Applies an interval step to a collection of values
*/
function applyInterval(values: number[], step?: number) {
if (step) {
const minVal = values[0]
values = values.filter((value) => {
return value % step === minVal % step || value === minVal
})
}
return values
}
/**
* Validates a range of positive integers
*/
export function parsePartArray(arr: number[], unit: Unit) {
const values = sort(dedup(fixSunday(arr, unit)))
if (values.length === 0) {
return values
}
const value = outOfRange(values, unit)
if (typeof value !== 'undefined') {
throw new Error(`Value "${value}" out of range for ${unit.type}`)
}
return values
}
/**
* Returns true if range has all the values of the unit
*/
function isFull(values: number[], unit: Unit) {
return values.length === unit.max - unit.min + 1
}
/**
* Returns the difference between first and second elements in the range
*/
function getStep(values: number[]) {
if (values.length > 2) {
const step = values[1] - values[0]
if (step > 1) {
return step
}
}
}
/**
* Returns true if the range can be represented as an interval
*/
function isInterval(values: number[], step: number) {
for (let i = 1; i < values.length; i++) {
const prev = values[i - 1]
const value = values[i]
if (value - prev !== step) {
return false
}
}
return true
}
/**
* Returns true if the range contains all the interval values
*/
function isFullInterval(values: number[], unit: Unit, step: number) {
const min = getMin(values)
const max = getMax(values)
const haveAllValues = values.length === (max - min) / step + 1
if (min === unit.min && max + step > unit.max && haveAllValues) {
return true
}
return false
}
/**
* Returns the smallest value in the range
*/
function getMin(values: number[]) {
return values[0]
}
/**
* Returns the largest value in the range
*/
function getMax(values: number[]) {
return values[values.length - 1]
}
/**
* Returns the range as an array of ranges
* defined as arrays of positive integers
*/
function toRanges(values: number[]) {
const retval: (number[] | number)[] = []
let startPart: number | null = null
values.forEach((value, index, self) => {
if (value !== self[index + 1] - 1) {
if (startPart !== null) {
retval.push([startPart, value])
startPart = null
} else {
retval.push(value)
}
} else if (startPart === null) {
startPart = value
}
})
return retval
}