quartzcron
version: 0.1.0
CHANGELOG
Quartz scheduler offers way more flexibility compared to traditional cron tool.
That additional freedom reflects into less trivial composition for the cron strings, this library aims to
- help to programmatically create cron expressions
- validate cron expressions
- get the n next precise occurrences
A quartz cron expression
is characterized by the following structure:
s i h dom m dow y
⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜
'--+--+--+--+--+--+--> 1st: seconds
'--+--+--+--+--+--> 2nd: minutes
'--+--+--+--+--> 3rd: hours
'--+--+--+--> 4th: days of month ---\
'--+--+--> 5th: months | mutually exclusive
'--+--> 6th: days of week ---/
'--> 7th: years
sample usage
const QuartzCron = require('quartzcron'),
qct = new QuartzCron();
let exp = qct.out(); // -> 0 0 0 * * ? *
// thus the default is
// at midnight of everyday
// but default values can be changed when calling the constructor
qct.atHour(12)
.atHourAdd(22)
.onLastMonthDay()
.everyNYears(5, 2025);
exp = qct.out(); //-> 0 0 12,22 L * ? 2025/5
/*
alternatively the cron expression is also returned
as the instance _toString_ invokation
*/
const next = qct.next({
date: new Date('00:00:00 01-01-2024'),
n: 3
})
/*
[
2025-01-31T12:00:00.000Z,
2025-01-31T22:00:00.000Z,
2025-02-28T12:00:00.000Z
]
*/
API
constructor 👷🏽♂️
Constructor can handle:
- 0 parameters
default used0 0 0 * * ? *
const qc = new Quartzcron();
- 1 valid expression string
when invalid throws and exceptionconst qc = new Quartzcron('0 0 12,22 L * ? 2025/5');
- 1 object corresponding to a valid expression
throws an exception when the resulting expression is not valid.const exp = { s:0, i: 0, h: 0, dom:'*', m:'*', dow: '?', y: '*' } const qc = new Quartzcron(exp);
get the quartz cron expression 🧊
Invoke out()
ƒunction on the quartzcron instance to get the related expression
qct.out(); // -> "0 0 12,22 L * ? 2025/5"
validation API ✅
Validation can be done on the quartzcron instance just invoking the validate
ƒunction. Pass the string to be evaluated as parameter.
When nothing is passed it will validate the expression it would get from out
(as useful as expect(true).toBe(true)
).
qct.validate('0 0 12,22 L * ? 2025/5')
// -> { valid: true, errors:[]}
returning an object shaped like follows:
{ valid: Boolean, errors:[String]}
Alternatively a static method is available:
QuartzCron.validate(yourExp)
// -> { valid: ?, errors:[?]}
composition API 🧱
Almost all 7 fields composing the final cron expression are independent.
The only exception is represented by the "days of month" (4th field) and the "days of week" (6th field) cause they cannot coexsist.
Within the involved months/years:
dom
sets target days referencing the monthdow
sets target days referencing the week
One of the two must hold something valid different from ?
and the other one must just contain ?
.
week days and month names aliases
For weeekdays one can use seamlessly:
[1,2,3,4,5,6,7]
// OR
['SUN','MON','TUE','WED',
'THU', 'FRI', 'SAT']
similarly for months:
[1,2,3,4,5,6,7,8,9,10,11,12]
// OR
['JAN','FEB','MAR','APR',
'MAY','JUN','JUL','AUG',
'SEP','OCT','NOV','DEC']`.
seconds ⏱️
everySecond()
no explanation needed; still one should consider that this command will only update thes
to*
.
Which minute/s will actually be part of the target depends on how the instance was constructed.
If no other command is executed the target will be from 0-th to 59-th second of the first minutes of the first hour of the following day and this is cause the default values are0 0 0 * * ? *
.
This clearly applies similarly also for almost all other commands.everyNSeconds(x, start = 0)
everyx
seconds (starting fromstart
)atSecond(sec, cad = false)
overrides any previous value set there;
when cadence is not passed can be called passing multiple comma separated values within[0, 59]
atSecondAdd(sec, cad = false)
additive version of the previous setter.betweenSeconds(from, to, every)
all seconds fromfrom
toto
seconds; optionally set the cadence passing anevery
integer.betweenSecondsAdd(from, to, every)
additive version of the previous setter.
minutes ⏱️
everyMinute()
no explanation neededeveryNMinutes(x, start = 0)
everyx
minutes (starting fromstart
)atMinute(min, cad = false)
overrides any previous value set there;
when cadence is not passed can be called passing multiple comma separated values within[0, 59]
atMinuteAdd(min, cad = false)
additive version of the previous setter.betweenMinutes(from, to, every)
all minutes fromfrom
toto
minutes; optionally set the cadence passing anevery
integer.betweenMinutesAdd(from, to, every)
additive version of the previous setter.
hours ⏱️
everyHour()
no explanation neededeveryNHours(x, start = 0)
everyx
hours (starting fromstart
)atHour(h, cad = false)
no explanation needed;
overrides any previous value set there;
when cadence is not passed can be called passing multiple comma separated values within[0, 23]
atHourAdd(h, cad = false)
addsh
to the list of already set hours (0 there by default); as in the previous can pass multiple values comma separated (when cadence is not passed).betweenHours(from, to, every)
all hours fromfrom
toto
hours; optionally set the cadence passing anevery
integer.betweenHoursAdd(from, to, every)
additive version of the previous setter.
day of month / day of week 📆
everyDay()
no explanation neededqct.everyDay() // { dom: '*', dow:'?', ...}
everyNDays(x, y)
everyx
[1-31][1-31]
days starting fromy
th day[1-31]
of the target months.qct.everyNDays('13, 10) // { dom: `10/13`, dow: '?', ...}
atWeekDay(wd, cad = false)
overrides any previous value set there; when cadence is not passed even here more than one comma separated value can be passed.qtc.atWeekDay(4) // { dom: `?`, dow: 4, ...} qtc.atWeekDay(4, 3) // { dom: `?`, dow: '4/3', ...} qtc.atWeekDay('4,5') // { dom: `?`, dow: '4,5', ...} qtc.atWeekDay('2-5', 2) // or qtc.atWeekDay('2-5/2') // { dom: '?`, dow: '2-5/2', ...}
atWeekDayAdd(wd, cad = false)
everywd
in[1,7]
or (...and corresponding to){SUN,MON,TUE,WED,THU,FRI,SAT}
; adds one more weekday in the current (default empty) list.qtc.atWeekDayAdd('MON') // { dom: `?`, dow: 'MON', ...} qtc.atWeekDayAdd('WED', 2) // { dom: `?`, dow: 'MON,WED/2', ...} qtc.atWeekDayAdd('FRI-SAT') // { dom: '?`, dow: 'MON,WED/2,FRI-SAT', ...}
everyWeekDay()
shortcut to set Saturnday and Sundayqtc.everyWeekDay() // { dom: `?`, dow: '2-6', ...}
everyWeekEnd()
shortcut to set Saturnday and Sundayqtc.everyWeekEnd() // { dom: `?`, dow: '7-1', ...}
atMonthDay(dom, cad = false)
sets the target day of month, can be:*
: all daysn
: with n in[1,31]
n,m,...
: comma separated values all in[1,31]
n/c
: every c starting from nn-m
: fromn
tom
(in[1,31]
)n-m/c
: fromn
tom
(in[1,31]
) withc
cadence for the last two examples there's also an on purpose method namedbetweenMonthDays
qtc.atMonthDay('*') //same as qtc.everyDay() // { dom: '*', dow: '?', ...} qtc.atMonthDay(10) // { dom: '10', dow: '?', ...} qtc.atMonthDay('10,20') // { dom: '10,20', dow: '?', ...} qtc.atMonthDay('10-20') // { dom: '10-20', dow: '?', ...} qtc.atMonthDay('10-20/2') // { dom: '10-20/2', dow: '?', ...} qtc.atMonthDay('10/2') // { dom: '10/2', dow: '?', ...}
atMonthDayAdd(dom, cad = false)
allows to add one or more days to the existing targetqtc.atMonthDayAdd('10') // { dom: '10', dow: '?', ...} qtc.atMonthDayAdd('13/3') // { dom: '10,13/3', dow: '?', ...} qtc.atMonthDayAdd('23') // { dom: '10,13/3,23', dow: '?', ...}
betweenMonthDays(from, to, every)
set target days fromfrom
toto
with, if passed > 1, a cadence bigger than 1qtc.betweenMonthDays(10, 20) // { dom: '10-20', dow: '?', ...} qtc.betweenMonthDays(10, 20, 2) // { dom: '10-20/2', dow: '?', ...}
onLastMonthDay
set the target day to the last day of the target monthsqtc.onLastMonthDay() // { dom: 'L', dow: '?', ...}
onFirstMonthWeekDay
set as target day the first weekday of the month (working day)
(same asqtc.onClosestWorkingDayToTheNMonthDay(1)
)qtc.onFirstMonthWeekDay() // { dom: '1W', dow: '?', ...}
onLastMonthWeekDay
set as target day the last weekday of the month (working day)qtc.onLastMonthWeekDay() // { dom: 'LW', dow: '?', ...}
onLastMonthNWeekDay(x)
set as target day the last selected week day of the monthqtc.onLastMonthNWeekDay(2) // last monday of the month // { dom: '?', dow: '2L', ...}
onNDayBeforeTheEndOfTheMonth(n)
set as target the X-th day before the end of the monthqtc.onNDayBeforeTheEndOfTheMonth(9) // nine days before the end of the month // { dom: 'L-9', dow: '?', ...}
onClosestWorkingDayToTheNMonthDay(x)
set as target the nearest weekday (working day) to the x-th day of the monthqtc.onClosestWorkingDayToTheNMonthDay(15) // the closest woring day (mon->fri) to the 15th // { dom: '15W', dow: '?', ...}
onNWeekDayOfTheMonth(n, wd)
set as target the n-th week day of the monthqtc.onNWeekDayOfTheMonth(4, 2) // the 4th tuesday // { dom: '?', dow: '2#4', ...}
months 📆
everyMonth()
no explanation neededeveryNMonths(freq, start)
everyfreq
months (starting fromstart
)atMonth(m, cad = false)
overrides any previous value set there;
when cadence is not passed can be called passing multiple comma separated values within[1, 12]
or [JAN -> DEC]atMonthAdd(m, cad = false)
addsm
to the list of already set months; as in the previous can pass multiple values comma separated (when cadence is not passed).betweenMonths(from, to, every)
all months fromfrom
month toto
month; optionally set the cadence passing anevery
integer.
years 📆
everyYear()
no explanation neededeveryNYears(freq, start)
everyx
years (starting fromstart
)atYear(y, cad = false)
overrides any previous value set there;
when cadence is not passed can be called passing multiple comma separated values within[1970, 2099]
atYearAdd(y, cad = false)
addsmin
to the list of already set minutes; as in the previous can pass multiple values comma separated (when cadence is not passed), within[1970, 2099]
betweenYears(from, to, every)
all years fromfrom
year toto
year; optionally set the cadence passing anevery
integer.
more ➕
This library was primarily designed to achieve a quite simple task: compose the expression through methods; then validation, occurrences, ..
The occurrences composition depends 100% on two data:
1) the current expression
2) the fererence date
thus, calculate the occurrences of an already existing expression (for example given back from and endpoint) one could
override manually one or more elements in the expression s,i,h,dom,m,dow,y
inst.elements.s = 1
OR use
updateExp(exp)
updates the current instance expression, handles- an expression string
when invalid throws and exception - an object corresponding to an expression
throws an exception when the resulting expression is not valid{ s:0, i: 0, h: 0, dom:'*', m:'*', dow: '?', y: '*' }
- an expression string
range resolvers 🛸
Range resolvers might be useful, initially only part of an internal utility, now are exposed in a static object.
qct.out()// -> 45/2 1,2,3 15-19 L 1/2 ? *
Quartzcron.solvers.solve_0_59_ranges(qtc.elements.s)
// -> [45,47,49,51,53,55,57,59]
Quartzcron.solvers.solve_0_59_ranges(qtc.elements.i)
// -> [1,2,3]
Quartzcron.solvers.solve_hours_ranges(qtc.elements.h)
// -> [15,16,17,18,19]
Quartzcron.solvers.solve_dom(2025, 2, qtc.elements.dom)
// -> [28]
Quartzcron.solvers.solve_month_ranges(qtc.elements.m)
// [1,3,5,7,9,11]
Quartzcron.solvers.solve_dow(2025, 2, qtc.elements.dow)
// -> []
Quartzcron.solvers.solve_year_ranges(qtc.elements.y)
// -> [2025, ..., 2099] // here supposing we are in 2025
Clearly enough the two about dom
and dow
are function since they strictly depend on the year and the month.
Solvers come quite in hand when from an expression one needs the actual targets.
Months and week days will be always resolved numerically, thus [1,12]
and [1,7]
respectively.
Occurrences 🔭
From an instance call the next
ƒunction:
const nextOccurrence = qct.next().map(s=>s.toString())
// ["Mon Jan 01 2024 02:00:00 GMT+0100"],
this ƒunction accepts three options:
n
: the number of occurrences needed (1 is the default)date
: a reference date (js Date object) to be used as present date (default is the current date). No dates before the present date will be returned.
🩼 Limitations and plans 💡
⏱️ Timezones 🗺️
If the plan to use that utility on a browser the chances the client and server run on different timezones is quite high.
A workaround would be so set the timezone to UTC on the server and in the UI explicitly inform the user that all dates & times are UTC.
For the moment this library relies 100% on UTC
The plan is to provide 2 static methods to allow to set the timezones for the client and the server.
QuartzCron.useClientLocalTimezone() // auto (e.g -2)
// or useClientUTCTimezone()
QuartzCron.setServerTimezone("America/Los_Angeles"); // +6
// or setServerUTCTimezone()
Descriptions 📜
Having a quick way to get a user readable internationalized description out of an expression is quite useful.
Meanwhile give a try to the awesome cronstrue npm package.