Skip to content

Commit f40f711

Browse files
committed
refactor(calendar): add handling for dates older than the 2000 epoch
1 parent ce44999 commit f40f711

File tree

2 files changed

+109
-1
lines changed

2 files changed

+109
-1
lines changed

src/versioning-strategies/calendar.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
} from '../versioning-strategy';
2222
import {logger as defaultLogger, Logger} from '../util/logger';
2323

24+
const SHORT_YEAR_EPOCH = 2000;
25+
2426
const CALVER_PLACEHOLDERS: Record<string, string> = {
2527
YYYY: '\\d{4}',
2628
'0Y': '\\d{2,3}',
@@ -246,6 +248,14 @@ class CalendarVersionUpdate implements VersionUpdater {
246248
);
247249
}
248250

251+
const versionDate = extractDateFromSegments(parsed.segments, tokens);
252+
if (versionDate && versionDate > this.currentDate) {
253+
throw new Error(
254+
`Cannot bump version "${version}": the version date is newer than the current date. ` +
255+
`Ensure the system date is correct or use a release-as override.`
256+
);
257+
}
258+
249259
const newSegments: CalVerSegment[] = [];
250260
let dateChanged = false;
251261
let shouldResetLower = false;
@@ -380,6 +390,64 @@ function isSemanticSegment(type: string): type is 'MAJOR' | 'MINOR' | 'MICRO' {
380390
return ['MAJOR', 'MINOR', 'MICRO'].includes(type);
381391
}
382392

393+
function extractDateFromSegments(
394+
segments: CalVerSegment[],
395+
tokens: CalVerSegment['type'][]
396+
): Date | null {
397+
let year: number | null = null;
398+
let month: number | null = null;
399+
let day: number | null = null;
400+
let week: number | null = null;
401+
402+
for (let i = 0; i < segments.length; i++) {
403+
const token = tokens[i];
404+
const value = segments[i].value;
405+
406+
switch (token) {
407+
case 'YYYY':
408+
year = value;
409+
break;
410+
case 'YY':
411+
case '0Y':
412+
year = value + SHORT_YEAR_EPOCH;
413+
break;
414+
case 'MM':
415+
case '0M':
416+
month = value;
417+
break;
418+
case 'DD':
419+
case '0D':
420+
day = value;
421+
break;
422+
case 'WW':
423+
case '0W':
424+
week = value;
425+
break;
426+
}
427+
}
428+
429+
if (year === null) {
430+
return null;
431+
}
432+
433+
if (month !== null && day !== null) {
434+
return new Date(Date.UTC(year, month - 1, day));
435+
}
436+
437+
if (month !== null) {
438+
return new Date(Date.UTC(year, month - 1, 1));
439+
}
440+
441+
if (week !== null) {
442+
const jan1 = new Date(Date.UTC(year, 0, 1));
443+
const jan1DayOfWeek = (jan1.getUTCDay() + 6) % 7;
444+
const daysToAdd = (week - 1) * 7 - jan1DayOfWeek;
445+
return new Date(Date.UTC(year, 0, 1 + daysToAdd));
446+
}
447+
448+
return new Date(Date.UTC(year, 0, 1));
449+
}
450+
383451
// Week of year where January 1 is always in week 1.
384452
// Weeks align with calendar weeks (starting Monday).
385453
function getWeekOfYear(date: Date): number {
@@ -397,7 +465,7 @@ function formatSegment(type: CalVerSegment['type'], date: Date): string {
397465
const month = date.getUTCMonth() + 1;
398466
const day = date.getUTCDate();
399467
const week = getWeekOfYear(date);
400-
const shortYear = year - 2000;
468+
const shortYear = year - SHORT_YEAR_EPOCH;
401469

402470
switch (type) {
403471
case 'YYYY':

test/versioning-strategies/calendar.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,46 @@ describe('CalendarVersioningStrategy', () => {
619619
});
620620
});
621621

622+
describe('error handling for older date versions', () => {
623+
it('throws error when current date is older than version date', async () => {
624+
const strategy = new CalendarVersioningStrategy({
625+
dateFormat: 'YYYY.0M.0D',
626+
});
627+
strategy.setCurrentDate(new Date(Date.UTC(2023, 5, 15)));
628+
const futureVersion = new CalendarVersion(
629+
2024,
630+
6,
631+
15,
632+
undefined,
633+
undefined,
634+
'2024.06.15'
635+
);
636+
try {
637+
await strategy.bump(futureVersion, fixCommits);
638+
expect.fail('Expected an error to be thrown');
639+
} catch (error) {
640+
expect((error as Error).message).to.include('Cannot bump version');
641+
expect((error as Error).message).to.include(
642+
'version date is newer than the current date'
643+
);
644+
}
645+
});
646+
647+
it('throws error when year is older', async () => {
648+
const strategy = new CalendarVersioningStrategy({
649+
dateFormat: 'YYYY.MINOR.MICRO',
650+
});
651+
strategy.setCurrentDate(new Date(Date.UTC(2023, 0, 1)));
652+
const futureVersion = Version.parse('2024.5.3');
653+
try {
654+
await strategy.bump(futureVersion, fixCommits);
655+
expect.fail('Expected an error to be thrown');
656+
} catch (error) {
657+
expect((error as Error).message).to.include('Cannot bump version');
658+
}
659+
});
660+
});
661+
622662
describe('error handling for invalid version strings', () => {
623663
it('throws error when version has fewer segments than format requires', async () => {
624664
const strategy = new CalendarVersioningStrategy({

0 commit comments

Comments
 (0)