Skip to content

Commit

Permalink
Refactor/bugfix of nth weekday feature. Release 7.0.0-dev.1.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Aug 9, 2023
1 parent 7e24df5 commit 8c3259b
Show file tree
Hide file tree
Showing 14 changed files with 2,229 additions and 2,254 deletions.
1,428 changes: 704 additions & 724 deletions dist/croner.cjs

Large diffs are not rendered by default.

1,428 changes: 704 additions & 724 deletions dist/croner.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.min.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.min.cjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.min.js.map

Large diffs are not rendered by default.

1,428 changes: 704 additions & 724 deletions dist/croner.umd.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.umd.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/croner.umd.min.js.map

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "croner",
"version": "7.0.0-dev.0",
"version": "7.0.0-dev.1",
"description": "Trigger functions and/or evaluate cron expressions in JavaScript. No dependencies. Most features. All environments.",
"author": "Hexagon <github.com/hexagon>",
"homepage": "https://hexagon.github.io/croner",
Expand Down
68 changes: 25 additions & 43 deletions src/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { minitz } from "./helpers/minitz.js";
// This import is only used by tsc for generating type definitions from js/jsdoc
// deno-lint-ignore no-unused-vars
import { CronOptions as CronOptions } from "./options.js"; // eslint-disable-line no-unused-vars
import { LAST_OCCURRENCE, ANY_OCCURRENCE, OCCURRENCE_BITMASKS } from "./pattern.js";

/**
* Constant defining the minimum number of days per month where index 0 = January etc.
Expand All @@ -17,16 +18,6 @@ import { CronOptions as CronOptions } from "./options.js"; // eslint-disable-lin
*/
const DaysOfMonth = [31,28,31,30,31,30,31,31,30,31,30,31];

// Define a mapping of bitwise representations to their Nth values.
const NTH_WEEKDAY_MAP = {
0b1: 1,
0b10: 2,
0b100: 3,
0b1000: 4,
0b10000: 5,
0b100000: -1 // Represents "Last"
};

/**
* Array of work to be done, consisting of subarrays described below:
* @private
Expand Down Expand Up @@ -90,46 +81,39 @@ function CronDate (d, tz) {
* @param {number} year - The year.
* @param {number} month - The month (0 for January, 11 for December).
* @param {number} day - The day of the month.
* @param {number} nth - The nth occurrence (-1 for last).
* @param {number} nth - The nth occurrence (bitmask).
* @return {boolean} - True if the date is the nth occurrence of its weekday, false otherwise.
*/
CronDate.prototype.isNthWeekdayOfMonth = function(year, month, day, nth) {
const date = new Date(Date.UTC(year, month, day));
const weekday = date.getUTCDay();

// For positive nth values
if (nth > 0 && nth < 6) {
let count = 0;
for (let d = 1; d <= day; d++) {
const tempDate = new Date(Date.UTC(year, month, d));
if (tempDate.getUTCDay() === weekday) {
count++;
}
if (count === nth + 1) {
return false; // Found another occurrence in the same month after the nth one
}
// Count occurrences of the weekday up to and including the current date
let count = 0;
for (let d = 1; d <= day; d++) {
if (new Date(Date.UTC(year, month, d)).getUTCDay() === weekday) {
count++;
}
return count === nth;
}
// -1 for last occurrence
else if ( nth === -1 ) {
let count = 0;

// Check for nth occurrence
if (nth & ANY_OCCURRENCE && OCCURRENCE_BITMASKS[count-1] & nth) {
return true;
}

// Check for last occurrence
if (nth & LAST_OCCURRENCE) {
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
for (let d = daysInMonth; d >= day; d--) {
const tempDate = new Date(Date.UTC(year, month, d));
if (tempDate.getUTCDay() === weekday) {
count++;
}
if (count === (-nth) + 1) {
return false; // Found another occurrence in the same month before the -nth one
for (let d = day + 1; d <= daysInMonth; d++) {
if (new Date(Date.UTC(year, month, d)).getUTCDay() === weekday) {
return false; // There's another occurrence of the same weekday later in the month
}
}
return count === -nth;
} else {
throw new Error("CronDate: Invalid nth received by isNthWeekdayOfMonth");
return true; // The current date is the last occurrence of the weekday in the month
}
};

return false;
};

/**
* Sets internals using a Date
Expand Down Expand Up @@ -315,12 +299,10 @@ CronDate.prototype.findNext = function (options, target, pattern, offset) {
// Extra check for nth weekday of month
// 0b011111 === All occurences of weekday in month
// 0b100000 === Last occurence of weekday in month
if (dowMatch && dowMatch !== 0b11111) {
if (NTH_WEEKDAY_MAP[dowMatch] !== undefined) {
dowMatch = this.isNthWeekdayOfMonth(this.year, this.month, i - offset, NTH_WEEKDAY_MAP[dowMatch]);
} else {
throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${dowMatch}`);
}
if (dowMatch && (dowMatch & ANY_OCCURRENCE)) {
dowMatch = this.isNthWeekdayOfMonth(this.year, this.month, i - offset, dowMatch);
} else if (dowMatch) {
throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${dowMatch}`);
}

// If we use legacyMode, and dayOfMonth is specified - use "OR" to combine day of week with day of month
Expand Down
47 changes: 23 additions & 24 deletions src/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import { CronDate } from "./date.js";
* @typedef {Number} CronIndexOffset
*/

/**
* Constants to represent different occurrences of a weekday in its month.
* - `LAST_OCCURRENCE`: The last occurrence of a weekday.
* - `ANY_OCCURRENCE`: Combines all individual weekday occurrence bitmasks, including the last.
* - `OCCURRENCE_BITMASKS`: An array of bitmasks, with each index representing the respective occurrence of a weekday (0-indexed).
*/
export const LAST_OCCURRENCE = 0b100000;
export const ANY_OCCURRENCE = 0b00001 | 0b00010 | 0b00100 | 0b01000 | 0b10000 | LAST_OCCURRENCE;
export const OCCURRENCE_BITMASKS = [0b00001, 0b00010, 0b00100, 0b010000, 0b10000];

/**
* Create a CronPattern instance from pattern string ('* * * * * *')
* @constructor
Expand All @@ -30,7 +40,7 @@ function CronPattern (pattern, timezone) {
this.hour = Array(24).fill(0); // 0-23
this.day = Array(31).fill(0); // 0-30 in array, 1-31 in config
this.month = Array(12).fill(0); // 0-11 in array, 1-12 in config
this.dayOfWeek = Array(8).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday; Value is a bitmask
this.dayOfWeek = Array(7).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday; Value is a bitmask

this.lastDayOfMonth = false;

Expand Down Expand Up @@ -108,7 +118,7 @@ CronPattern.prototype.parse = function () {
this.partToArray("hour", parts[2], 0, 1);
this.partToArray("day", parts[3], -1, 1);
this.partToArray("month", parts[4], -1, 1);
this.partToArray("dayOfWeek", parts[5], 0, 0b11111);
this.partToArray("dayOfWeek", parts[5], 0, ANY_OCCURRENCE);

// 0 = Sunday, 7 = Sunday
if(this.dayOfWeek[7]) {
Expand Down Expand Up @@ -193,10 +203,6 @@ CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset, def
throw new TypeError("CronPattern: " + type + " is not a number: '" + conf + "'");
}

if( i < 0 || i >= this[type].length ) {
throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
}

this.setPart(type, i, result[1] || defaultValue);
};

Expand All @@ -216,8 +222,10 @@ CronPattern.prototype.setPart = function(part, index, value) {

// Special handling for dayOfWeek
if (part === "dayOfWeek") {
if ((index < 0 || index > 7) && index !== "L") {
throw new RangeError("CronPattern: Invalid value for " + part + ": " + index);
// SUN can both be 7 and 0, normalize to 0 here
if (index === 7) index = 0;
if ((index < 0 || index > 6) && index !== "L") {
throw new RangeError("CronPattern: Invalid value for dayOfWeek: " + index);
}
this.setNthWeekdayOfMonth(index, value);
return;
Expand Down Expand Up @@ -274,7 +282,6 @@ CronPattern.prototype.handleRangeWithStepping = function (conf, type, valueIndex
if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");

if( lower < 0 || upper >= this[type].length ) throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
if( lower > upper ) throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");

for (let i = lower; i <= upper; i += steps) {
Expand Down Expand Up @@ -324,11 +331,6 @@ CronPattern.prototype.handleRange = function (conf, type, valueIndexOffset, defa
throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
}

// Check that value is within range
if( lower < 0 || upper >= this[type].length ) {
throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
}

//
if( lower > upper ) {
throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
Expand Down Expand Up @@ -372,7 +374,6 @@ CronPattern.prototype.handleStepping = function (conf, type, _valueIndexOffset,
}
};


/**
* Replace day name with day numbers
* @private
Expand Down Expand Up @@ -431,7 +432,7 @@ CronPattern.prototype.handleNicknames = function (pattern) {
if (cleanPattern === "@yearly" || cleanPattern === "@annually") {
return "0 0 1 1 *";
} else if (cleanPattern === "@monthly") {
return "0 0 1 * *";
return "0 0 1 * *";
} else if (cleanPattern === "@weekly") {
return "0 0 * * 0";
} else if (cleanPattern === "@daily") {
Expand All @@ -447,21 +448,19 @@ CronPattern.prototype.handleNicknames = function (pattern) {
* Handle the nth weekday of the month logic using hash sign (e.g. FRI#2 for the second Friday of the month)
* @private
*
* @param {number|string} index - 5 for friday, 31 (0b11111) for any day
* @param {number} nth - 2 for 2nd friday
* @param {number} index - Weekday, example: 5 for friday
* @param {number} nthWeekday - bitmask, 2 (0x00010) for 2nd friday, 31 (ANY_OCCURRENCE, 0b100000) for any day
*/
CronPattern.prototype.setNthWeekdayOfMonth = function(index, nthWeekday) {
const bitmask = [0b001, 0b010, 0b100, 0b1000, 0b10000];
if (nthWeekday === "L") {
this["dayOfWeek"][index] = 0b100000;
this["dayOfWeek"][index] = this["dayOfWeek"][index] | LAST_OCCURRENCE;
} else if (nthWeekday < 6 && nthWeekday > 0) {
this["dayOfWeek"][index] = bitmask[nthWeekday - 1];
} else if (nthWeekday === 0b11111) {
this["dayOfWeek"][index] = 0b11111;
this["dayOfWeek"][index] = this["dayOfWeek"][index] | OCCURRENCE_BITMASKS[nthWeekday - 1];
} else if (nthWeekday === ANY_OCCURRENCE) {
this["dayOfWeek"][index] = ANY_OCCURRENCE;
} else {
throw new TypeError(`CronPattern: nth weekday of of range, should be 1-5 or L. Value: ${nthWeekday}`);
}

};

export { CronPattern };
54 changes: 54 additions & 0 deletions test/node/js/src/suites/pattern.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,58 @@ module.exports = function (Cron, test) {
assert.equal(nextRun.getMonth(),1);
assert.equal(nextRun.getFullYear(),2024);
});

test("0 0 0 * * SAT-SUN#L,SUN#1 should find last saturday or sunday of august 2023 (26-27/8 2023) as well as fist sunday of september", function () {

let scheduler = new Cron("0 0 0 * * SAT-SUN#L,SUN#1"),
prevRun = new Date(1691536579072), // From 9th of august 2023
nextRun = scheduler.nextRuns(prevRun);

// Do comparison
assert.equal(nextRun[0].getDate(),26);
assert.equal(nextRun[0].getMonth(),7);
assert.equal(nextRun[0].getFullYear(),2023);

assert.equal(nextRun[1].getDate(),27);
assert.equal(nextRun[1].getMonth(),7);
assert.equal(nextRun[1].getFullYear(),2023);

assert.equal(nextRun[2].getDate(),3);
assert.equal(nextRun[2].getMonth(),8);
assert.equal(nextRun[2].getFullYear(),2023);

assert.equal(nextRun[3].getDate(),24);
assert.equal(nextRun[3].getMonth(),8);
assert.equal(nextRun[3].getFullYear(),2023);

assert.equal(nextRun[4].getDate(),30);
assert.equal(nextRun[4].getMonth(),8);
assert.equal(nextRun[4].getFullYear(),2023);

});

test("0 0 0 * * SUN-MON#3,MON-TUE#1 should work", function () {

let scheduler = new Cron("0 0 0 * * SUN-MON#3,MON-TUE#1"),
prevRun = new Date(1691536579072), // From 9th of august 2023
nextRun = scheduler.nextRuns(prevRun);

// Do comparison
assert.equal(nextRun[0].getDate(),20);
assert.equal(nextRun[0].getMonth(),7);
assert.equal(nextRun[0].getFullYear(),2023);

assert.equal(nextRun[1].getDate(),21);
assert.equal(nextRun[1].getMonth(),7);
assert.equal(nextRun[1].getFullYear(),2023);

assert.equal(nextRun[2].getDate(),4);
assert.equal(nextRun[2].getMonth(),8);
assert.equal(nextRun[2].getFullYear(),2023);

assert.equal(nextRun[3].getDate(),5);
assert.equal(nextRun[3].getMonth(),8);
assert.equal(nextRun[3].getFullYear(),2023);

});
};

0 comments on commit 8c3259b

Please sign in to comment.