Skip to content

Commit

Permalink
Clamp dates to supported range.
Browse files Browse the repository at this point in the history
We no longer throw an error when an out-of-range date is constructed.
Instead, if an epoch timestamp is used that is too small or large, we
clamp the range to our supported min/max.
  • Loading branch information
phensley committed Aug 23, 2024
1 parent feab4a9 commit 54823a2
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 39 deletions.
15 changes: 11 additions & 4 deletions packages/cldr-core/__tests__/api/calendars/fromfields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ test('gregorian bounds', () => {
let d: CalendarDate;
const api = calendarsApi('en');

expect(() => api.newGregorianDate({ year: -9999 })).toThrow();
expect(() => api.newGregorianDate({ year: 9999 })).toThrow();
d = api.newGregorianDate({ year: -9999 });
expect(d.toString()).toEqual('Gregorian -4712-01-01 00:00:00.000 Etc/UTC');

d = api.newGregorianDate({ year: 9999 });
expect(d.toString()).toEqual('Gregorian 8652-12-31 00:00:00.000 Etc/UTC');

d = api.newGregorianDate({ year: 2020, month: -1 });
expect(d.toString()).toEqual('Gregorian 2020-01-01 00:00:00.000 Etc/UTC');
Expand Down Expand Up @@ -194,9 +197,13 @@ test('persian defaults', () => {

test('persian bounds', () => {
const api = calendarsApi('en');
let d: CalendarDate;

d = api.newPersianDate({ year: -9999 });
expect(d.toString()).toEqual('Persian -5334-09-03 00:00:00.000 Etc/UTC');

expect(() => api.newPersianDate({ year: -9999 })).toThrow();
expect(() => api.newPersianDate({ year: 9999 })).toThrow();
d = api.newPersianDate({ year: 9999 });
expect(d.toString()).toEqual('Persian 8031-10-11 00:00:00.000 Etc/UTC');
});

test('persian from fields', () => {
Expand Down
41 changes: 19 additions & 22 deletions packages/cldr-core/__tests__/systems/calendars/gregorian.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { CalendarDate, GregorianDate } from '../../../src';

const make = (e: number, z: string) => GregorianDate.fromUnixEpoch(e, z, DayOfWeek.SUNDAY, 1);

const unixEpochFromJD = (jd: number, msDay: number = 0): number => {
const days = jd - CalendarConstants.JD_UNIX_EPOCH;
return days * CalendarConstants.ONE_DAY_MS + Math.round(msDay);
};

const NEW_YORK = 'America/New_York';
const LOS_ANGELES = 'America/Los_Angeles';
const PARIS = 'Europe/Paris';
Expand Down Expand Up @@ -107,33 +112,25 @@ test('gregorian date', () => {
expect(d.yearOfWeekOfYear()).toEqual(2018);
});

test('min / max date', () => {
let d: GregorianDate;
let n: number;
test('min / max / clamp', () => {
let d: CalendarDate;

const unix = CalendarConstants.JD_UNIX_EPOCH;
const min = unixEpochFromJD(CalendarConstants.JD_MIN, 0);
const max = unixEpochFromJD(CalendarConstants.JD_MAX, 0);

n = (unix - 1) * CalendarConstants.ONE_DAY_MS;
d = make(-n, NEW_YORK);
expect(d.era()).toEqual(0);
expect(d.extendedYear()).toEqual(-4712);
expect(d.year()).toEqual(4713);
expect(d.month()).toEqual(1); // Jan
expect(d.dayOfMonth()).toEqual(1);
d = make(min, 'UTC');
expect(d.toString()).toEqual('Gregorian -4712-01-01 00:00:00.000 Etc/UTC');

// Attempting to represent Dec 31 4714 BC
expect(() => make(-n - CalendarConstants.ONE_DAY_MS, NEW_YORK)).toThrowError();
// Clamp to minimum date
d = make(min - CalendarConstants.ONE_DAY_MS, 'UTC');
expect(d.toString()).toEqual('Gregorian -4712-01-01 00:00:00.000 Etc/UTC');

n = (CalendarConstants.JD_MAX - unix + 1) * CalendarConstants.ONE_DAY_MS;
d = make(n, NEW_YORK);
expect(d.era()).toEqual(1);
expect(d.extendedYear()).toEqual(8652);
expect(d.year()).toEqual(8652);
expect(d.month()).toEqual(12); // Dec
expect(d.dayOfMonth()).toEqual(31);
d = make(max, 'UTC');
expect(d.toString()).toEqual('Gregorian 8652-12-31 00:00:00.000 Etc/UTC');

// Attempt to represent Jan 1 8653 AD
expect(() => make(n + CalendarConstants.ONE_DAY_MS, NEW_YORK)).toThrowError();
// Clamp to maximum date
d = make(max + CalendarConstants.ONE_DAY_MS, 'UTC');
expect(d.toString()).toEqual('Gregorian 8652-12-31 00:00:00.000 Etc/UTC');
});

test('millis in day', () => {
Expand Down
23 changes: 12 additions & 11 deletions packages/cldr-core/src/systems/calendars/calendar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DateTimePatternField, DateTimePatternFieldType, MetaZoneType } from '@phensley/cldr-types';

import { dateFields, DateField, DayOfWeek } from './fields';
import { CalendarConstants, ConstantsDesc } from './constants';
import { CalendarConstants } from './constants';
import { substituteZoneAlias, zoneInfoFromUTC, ZoneInfo } from './timezone';
import { INTERNAL_NUMBERING } from '../numbering';
import { timePeriodFieldFlags, TimePeriod, TimePeriodField, TimePeriodFieldFlag, TIME_PERIOD_FIELDS } from './interval';
Expand Down Expand Up @@ -972,8 +972,8 @@ const jdFromUnixEpoch = (ms: number, f: number[]): void => {
* is relative to these.
*/
const computeBaseFields = (f: number[]): void => {
const jd = f[DateField.JULIAN_DAY];
checkJDRange(jd);
const jd = clamp(f[DateField.JULIAN_DAY], CalendarConstants.JD_MIN, CalendarConstants.JD_MAX);
// checkJDRange(jd);

let msDay = f[DateField.MILLIS_IN_DAY];
const ms = msDay + (jd - CalendarConstants.JD_UNIX_EPOCH) * CalendarConstants.ONE_DAY_MS;
Expand Down Expand Up @@ -1001,14 +1001,15 @@ const computeBaseFields = (f: number[]): void => {
f[DateField.DAY_OF_WEEK] = dow;
};

const checkJDRange = (jd: number): void => {
if (jd < CalendarConstants.JD_MIN || jd > CalendarConstants.JD_MAX) {
throw new Error(
`Julian day ${jd} is outside the supported range of this library: ` +
`${ConstantsDesc.JD_MIN} to ${ConstantsDesc.JD_MAX}`,
);
}
};
// TODO: clamp range instead of throwing error.
// const checkJDRange = (jd: number): void => {
// if (jd < CalendarConstants.JD_MIN || jd > CalendarConstants.JD_MAX) {
// throw new Error(
// `Julian day ${jd} is outside the supported range of this library: ` +
// `${ConstantsDesc.JD_MIN} to ${ConstantsDesc.JD_MAX}`,
// );
// }
// };

/**
* Given a Julian day and local milliseconds (in UTC), return the Unix
Expand Down
4 changes: 2 additions & 2 deletions packages/cldr-core/src/systems/calendars/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export const enum CalendarConstants {
// TODO: revisit to expand range of julian days

// Min and max Julian day form a range of full years whose midpoint is the
// UNIX epoch Jan 1 1970
MIN_YEAR = 4713,
// UNIX epoch Jan 1 1970. The goal is to constrain the epoch
// timestamps to a reasonable range. This range may expand in the future.

// Mon Jan 1 4713 BC
JD_MIN = 0,
Expand Down
1 change: 1 addition & 0 deletions packages/cldr-core/src/systems/calendars/gregorian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const computeJulianFields = (f: number[]): void => {
if (doy >= mar1) {
corr = isLeap ? 1 : 2;
}

const month = floor((12 * (doy + corr) + 6) / 367);
const dom = doy - MONTH_COUNT[month][isLeap ? 3 : 2] + 1;

Expand Down

0 comments on commit 54823a2

Please sign in to comment.