Skip to content

Commit

Permalink
fix: Handle more BC date cases (#7410)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Nov 20, 2024
1 parent 9170093 commit 2e1dd22
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/@internationalized/date/src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function fromAbsolute(ms: number, timeZone: string): ZonedDateTime {
let second = date.getUTCSeconds();
let millisecond = date.getUTCMilliseconds();

return new ZonedDateTime(year < 1 ? 'BC' : 'AD', year < 0 ? Math.abs(year) + 1 : year, month, day, timeZone, offset, hour, minute, second, millisecond);
return new ZonedDateTime(year < 1 ? 'BC' : 'AD', year < 1 ? -year + 1 : year, month, day, timeZone, offset, hour, minute, second, millisecond);
}

/**
Expand Down
36 changes: 28 additions & 8 deletions packages/@internationalized/date/src/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import {GregorianCalendar} from './calendars/GregorianCalendar';
import {Mutable} from './utils';

const TIME_RE = /^(\d{2})(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?$/;
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
const DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?$/;
const ZONED_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:([+-]\d{2})(?::?(\d{2}))?)?\[(.*?)\]$/;
const ABSOLUTE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:(?:([+-]\d{2})(?::?(\d{2}))?)|Z)$/;
const DATE_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})$/;
const DATE_TIME_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?$/;
const ZONED_DATE_TIME_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:([+-]\d{2})(?::?(\d{2}))?)?\[(.*?)\]$/;
const ABSOLUTE_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:(?:([+-]\d{2})(?::?(\d{2}))?)|Z)$/;
const DATE_TIME_DURATION_RE =
/^((?<negative>-)|\+)?P((?<years>\d*)Y)?((?<months>\d*)M)?((?<weeks>\d*)W)?((?<days>\d*)D)?((?<time>T)((?<hours>\d*[.,]?\d{1,9})H)?((?<minutes>\d*[.,]?\d{1,9})M)?((?<seconds>\d*[.,]?\d{1,9})S)?)?$/;
const requiredDurationTimeGroups = ['hours', 'minutes', 'seconds'];
Expand Down Expand Up @@ -66,8 +66,12 @@ export function parseDateTime(value: string): CalendarDateTime {
throw new Error('Invalid ISO 8601 date time string: ' + value);
}

let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';

let date: Mutable<CalendarDateTime> = new CalendarDateTime(
parseNumber(m[1], 1, 9999),
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
m[4] ? parseNumber(m[4], 0, 23) : 0,
Expand All @@ -92,8 +96,12 @@ export function parseZonedDateTime(value: string, disambiguation?: Disambiguatio
throw new Error('Invalid ISO 8601 date time string: ' + value);
}

let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';

let date: Mutable<ZonedDateTime> = new ZonedDateTime(
parseNumber(m[1], 1, 9999),
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
m[10],
Expand Down Expand Up @@ -136,8 +144,12 @@ export function parseAbsolute(value: string, timeZone: string): ZonedDateTime {
throw new Error('Invalid ISO 8601 date time string: ' + value);
}

let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';

let date: Mutable<ZonedDateTime> = new ZonedDateTime(
parseNumber(m[1], 1, 9999),
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
timeZone,
Expand Down Expand Up @@ -180,7 +192,15 @@ export function timeToString(time: Time): string {

export function dateToString(date: CalendarDate): string {
let gregorianDate = toCalendar(date, new GregorianCalendar());
return `${String(gregorianDate.year).padStart(4, '0')}-${String(gregorianDate.month).padStart(2, '0')}-${String(gregorianDate.day).padStart(2, '0')}`;
let year: string;
if (gregorianDate.era === 'BC') {
year = gregorianDate.year === 1
? '0000'
: '-' + String(Math.abs(1 - gregorianDate.year)).padStart(6, '00');
} else {
year = String(gregorianDate.year).padStart(4, '0');
}
return `${year}-${String(gregorianDate.month).padStart(2, '0')}-${String(gregorianDate.day).padStart(2, '0')}`;
}

export function dateTimeToString(date: AnyDateTime): string {
Expand Down
13 changes: 12 additions & 1 deletion packages/@internationalized/date/tests/conversion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ describe('CalendarDate conversion', function () {
let date = new CalendarDate(2020, 2, 3);
expect(toAbsolute(date, 'America/Los_Angeles')).toBe(new Date('2020-02-03T08:00Z').getTime());
});

it('should support BC dates', function () {
let date = new CalendarDateTime('BC', 2, 1, 1);
expect(toAbsolute(date, 'UTC')).toEqual(new Date('-000001-01-01T00:00Z').getTime());
});
});

describe('toDate', function () {
Expand Down Expand Up @@ -104,6 +109,11 @@ describe('CalendarDate conversion', function () {
let date = new CalendarDate(2020, 2, 3);
expect(toDate(date, 'America/Los_Angeles')).toEqual(new Date('2020-02-03T08:00Z'));
});

it('should support BC dates', function () {
let date = new CalendarDateTime('BC', 2, 1, 1);
expect(toDate(date, 'UTC')).toEqual(new Date('-000001-01-01T00:00Z'));
});
});

describe('possibleAbsolutes', function () {
Expand Down Expand Up @@ -142,7 +152,8 @@ describe('CalendarDate conversion', function () {
expect(date).toEqual(new ZonedDateTime('BC', 1, 1, 1, 'UTC', 0, 0, 0, 0));
date = fromAbsolute(new Date('0001-01-01T00:00:00.000Z').getTime(), 'UTC');
expect(date).toEqual(new ZonedDateTime('AD', 1, 1, 1, 'UTC', 0, 0, 0, 0));

date = fromAbsolute(new Date('-000001-01-01T00:00:00.000Z').getTime(), 'UTC');
expect(date).toEqual(new ZonedDateTime('BC', 2, 1, 1, 'UTC', 0, 0, 0, 0));
date = fromAbsolute(new Date('-000009-01-01T00:00:00.000Z').getTime(), 'UTC');
expect(date).toEqual(new ZonedDateTime('BC', 10, 1, 1, 'UTC', 0, 0, 0, 0));
});
Expand Down
62 changes: 62 additions & 0 deletions packages/@internationalized/date/tests/string.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ describe('string conversion', function () {
let date = new CalendarDate(123, 2, 3);
expect(date.toString()).toBe('0123-02-03');
});

it('should stringify a BC date', function () {
let date = new CalendarDate('BC', 1, 1, 1);
expect(date.toString()).toBe('0000-01-01');

date = new CalendarDate('BC', 2, 1, 1);
expect(date.toString()).toBe('-000001-01-01');
});
});

describe('parseDateTime', function () {
Expand Down Expand Up @@ -155,6 +163,16 @@ describe('string conversion', function () {
expect(date).toEqual(expected);
});

it('should parse BC dates', function () {
let date = parseDateTime('0000-01-01');
let expected = new CalendarDateTime('BC', 1, 1, 1);
expect(date).toEqual(expected);

date = parseDateTime('-000002-01-01');
expected = new CalendarDateTime('BC', 3, 1, 1);
expect(date).toEqual(expected);
});

it('should error if date is not padded', function () {
expect(() => parseDateTime('123-02-03T12:24:45')).toThrow();
expect(() => parseDateTime('2020-2-03T12:24:45')).toThrow();
Expand Down Expand Up @@ -193,6 +211,14 @@ describe('string conversion', function () {
let date = new CalendarDateTime(2020, 2, 3, 12, 23, 45, 120);
expect(date.toString()).toBe('2020-02-03T12:23:45.12');
});

it('should stringify a BC date', function () {
let date = new CalendarDateTime('BC', 1, 1, 1);
expect(date.toString()).toBe('0000-01-01T00:00:00');

date = new CalendarDateTime('BC', 2, 1, 1);
expect(date.toString()).toBe('-000001-01-01T00:00:00');
});
});

describe('parseZonedDateTime', function () {
Expand Down Expand Up @@ -274,6 +300,16 @@ describe('string conversion', function () {
expect(date).toEqual(expected);
});

it('should parse BC dates', function () {
let date = parseZonedDateTime('0000-01-01T01:00[America/Los_Angeles]');
let expected = new ZonedDateTime('BC', 1, 1, 1, 'America/Los_Angeles', -28378000, 1, 0, 0);
expect(date).toEqual(expected);

date = parseZonedDateTime('-000002-01-01T01:00[America/Los_Angeles]');
expected = new ZonedDateTime('BC', 3, 1, 1, 'America/Los_Angeles', -28378000, 1, 0, 0);
expect(date).toEqual(expected);
});

it('should error if parsing a date with an invalid offset', function () {
expect(() => parseZonedDateTime('2020-02-03T12:24:45.12-04:00[America/Los_Angeles]')).toThrow();
expect(() => parseZonedDateTime('2020-02-03T12:24:45.12-08:24[America/Los_Angeles]')).toThrow();
Expand Down Expand Up @@ -302,6 +338,14 @@ describe('string conversion', function () {
let date = new ZonedDateTime(2020, 2, 3, 'America/Los_Angeles', -28800000, 12, 24, 45, 120);
expect(date.toString()).toBe('2020-02-03T12:24:45.12-08:00[America/Los_Angeles]');
});

it('should stringify a BC date', function () {
let date = new ZonedDateTime('BC', 1, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date.toString()).toBe('0000-01-01T01:00:00+00:00[UTC]');

date = new ZonedDateTime('BC', 2, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date.toString()).toBe('-000001-01-01T01:00:00+00:00[UTC]');
});
});

describe('parseAbsolute', function () {
Expand Down Expand Up @@ -355,6 +399,16 @@ describe('string conversion', function () {
expect(date).toEqual(expected);
});

it('should parse BC dates', function () {
let date = parseAbsolute('0000-01-01T01:00Z', 'UTC');
let expected = new ZonedDateTime('BC', 1, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date).toEqual(expected);

date = parseAbsolute('-000002-01-01T01:00Z', 'UTC');
expected = new ZonedDateTime('BC', 3, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date).toEqual(expected);
});

it('should error if missing offset or Z', function () {
expect(() => parseAbsolute('2020-02-03')).toThrow();
});
Expand All @@ -375,6 +429,14 @@ describe('string conversion', function () {
let date = new ZonedDateTime(2020, 2, 3, 'America/Los_Angeles', -28800000, 14, 32, 45);
expect(date.toAbsoluteString()).toBe('2020-02-03T22:32:45.000Z');
});

it('should stringify a BC date', function () {
let date = new ZonedDateTime('BC', 1, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date.toAbsoluteString()).toBe('0000-01-01T01:00:00.000Z');

date = new ZonedDateTime('BC', 2, 1, 1, 'UTC', 0, 1, 0, 0);
expect(date.toAbsoluteString()).toBe('-000001-01-01T01:00:00.000Z');
});
});

describe('parseDuration', function () {
Expand Down

1 comment on commit 2e1dd22

@rspbot
Copy link

@rspbot rspbot commented on 2e1dd22 Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.