Skip to content

Commit e6b0046

Browse files
committed
Fix last day of month handling when explicit month is set. Fixes #375
1 parent 5e0cf18 commit e6b0046

File tree

2 files changed

+75
-71
lines changed

2 files changed

+75
-71
lines changed

src/CronFieldCollection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class CronFieldCollection {
158158
throw new Error('Validation error, Field dayOfWeek is missing');
159159
}
160160

161-
if (month.values.length === 1) {
161+
if (month.values.length === 1 && !dayOfMonth.hasLastChar) {
162162
if (!(parseInt(dayOfMonth.values[0] as string, 10) <= CronMonth.daysInMonth[month.values[0] - 1])) {
163163
throw new Error('Invalid explicit day of month definition');
164164
}

tests/CronExpressionParser.test.ts

+74-70
Original file line numberDiff line numberDiff line change
@@ -1559,81 +1559,99 @@ describe('CronExpressionParser', () => {
15591559
{ expression: '0 0 0 * * 7L', expectedDate: 26 },
15601560
];
15611561

1562-
test('parse cron with last day in a month', () => {
1562+
testCasesLastWeekdayOfMonth.forEach(({ expression, expectedDate }) => {
1563+
const options = {
1564+
currentDate: new Date(2021, 8, 1),
1565+
endDate: new Date(2021, 11, 1),
1566+
};
1567+
1568+
test(`parses cron with last weekday of the month: ${expression}`, () => {
1569+
const interval = CronExpressionParser.parse(expression, options);
1570+
expect(interval.next().getDate()).toBe(expectedDate);
1571+
});
1572+
});
1573+
1574+
test('parses cron with last day in a month (wildcard month)', () => {
15631575
const options = {
15641576
currentDate: new Date(2014, 0, 1),
15651577
endDate: new Date(2014, 10, 1),
15661578
};
15671579

15681580
const interval = CronExpressionParser.parse('0 0 L * *', options);
1569-
expect(interval.hasNext()).toBe(true);
1570-
1571-
for (let i = 0; i < 10; ++i) {
1572-
const next = interval.next();
1573-
expect(next).toBeDefined();
1574-
}
1581+
expect(interval.next().getDate()).toBe(31);
1582+
expect(interval.next().getDate()).toBe(28);
1583+
expect(interval.next().getDate()).toBe(31);
1584+
expect(interval.next().getDate()).toBe(30);
1585+
expect(interval.next().getDate()).toBe(31);
1586+
expect(interval.next().getDate()).toBe(30);
1587+
expect(interval.next().getDate()).toBe(31);
1588+
expect(interval.next().getDate()).toBe(31);
1589+
expect(interval.next().getDate()).toBe(30);
1590+
expect(interval.next().getDate()).toBe(31);
15751591
});
15761592

1577-
test('parse cron with last day in feb', () => {
1593+
test('parses cron with last day in a month (explicit month)', () => {
15781594
const options = {
1579-
currentDate: new Date(2016, 0, 1),
1595+
currentDate: new Date(2014, 0, 1),
15801596
endDate: new Date(2016, 10, 1),
15811597
};
15821598

1583-
const interval = CronExpressionParser.parse('0 0 6-20/2,L 2 *', options);
1584-
expect(interval.hasNext()).toBe(true);
1599+
const interval = CronExpressionParser.parse('0 0 L 10 *', options);
15851600

1586-
const items = 9;
1587-
let next;
1588-
let i = 0;
1589-
while (interval.hasNext()) {
1590-
next = interval.next();
1591-
i += 1;
1592-
expect(next).toBeDefined();
1593-
}
1601+
let date = interval.next();
1602+
expect(date.getDate()).toBe(31);
1603+
expect(date.getMonth()).toBe(9);
1604+
expect(date.getFullYear()).toBe(2014);
15941605

1595-
//leap year
1596-
if (!next) {
1597-
throw new Error('Invalid date');
1598-
}
1599-
expect(next).not.toBeUndefined();
1600-
expect(next.getDate()).toBe(29);
1601-
expect(i).toBe(items);
1606+
date = interval.next();
1607+
expect(date.getDate()).toBe(31);
1608+
expect(date.getMonth()).toBe(9);
1609+
expect(date.getFullYear()).toBe(2015);
1610+
1611+
date = interval.next();
1612+
expect(date.getDate()).toBe(31);
1613+
expect(date.getMonth()).toBe(9);
1614+
expect(date.getFullYear()).toBe(2016);
16021615
});
16031616

1604-
test('parse cron with last day in feb', () => {
1617+
test('parses cron with last day in feb', () => {
16051618
const options = {
16061619
currentDate: new Date(2014, 0, 1),
1607-
endDate: new Date(2014, 10, 1),
1620+
endDate: new Date(2018, 10, 1),
16081621
};
16091622

1610-
const interval = CronExpressionParser.parse('0 0 1,3,6-10,L 2 *', options);
1611-
expect(interval.hasNext()).toBe(true);
1623+
const interval = CronExpressionParser.parse('0 0 L 2 *', options);
16121624

1613-
let next;
1614-
while (interval.hasNext()) {
1615-
next = interval.next();
1616-
expect(next).toBeDefined();
1617-
}
1618-
if (!next) {
1619-
throw new Error('Invalid date');
1620-
}
1621-
expect(next).not.toBeUndefined();
1622-
expect(next.getDate()).toBe(28);
1625+
let date = interval.next();
1626+
expect(date.getDate()).toBe(28);
1627+
expect(date.getMonth()).toBe(1);
1628+
expect(date.getFullYear()).toBe(2014);
1629+
1630+
date = interval.next();
1631+
expect(date.getDate()).toBe(28);
1632+
expect(date.getMonth()).toBe(1);
1633+
expect(date.getFullYear()).toBe(2015);
1634+
1635+
date = interval.next();
1636+
expect(date.getDate()).toBe(29);
1637+
expect(date.getMonth()).toBe(1);
1638+
expect(date.getFullYear()).toBe(2016);
16231639
});
16241640

1625-
testCasesLastWeekdayOfMonth.forEach(({ expression, expectedDate }) => {
1641+
test('parses cron with mix of sequence and range and last day in feb', () => {
16261642
const options = {
1627-
currentDate: new Date(2021, 8, 1),
1628-
endDate: new Date(2021, 11, 1),
1643+
currentDate: new Date(2014, 0, 1),
1644+
endDate: new Date(2014, 10, 1),
16291645
};
16301646

1631-
test(`parse cron with last weekday of the month: ${expression}`, () => {
1632-
const interval = CronExpressionParser.parse(expression, options);
1633-
expect(interval.hasNext()).toBe(true);
1634-
const next = interval.next();
1635-
expect(next.getDate()).toBe(expectedDate);
1636-
});
1647+
const interval = CronExpressionParser.parse('0 0 1,3,6-8,L 2 *', options);
1648+
1649+
expect(interval.next().getDate()).toBe(1);
1650+
expect(interval.next().getDate()).toBe(3);
1651+
expect(interval.next().getDate()).toBe(6);
1652+
expect(interval.next().getDate()).toBe(7);
1653+
expect(interval.next().getDate()).toBe(8);
1654+
expect(interval.next().getDate()).toBe(28);
16371655
});
16381656

16391657
test('parses expression that runs on both last monday and friday of the month', () => {
@@ -1642,12 +1660,8 @@ describe('CronExpressionParser', () => {
16421660
endDate: new Date(2021, 11, 1),
16431661
};
16441662
const interval = CronExpressionParser.parse('0 0 0 * * 1L,5L', options);
1645-
let next = interval.next();
1646-
1647-
expect(next.getDate()).toBe(24);
1648-
next = interval.next();
1649-
1650-
expect(next.getDate()).toBe(27);
1663+
expect(interval.next().getDate()).toBe(24);
1664+
expect(interval.next().getDate()).toBe(27);
16511665
});
16521666

16531667
test('parses expression that runs on both every monday and last friday of month', () => {
@@ -1656,21 +1670,11 @@ describe('CronExpressionParser', () => {
16561670
endDate: new Date(2021, 8, 30),
16571671
};
16581672
const interval = CronExpressionParser.parse('0 0 0 * * 1,5L', options);
1659-
const dates: number[] = [];
1660-
let isNotDone = true;
1661-
while (isNotDone) {
1662-
try {
1663-
const next = interval.next();
1664-
dates.push(next.getDate());
1665-
} catch (e) {
1666-
if (e instanceof Error && e.message !== 'Out of the timespan range') {
1667-
throw e;
1668-
}
1669-
isNotDone = false;
1670-
break;
1671-
}
1672-
}
1673-
expect(dates).toEqual([6, 13, 20, 24, 27]);
1673+
expect(interval.next().getDate()).toBe(6);
1674+
expect(interval.next().getDate()).toBe(13);
1675+
expect(interval.next().getDate()).toBe(20);
1676+
expect(interval.next().getDate()).toBe(24);
1677+
expect(interval.next().getDate()).toBe(27);
16741678
});
16751679

16761680
test('throw new Errors to parse for invalid last weekday of month expression', () => {

0 commit comments

Comments
 (0)