diff --git a/.changeset/khaki-apples-bake.md b/.changeset/khaki-apples-bake.md new file mode 100644 index 0000000..e80a223 --- /dev/null +++ b/.changeset/khaki-apples-bake.md @@ -0,0 +1,5 @@ +--- +'chronoshift': major +--- + +Switch to @internationalized/date diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..b0c1c68 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ea6e62b..a2bf733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.10.3", "license": "Apache-2.0", "dependencies": { + "@internationalized/date": "^3.5.6", "immutable-class": "^0.11.0", - "moment-timezone": "^0.5.26", "tslib": "^2.8.1" }, "devDependencies": { @@ -28,7 +28,7 @@ "husky": "^2.4.1", "immutable-class-tester": "^0.7.2", "jest": "^29.7.0", - "jest-expect-message": "^1.0.2", + "jest-expect-message": "^1.1.3", "prettier": "^3.4.1", "ts-jest": "^29.2.5", "typescript": "^5.7.2" @@ -1249,6 +1249,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@internationalized/date": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.6.0.tgz", + "integrity": "sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1962,6 +1971,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5924,10 +5942,11 @@ } }, "node_modules/jest-expect-message": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jest-expect-message/-/jest-expect-message-1.0.2.tgz", - "integrity": "sha512-WFiXMgwS2lOqQZt1iJMI/hOXpUm32X+ApsuzYcQpW5m16Pv6/Gd9kgC+Q+Q1YVNU04kYcAOv9NXMnjg6kKUy6Q==", - "dev": true + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/jest-expect-message/-/jest-expect-message-1.1.3.tgz", + "integrity": "sha512-bTK77T4P+zto+XepAX3low8XVQxDgaEqh3jSTQOG8qvPpD69LsIdyJTa+RmnJh3HNSzJng62/44RPPc7OIlFxg==", + "dev": true, + "license": "MIT" }, "node_modules/jest-get-type": { "version": "29.6.3", @@ -6631,23 +6650,12 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true, "license": "MIT", "engines": { "node": "*" } }, - "node_modules/moment-timezone": { - "version": "0.5.46", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", - "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/package.json b/package.json index 215e8b1..45e0101 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "prettier": "@awesome-code-style/prettier-config", "dependencies": { "immutable-class": "^0.11.0", - "moment-timezone": "^0.5.26", + "@internationalized/date": "^3.5.6", "tslib": "^2.8.1" }, "devDependencies": { @@ -75,7 +75,7 @@ "husky": "^2.4.1", "immutable-class-tester": "^0.7.2", "jest": "^29.7.0", - "jest-expect-message": "^1.0.2", + "jest-expect-message": "^1.1.3", "prettier": "^3.4.1", "ts-jest": "^29.2.5", "typescript": "^5.7.2" diff --git a/src/date-parser/date-parser.spec.ts b/src/date-parser/date-parser.spec.ts index c5c8808..f23df11 100644 --- a/src/date-parser/date-parser.spec.ts +++ b/src/date-parser/date-parser.spec.ts @@ -206,17 +206,17 @@ describe('date parser', () => { ).toBeUndefined(); }); - it('date-time (tz = America/Los_Angeles)', () => { - const tz = Timezone.fromJS('America/Los_Angeles'); + it('date-time (tz = America/New_York)', () => { + const tz = Timezone.fromJS('America/New_York'); expect(parseISODate('2001-02-03T04:05', tz), '2001-02-03T04:05').toEqual( - new Date(Date.UTC(2001, 1, 3, 4 + 8, 5, 0, 0)), + new Date(Date.UTC(2001, 1, 3, 4 + 5, 5, 0, 0)), ); expect(parseISODate('2001-02-03T04:05:06', tz), '2001-02-03T04:05:06').toEqual( - new Date(Date.UTC(2001, 1, 3, 4 + 8, 5, 6, 0)), + new Date(Date.UTC(2001, 1, 3, 4 + 5, 5, 6, 0)), ); expect(parseISODate('2001-02-03T04:05:06.007', tz), '2001-02-03T04:05:06.007').toEqual( - new Date(Date.UTC(2001, 1, 3, 4 + 8, 5, 6, 7)), + new Date(Date.UTC(2001, 1, 3, 4 + 5, 5, 6, 7)), ); expect(parseISODate('2001-02-03T04:05Z', tz), '2001-02-03T04:05Z').toEqual( @@ -231,25 +231,23 @@ describe('date parser', () => { }); it('date-time (tz = null / local)', () => { - const tz: any = null; - - expect(parseISODate('2001-02-03T04:05', tz), '2001-02-03T04:05').toEqual( + expect(parseISODate('2001-02-03T04:05', null), '2001-02-03T04:05').toEqual( new Date(2001, 1, 3, 4, 5, 0, 0), ); - expect(parseISODate('2001-02-03T04:05:06', tz), '2001-02-03T04:05:06').toEqual( + expect(parseISODate('2001-02-03T04:05:06', null), '2001-02-03T04:05:06').toEqual( new Date(2001, 1, 3, 4, 5, 6, 0), ); - expect(parseISODate('2001-02-03T04:05:06.007', tz), '2001-02-03T04:05:06.007').toEqual( + expect(parseISODate('2001-02-03T04:05:06.007', null), '2001-02-03T04:05:06.007').toEqual( new Date(2001, 1, 3, 4, 5, 6, 7), ); - expect(parseISODate('2001-02-03T04:05Z', tz), '2001-02-03T04:05Z').toEqual( + expect(parseISODate('2001-02-03T04:05Z', null), '2001-02-03T04:05Z').toEqual( new Date(Date.UTC(2001, 1, 3, 4, 5, 0, 0)), ); - expect(parseISODate('2001-02-03T04:05:06Z', tz), '2001-02-03T04:05:06Z').toEqual( + expect(parseISODate('2001-02-03T04:05:06Z', null), '2001-02-03T04:05:06Z').toEqual( new Date(Date.UTC(2001, 1, 3, 4, 5, 6, 0)), ); - expect(parseISODate('2001-02-03T04:05:06.007Z', tz), '2001-02-03T04:05:06.007Z').toEqual( + expect(parseISODate('2001-02-03T04:05:06.007Z', null), '2001-02-03T04:05:06.007Z').toEqual( new Date(Date.UTC(2001, 1, 3, 4, 5, 6, 7)), ); }); diff --git a/src/date-parser/date-parser.ts b/src/date-parser/date-parser.ts index d7deffc..ddf4474 100644 --- a/src/date-parser/date-parser.ts +++ b/src/date-parser/date-parser.ts @@ -17,7 +17,7 @@ /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */ /* eslint-disable no-useless-escape */ -import moment from 'moment-timezone'; +import { fromDate } from '@internationalized/date'; import { Duration } from '../duration/duration'; import { Timezone } from '../timezone/timezone'; @@ -118,7 +118,10 @@ export function parseSQLDate(type: string, v: string): Date { // Taken from: https://github.com/csnover/js-iso8601/blob/lax/iso8601.js const numericKeys = [1, 4, 5, 6, 10, 11]; -export function parseISODate(date: string, timezone = Timezone.UTC): Date | undefined { +export function parseISODate( + date: string, + timezone: Timezone | null = Timezone.UTC, +): Date | undefined { let struct: any; let minutesOffset = 0; @@ -182,41 +185,28 @@ export function parseISODate(date: string, timezone = Timezone.UTC): Date | unde struct[3] = +struct[3] || 1; // allow arbitrary sub-second precision beyond milliseconds - struct[7] = struct[7] ? +(struct[7] + '00').substr(0, 3) : 0; + struct[7] = struct[7] ? +(struct[7] + '00').slice(0, 3) : 0; if ( (struct[8] === undefined || struct[8] === '') && (struct[9] === undefined || struct[9] === '') && - !Timezone.UTC.equals(timezone) + !Timezone.UTC.equals(timezone || undefined) ) { + const dt = Date.UTC( + struct[1], + struct[2], + struct[3], + struct[4], + struct[5], + struct[6], + struct[7], + ); if (timezone === null) { // timezone explicitly set to null = use local timezone - return new Date( - struct[1], - struct[2], - struct[3], - struct[4], - struct[5], - struct[6], - struct[7], - ); + return new Date(dt); } else { - return new Date( - moment - .tz( - { - year: struct[1], - month: struct[2], - day: struct[3], - hour: struct[4], - minute: struct[5], - second: struct[6], - millisecond: struct[7], - }, - timezone.toString(), - ) - .valueOf(), - ); + const tzd = fromDate(new Date(dt), timezone.toString()); + return new Date(dt - tzd.offset); } } else { if (struct[8] !== 'Z' && struct[9] !== undefined) { diff --git a/src/floor-shift-ceil/floor-shift-ceil.spec.ts b/src/floor-shift-ceil/floor-shift-ceil.spec.ts index cd90c0a..27fa8c1 100644 --- a/src/floor-shift-ceil/floor-shift-ceil.spec.ts +++ b/src/floor-shift-ceil/floor-shift-ceil.spec.ts @@ -59,8 +59,8 @@ describe('floor/shift/ceil', () => { new Date('2012-11-04T01:00:00-07:00'), ); - expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-08:00'), tz), 'C').toEqual( - new Date('2012-11-04T01:00:00-08:00'), + expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-08:00'), tz)).toEqual( + new Date('2012-11-04T01:00:00-07:00'), ); expect(shifters.hour.floor(new Date('2012-11-04T02:30:00-08:00'), tz), 'D').toEqual( @@ -93,7 +93,7 @@ describe('floor/shift/ceil', () => { pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2)); }); - it('shifts hour over DST 1', () => { + it('floors hour over DST 1', () => { expect(shifters.hour.floor(new Date('2012-11-04T00:05:00-07:00'), tz)).toEqual( new Date('2012-11-04T00:00:00-07:00'), ); @@ -101,7 +101,7 @@ describe('floor/shift/ceil', () => { new Date('2012-11-04T01:00:00-07:00'), ); expect(shifters.hour.floor(new Date('2012-11-04T02:05:00-07:00'), tz)).toEqual( - new Date('2012-11-04T02:00:00-07:00'), + new Date('2012-11-04T01:00:00-07:00'), ); expect(shifters.hour.floor(new Date('2012-11-04T03:05:00-07:00'), tz)).toEqual( new Date('2012-11-04T03:00:00-07:00'), diff --git a/src/floor-shift-ceil/floor-shift-ceil.ts b/src/floor-shift-ceil/floor-shift-ceil.ts index 58b5744..4abbcfb 100644 --- a/src/floor-shift-ceil/floor-shift-ceil.ts +++ b/src/floor-shift-ceil/floor-shift-ceil.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import moment from 'moment-timezone'; +import { fromDate, startOfWeek } from '@internationalized/date'; import type { Timezone } from '../timezone/timezone'; @@ -115,11 +115,10 @@ export const hour = timeShifterFiller({ if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCMinutes(0, 0, 0); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.second(0).minute(0).millisecond(0).valueOf()); + return fromDate(dt, tz.toString()).set({ second: 0, minute: 0, millisecond: 0 }).toDate(); } - return dt; }, round: (dt, roundTo, tz) => { if (tz.isUTC()) { @@ -127,8 +126,7 @@ export const hour = timeShifterFiller({ const adj = floorTo(cur, roundTo); if (cur !== adj) dt.setUTCHours(adj); } else { - const wt = moment.tz(dt, tz.toString()); - const cur = wt.hour(); + const cur = fromDate(dt, tz.toString()).hour; const adj = floorTo(cur, roundTo); if (cur !== adj) return hourMove(dt, tz, adj - cur); } @@ -143,21 +141,21 @@ export const day = timeShifterFiller({ if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCHours(0, 0, 0, 0); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.hour(0).second(0).minute(0).millisecond(0).valueOf()); + return fromDate(dt, tz.toString()) + .set({ hour: 0, second: 0, minute: 0, millisecond: 0 }) + .toDate(); } - return dt; }, shift: (dt, tz, step) => { if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCDate(dt.getUTCDate() + step); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.add(step, 'days').valueOf()); + return fromDate(dt, tz.toString()).add({ days: step }).toDate(); } - return dt; }, round: () => { throw new Error('missing day round'); @@ -172,16 +170,11 @@ export const week = timeShifterFiller({ dt.setUTCHours(0, 0, 0, 0); dt.setUTCDate(dt.getUTCDate() - adjustDay(dt.getUTCDay())); } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date( - wt - .date(wt.date() - adjustDay(wt.day())) - .hour(0) - .second(0) - .minute(0) - .millisecond(0) - .valueOf(), - ); + const zd = fromDate(dt, tz.toString()); + return startOfWeek( + zd.set({ hour: 0, second: 0, minute: 0, millisecond: 0 }), + 'fr-FR', // We want the week to start on Monday + ).toDate(); } return dt; }, @@ -189,11 +182,10 @@ export const week = timeShifterFiller({ if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCDate(dt.getUTCDate() + step * 7); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.add(step * 7, 'days').valueOf()); + return fromDate(dt, tz.toString()).add({ weeks: step }).toDate(); } - return dt; }, round: () => { throw new Error('missing week round'); @@ -204,11 +196,10 @@ function monthShift(dt: Date, tz: Timezone, step: number) { if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCMonth(dt.getUTCMonth() + step); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.add(step, 'month').valueOf()); + return fromDate(dt, tz.toString()).add({ months: step }).toDate(); } - return dt; } export const month = timeShifterFiller({ @@ -219,11 +210,12 @@ export const month = timeShifterFiller({ dt = new Date(dt.valueOf()); dt.setUTCHours(0, 0, 0, 0); dt.setUTCDate(1); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.date(1).hour(0).second(0).minute(0).millisecond(0).valueOf()); + return fromDate(dt, tz.toString()) + .set({ day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 }) + .toDate(); } - return dt; }, round: (dt, roundTo, tz) => { if (tz.isUTC()) { @@ -231,8 +223,7 @@ export const month = timeShifterFiller({ const adj = floorTo(cur, roundTo); if (cur !== adj) dt.setUTCMonth(adj); } else { - const wt = moment.tz(dt, tz.toString()); - const cur = wt.month(); + const cur = fromDate(dt, tz.toString()).month - 1; // Needs to be zero indexed const adj = floorTo(cur, roundTo); if (cur !== adj) return monthShift(dt, tz, adj - cur); } @@ -245,11 +236,10 @@ function yearShift(dt: Date, tz: Timezone, step: number) { if (tz.isUTC()) { dt = new Date(dt.valueOf()); dt.setUTCFullYear(dt.getUTCFullYear() + step); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.add(step, 'years') as any); + return fromDate(dt, tz.toString()).add({ years: step }).toDate(); } - return dt; } export const year = timeShifterFiller({ @@ -260,11 +250,12 @@ export const year = timeShifterFiller({ dt = new Date(dt.valueOf()); dt.setUTCHours(0, 0, 0, 0); dt.setUTCMonth(0, 1); + return dt; } else { - const wt = moment.tz(dt, tz.toString()); - dt = new Date(wt.month(0).date(1).hour(0).second(0).minute(0).millisecond(0).valueOf()); + return fromDate(dt, tz.toString()) + .set({ month: 1, day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 }) + .toDate(); } - return dt; }, round: (dt, roundTo, tz) => { if (tz.isUTC()) { @@ -272,8 +263,7 @@ export const year = timeShifterFiller({ const adj = floorTo(cur, roundTo); if (cur !== adj) dt.setUTCFullYear(adj); } else { - const wt = moment.tz(dt, tz.toString()); - const cur = wt.year(); + const cur = fromDate(dt, tz.toString()).year; const adj = floorTo(cur, roundTo); if (cur !== adj) return yearShift(dt, tz, adj - cur); } @@ -306,11 +296,11 @@ export interface Shifters { } export const shifters: Shifters = { - second: second, - minute: minute, - hour: hour, - day: day, - week: week, - month: month, - year: year, + second, + minute, + hour, + day, + week, + month, + year, }; diff --git a/src/timezone/timezone.ts b/src/timezone/timezone.ts index f238b0e..13db4b6 100644 --- a/src/timezone/timezone.ts +++ b/src/timezone/timezone.ts @@ -15,8 +15,8 @@ * limitations under the License. */ +import { fromDate } from '@internationalized/date'; import type { Instance } from 'immutable-class'; -import moment from 'moment-timezone'; /** * Represents timezones @@ -29,7 +29,7 @@ export class Timezone implements Instance { static formatDateWithTimezone(d: Date, timezone?: Timezone) { let str: string; if (timezone && !timezone.isUTC()) { - str = moment.tz(d, timezone.toString()).format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + str = fromDate(d, timezone.toString()).toString().replace(/\[.+$/, ''); } else { str = d.toISOString(); } @@ -47,8 +47,12 @@ export class Timezone implements Instance { if (typeof timezone !== 'string') { throw new TypeError('timezone description must be a string'); } - if (timezone !== 'Etc/UTC' && !moment.tz.zone(timezone)) { - throw new Error(`timezone '${timezone}' does not exist`); + if (timezone !== 'Etc/UTC') { + try { + fromDate(new Date(), timezone); + } catch { + throw new Error(`timezone '${timezone}' does not exist`); + } } this.timezone = timezone; } @@ -78,7 +82,7 @@ export class Timezone implements Instance { } public toUtcOffsetString() { - const utcOffset = moment.tz(this.toString()).utcOffset(); + const utcOffset = fromDate(new Date(), this.toString()).offset; const hours = String(Math.abs(Math.floor(utcOffset / 60))).padStart(2, '0'); const minutes = String(Math.abs(utcOffset % 60)).padStart(2, '0'); diff --git a/tsconfig.json b/tsconfig.json index 6ff1610..389e677 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "strict": true, "importHelpers": true, - "target": "es5", + "target": "es2015", "module": "commonjs", "rootDir": "src", "outDir": "build",