├── pnpm-workspace.yaml ├── .gitignore ├── .oxfmtrc.json ├── src ├── datetime │ ├── _padLeadingZeros.ts │ ├── _createRecord.ts │ ├── _escapeRegExp.ts │ ├── _padLeadingZeros.test.ts │ ├── _unionRegExp.ts │ ├── _formatDateIso.ts │ ├── _formatHmsIso.ts │ ├── _secondsToHms.ts │ ├── _isValidHms.ts │ ├── _isValidYmd.ts │ ├── _getMonthAbbreviationFromNumber.ts │ ├── _utcTimeStamp.ts │ ├── _getDayOfWeekFromYmd.ts │ ├── _getDayOfWeekAbbreviationFromNumber.ts │ ├── epochSeconds.ts │ ├── _getDayOfWeekNumberFromAbbreviation.ts │ ├── julianDate.ts │ ├── _formatExactTimeIso.ts │ ├── _getMonthNumberFromAbbreviation.ts │ ├── julianDate.test.ts │ ├── epochMicroseconds.ts │ ├── modifiedJulianDate.test.ts │ ├── modifiedJulianDate.ts │ ├── _formatDateTimeIso.ts │ ├── _formatOffsetIso.ts │ ├── startOfSecond.ts │ ├── _isNativeMethod.ts │ ├── endOfSecond.ts │ ├── compareAsc.ts │ ├── compareDesc.ts │ ├── _ldmrDatePattern.test.ts │ ├── fromJulianDate.ts │ ├── _isValidHms.test.ts │ ├── isAfter.ts │ ├── isBefore.ts │ ├── _getDayOfWeekAbbreviationFromNumber.test.ts │ ├── fromJulianDate.test.ts │ ├── _isNativeMethod.test.ts │ ├── startOfSecond.test.ts │ ├── endOfSecond.test.ts │ ├── fromModifiedJulianDate.test.ts │ ├── latest.ts │ ├── earliest.ts │ ├── toDateFromExactTime.ts │ ├── _createDateFromClockTime.ts │ ├── _startOfTimeForZonedDateTime.ts │ ├── endOfMinute.ts │ ├── startOfMinute.ts │ ├── endOfDay.ts │ ├── endOfHour.ts │ ├── startOfHour.ts │ ├── _endOfTimeForZonedDateTime.ts │ ├── closestTo.ts │ ├── fromModifiedJulianDate.ts │ ├── isWithinInterval.ts │ ├── _getDayOfWeekNumberFromAbbreviation.test.ts │ ├── epochSeconds.test.ts │ ├── startOfMonth.ts │ ├── _getTimeZoneTransitionBetween.test.ts │ ├── _getMonthAbbreviationFromNumber.test.ts │ ├── epochMicroseconds.test.ts │ ├── endOfMonth.ts │ ├── toDateFromExactTime.test.ts │ ├── startOfDay.ts │ ├── toTemporalFromClockTime.ts │ ├── startOfYear.ts │ ├── _getMonthNumberFromAbbreviation.test.ts │ ├── startOfMinute.test.ts │ ├── clamp.ts │ ├── endOfYear.ts │ ├── _getDayOfWeekFromYmd.test.ts │ ├── formatRfc7231.ts │ ├── isSameWeek.test.ts │ ├── endOfMinute.test.ts │ ├── endOfDay.test.ts │ ├── toTemporalFromClockTime.test.ts │ ├── _getTimeZoneTransitionBetween.ts │ ├── areIntervalsOverlapping.ts │ ├── isSameWeek.ts │ ├── latest.test.ts │ ├── earliest.test.ts │ ├── endOfHour.test.ts │ ├── compareAsc.test.ts │ ├── compareDesc.test.ts │ ├── startOfMonth.test.ts │ ├── _equals.ts │ ├── startOfDay.test.ts │ ├── isAfter.test.ts │ ├── startOfHour.test.ts │ ├── _compare.ts │ ├── isBefore.test.ts │ ├── formatRfc7231.test.ts │ ├── closestIndexTo.ts │ ├── endOfWeek.ts │ ├── _ldmrDatePattern.ts │ ├── startOfWeek.ts │ ├── withDayOfWeek.test.ts │ ├── endOfYear.test.ts │ ├── startOfYear.test.ts │ ├── index.ts │ ├── closestTo.test.ts │ ├── endOfMonth.test.ts │ ├── closestIndexTo.test.ts │ ├── startOfWeek.test.ts │ ├── withDayOfWeek.ts │ ├── fromRfc7231.ts │ ├── clamp.test.ts │ ├── isWithinInterval.test.ts │ ├── fromRfc7231.test.ts │ ├── endOfWeek.test.ts │ ├── toDateFromClockTime.ts │ ├── toDateFromClockTime.test.ts │ ├── areIntervalsOverlapping.test.ts │ ├── fromRfc2822.ts │ └── toObject.ts ├── duration │ ├── _compare.ts │ ├── isEqual.test.ts │ ├── index.ts │ ├── longest.ts │ ├── shortest.ts │ ├── longest.test.ts │ ├── shortest.test.ts │ └── isEqual.ts ├── _test │ └── modifyTimeZone.ts ├── index.ts ├── assert.ts ├── types.ts └── type-utils.test.ts ├── tsconfig.json ├── vitest.config.ts ├── jsr.json ├── .editorconfig ├── tsconfig.build.json ├── vitest.setup.ts ├── LICENSE ├── script ├── repl.ts ├── build.ts └── check-build.ts ├── .github └── workflows │ ├── publish.yml │ ├── tests.yml │ └── code_check.yml ├── README.md ├── package.json └── eslint.config.js /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - "esbuild" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .vscode/* 5 | 6 | node_modules 7 | /dist 8 | /_site 9 | -------------------------------------------------------------------------------- /.oxfmtrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 | "ignorePatterns": [ 4 | "pnpm-lock.yaml", 5 | "*.json", 6 | "src/temporal.d.ts" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/datetime/_padLeadingZeros.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function padLeadingZeros(num: number | string, maxLength: number): string { 3 | return num.toString().padStart(maxLength, "0"); 4 | } 5 | -------------------------------------------------------------------------------- /src/datetime/_createRecord.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function createRecord(object: Record = {}): Record { 3 | return Object.assign(Object.create(null) as Record, object); 4 | } 5 | -------------------------------------------------------------------------------- /src/datetime/_escapeRegExp.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function escapeRegExp(str: string): string { 3 | return str.replaceAll( 4 | /^[0-9a-fA-F]|[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]/g, 5 | (char) => `\\x${char.charCodeAt(0).toString(16)}`, 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/datetime/_padLeadingZeros.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 4 | 5 | test("padLeadingZeros", () => { 6 | expect(padLeadingZeros(3, 2)).toEqual("03"); 7 | expect(padLeadingZeros(23, 2)).toEqual("23"); 8 | }); 9 | -------------------------------------------------------------------------------- /src/datetime/_unionRegExp.ts: -------------------------------------------------------------------------------- 1 | import { escapeRegExp } from "./_escapeRegExp.js"; 2 | 3 | /** @internal */ 4 | export function unionRegExp(strings: string[]): string { 5 | return strings 6 | .map((str) => escapeRegExp(str)) 7 | .filter((str) => str !== "") 8 | .join("|"); 9 | } 10 | -------------------------------------------------------------------------------- /src/datetime/_formatDateIso.ts: -------------------------------------------------------------------------------- 1 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 2 | 3 | /** @internal */ 4 | export function formatDateIso(year: number, month: number, day: number): string { 5 | return `${padLeadingZeros(year, 4)}-${padLeadingZeros(month, 2)}-${padLeadingZeros(day, 2)}`; 6 | } 7 | -------------------------------------------------------------------------------- /src/datetime/_formatHmsIso.ts: -------------------------------------------------------------------------------- 1 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 2 | 3 | /** @internal */ 4 | export function formatHmsIso(hour: number, minute: number, second: number): string { 5 | return `${padLeadingZeros(hour, 2)}:${padLeadingZeros(minute, 2)}:${padLeadingZeros(second, 2)}`; 6 | } 7 | -------------------------------------------------------------------------------- /src/datetime/_secondsToHms.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function secondsToHms(sec: number): [hour: number, minute: number, second: number] { 3 | const second = sec - Math.floor(sec / 60) * 60; 4 | const minute = Math.floor(sec / 60) % 60; 5 | const hour = Math.floor(sec / 3600); 6 | return [hour, minute, second]; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/strictest/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "rootDir": ".", 6 | "noEmit": true, 7 | "module": "Node16", 8 | "moduleResolution": "Node16", 9 | "allowJs": true, 10 | "checkJs": false 11 | }, 12 | "exclude": ["dist", "_site"] 13 | } 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { playwright } from "@vitest/browser-playwright"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: ["vitest.setup.ts"], 7 | browser: { 8 | provider: playwright(), 9 | instances: [{ browser: "firefox" }], 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/datetime/_isValidHms.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function isValidHms( 3 | hour: number, 4 | minute: number, 5 | second: number, 6 | allowLeapSecond: boolean, 7 | ): boolean { 8 | return ( 9 | hour >= 0 && 10 | hour < 24 && 11 | minute >= 0 && 12 | minute < 60 && 13 | second >= 0 && 14 | second < (allowLeapSecond ? 61 : 60) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/datetime/_isValidYmd.ts: -------------------------------------------------------------------------------- 1 | import { utcTimeStamp } from "./_utcTimeStamp.js"; 2 | 3 | /** @internal */ 4 | export function isValidYmd(year: number, month: number, day: number): boolean { 5 | const date = new Date(utcTimeStamp(year, month, day)); 6 | return ( 7 | date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/datetime/_getMonthAbbreviationFromNumber.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function getMonthAbbreviationFromNumber(num: number): string { 3 | const name = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][ 4 | num - 1 5 | ]; 6 | if (name === undefined) { 7 | throw new Error(`Invalid month number: ${num}`); 8 | } 9 | return name; 10 | } 11 | -------------------------------------------------------------------------------- /src/datetime/_utcTimeStamp.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function utcTimeStamp( 3 | year: number, 4 | month = 1, 5 | day = 1, 6 | hour = 0, 7 | minute = 0, 8 | second = 0, 9 | millisecond = 0, 10 | ): number { 11 | const date = new Date(); 12 | date.setUTCFullYear(year, month - 1, day); 13 | date.setUTCHours(hour, minute, second, millisecond); 14 | return date.getTime(); 15 | } 16 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekFromYmd.ts: -------------------------------------------------------------------------------- 1 | import { utcTimeStamp } from "./_utcTimeStamp.js"; 2 | 3 | /** 4 | * @internal 5 | * same as temporal's spec, Monday: 1, Tuesday: 2, ... Sunday: 7 6 | */ 7 | export function getDayOfWeekFromYmd(year: number, month: number, day: number): number { 8 | const date = new Date(utcTimeStamp(year, month, day)); 9 | return ((date.getUTCDay() + 6) % 7) + 1; 10 | } 11 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fabon/vremel", 3 | "version": "0.6.2", 4 | "exports": { 5 | ".": "./src/index.ts", 6 | "./duration": "./src/duration/index.ts" 7 | }, 8 | "publish": { 9 | "include": [ 10 | "CHANGELOG.md", 11 | "README.md", 12 | "LICENSE", 13 | "src/**/*.ts", 14 | "jsr.json" 15 | ], 16 | "exclude": [ 17 | "**/*.test.ts", 18 | "src/_test" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{yaml,yml}] 15 | indent_style = space 16 | 17 | [src/temporal.d.ts] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /src/duration/_compare.ts: -------------------------------------------------------------------------------- 1 | import { getConstructor } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | 4 | /** @internal */ 5 | export function compare( 6 | one: Temporal.Duration, 7 | two: Temporal.Duration, 8 | options?: Temporal.DurationArithmeticOptions, 9 | ): Temporal.ComparisonResult { 10 | const Duration = getConstructor(one); 11 | return Duration.compare(one, two, options); 12 | } 13 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekAbbreviationFromNumber.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * same as temporal's spec, Monday: 1, Tuesday: 2, ... Sunday: 7 4 | */ 5 | export function getDayOfWeekAbbreviationFromNumber(num: number): string { 6 | const name = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][num - 1]; 7 | if (name === undefined) { 8 | throw new Error(`Invalid week of day number: ${num}`); 9 | } 10 | return name; 11 | } 12 | -------------------------------------------------------------------------------- /src/datetime/epochSeconds.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns an integer of seconds from Unix epoch until the exact time which the given temporal object represents. 5 | * 6 | * @param dt Temporal object 7 | * @returns epoch seconds 8 | */ 9 | export function epochSeconds(dt: Temporal.Instant | Temporal.ZonedDateTime): number { 10 | return Math.floor(dt.epochMilliseconds / 1000); 11 | } 12 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekNumberFromAbbreviation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * same as temporal's spec, Monday: 1, Tuesday: 2, ... Sunday: 7 4 | */ 5 | export function getDayOfWeekNumberFromAbbreviation(dayOfWeek: string): number { 6 | const index = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].indexOf(dayOfWeek); 7 | if (index === -1) { 8 | throw new Error(`Unknown day of week: ${dayOfWeek}`); 9 | } 10 | return index + 1; 11 | } 12 | -------------------------------------------------------------------------------- /src/datetime/julianDate.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { modifiedJulianDate } from "./modifiedJulianDate.js"; 3 | 4 | /** 5 | * Returns the julian date of the exact time which the given temporal object represents. 6 | * 7 | * @param instant instant 8 | * @returns julian date 9 | */ 10 | export function julianDate(instant: Temporal.Instant): number { 11 | return modifiedJulianDate(instant) + 2400000.5; 12 | } 13 | -------------------------------------------------------------------------------- /src/duration/isEqual.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isEqual } from "./isEqual.js"; 4 | 5 | test("isEqual()", () => { 6 | const durations = ["-P4D", "P4D", "P4D", "P3DT24H"].map((d) => Temporal.Duration.from(d)); 7 | expect(isEqual(durations[0]!, durations[1]!)).toBe(false); 8 | expect(isEqual(durations[1]!, durations[3]!)).toBe(false); 9 | expect(isEqual(durations[1]!, durations[2]!)).toBe(true); 10 | }); 11 | -------------------------------------------------------------------------------- /src/datetime/_formatExactTimeIso.ts: -------------------------------------------------------------------------------- 1 | import { formatDateTimeIso } from "./_formatDateTimeIso.js"; 2 | 3 | /** @internal */ 4 | export function formatExactTimeIso( 5 | year: number, 6 | month: number, 7 | day: number, 8 | hour: number, 9 | minute: number, 10 | second: number, 11 | millisecond: number, 12 | offsetString: string, 13 | ): string { 14 | return `${formatDateTimeIso(year, month, day, hour, minute, second, millisecond)}${offsetString}`; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": [], 5 | "noEmit": false, 6 | "rootDir": "src", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "declarationMap": true, 11 | "allowJs": false, 12 | "isolatedDeclarations": true, 13 | "erasableSyntaxOnly": true, 14 | "stripInternal": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["**/*.test.ts", "src/_test"] 18 | } 19 | -------------------------------------------------------------------------------- /src/datetime/_getMonthNumberFromAbbreviation.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function getMonthNumberFromAbbreviation(monthName: string): number { 3 | const index = [ 4 | "Jan", 5 | "Feb", 6 | "Mar", 7 | "Apr", 8 | "May", 9 | "Jun", 10 | "Jul", 11 | "Aug", 12 | "Sep", 13 | "Oct", 14 | "Nov", 15 | "Dec", 16 | ].indexOf(monthName); 17 | if (index === -1) { 18 | throw new Error(`Unknown month name: ${monthName}`); 19 | } 20 | return index + 1; 21 | } 22 | -------------------------------------------------------------------------------- /src/datetime/julianDate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { julianDate } from "./julianDate.js"; 4 | 5 | test("julian date", () => { 6 | expect(julianDate(Temporal.Instant.from("2024-01-01T12:34:56Z"))).toBeCloseTo( 7 | 2460311.024259259, 8 | 8, 9 | ); 10 | expect(julianDate(Temporal.Instant.from("1858-11-17T00:00:00Z"))).toEqual(2400000.5); 11 | expect(julianDate(Temporal.Instant.from("1858-11-01T00:00:00Z"))).toEqual(2399984.5); 12 | }); 13 | -------------------------------------------------------------------------------- /src/duration/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * 4 | * This module contains functions for `Temporal.Duration`. 5 | * 6 | * ```typescript 7 | * import { isEqual } from "vremel/duration"; 8 | * isEqual( 9 | * Temporal.Duration.from({ hours: 3 }), 10 | * Temporal.Duration.from({ hours: 3 }), 11 | * ); // true 12 | * ``` 13 | */ 14 | 15 | export { isEqual } from "./isEqual.js"; 16 | export { longest } from "./longest.js"; 17 | export { shortest } from "./shortest.js"; 18 | -------------------------------------------------------------------------------- /src/datetime/epochMicroseconds.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns an integer of microseconds from Unix epoch until the exact time which the given temporal object represents. 5 | * 6 | * @param dt Temporal object 7 | * @returns epoch microseconds 8 | */ 9 | export function epochMicroseconds(dt: Temporal.Instant | Temporal.ZonedDateTime): bigint { 10 | return (dt.epochNanoseconds - (((dt.epochNanoseconds % 1000n) + 1000n) % 1000n)) / 1000n; 11 | } 12 | -------------------------------------------------------------------------------- /src/datetime/modifiedJulianDate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { modifiedJulianDate } from "./modifiedJulianDate.js"; 4 | 5 | test("modified julian date", () => { 6 | expect(modifiedJulianDate(Temporal.Instant.from("2024-01-01T12:34:56Z"))).toBeCloseTo( 7 | 60310.524259259, 8 | 8, 9 | ); 10 | expect(modifiedJulianDate(Temporal.Instant.from("1858-11-17T00:00:00Z"))).toEqual(0); 11 | expect(modifiedJulianDate(Temporal.Instant.from("1858-11-01T00:00:00Z"))).toEqual(-16); 12 | }); 13 | -------------------------------------------------------------------------------- /src/_test/modifyTimeZone.ts: -------------------------------------------------------------------------------- 1 | export function modifyTimeZone(timeZoneId: string) { 2 | if (typeof process !== "object") { 3 | // no-op in browser 4 | return { 5 | restore() {}, 6 | [Symbol.dispose]() {}, 7 | }; 8 | } 9 | const originalTimeZone = process.env.TZ; 10 | process.env.TZ = timeZoneId; 11 | return { 12 | restore() { 13 | if (originalTimeZone !== undefined) { 14 | process.env.TZ = originalTimeZone; 15 | } else { 16 | delete process.env.TZ; 17 | } 18 | }, 19 | [Symbol.dispose]() { 20 | this.restore(); 21 | }, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/duration/longest.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Returns the longest of the given durations. 6 | * @param durations array of durations 7 | * @param options the options passed to `Temporal.Duration.compare` 8 | * @returns the longest of the duration 9 | */ 10 | export function longest( 11 | durations: Temporal.Duration[], 12 | options?: Temporal.DurationArithmeticOptions, 13 | ): Temporal.Duration { 14 | return durations.reduce((a, b) => (compare(a, b, options) === -1 ? b : a)); 15 | } 16 | -------------------------------------------------------------------------------- /src/duration/shortest.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Returns the shortest of the given durations. 6 | * @param durations array of durations 7 | * @param options the options passed to `Temporal.Duration.compare` 8 | * @returns the shortest of the duration 9 | */ 10 | export function shortest( 11 | durations: Temporal.Duration[], 12 | options?: Temporal.DurationArithmeticOptions, 13 | ): Temporal.Duration { 14 | return durations.reduce((a, b) => (compare(a, b, options) === 1 ? b : a)); 15 | } 16 | -------------------------------------------------------------------------------- /src/datetime/modifiedJulianDate.ts: -------------------------------------------------------------------------------- 1 | import { getConstructor } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | 4 | /** 5 | * Returns the modified julian date of the exact time which the given temporal object represents. 6 | * 7 | * @param instant instant 8 | * @returns modified julian date 9 | */ 10 | export function modifiedJulianDate(instant: Temporal.Instant): number { 11 | const Instant = getConstructor(instant); 12 | // modified julian date epoch: 1858-11-17T00:00:00Z 13 | return instant.since(Instant.from("1858-11-17T00:00:00Z")).total("day"); 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * This module contains functions for datetime objects from Temporal API, 4 | * for example `Temporal.ZonedDateTime` and `Temporal.PlainDate`. 5 | * 6 | * ```typescript 7 | * import { latest } from "vremel"; 8 | * latest([ 9 | * Temporal.PlainDate.from("2024-01-01"), 10 | * Temporal.PlainDate.from("2024-02-01"), 11 | * Temporal.PlainDate.from("2023-11-30"), 12 | * ]).toString(); // "2024-02-01" 13 | * ``` 14 | */ 15 | 16 | export * from "./datetime/index.js"; 17 | export type { ArrayOf, GenericDateConstructor, Interval } from "./types.js"; 18 | -------------------------------------------------------------------------------- /src/datetime/_formatDateTimeIso.ts: -------------------------------------------------------------------------------- 1 | import { formatDateIso } from "./_formatDateIso.js"; 2 | import { formatHmsIso } from "./_formatHmsIso.js"; 3 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 4 | 5 | /** @internal */ 6 | export function formatDateTimeIso( 7 | year: number, 8 | month: number, 9 | day: number, 10 | hour: number, 11 | minute: number, 12 | second: number, 13 | millisecond: number, 14 | ): string { 15 | const millisecondStr = padLeadingZeros(millisecond, 3); 16 | return `${formatDateIso(year, month, day)}T${formatHmsIso(hour, minute, second)}.${millisecondStr}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/datetime/_formatOffsetIso.ts: -------------------------------------------------------------------------------- 1 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 2 | import { secondsToHms } from "./_secondsToHms.js"; 3 | 4 | /** 5 | * @internal 6 | * The result includes sub-minute offset 7 | */ 8 | export function formatOffsetIso(offsetSeconds: number, includeColon: boolean): string { 9 | const sign = offsetSeconds < 0 ? "-" : "+"; 10 | const [h, m, s] = secondsToHms(Math.abs(offsetSeconds)).map((v) => padLeadingZeros(v, 2)) as [ 11 | string, 12 | string, 13 | string, 14 | ]; 15 | return includeColon ? `${sign}${h}:${m}:${s}` : `${sign}${h}${m}${s}`; 16 | } 17 | -------------------------------------------------------------------------------- /src/datetime/startOfSecond.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns the start of a second for the given datetime 5 | * @param dt datetime object which includes time info 6 | * @returns Temporal object which represents the start of a second 7 | */ 8 | export function startOfSecond< 9 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 10 | >(dt: DateTime): DateTime { 11 | // assumption: no sub-second offset transition in timezone database 12 | return dt.with({ millisecond: 0, microsecond: 0, nanosecond: 0 }) as DateTime; 13 | } 14 | -------------------------------------------------------------------------------- /src/datetime/_isNativeMethod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * This method is not reliable when a polyfill overwrites `Function.prototype.toString` (as core-js do), 4 | * but neither `temporal-polyfill` or `@js-temporal/polyfill` does that. 5 | */ 6 | export function isNativeMethod(obj: T, name: keyof T): boolean { 7 | return new RegExp( 8 | [ 9 | "^function", 10 | name, 11 | "\\(", 12 | "[^)]*", 13 | "\\)", 14 | "\\{", 15 | "\\[", 16 | "native", 17 | "code", 18 | "\\]", 19 | "\\}", 20 | "$", 21 | ].join("\\s*"), 22 | ).test(Function.prototype.toString.call(obj[name])); 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/endOfSecond.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns the end of a second for the given datetime 5 | * @param dt datetime object which includes time info 6 | * @returns Temporal object which represents the end of the second 7 | */ 8 | export function endOfSecond< 9 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 10 | >(dt: DateTime): DateTime { 11 | // assumption: no sub-second offset transition in timezone database 12 | return dt.with({ 13 | millisecond: 999, 14 | microsecond: 999, 15 | nanosecond: 999, 16 | }) as DateTime; 17 | } 18 | -------------------------------------------------------------------------------- /src/datetime/compareAsc.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Compares two datetime objects in chronological order and returns -1, 0, or 1. 6 | * @param a datetime object 7 | * @param b datetime object 8 | * @returns the result of the comparison 9 | */ 10 | export function compareAsc< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(a: DateTime, b: DateTime): -1 | 0 | 1 { 19 | return compare(a, b); 20 | } 21 | -------------------------------------------------------------------------------- /src/assert.ts: -------------------------------------------------------------------------------- 1 | import { compare } from "./datetime/_compare.js"; 2 | import { getTypeName } from "./type-utils.js"; 3 | import type { Interval, TemporalType } from "./types.js"; 4 | 5 | /** @internal */ 6 | export function assertSameType(a: TemporalType, b: TemporalType): void { 7 | if (getTypeName(a) !== getTypeName(b)) { 8 | throw new Error(`Temporal type mismatch: ${getTypeName(a)} and ${getTypeName(b)}`); 9 | } 10 | } 11 | 12 | /** @internal */ 13 | export function assertValidInterval(i: Interval): void { 14 | assertSameType(i.start, i.end); 15 | if (compare(i.start, i.end) === 1) { 16 | throw new Error("Invalid interval"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/datetime/compareDesc.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Compares two datetime objects in reverse chronological order and returns -1, 0, or 1. 6 | * @param a datetime object 7 | * @param b datetime object 8 | * @returns the result of the comparison 9 | */ 10 | export function compareDesc< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(a: DateTime, b: DateTime): -1 | 0 | 1 { 19 | return compare(b, a); 20 | } 21 | -------------------------------------------------------------------------------- /src/duration/longest.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { longest } from "./longest.js"; 4 | 5 | test("longest()", () => { 6 | const a = ["PT3H29M", "PT208M", "PT210M"].map((d) => Temporal.Duration.from(d)); 7 | expect(longest(a)).toBe(a[2]); 8 | }); 9 | 10 | test("longest() with relativeTo option", () => { 11 | const a = ["P3M", "P90D"].map((d) => Temporal.Duration.from(d)); 12 | expect( 13 | longest(a, { 14 | relativeTo: Temporal.PlainDate.from("2023-02-01"), 15 | }), 16 | ).toBe(a[1]); 17 | expect( 18 | longest(a, { 19 | relativeTo: Temporal.PlainDate.from("2023-05-01"), 20 | }), 21 | ).toBe(a[0]); 22 | }); 23 | -------------------------------------------------------------------------------- /src/datetime/_ldmrDatePattern.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { replaceToken, tokenize } from "./_ldmrDatePattern.js"; 4 | 5 | const replacer = (token: string) => `0x${token.charCodeAt(0).toString(16)}`.repeat(token.length); 6 | 7 | test("replaceToken", () => { 8 | expect(replaceToken(`'' d 'dd'''`, replacer)).toEqual(`' 0x64 dd'`); 9 | }); 10 | 11 | test("tokenize", () => { 12 | expect(tokenize(`Ж '' d+ 'dd'''`)).toEqual([ 13 | { type: "literal", value: `Ж ' ` }, 14 | { type: "field", value: "d" }, 15 | { type: "literal", value: `+ dd'` }, 16 | ]); 17 | expect(() => { 18 | tokenize(`'a ''`); 19 | }).toThrow(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/duration/shortest.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { shortest } from "./shortest.js"; 4 | 5 | test("shortest()", () => { 6 | const a = ["PT3H29M", "PT208M", "PT210M"].map((d) => Temporal.Duration.from(d)); 7 | expect(shortest(a)).toBe(a[1]); 8 | }); 9 | 10 | test("shortest() with relativeTo option", () => { 11 | const a = ["P3M", "P90D"].map((d) => Temporal.Duration.from(d)); 12 | expect( 13 | shortest(a, { 14 | relativeTo: Temporal.PlainDate.from("2023-02-01"), 15 | }), 16 | ).toBe(a[0]); 17 | expect( 18 | shortest(a, { 19 | relativeTo: Temporal.PlainDate.from("2023-05-01"), 20 | }), 21 | ).toBe(a[1]); 22 | }); 23 | -------------------------------------------------------------------------------- /src/datetime/fromJulianDate.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { fromModifiedJulianDate } from "./fromModifiedJulianDate.js"; 3 | 4 | /** 5 | * Returns a temporal object which corresponds to the given julian date. 6 | * 7 | * @param julianDate julian date 8 | * @param Instant `Temporal.Instant` class 9 | * @returns `Temporal.Instant` which corresponds to the given julian date 10 | */ 11 | export function fromJulianDate( 12 | julianDate: number, 13 | Instant: InstantClassType, 14 | ): InstanceType { 15 | return fromModifiedJulianDate(julianDate - 2400000.5, Instant); 16 | } 17 | -------------------------------------------------------------------------------- /src/datetime/_isValidHms.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isValidHms } from "./_isValidHms.js"; 4 | 5 | test("isValidHms", () => { 6 | expect(isValidHms(0, 0, 0, false)).toEqual(true); 7 | expect(isValidHms(23, 59, 59, false)).toEqual(true); 8 | expect(isValidHms(23, 59, 60, false)).toEqual(false); 9 | expect(isValidHms(23, 60, 0, false)).toEqual(false); 10 | expect(isValidHms(24, 0, 0, false)).toEqual(false); 11 | }); 12 | 13 | test("allowing leap second", () => { 14 | expect(isValidHms(23, 59, 60, true)).toEqual(true); 15 | expect(isValidHms(23, 58, 60, true)).toEqual(true); 16 | expect(isValidHms(24, 0, 60, true)).toEqual(false); 17 | }); 18 | -------------------------------------------------------------------------------- /src/duration/isEqual.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Check whether two durations are equal 5 | * @param a duration 6 | * @param b duration 7 | * @returns whether two durations are equal 8 | */ 9 | export function isEqual(a: Temporal.Duration, b: Temporal.Duration): boolean { 10 | return ( 11 | a.years === b.years && 12 | a.months === b.months && 13 | a.weeks === b.weeks && 14 | a.days === b.days && 15 | a.hours === b.hours && 16 | a.minutes === b.minutes && 17 | a.seconds === b.seconds && 18 | a.milliseconds === b.milliseconds && 19 | a.microseconds === b.microseconds && 20 | a.nanoseconds === b.nanoseconds 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import "temporal-spec/global"; 3 | 4 | async function loadPolyfill(packageName: unknown) { 5 | if (packageName === undefined) { 6 | return; 7 | } 8 | if (packageName !== "temporal-polyfill" && packageName !== "@js-temporal/polyfill") { 9 | throw new Error("Unknown polyfill"); 10 | } 11 | const { Temporal, toTemporalInstant } = 12 | packageName === "temporal-polyfill" 13 | ? await import("temporal-polyfill") 14 | : await import("@js-temporal/polyfill"); 15 | globalThis.Temporal = Temporal; 16 | Date.prototype.toTemporalInstant = toTemporalInstant; 17 | } 18 | 19 | await loadPolyfill(import.meta.env["POLYFILL"]); 20 | -------------------------------------------------------------------------------- /src/datetime/isAfter.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Checks whether the first datetime is after the second one. 6 | * @param dateTime datetime object 7 | * @param dateTimeToCompare datetime object to compare with 8 | * @returns whether the first datetime is after the second one 9 | */ 10 | export function isAfter< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(dateTime: DateTime, dateTimeToCompare: DateTime): boolean { 19 | return compare(dateTime, dateTimeToCompare) === 1; 20 | } 21 | -------------------------------------------------------------------------------- /src/datetime/isBefore.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { compare } from "./_compare.js"; 3 | 4 | /** 5 | * Checks whether the first datetime is before the second one. 6 | * @param dateTime datetime object 7 | * @param dateTimeToCompare datetime object to compare with 8 | * @returns whether the first datetime is before the second one 9 | */ 10 | export function isBefore< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(dateTime: DateTime, dateTimeToCompare: DateTime): boolean { 19 | return compare(dateTime, dateTimeToCompare) === -1; 20 | } 21 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekAbbreviationFromNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { getDayOfWeekAbbreviationFromNumber } from "./_getDayOfWeekAbbreviationFromNumber.js"; 4 | 5 | test("getDayOfWeekAbbreviationFromNumber", () => { 6 | expect(getDayOfWeekAbbreviationFromNumber(1)).toEqual("Mon"); 7 | expect(getDayOfWeekAbbreviationFromNumber(2)).toEqual("Tue"); 8 | expect(getDayOfWeekAbbreviationFromNumber(3)).toEqual("Wed"); 9 | expect(getDayOfWeekAbbreviationFromNumber(4)).toEqual("Thu"); 10 | expect(getDayOfWeekAbbreviationFromNumber(5)).toEqual("Fri"); 11 | expect(getDayOfWeekAbbreviationFromNumber(6)).toEqual("Sat"); 12 | expect(getDayOfWeekAbbreviationFromNumber(7)).toEqual("Sun"); 13 | }); 14 | -------------------------------------------------------------------------------- /src/datetime/fromJulianDate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { fromJulianDate } from "./fromJulianDate.js"; 4 | 5 | test("create Instant from julian date", () => { 6 | // julian date 2460311.024259259 corresponds to 2024-01-01T12:34:55.999977600Z, 7 | // but rounding to milliseconds to deal with floating point error 8 | expect(fromJulianDate(2460311.024259259, Temporal.Instant).round("millisecond")).toEqual( 9 | Temporal.Instant.from("2024-01-01T12:34:56Z"), 10 | ); 11 | expect(fromJulianDate(2400000.5, Temporal.Instant)).toEqual( 12 | Temporal.Instant.from("1858-11-17T00:00:00Z"), 13 | ); 14 | expect(fromJulianDate(2399984.5, Temporal.Instant)).toEqual( 15 | Temporal.Instant.from("1858-11-01T00:00:00Z"), 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /src/datetime/_isNativeMethod.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isNativeMethod } from "./_isNativeMethod.js"; 4 | 5 | class Dummy { 6 | static classMethod() {} 7 | instanceMethod() {} 8 | } 9 | 10 | test("isNativeMethod on native methods", () => { 11 | expect(isNativeMethod(new Date(), "getTime")).toEqual(true); 12 | expect(isNativeMethod(Date.prototype, "getTime")).toEqual(true); 13 | expect(isNativeMethod(Date, "parse")).toEqual(true); 14 | }); 15 | 16 | test("isNativeMethod on userland methods", () => { 17 | expect(isNativeMethod(new Dummy(), "instanceMethod")).toEqual(false); 18 | expect(isNativeMethod(Dummy.prototype, "instanceMethod")).toEqual(false); 19 | expect(isNativeMethod(Dummy, "classMethod")).toEqual(false); 20 | }); 21 | -------------------------------------------------------------------------------- /src/datetime/startOfSecond.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfSecond } from "./startOfSecond.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfSecond(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:34:56"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(startOfSecond(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:34:56"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime", () => { 18 | expect( 19 | startOfSecond(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T12:34:56+09:00[Asia/Tokyo]")); 21 | }); 22 | -------------------------------------------------------------------------------- /src/datetime/endOfSecond.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfSecond } from "./endOfSecond.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfSecond(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:34:56.999999999"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(endOfSecond(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:34:56.999999999"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime", () => { 18 | expect( 19 | endOfSecond(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 20 | ).toEqual(Temporal.PlainDateTime.from("2024-01-01T12:34:56.999999999+09:00[Asia/Tokyo]")); 21 | }); 22 | -------------------------------------------------------------------------------- /src/datetime/fromModifiedJulianDate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { fromModifiedJulianDate } from "./fromModifiedJulianDate.js"; 4 | 5 | test("create Instant from julian date", () => { 6 | // modified julian date 60310.524259259 corresponds to 2024-01-01T12:34:55.999977600Z, 7 | // but rounding to microseconds to deal with floating point error 8 | expect(fromModifiedJulianDate(60310.524259259, Temporal.Instant).round("microseconds")).toEqual( 9 | Temporal.Instant.from("2024-01-01T12:34:55.999978Z"), 10 | ); 11 | expect(fromModifiedJulianDate(0, Temporal.Instant)).toEqual( 12 | Temporal.Instant.from("1858-11-17T00:00:00Z"), 13 | ); 14 | expect(fromModifiedJulianDate(-16, Temporal.Instant)).toEqual( 15 | Temporal.Instant.from("1858-11-01T00:00:00Z"), 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /src/datetime/latest.ts: -------------------------------------------------------------------------------- 1 | import { isPlainMonthDay } from "../type-utils.js"; 2 | import type { ArrayOf, Temporal } from "../types.js"; 3 | import { compare } from "./_compare.js"; 4 | 5 | /** 6 | * Returns the latest of the given datetime objects. 7 | * @param dateTimes array of datetime objects 8 | * @returns the latest of the datetime objects 9 | */ 10 | export function latest< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(dateTimes: ArrayOf): DateTime { 19 | if (dateTimes.some(isPlainMonthDay)) { 20 | throw new Error("Can't compare PlainMonthDay"); 21 | } 22 | return dateTimes.reduce((a, b) => (compare(a, b) === -1 ? b : a)) as DateTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/earliest.ts: -------------------------------------------------------------------------------- 1 | import { isPlainMonthDay } from "../type-utils.js"; 2 | import type { ArrayOf, Temporal } from "../types.js"; 3 | import { compare } from "./_compare.js"; 4 | 5 | /** 6 | * Returns the earliest of the given datetime objects. 7 | * @param dateTimes array of datetime objects 8 | * @returns the earliest of the datetime objects 9 | */ 10 | export function earliest< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(dateTimes: ArrayOf): DateTime { 19 | if (dateTimes.some(isPlainMonthDay)) { 20 | throw new Error("Can't compare PlainMonthDay"); 21 | } 22 | return dateTimes.reduce((a, b) => (compare(a, b) === 1 ? b : a)) as DateTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/toDateFromExactTime.ts: -------------------------------------------------------------------------------- 1 | import type { GenericDateConstructor, Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns `Date` which represents exact time of the given temporal object. 5 | * 6 | * @param dateTime Temporal object which includes exact time info 7 | * @param DateConstructor `Date` or extended class for return type, default is `Date` 8 | * @returns `Date` (or its subclass) object which represents the exact time of the given Temporal object 9 | */ 10 | export function toDateFromExactTime( 11 | dateTime: Temporal.ZonedDateTime | Temporal.Instant, 12 | DateConstructor?: GenericDateConstructor, 13 | ): DateType { 14 | const DateConstructorFunction = DateConstructor ?? Date; 15 | return new DateConstructorFunction(dateTime.epochMilliseconds) as DateType; 16 | } 17 | -------------------------------------------------------------------------------- /src/datetime/_createDateFromClockTime.ts: -------------------------------------------------------------------------------- 1 | import type { GenericDateConstructor } from "../types.js"; 2 | 3 | /** @internal */ 4 | export function createDateFromClockTime( 5 | DateConstructor: GenericDateConstructor, 6 | year: number, 7 | month: number, 8 | day = 1, 9 | hour = 0, 10 | minute = 0, 11 | second = 0, 12 | millisecond = 0, 13 | ): DateType { 14 | if (year >= 0 && year <= 99) { 15 | // assumption: there is no time zone transition in ancient times 16 | // therefore it's safe to run `setFullYear` and `setHours` separately 17 | const date = new DateConstructor(500, 0, 1); 18 | date.setFullYear(year, month - 1, day); 19 | date.setHours(hour, minute, second, millisecond); 20 | return date; 21 | } 22 | return new DateConstructor(year, month - 1, day, hour, minute, second, millisecond); 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/_startOfTimeForZonedDateTime.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { getTimeZoneTransitionBetween } from "./_getTimeZoneTransitionBetween.js"; 3 | 4 | /** @internal */ 5 | export function startOfTimeForZonedDateTime( 6 | zdt: Temporal.ZonedDateTime, 7 | withArg: Temporal.PlainDateTimeLike, 8 | ): Temporal.ZonedDateTime { 9 | const [earlier, later] = (["earlier", "later"] as const).map((disambiguation) => 10 | zdt.with(withArg, { 11 | offset: "ignore", 12 | disambiguation, 13 | }), 14 | ) as [Temporal.ZonedDateTime, Temporal.ZonedDateTime]; 15 | 16 | if (earlier.toPlainDateTime().equals(later.toPlainDateTime())) { 17 | // backward transition or no transition 18 | return earlier; 19 | } else { 20 | // forward transition 21 | return getTimeZoneTransitionBetween(earlier, later); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/endOfMinute.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the end of a minute for the given datetime 7 | * @param dt datetime object which includes time info 8 | * @returns Temporal object which represents the end of the minute 9 | */ 10 | export function endOfMinute< 11 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | const withArg = { 14 | second: 59, 15 | millisecond: 999, 16 | microsecond: 999, 17 | nanosecond: 999, 18 | }; 19 | if (!isZonedDateTime(dt)) { 20 | return dt.with(withArg) as DateTime; 21 | } 22 | return endOfTimeForZonedDateTime(dt, withArg) as DateTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/startOfMinute.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the start of a minute for the given datetime 7 | * @param dt datetime object which includes time info 8 | * @returns Temporal object which represents the start of a minute 9 | */ 10 | export function startOfMinute< 11 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | const withArg = { 14 | second: 0, 15 | millisecond: 0, 16 | microsecond: 0, 17 | nanosecond: 0, 18 | }; 19 | if (!isZonedDateTime(dt)) { 20 | return dt.with(withArg) as DateTime; 21 | } 22 | return startOfTimeForZonedDateTime(dt, withArg) as DateTime; 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/endOfDay.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the end of a day for the given datetime 7 | * @param dt datetime object which includes date and time info 8 | * @returns Temporal object which represents the end of the day 9 | */ 10 | export function endOfDay( 11 | dt: DateTime, 12 | ): DateTime { 13 | const withArg = { 14 | hour: 23, 15 | minute: 59, 16 | second: 59, 17 | millisecond: 999, 18 | microsecond: 999, 19 | nanosecond: 999, 20 | }; 21 | if (!isZonedDateTime(dt)) { 22 | return dt.with(withArg) as DateTime; 23 | } 24 | return endOfTimeForZonedDateTime(dt, withArg) as DateTime; 25 | } 26 | -------------------------------------------------------------------------------- /src/datetime/endOfHour.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the end of a hour for the given datetime 7 | * @param dt datetime object which includes time info 8 | * @returns Temporal object which represents the end of the hour 9 | */ 10 | export function endOfHour< 11 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | const withArg = { 14 | minute: 59, 15 | second: 59, 16 | millisecond: 999, 17 | microsecond: 999, 18 | nanosecond: 999, 19 | }; 20 | if (!isZonedDateTime(dt)) { 21 | return dt.with(withArg) as DateTime; 22 | } 23 | return endOfTimeForZonedDateTime(dt, withArg) as DateTime; 24 | } 25 | -------------------------------------------------------------------------------- /src/datetime/startOfHour.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the start of a hour for the given datetime 7 | * @param dt datetime object which includes time info 8 | * @returns Temporal object which represents the start of a hour 9 | */ 10 | export function startOfHour< 11 | DateTime extends Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | const withArg = { 14 | minute: 0, 15 | second: 0, 16 | millisecond: 0, 17 | microsecond: 0, 18 | nanosecond: 0, 19 | }; 20 | if (!isZonedDateTime(dt)) { 21 | return dt.with(withArg) as DateTime; 22 | } 23 | 24 | return startOfTimeForZonedDateTime(dt, withArg) as DateTime; 25 | } 26 | -------------------------------------------------------------------------------- /src/datetime/_endOfTimeForZonedDateTime.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | import { getTimeZoneTransitionBetween } from "./_getTimeZoneTransitionBetween.js"; 3 | 4 | /** @internal */ 5 | export function endOfTimeForZonedDateTime( 6 | zdt: Temporal.ZonedDateTime, 7 | withArg: Temporal.PlainDateTimeLike, 8 | ): Temporal.ZonedDateTime { 9 | const [earlier, later] = (["earlier", "later"] as const).map((disambiguation) => 10 | zdt.with(withArg, { 11 | offset: "ignore", 12 | disambiguation, 13 | }), 14 | ) as [Temporal.ZonedDateTime, Temporal.ZonedDateTime]; 15 | 16 | if (earlier.toPlainDateTime().equals(later.toPlainDateTime())) { 17 | // backward transition or no transition 18 | return later; 19 | } else { 20 | // forward transition 21 | return getTimeZoneTransitionBetween(earlier, later).subtract({ 22 | nanoseconds: 1, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/datetime/closestTo.ts: -------------------------------------------------------------------------------- 1 | import type { ArrayOf, Temporal } from "../types.js"; 2 | import { closestIndexTo } from "./closestIndexTo.js"; 3 | 4 | /** 5 | * Returns the closest datetime object to the given datetime object from the passed array. 6 | * @param dateTimeToCompare the date to compare with 7 | * @param dateTimes array of datetime objects 8 | * @returns the closest datetime object 9 | */ 10 | export function closestTo< 11 | DateTime extends 12 | | Temporal.Instant 13 | | Temporal.ZonedDateTime 14 | | Temporal.PlainDate 15 | | Temporal.PlainTime 16 | | Temporal.PlainDateTime 17 | | Temporal.PlainYearMonth, 18 | >(dateTimeToCompare: DateTime, dateTimes: ArrayOf): DateTime { 19 | const ret = dateTimes[closestIndexTo(dateTimeToCompare, dateTimes)]; 20 | if (ret === undefined) { 21 | throw new Error("Something wrong..."); 22 | } 23 | return ret as DateTime; 24 | } 25 | -------------------------------------------------------------------------------- /src/datetime/fromModifiedJulianDate.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns a temporal object which corresponds to the given modified julian date. 5 | * 6 | * @param modifiedJulianDate modified julian date 7 | * @param Instant `Temporal.Instant` class 8 | * @returns `Temporal.Instant` which corresponds to the given modified julian date 9 | */ 10 | export function fromModifiedJulianDate( 11 | modifiedJulianDate: number, 12 | Instant: InstantClassType, 13 | ): InstanceType { 14 | const modifiedJulianDayInt = Math.floor(modifiedJulianDate); 15 | const nanoseconds = Math.floor((modifiedJulianDate - modifiedJulianDayInt) * 8.64e13); 16 | return Instant.from("1858-11-17T00:00:00Z").add({ 17 | hours: modifiedJulianDayInt * 24, 18 | nanoseconds, 19 | }) as InstanceType; 20 | } 21 | -------------------------------------------------------------------------------- /src/datetime/isWithinInterval.ts: -------------------------------------------------------------------------------- 1 | import { assertSameType, assertValidInterval } from "../assert.js"; 2 | import type { Interval, Temporal } from "../types.js"; 3 | import { compare } from "./_compare.js"; 4 | 5 | /** 6 | * Checks whether the given datetime is within the interval. 7 | * @param dateTime temporal object 8 | * @param interval interval 9 | * @returns Whether the given datetime is within the interval 10 | */ 11 | export function isWithinInterval< 12 | DateTime extends 13 | | Temporal.Instant 14 | | Temporal.ZonedDateTime 15 | | Temporal.PlainDate 16 | | Temporal.PlainTime 17 | | Temporal.PlainDateTime 18 | | Temporal.PlainYearMonth, 19 | >(dateTime: DateTime, interval: Interval): boolean { 20 | assertValidInterval(interval); 21 | assertSameType(dateTime, interval.start); 22 | return compare(dateTime, interval.start) !== -1 && compare(dateTime, interval.end) !== 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekNumberFromAbbreviation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { getDayOfWeekNumberFromAbbreviation } from "./_getDayOfWeekNumberFromAbbreviation.js"; 4 | 5 | test("getDayOfWeekNumberFromAbbreviation", () => { 6 | expect(getDayOfWeekNumberFromAbbreviation("Mon")).toEqual(1); 7 | expect(getDayOfWeekNumberFromAbbreviation("Tue")).toEqual(2); 8 | expect(getDayOfWeekNumberFromAbbreviation("Wed")).toEqual(3); 9 | expect(getDayOfWeekNumberFromAbbreviation("Thu")).toEqual(4); 10 | expect(getDayOfWeekNumberFromAbbreviation("Fri")).toEqual(5); 11 | expect(getDayOfWeekNumberFromAbbreviation("Sat")).toEqual(6); 12 | expect(getDayOfWeekNumberFromAbbreviation("Sun")).toEqual(7); 13 | }); 14 | 15 | test("getDayOfWeekNumberFromAbbreviation with unknown week of day", () => { 16 | expect(() => { 17 | getDayOfWeekNumberFromAbbreviation("Mo"); 18 | }).toThrowError(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/datetime/epochSeconds.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { epochSeconds } from "./epochSeconds.js"; 4 | 5 | test("epochSeconds with Instant", () => { 6 | expect(epochSeconds(Temporal.Instant.from("1970-01-01T00:00:01Z"))).toEqual(1); 7 | expect(epochSeconds(Temporal.Instant.from("1969-12-31T23:59:59Z"))).toEqual(-1); 8 | }); 9 | 10 | test("epochSeconds with ZonedDateTime", () => { 11 | expect( 12 | epochSeconds(Temporal.Instant.from("1970-01-01T00:00:01Z").toZonedDateTimeISO("Europe/London")), 13 | ).toEqual(1); 14 | expect( 15 | epochSeconds(Temporal.Instant.from("1969-12-31T23:59:59Z").toZonedDateTimeISO("Europe/London")), 16 | ).toEqual(-1); 17 | }); 18 | 19 | test("epochSeconds with fractional second", () => { 20 | expect(epochSeconds(Temporal.Instant.from("1970-01-01T00:00:01.3Z"))).toEqual(1); 21 | expect(epochSeconds(Temporal.Instant.from("1969-12-31T23:59:59.3Z"))).toEqual(-1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/datetime/startOfMonth.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the start of a month for the given datetime 7 | * @param dt datetime object which includes date info 8 | * @returns Temporal object which represents the start of a month 9 | */ 10 | export function startOfMonth< 11 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | if (isPlainDate(dt)) { 14 | return dt.with({ day: 1 }) as DateTime; 15 | } 16 | const withArg = { 17 | day: 1, 18 | hour: 0, 19 | minute: 0, 20 | second: 0, 21 | millisecond: 0, 22 | microsecond: 0, 23 | nanosecond: 0, 24 | }; 25 | if (!isZonedDateTime(dt)) { 26 | return dt.with(withArg) as DateTime; 27 | } 28 | return startOfTimeForZonedDateTime(dt, withArg) as DateTime; 29 | } 30 | -------------------------------------------------------------------------------- /src/datetime/_getTimeZoneTransitionBetween.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { getTimeZoneTransitionBetween } from "./_getTimeZoneTransitionBetween.js"; 4 | 5 | test("getTimeZoneTransitionBetween", () => { 6 | const transition1 = Temporal.ZonedDateTime.from("2024-03-31T02:00:00+01:00[Europe/London]"); 7 | const transition2 = Temporal.ZonedDateTime.from("2024-10-27T01:00:00+00:00[Europe/London]"); 8 | expect( 9 | getTimeZoneTransitionBetween(transition1.subtract({ nanoseconds: 1 }), transition1), 10 | ).toEqual(transition1); 11 | expect(getTimeZoneTransitionBetween(transition1, transition2)).toEqual(transition2); 12 | }); 13 | 14 | test("getTimeZoneTransitionBetween and non-ISO calendar", () => { 15 | const transition = Temporal.ZonedDateTime.from( 16 | "2024-03-31T02:00:00+01:00[Europe/London][u-ca=hebrew]", 17 | ); 18 | expect(getTimeZoneTransitionBetween(transition.subtract({ nanoseconds: 1 }), transition)).toEqual( 19 | transition, 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /src/datetime/_getMonthAbbreviationFromNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { getMonthAbbreviationFromNumber } from "./_getMonthAbbreviationFromNumber.js"; 4 | 5 | test("getMonthAbbreviationFromNumber", () => { 6 | expect(getMonthAbbreviationFromNumber(1)).toEqual("Jan"); 7 | expect(getMonthAbbreviationFromNumber(2)).toEqual("Feb"); 8 | expect(getMonthAbbreviationFromNumber(3)).toEqual("Mar"); 9 | expect(getMonthAbbreviationFromNumber(4)).toEqual("Apr"); 10 | expect(getMonthAbbreviationFromNumber(5)).toEqual("May"); 11 | expect(getMonthAbbreviationFromNumber(6)).toEqual("Jun"); 12 | expect(getMonthAbbreviationFromNumber(7)).toEqual("Jul"); 13 | expect(getMonthAbbreviationFromNumber(8)).toEqual("Aug"); 14 | expect(getMonthAbbreviationFromNumber(9)).toEqual("Sep"); 15 | expect(getMonthAbbreviationFromNumber(10)).toEqual("Oct"); 16 | expect(getMonthAbbreviationFromNumber(11)).toEqual("Nov"); 17 | expect(getMonthAbbreviationFromNumber(12)).toEqual("Dec"); 18 | }); 19 | -------------------------------------------------------------------------------- /src/datetime/epochMicroseconds.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { epochMicroseconds } from "./epochMicroseconds.js"; 4 | 5 | test("Instant", () => { 6 | expect(epochMicroseconds(Temporal.Instant.from("1970-01-01T00:00:00.000001Z"))).toEqual(1n); 7 | expect(epochMicroseconds(Temporal.Instant.from("1969-12-31T23:59:59.999999Z"))).toEqual(-1n); 8 | }); 9 | 10 | test("ZonedDateTime", () => { 11 | expect( 12 | epochMicroseconds( 13 | Temporal.Instant.from("1970-01-01T00:00:00.000001Z").toZonedDateTimeISO("Europe/London"), 14 | ), 15 | ).toEqual(1n); 16 | expect( 17 | epochMicroseconds( 18 | Temporal.Instant.from("1969-12-31T23:59:59.999999Z").toZonedDateTimeISO("Europe/London"), 19 | ), 20 | ).toEqual(-1n); 21 | }); 22 | 23 | test("Instant with nanoseconds", () => { 24 | expect(epochMicroseconds(Temporal.Instant.from("1970-01-01T00:00:00.000001234Z"))).toEqual(1n); 25 | expect(epochMicroseconds(Temporal.Instant.from("1969-12-31T23:59:59.999999876Z"))).toEqual(-1n); 26 | }); 27 | -------------------------------------------------------------------------------- /src/datetime/endOfMonth.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the end of a month for the given datetime 7 | * @param dt datetime object which includes date info 8 | * @returns Temporal object which represents the end of a month 9 | */ 10 | export function endOfMonth< 11 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 12 | >(dt: DateTime): DateTime { 13 | if (isPlainDate(dt)) { 14 | return dt.with({ 15 | day: Number.MAX_VALUE, 16 | }) as DateTime; 17 | } 18 | const withArg = { 19 | day: Number.MAX_VALUE, 20 | hour: 23, 21 | minute: 59, 22 | second: 59, 23 | millisecond: 999, 24 | microsecond: 999, 25 | nanosecond: 999, 26 | }; 27 | if (!isZonedDateTime(dt)) { 28 | return dt.with(withArg) as DateTime; 29 | } 30 | return endOfTimeForZonedDateTime(dt, withArg) as DateTime; 31 | } 32 | -------------------------------------------------------------------------------- /src/datetime/toDateFromExactTime.test.ts: -------------------------------------------------------------------------------- 1 | import { UTCDate } from "@date-fns/utc"; 2 | import { expect, test } from "vitest"; 3 | 4 | import { toDateFromExactTime } from "./toDateFromExactTime.js"; 5 | 6 | test("Instant", () => { 7 | expect(toDateFromExactTime(Temporal.Instant.from("2024-01-01T01:23:45.678Z"))).toStrictEqual( 8 | new Date("2024-01-01T01:23:45.678Z"), 9 | ); 10 | }); 11 | 12 | test("units smaller than millisecond", () => { 13 | expect(toDateFromExactTime(Temporal.Instant.from("2024-01-01T00:00:00.0009Z"))).toStrictEqual( 14 | new Date("2024-01-01T00:00:00Z"), 15 | ); 16 | }); 17 | 18 | test("ZonedDateTime", () => { 19 | expect( 20 | toDateFromExactTime(Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]")), 21 | ).toStrictEqual(new Date("2023-12-31T15:00:00Z")); 22 | }); 23 | 24 | test("DateConstructor option", () => { 25 | const result = toDateFromExactTime( 26 | Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]"), 27 | UTCDate, 28 | ); 29 | expect(result).toStrictEqual(new UTCDate("2023-12-31T15:00:00Z")); 30 | }); 31 | -------------------------------------------------------------------------------- /src/datetime/startOfDay.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { isNativeMethod } from "./_isNativeMethod.js"; 4 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 5 | 6 | /** 7 | * Returns the start of a day for the given datetime 8 | * @param dt datetime object which includes date and time info 9 | * @returns Temporal object which represents the start of a day 10 | */ 11 | export function startOfDay( 12 | dt: DateTime, 13 | ): DateTime { 14 | const withArg = { 15 | hour: 0, 16 | minute: 0, 17 | second: 0, 18 | millisecond: 0, 19 | microsecond: 0, 20 | nanosecond: 0, 21 | }; 22 | // `startOfDay` method can return wrong result in polyfill (see https://github.com/tc39/proposal-temporal/issues/3110) 23 | return ( 24 | isZonedDateTime(dt) 25 | ? isNativeMethod(dt, "startOfDay") 26 | ? dt.startOfDay() 27 | : startOfTimeForZonedDateTime(dt, withArg) 28 | : dt.with(withArg) 29 | ) as DateTime; 30 | } 31 | -------------------------------------------------------------------------------- /src/datetime/toTemporalFromClockTime.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "../types.js"; 2 | 3 | /** 4 | * Returns Temporal instance which represents clock (local) time of given date. 5 | * @param date `Date` object 6 | * @param TemporalClass Temporal class (such as `Temporal.Plaindate`) which will be returned 7 | * @returns an instance of Temporal class specified in `temporalClass` argument, which represents the clock time of original date 8 | */ 9 | export function toTemporalFromClockTime< 10 | TemporalClassType extends 11 | | typeof Temporal.PlainDate 12 | | typeof Temporal.PlainTime 13 | | typeof Temporal.PlainDateTime 14 | | typeof Temporal.PlainYearMonth 15 | | typeof Temporal.PlainMonthDay, 16 | >(date: Date, TemporalClass: TemporalClassType): InstanceType { 17 | return TemporalClass.from({ 18 | year: date.getFullYear(), 19 | month: date.getMonth() + 1, 20 | day: date.getDate(), 21 | hour: date.getHours(), 22 | minute: date.getMinutes(), 23 | second: date.getSeconds(), 24 | millisecond: date.getMilliseconds(), 25 | }) as InstanceType; 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 fabon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/datetime/startOfYear.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isPlainYearMonth, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the start of a year for the given datetime 7 | * @param dt datetime object which includes date info 8 | * @returns Temporal object which represents the start of a year 9 | */ 10 | export function startOfYear< 11 | DateTime extends 12 | | Temporal.PlainDate 13 | | Temporal.PlainDateTime 14 | | Temporal.PlainYearMonth 15 | | Temporal.ZonedDateTime, 16 | >(dt: DateTime): DateTime { 17 | if (isPlainYearMonth(dt)) { 18 | return dt.with({ month: 1 }) as DateTime; 19 | } 20 | if (isPlainDate(dt)) { 21 | return dt.with({ month: 1, day: 1 }) as DateTime; 22 | } 23 | const withArg = { 24 | month: 1, 25 | day: 1, 26 | hour: 0, 27 | minute: 0, 28 | second: 0, 29 | millisecond: 0, 30 | microsecond: 0, 31 | nanosecond: 0, 32 | }; 33 | if (!isZonedDateTime(dt)) { 34 | return dt.with(withArg) as DateTime; 35 | } 36 | return startOfTimeForZonedDateTime(dt, withArg) as DateTime; 37 | } 38 | -------------------------------------------------------------------------------- /src/datetime/_getMonthNumberFromAbbreviation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { getMonthNumberFromAbbreviation } from "./_getMonthNumberFromAbbreviation.js"; 4 | 5 | test("getMonthNumberFromAbbreviation", () => { 6 | expect(getMonthNumberFromAbbreviation("Jan")).toEqual(1); 7 | expect(getMonthNumberFromAbbreviation("Feb")).toEqual(2); 8 | expect(getMonthNumberFromAbbreviation("Mar")).toEqual(3); 9 | expect(getMonthNumberFromAbbreviation("Apr")).toEqual(4); 10 | expect(getMonthNumberFromAbbreviation("May")).toEqual(5); 11 | expect(getMonthNumberFromAbbreviation("Jun")).toEqual(6); 12 | expect(getMonthNumberFromAbbreviation("Jul")).toEqual(7); 13 | expect(getMonthNumberFromAbbreviation("Aug")).toEqual(8); 14 | expect(getMonthNumberFromAbbreviation("Sep")).toEqual(9); 15 | expect(getMonthNumberFromAbbreviation("Oct")).toEqual(10); 16 | expect(getMonthNumberFromAbbreviation("Nov")).toEqual(11); 17 | expect(getMonthNumberFromAbbreviation("Dec")).toEqual(12); 18 | }); 19 | 20 | test("getMonthNumberFromAbbreviation with unknown month abbreviation", () => { 21 | expect(() => { 22 | getMonthNumberFromAbbreviation("Ja"); 23 | }).toThrowError(/Ja/); 24 | }); 25 | -------------------------------------------------------------------------------- /script/repl.ts: -------------------------------------------------------------------------------- 1 | import repl from "node:repl"; 2 | import { inspect } from "node:util"; 3 | 4 | import { consola } from "consola"; 5 | // @ts-ignore (type error when `dist` doesn't exist) 6 | import * as vremel from "vremel"; 7 | // @ts-ignore (type error when `dist` doesn't exist) 8 | import * as vremelDuration from "vremel/duration"; 9 | 10 | const choice = await consola.prompt("Select polyfill", { 11 | type: "select", 12 | options: ["temporal-polyfill", "@js-temporal/polyfill"], 13 | cancel: "undefined", 14 | }); 15 | 16 | if (choice === undefined) { 17 | process.exit(0); 18 | } 19 | 20 | const { Temporal } = await import(choice); 21 | const server = repl.start("> "); 22 | server.context["Temporal"] = Temporal; 23 | server.context["vremel"] = vremel; 24 | server.context["vremelDuration"] = vremelDuration; 25 | 26 | for (const type of [ 27 | Temporal.Instant, 28 | Temporal.ZonedDateTime, 29 | Temporal.PlainDateTime, 30 | Temporal.PlainDate, 31 | Temporal.PlainTime, 32 | Temporal.PlainYearMonth, 33 | Temporal.PlainMonthDay, 34 | Temporal.Duration, 35 | ]) { 36 | type.prototype[inspect.custom] = function () { 37 | return `${this[Symbol.toStringTag]}: ${this.toString()}`; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/datetime/startOfMinute.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfMinute } from "./startOfMinute.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfMinute(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:34:00"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(startOfMinute(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:34:00"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime and forward transition", () => { 18 | expect( 19 | startOfMinute(Temporal.ZonedDateTime.from("1972-01-07T00:44:59+00:00[Africa/Monrovia]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("1972-01-07T00:44:30+00:00[Africa/Monrovia]")); 21 | }); 22 | 23 | test("ZonedDateTime and backward transition", () => { 24 | expect( 25 | startOfMinute( 26 | // TODO: create ZonedDateTime directly when https://github.com/tc39/proposal-temporal/issues/3099 is fixed in polyfills 27 | Temporal.Instant.from("1952-10-15T23:59:50-11:20:00").toZonedDateTimeISO("Pacific/Niue"), 28 | ), 29 | ).toEqual(Temporal.ZonedDateTime.from("1952-10-15T23:59:00-11:19:40[Pacific/Niue]")); 30 | }); 31 | -------------------------------------------------------------------------------- /src/datetime/clamp.ts: -------------------------------------------------------------------------------- 1 | import { assertSameType, assertValidInterval } from "../assert.js"; 2 | import type { Interval, Temporal } from "../types.js"; 3 | import { compare } from "./_compare.js"; 4 | 5 | /** 6 | * Returns a datetime object clamped within the given interval. 7 | * * When the given datetime is earlier than the start of the interval, the start will be returned. 8 | * * When the given datetime is later than the end of the interval, the end will be returned. 9 | * * Otherwise the given datetime will be returned. 10 | * @param dateTime datetime object 11 | * @param interval interval 12 | * @returns clamped datetime object 13 | */ 14 | export function clamp< 15 | DateTime extends 16 | | Temporal.Instant 17 | | Temporal.ZonedDateTime 18 | | Temporal.PlainDate 19 | | Temporal.PlainTime 20 | | Temporal.PlainDateTime 21 | | Temporal.PlainYearMonth, 22 | >(dateTime: DateTime, interval: Interval): DateTime { 23 | assertValidInterval(interval); 24 | assertSameType(dateTime, interval.start); 25 | if (compare(dateTime, interval.start) === -1) { 26 | return interval.start as DateTime; 27 | } 28 | if (compare(dateTime, interval.end) === 1) { 29 | return interval.end as DateTime; 30 | } 31 | return dateTime; 32 | } 33 | -------------------------------------------------------------------------------- /src/datetime/endOfYear.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isPlainYearMonth, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | /** 6 | * Returns the end of a year for the given datetime 7 | * @param dt datetime object which includes date info 8 | * @returns Temporal object which represents the end of a year 9 | */ 10 | export function endOfYear< 11 | DateTime extends 12 | | Temporal.PlainDate 13 | | Temporal.PlainDateTime 14 | | Temporal.PlainYearMonth 15 | | Temporal.ZonedDateTime, 16 | >(dt: DateTime): DateTime { 17 | if (isPlainYearMonth(dt)) { 18 | return dt.with({ 19 | month: dt.monthsInYear, 20 | }) as DateTime; 21 | } 22 | if (isPlainDate(dt)) { 23 | return dt.with({ 24 | month: dt.monthsInYear, 25 | day: Number.MAX_VALUE, 26 | }) as DateTime; 27 | } 28 | const withArg = { 29 | month: dt.monthsInYear, 30 | day: Number.MAX_VALUE, 31 | hour: 23, 32 | minute: 59, 33 | second: 59, 34 | millisecond: 999, 35 | microsecond: 999, 36 | nanosecond: 999, 37 | }; 38 | if (!isZonedDateTime(dt)) { 39 | return dt.with(withArg) as DateTime; 40 | } 41 | return endOfTimeForZonedDateTime(dt, withArg) as DateTime; 42 | } 43 | -------------------------------------------------------------------------------- /script/build.ts: -------------------------------------------------------------------------------- 1 | import { copyFile, glob, readFile, rm } from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | import { exec } from "tinyexec"; 5 | 6 | async function clearDist() { 7 | const distPath = path.join(import.meta.dirname, "../dist"); 8 | await rm(distPath, { recursive: true, force: true }); 9 | } 10 | 11 | async function build(isDevMode: boolean) { 12 | if (!isDevMode) { 13 | await clearDist(); 14 | } 15 | 16 | // this script should be run within npm script 17 | const res = isDevMode 18 | ? await exec("tsgo", ["-p", "tsconfig.build.json", "--noCheck"]) 19 | : await exec("tsc", ["-p", "tsconfig.build.json"]); 20 | if (res.exitCode !== 0) { 21 | console.log(res.stdout); 22 | process.exit(res.exitCode ?? 1); 23 | } 24 | 25 | await copyFile("src/temporal.d.ts", "dist/temporal.d.ts"); 26 | 27 | // Remove empty declaration files 28 | for await (const dtsFile of glob("dist/**/*.d.ts")) { 29 | const source = await readFile(dtsFile, "utf-8"); 30 | const match = /^export {};\n\/\/# sourceMappingURL=(.+)$/.exec(source); 31 | if (match) { 32 | const sourcemapPath = path.resolve(path.dirname(dtsFile), match[1]!); 33 | await rm(dtsFile); 34 | await rm(sourcemapPath); 35 | } 36 | } 37 | } 38 | 39 | const isDevMode = process.argv.includes("--dev"); 40 | await build(isDevMode); 41 | -------------------------------------------------------------------------------- /src/datetime/_getDayOfWeekFromYmd.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { modifyTimeZone } from "../_test/modifyTimeZone.js"; 4 | import { getDayOfWeekFromYmd } from "./_getDayOfWeekFromYmd.js"; 5 | 6 | test("getDayOfWeekFromYmd", () => { 7 | expect(getDayOfWeekFromYmd(2024, 1, 1)).toEqual(1); 8 | expect(getDayOfWeekFromYmd(2024, 1, 2)).toEqual(2); 9 | expect(getDayOfWeekFromYmd(2024, 1, 3)).toEqual(3); 10 | expect(getDayOfWeekFromYmd(2024, 1, 4)).toEqual(4); 11 | expect(getDayOfWeekFromYmd(2024, 1, 5)).toEqual(5); 12 | expect(getDayOfWeekFromYmd(2024, 1, 6)).toEqual(6); 13 | expect(getDayOfWeekFromYmd(2024, 1, 7)).toEqual(7); 14 | }); 15 | 16 | test("result of getDayOfWeekFromYmd should match to Temporal's dayOfWeek", () => { 17 | expect(getDayOfWeekFromYmd(2024, 1, 7)).toEqual(Temporal.PlainDate.from("2024-01-07").dayOfWeek); 18 | }); 19 | 20 | test("2-digit year", () => { 21 | expect(getDayOfWeekFromYmd(0, 1, 1)).toEqual(6); 22 | expect(getDayOfWeekFromYmd(99, 12, 31)).toEqual(4); 23 | }); 24 | 25 | test("getDayOfWeekFromYmd with a day which doesn't exist in local time zone", () => { 26 | using _modifier = modifyTimeZone("Pacific/Apia"); 27 | // 2011/12/30 was skipped in Pacific/Apia due to offset change (UTC-11:00 -> UTC+13:00) 28 | expect(getDayOfWeekFromYmd(2011, 12, 30)).toEqual(5); 29 | }); 30 | -------------------------------------------------------------------------------- /src/datetime/formatRfc7231.ts: -------------------------------------------------------------------------------- 1 | import { isInstant } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { formatHmsIso } from "./_formatHmsIso.js"; 4 | import { getDayOfWeekAbbreviationFromNumber } from "./_getDayOfWeekAbbreviationFromNumber.js"; 5 | import { getMonthAbbreviationFromNumber } from "./_getMonthAbbreviationFromNumber.js"; 6 | import { padLeadingZeros } from "./_padLeadingZeros.js"; 7 | 8 | /** 9 | * Returns a string in RFC 7231's format which represents an exact time of given temporal object. 10 | * Units smaller than second are ignored (rounded down). 11 | * 12 | * @param dt temporal object which includes exact time info (`Instant` and `ZonedDateTime`) 13 | * @returns a string formatted according to RFC 7231 14 | */ 15 | export function formatRfc7231(dt: Temporal.Instant | Temporal.ZonedDateTime): string { 16 | // timeZone: 'UTC', calendar: 'iso8601' 17 | const zdt = (isInstant(dt) ? dt : dt.toInstant()).toZonedDateTimeISO("UTC"); 18 | const dayOfWeek = getDayOfWeekAbbreviationFromNumber(zdt.dayOfWeek); 19 | if (zdt.year < 0 || zdt.year > 9999) { 20 | throw new Error(`RFC 7231 format can't represent year ${zdt.year}`); 21 | } 22 | const year = padLeadingZeros(zdt.year, 4); 23 | const day = padLeadingZeros(zdt.day, 2); 24 | const month = getMonthAbbreviationFromNumber(zdt.month); 25 | return `${dayOfWeek}, ${day} ${month} ${year} ${formatHmsIso(zdt.hour, zdt.minute, zdt.second)} GMT`; 26 | } 27 | -------------------------------------------------------------------------------- /src/datetime/isSameWeek.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isSameWeek } from "./isSameWeek.js"; 4 | 5 | test.for([ 6 | ["2025-01-05", "2025-01-06", 1, false], 7 | ["2025-01-05", "2025-01-06", 7, true], 8 | ["2024-12-30", "2025-01-05", 1, true], 9 | ["2024-12-30", "2025-01-05", 7, false], 10 | ] as [string, string, number, boolean][])( 11 | "PlainDate (%s and %s, firstDayOfWeek: %i)", 12 | ([date1, date2, firstDayOfWeek, expected]) => { 13 | expect( 14 | isSameWeek(Temporal.PlainDate.from(date1), Temporal.PlainDate.from(date2), { 15 | firstDayOfWeek, 16 | }), 17 | ).toEqual(expected); 18 | }, 19 | ); 20 | 21 | test("PlainDate with different calendar", () => { 22 | expect(() => { 23 | isSameWeek( 24 | Temporal.PlainDate.from("2025-01-01"), 25 | Temporal.PlainDate.from("2025-01-01[u-ca=hebrew]"), 26 | { firstDayOfWeek: 7 }, 27 | ); 28 | }).toThrow(); 29 | }); 30 | 31 | test("PlainDateTime", () => { 32 | expect( 33 | isSameWeek( 34 | Temporal.PlainDateTime.from("2025-01-01T00:00:00"), 35 | Temporal.PlainDateTime.from("2025-01-02T12:00:00"), 36 | { firstDayOfWeek: 7 }, 37 | ), 38 | ).toEqual(true); 39 | }); 40 | 41 | test("ZonedDateTime", () => { 42 | expect( 43 | isSameWeek( 44 | Temporal.ZonedDateTime.from("2025-01-12T23:00:00+09:00[Asia/Tokyo]"), 45 | Temporal.ZonedDateTime.from("2025-01-06T00:00:00-08:00[America/Los_Angeles]"), 46 | { firstDayOfWeek: 1 }, 47 | ), 48 | ).toEqual(true); 49 | }); 50 | -------------------------------------------------------------------------------- /src/datetime/endOfMinute.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfMinute } from "./endOfMinute.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfMinute(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:34:59.999999999"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(endOfMinute(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:34:59.999999999"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime without offset transition", () => { 18 | expect( 19 | endOfMinute(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T12:34:59.999999999+09:00[Asia/Tokyo]")); 21 | }); 22 | 23 | test("ZonedDateTime and forward transition", () => { 24 | expect( 25 | endOfMinute(Temporal.ZonedDateTime.from("1911-12-31T23:23:00-00:37[Europe/Lisbon]")), 26 | ).toEqual(Temporal.ZonedDateTime.from("1911-12-31T23:23:14.999999999-00:37[Europe/Lisbon]")); 27 | }); 28 | 29 | test("ZonedDateTime and backward transition", () => { 30 | expect( 31 | endOfMinute(Temporal.ZonedDateTime.from("1952-10-15T23:59:00-11:19:40[Pacific/Niue]")), 32 | ).toEqual( 33 | // TODO: create ZonedDateTime directly when https://github.com/tc39/proposal-temporal/issues/3099 is fixed in polyfills 34 | Temporal.Instant.from("1952-10-15T23:59:59.999999999-11:20:00").toZonedDateTimeISO( 35 | "Pacific/Niue", 36 | ), 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /src/datetime/endOfDay.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfDay } from "./endOfDay.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfDay(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T23:59:59.999999999"), 8 | ); 9 | }); 10 | 11 | test("ZonedDateTime without offset transition", () => { 12 | expect( 13 | endOfDay(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 14 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T23:59:59.999999999+09:00[Asia/Tokyo]")); 15 | }); 16 | 17 | test("ZonedDateTime and backward transition", () => { 18 | expect(endOfDay(Temporal.ZonedDateTime.from("2024-10-27T00:00:00+01:00[Europe/London]"))).toEqual( 19 | Temporal.ZonedDateTime.from("2024-10-27T23:59:59.999999999+00:00[Europe/London]"), 20 | ); 21 | expect( 22 | endOfDay(Temporal.ZonedDateTime.from("2010-11-06T12:00:00-02:30[America/St_Johns]")), 23 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-06T23:59:59.999999999-03:30[America/St_Johns]")); 24 | }); 25 | 26 | test("ZonedDateTime and forward transition", () => { 27 | expect(endOfDay(Temporal.ZonedDateTime.from("2024-03-31T00:30:00+00:00[Europe/London]"))).toEqual( 28 | Temporal.ZonedDateTime.from("2024-03-31T23:59:59.999999999+01:00[Europe/London]"), 29 | ); 30 | expect( 31 | endOfDay(Temporal.ZonedDateTime.from("1919-03-30T12:00:00-05:00[America/Toronto]")), 32 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-30T23:29:59.999999999-05:00[America/Toronto]")); 33 | }); 34 | -------------------------------------------------------------------------------- /script/check-build.ts: -------------------------------------------------------------------------------- 1 | import { readdir, rm } from "node:fs/promises"; 2 | import { createRequire } from "node:module"; 3 | import path from "node:path"; 4 | 5 | import { consola } from "consola"; 6 | 7 | const require = createRequire(import.meta.url); 8 | const srcPath = path.join(import.meta.dirname, "../src"); 9 | 10 | async function listFns(dirname: string) { 11 | const files = await readdir(path.join(srcPath, dirname)); 12 | return files 13 | .filter( 14 | (d) => d.endsWith(".ts") && !d.endsWith(".test.ts") && !d.startsWith("_") && d !== "index.ts", 15 | ) 16 | .map((f) => f.replace(/\.ts$/, "")); 17 | } 18 | 19 | async function check(dirname: string, moduleName: string) { 20 | let fail = false; 21 | const fns = await listFns(dirname); 22 | const mod1 = await import(moduleName); 23 | const mod2 = require(moduleName); 24 | 25 | for (const mod of [mod1, mod2]) { 26 | for (const fnName of fns) { 27 | const fn = mod[fnName]; 28 | if (typeof fn !== "function") { 29 | consola.error(`Missing function in ${dirname}/index.ts: ${fnName}`); 30 | fail = true; 31 | continue; 32 | } 33 | if (fn.name !== fnName) { 34 | consola.error( 35 | `Incorrect function name in ${dirname}/index.ts: expected ${fnName}, actual ${fn.name}`, 36 | ); 37 | } 38 | } 39 | } 40 | if (fail) { 41 | throw new Error(); 42 | } 43 | } 44 | 45 | try { 46 | await check("datetime", "vremel"); 47 | await check("duration", "vremel/duration"); 48 | } catch (e) { 49 | consola.error(e); 50 | await rm(path.join(srcPath, "../dist"), { recursive: true, force: true }); 51 | process.exit(1); 52 | } 53 | -------------------------------------------------------------------------------- /src/datetime/toTemporalFromClockTime.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { toTemporalFromClockTime } from "./toTemporalFromClockTime.js"; 4 | 5 | test("to PlainDate", () => { 6 | expect(toTemporalFromClockTime(new Date("2024-01-01T00:00:00"), Temporal.PlainDate)).toEqual( 7 | Temporal.PlainDate.from("2024-01-01"), 8 | ); 9 | }); 10 | 11 | test("to PlainTime", () => { 12 | expect(toTemporalFromClockTime(new Date("2024-01-01T00:00:00"), Temporal.PlainTime)).toEqual( 13 | Temporal.PlainTime.from("00:00:00"), 14 | ); 15 | }); 16 | 17 | test("to PlainDateTime", () => { 18 | expect(toTemporalFromClockTime(new Date("2024-01-01T00:00:00"), Temporal.PlainDateTime)).toEqual( 19 | Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 20 | ); 21 | }); 22 | 23 | test("to PlainYearMonth", () => { 24 | expect(toTemporalFromClockTime(new Date("2024-01-01T00:00:00"), Temporal.PlainYearMonth)).toEqual( 25 | Temporal.PlainYearMonth.from("2024-01"), 26 | ); 27 | }); 28 | 29 | test("to PlainMonthDay", () => { 30 | expect(toTemporalFromClockTime(new Date("2024-01-01T00:00:00"), Temporal.PlainMonthDay)).toEqual( 31 | Temporal.PlainMonthDay.from("01-01"), 32 | ); 33 | }); 34 | 35 | test.for([ 36 | "0111-01-01T00:00:00", 37 | "0011-01-01T00:00:00", 38 | "0001-01-01T00:00:00", 39 | "+010000-01-01T00:00:00", 40 | "+100000-01-01T00:00:00", 41 | "-000001-01-01T00:00:00", 42 | "-200000-01-01T00:00:00", 43 | ])("extreme range of year (%s)", (date) => { 44 | expect(toTemporalFromClockTime(new Date(date), Temporal.PlainDateTime)).toEqual( 45 | Temporal.PlainDateTime.from(date), 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish the package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | jsr: 11 | runs-on: ubuntu-latest 12 | concurrency: 13 | group: ${{ github.workflow }}-jsr 14 | permissions: 15 | contents: read 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 19 | with: 20 | persist-credentials: false 21 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 22 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 23 | with: 24 | node-version: 24 25 | - run: pnpm install 26 | - name: Publish package 27 | run: pnpm dlx jsr publish 28 | npm: 29 | runs-on: ubuntu-latest 30 | concurrency: 31 | group: ${{ github.workflow }}-npm 32 | permissions: 33 | contents: read 34 | id-token: write 35 | steps: 36 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 37 | with: 38 | persist-credentials: false 39 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 40 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 41 | with: 42 | node-version: 24 43 | registry-url: https://registry.npmjs.org 44 | - run: pnpm install 45 | - name: Build 46 | run: pnpm run build 47 | - name: Publish package 48 | # https://github.com/pnpm/pnpm/issues/5894 49 | run: pnpm publish --no-git-checks 50 | -------------------------------------------------------------------------------- /src/datetime/_getTimeZoneTransitionBetween.ts: -------------------------------------------------------------------------------- 1 | import { getConstructor } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { isNativeMethod } from "./_isNativeMethod.js"; 4 | 5 | /** 6 | * @internal 7 | * `getTimeZoneTransition` method can return inaccurate result in polyfill (see https://github.com/tc39/proposal-temporal/issues/3110). 8 | * However, if it is guaranteed that only 1 time zone transition occurs within the given range, 9 | * we can make sure to return a correct result using binary search. 10 | * 11 | */ 12 | export function getTimeZoneTransitionBetween( 13 | start: Temporal.ZonedDateTime, 14 | end: Temporal.ZonedDateTime, 15 | ): Temporal.ZonedDateTime { 16 | if (start.offsetNanoseconds === end.offsetNanoseconds) { 17 | throw new Error("Unknown error"); 18 | } 19 | if (isNativeMethod(start, "getTimeZoneTransition")) { 20 | const transition = start.getTimeZoneTransition("next"); 21 | if (transition === null) { 22 | throw new Error("Unknown error"); 23 | } 24 | return transition; 25 | } 26 | const Instant = getConstructor(start.toInstant()); 27 | // assumption: no sub-second offset nor offset transition in fractional seconds 28 | let left = Math.floor(start.epochMilliseconds / 1000); 29 | let right = Math.floor(end.epochMilliseconds / 1000); 30 | while (right - left > 1) { 31 | const mid = Math.floor((left + right) / 2); 32 | const midOffset = Instant.fromEpochMilliseconds(mid * 1000).toZonedDateTimeISO( 33 | start, 34 | ).offsetNanoseconds; 35 | if (midOffset === start.offsetNanoseconds) { 36 | left = mid; 37 | } else { 38 | right = mid; 39 | } 40 | } 41 | return Instant.fromEpochMilliseconds(right * 1000) 42 | .toZonedDateTimeISO(start) 43 | .withCalendar(start); 44 | } 45 | -------------------------------------------------------------------------------- /src/datetime/areIntervalsOverlapping.ts: -------------------------------------------------------------------------------- 1 | import { assertSameType, assertValidInterval } from "../assert.js"; 2 | import type { Interval } from "../types.js"; 3 | import { compare } from "./_compare.js"; 4 | 5 | export interface AreIntervalsOverlappingOptions { 6 | /** 7 | * Whether the comparison is inclusive or not. Default is `true`. 8 | */ 9 | inclusive?: boolean; 10 | } 11 | 12 | /** 13 | * Checks if the given two intervals are overlapping. 14 | * By default, it returns `true` if the end of one interval is exactly same time to the start of the other interval. 15 | * You can pass the `inclusive` option to change this behavior. 16 | * 17 | * @example 18 | * ```typescript 19 | * const interval1 = { 20 | * start: Temporal.PlainTime.from("00:00:00"), 21 | * end: Temporal.PlainTime.from("08:00:00"), 22 | * }; 23 | * const interval2 = { 24 | * start: Temporal.PlainTime.from("08:00:00"), 25 | * end: Temporal.PlainTime.from("16:00:00"), 26 | * }; 27 | * areIntervalsOverlapping(interval1, interval2); // true 28 | * areIntervalsOverlapping(interval1, interval2, { inclusive: false }); // false 29 | * ``` 30 | * 31 | * @param interval1 32 | * @param interval2 33 | * @param options 34 | * @returns Whether two intervals are overlapping 35 | */ 36 | export function areIntervalsOverlapping( 37 | interval1: Interval, 38 | interval2: Interval, 39 | options?: AreIntervalsOverlappingOptions, 40 | ): boolean { 41 | assertValidInterval(interval1); 42 | assertValidInterval(interval2); 43 | assertSameType(interval1.start, interval2.start); 44 | const inclusive = options?.inclusive ?? true; 45 | const threshould = inclusive ? 0 : 1; 46 | return ( 47 | compare(interval1.end, interval2.start) >= threshould && 48 | compare(interval2.end, interval1.start) >= threshould 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Temporal } from "./temporal.d.ts"; 2 | 3 | export type { Temporal }; 4 | 5 | /** @internal */ 6 | export type DateTimeType = 7 | | Temporal.Instant 8 | | Temporal.ZonedDateTime 9 | | Temporal.PlainDate 10 | | Temporal.PlainTime 11 | | Temporal.PlainDateTime 12 | | Temporal.PlainYearMonth 13 | | Temporal.PlainMonthDay; 14 | /** @internal */ 15 | export type TemporalType = DateTimeType | Temporal.Duration; 16 | /** @internal */ 17 | export type ComparableDateTimeType = 18 | | Temporal.Instant 19 | | Temporal.ZonedDateTime 20 | | Temporal.PlainDate 21 | | Temporal.PlainTime 22 | | Temporal.PlainDateTime 23 | | Temporal.PlainYearMonth; 24 | 25 | /** 26 | * `Date` or extended `Date` 27 | */ 28 | export interface GenericDateConstructor { 29 | new (value?: Date | number | string): DateType; 30 | new ( 31 | year: number, 32 | month: number, 33 | date?: number, 34 | hours?: number, 35 | minutes?: number, 36 | seconds?: number, 37 | ms?: number, 38 | ): DateType; 39 | } 40 | 41 | /** 42 | * Similar to `Array`, but with union distribution; `ArrayOf` is `A[] | B[]`, not `(A|B)[]`. 43 | */ 44 | export type ArrayOf = T extends unknown ? T[] : never; 45 | 46 | /** 47 | * The object which represents an interval. `start` and `end` should have the same type. 48 | */ 49 | export type Interval< 50 | DateTime = 51 | | Temporal.Instant 52 | | Temporal.ZonedDateTime 53 | | Temporal.PlainDate 54 | | Temporal.PlainTime 55 | | Temporal.PlainDateTime 56 | | Temporal.PlainYearMonth, 57 | > = DateTime extends 58 | | Temporal.Instant 59 | | Temporal.ZonedDateTime 60 | | Temporal.PlainDate 61 | | Temporal.PlainTime 62 | | Temporal.PlainDateTime 63 | | Temporal.PlainYearMonth 64 | ? { start: DateTime; end: DateTime } 65 | : never; 66 | -------------------------------------------------------------------------------- /src/datetime/isSameWeek.ts: -------------------------------------------------------------------------------- 1 | import { assertSameType } from "../assert.js"; 2 | import { isPlainDate } from "../type-utils.js"; 3 | import type { Temporal } from "../types.js"; 4 | import { startOfWeek } from "./startOfWeek.js"; 5 | 6 | export interface IsSameWeekOptions { 7 | /** 8 | * First day of the week. 9 | * For example, in ISO calendar Monday is `1`, Sunday is `7`. 10 | */ 11 | firstDayOfWeek: number; 12 | } 13 | 14 | function toPlainDate( 15 | dt: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 16 | ): Temporal.PlainDate { 17 | return isPlainDate(dt) ? dt : dt.toPlainDate(); 18 | } 19 | 20 | /** 21 | * Checks whether the two Temporal objects are in the same week. 22 | * 23 | * 'same week' is ambiguous and locale-dependent, 24 | * so `firstDayOfWeek` option is required. 25 | * 26 | * This function supports a calendar with a fixed `daysInWeek`, 27 | * even if the week contains more or less than 7 days. 28 | * But it doesn't support a calendar which lacks a fixed number of days. 29 | * 30 | * @param dt1 first date time object 31 | * @param dt2 second date time object 32 | */ 33 | export function isSameWeek< 34 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 35 | >(dt1: DateTime, dt2: DateTime, options: IsSameWeekOptions): boolean { 36 | assertSameType(dt1, dt2); 37 | const firstDayOfWeek = options.firstDayOfWeek; 38 | if (!Number.isInteger(firstDayOfWeek) || firstDayOfWeek < 1 || firstDayOfWeek > dt1.daysInWeek) { 39 | throw new Error(`${firstDayOfWeek} isn't a valid day of week`); 40 | } 41 | if (dt1.calendarId !== dt2.calendarId) { 42 | throw new Error(`Calendar mismatch: ${dt1.calendarId} and ${dt2.calendarId}`); 43 | } 44 | 45 | const pdt1 = toPlainDate(dt1); 46 | const pdt2 = toPlainDate(dt2); 47 | 48 | return startOfWeek(pdt1, { firstDayOfWeek }).equals(startOfWeek(pdt2, { firstDayOfWeek })); 49 | } 50 | -------------------------------------------------------------------------------- /src/datetime/latest.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { latest } from "./latest.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1700000000, 1720000000, 1600000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | expect(latest(target)).toBe(target[1]); 10 | }); 11 | test("ZonedDateTime", () => { 12 | const target = [ 13 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 14 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 15 | "2024-01-01T00:00:00-05:00[America/Toronto]", 16 | ].map((t) => Temporal.ZonedDateTime.from(t)); 17 | expect(latest(target)).toBe(target[2]); 18 | }); 19 | test("PlainDate", () => { 20 | const target = ["2024-01-01[u-ca=hebrew]", "2024-01-02", "2023-12-23"].map((t) => 21 | Temporal.PlainDate.from(t), 22 | ); 23 | expect(latest(target)).toBe(target[1]); 24 | }); 25 | test("PlainTime", () => { 26 | const target = ["03:00:00", "06:00:00", "23:45:00"].map((t) => Temporal.PlainTime.from(t)); 27 | expect(latest(target)).toBe(target[2]); 28 | }); 29 | test("PlainDateTime", () => { 30 | const target = [ 31 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 32 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 33 | "2024-01-01T00:00:00-05:00[America/Toronto]", 34 | ].map((t) => Temporal.PlainDateTime.from(t)); 35 | expect(latest(target)).toBe(target[0]); 36 | }); 37 | test("PlainYearMonth", () => { 38 | const target = ["2023-12", "2024-01", "2023-11"].map((t) => Temporal.PlainYearMonth.from(t)); 39 | expect(latest(target)).toBe(target[1]); 40 | }); 41 | test("PlainMonthDay", () => { 42 | expect(() => { 43 | // @ts-expect-error 44 | latest([Temporal.PlainMonthDay.from("12-03")]); 45 | }).toThrow(); 46 | }); 47 | 48 | test("Typecheck", () => { 49 | expect(() => { 50 | // @ts-expect-error 51 | latest([ 52 | Temporal.Now.instant(), 53 | Temporal.Now.zonedDateTimeISO(), 54 | ]); 55 | }).toThrow(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/datetime/earliest.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { earliest } from "./earliest.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1700000000, 1720000000, 1600000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | expect(earliest(target)).toBe(target[2]); 10 | }); 11 | test("ZonedDateTime", () => { 12 | const target = [ 13 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 14 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 15 | "2024-01-01T00:00:00-05:00[America/Toronto]", 16 | ].map((t) => Temporal.ZonedDateTime.from(t)); 17 | expect(earliest(target)).toBe(target[0]); 18 | }); 19 | test("PlainDate", () => { 20 | const target = ["2024-01-01[u-ca=hebrew]", "2024-01-02", "2023-12-23"].map((t) => 21 | Temporal.PlainDate.from(t), 22 | ); 23 | expect(earliest(target)).toBe(target[2]); 24 | }); 25 | test("PlainTime", () => { 26 | const target = ["03:00:00", "06:00:00", "23:45:00"].map((t) => Temporal.PlainTime.from(t)); 27 | expect(earliest(target)).toBe(target[0]); 28 | }); 29 | test("PlainDateTime", () => { 30 | const target = [ 31 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 32 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 33 | "2024-01-01T00:00:00-05:00[America/Toronto]", 34 | ].map((t) => Temporal.PlainDateTime.from(t)); 35 | expect(earliest(target)).toBe(target[2]); 36 | }); 37 | test("PlainYearMonth", () => { 38 | const target = ["2023-12", "2024-01", "2023-11"].map((t) => Temporal.PlainYearMonth.from(t)); 39 | expect(earliest(target)).toBe(target[2]); 40 | }); 41 | test("PlainMonthDay", () => { 42 | expect(() => { 43 | // @ts-expect-error 44 | earliest([Temporal.PlainMonthDay.from("12-03")]); 45 | }).toThrow(); 46 | }); 47 | 48 | test("Typecheck", () => { 49 | expect(() => { 50 | // @ts-expect-error 51 | earliest([ 52 | Temporal.Now.instant(), 53 | Temporal.Now.zonedDateTimeISO(), 54 | ]); 55 | }).toThrow(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/datetime/endOfHour.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfHour } from "./endOfHour.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfHour(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:59:59.999999999"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(endOfHour(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:59:59.999999999"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime without offset transition", () => { 18 | expect( 19 | endOfHour(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T12:59:59.999999999+09:00[Asia/Tokyo]")); 21 | }); 22 | 23 | test("ZonedDateTime and backward transition", () => { 24 | expect( 25 | endOfHour(Temporal.ZonedDateTime.from("2023-04-02T01:00:00+11:00[Australia/Lord_Howe]")), 26 | ).toEqual( 27 | Temporal.ZonedDateTime.from("2023-04-02T01:59:59.999999999+10:30[Australia/Lord_Howe]"), 28 | ); 29 | expect(endOfHour(Temporal.ZonedDateTime.from("2014-10-26T01:00:00+10:00[Asia/Chita]"))).toEqual( 30 | Temporal.ZonedDateTime.from("2014-10-26T01:59:59.999999999+08:00[Asia/Chita]"), 31 | ); 32 | expect( 33 | endOfHour(Temporal.ZonedDateTime.from("2010-11-06T23:30:00-02:30[America/St_Johns]")), 34 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-06T23:59:59.999999999-03:30[America/St_Johns]")); 35 | }); 36 | 37 | test("ZonedDateTime and forward transition", () => { 38 | expect( 39 | endOfHour(Temporal.ZonedDateTime.from("1916-07-28T00:00:59.999999999+01:34:52[Europe/Athens]")), 40 | ).toEqual(Temporal.ZonedDateTime.from("1916-07-28T00:59:59.999999999+02:00[Europe/Athens]")); 41 | expect( 42 | endOfHour(Temporal.ZonedDateTime.from("1919-03-30T23:10:00-05:00[America/Toronto]")), 43 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-30T23:29:59.999999999-05:00[America/Toronto]")); 44 | }); 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test-node: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [20, 22, 24, 25] 14 | steps: 15 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 16 | with: 17 | persist-credentials: false 18 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 19 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: pnpm 23 | - run: pnpm install 24 | - name: Test 25 | run: pnpm test-ci 26 | test-bun: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 30 | with: 31 | persist-credentials: false 32 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 33 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 34 | with: 35 | node-version: 24 36 | cache: pnpm 37 | - run: pnpm install 38 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 39 | with: 40 | bun-version: latest 41 | - name: Test 42 | run: bun --bun run test-ci 43 | test-firefox: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 47 | with: 48 | persist-credentials: false 49 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 50 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 51 | with: 52 | node-version: 24 53 | cache: pnpm 54 | - run: pnpm install 55 | - name: Test 56 | run: pnpm test-firefox 57 | -------------------------------------------------------------------------------- /src/datetime/compareAsc.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { compareAsc } from "./compareAsc.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1600000000, 1700000000, 1720000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | expect([...target].sort(compareAsc)).toEqual(target); 10 | }); 11 | test("ZonedDateTime", () => { 12 | const target = [ 13 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 14 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 15 | "2024-01-01T00:00:00-05:00[America/Toronto]", 16 | ].map((t) => Temporal.ZonedDateTime.from(t)); 17 | expect([...target].sort(compareAsc)).toEqual(target); 18 | }); 19 | test("PlainDate", () => { 20 | const target = ["2023-12-23", "2024-01-01[u-ca=hebrew]", "2024-01-02"].map((t) => 21 | Temporal.PlainDate.from(t), 22 | ); 23 | expect([...target].sort(compareAsc)).toEqual(target); 24 | }); 25 | test("PlainTime", () => { 26 | const target = ["03:00:00", "06:00:00", "23:45:00"].map((t) => Temporal.PlainTime.from(t)); 27 | expect([...target].sort(compareAsc)).toEqual(target); 28 | }); 29 | test("PlainDateTime", () => { 30 | const target = [ 31 | "2024-01-01T00:00:00-05:00[America/Toronto]", 32 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 33 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 34 | ].map((t) => Temporal.PlainDateTime.from(t)); 35 | expect([...target].sort(compareAsc)).toEqual(target); 36 | }); 37 | test("PlainYearMonth", () => { 38 | const target = ["2023-11", "2023-12", "2024-01"].map((t) => Temporal.PlainYearMonth.from(t)); 39 | expect([...target].sort(compareAsc)).toEqual(target); 40 | }); 41 | test("PlainMonthDay", () => { 42 | expect(() => { 43 | compareAsc( 44 | // @ts-expect-error 45 | Temporal.PlainMonthDay.from("12-03"), 46 | Temporal.PlainMonthDay.from("12-04"), 47 | ); 48 | }).toThrow(); 49 | }); 50 | 51 | test("Typecheck", () => { 52 | expect(() => { 53 | // @ts-expect-error 54 | compareAsc(Temporal.Now.instant(), Temporal.Now.zonedDateTimeISO()); 55 | }).toThrow(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/datetime/compareDesc.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { compareDesc } from "./compareDesc.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1720000000, 1700000000, 1600000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | expect([...target].sort(compareDesc)).toEqual(target); 10 | }); 11 | test("ZonedDateTime", () => { 12 | const target = [ 13 | "2024-01-01T00:00:00-05:00[America/Toronto]", 14 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 15 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 16 | ].map((t) => Temporal.ZonedDateTime.from(t)); 17 | expect([...target].sort(compareDesc)).toEqual(target); 18 | }); 19 | test("PlainDate", () => { 20 | const target = ["2024-01-02", "2024-01-01[u-ca=hebrew]", "2023-12-23"].map((t) => 21 | Temporal.PlainDate.from(t), 22 | ); 23 | expect([...target].sort(compareDesc)).toEqual(target); 24 | }); 25 | test("PlainTime", () => { 26 | const target = ["23:45:00", "06:00:00", "03:00:00"].map((t) => Temporal.PlainTime.from(t)); 27 | expect([...target].sort(compareDesc)).toEqual(target); 28 | }); 29 | test("PlainDateTime", () => { 30 | const target = [ 31 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 32 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 33 | "2024-01-01T00:00:00-05:00[America/Toronto]", 34 | ].map((t) => Temporal.PlainDateTime.from(t)); 35 | expect([...target].sort(compareDesc)).toEqual(target); 36 | }); 37 | test("PlainYearMonth", () => { 38 | const target = ["2024-01", "2023-12", "2023-11"].map((t) => Temporal.PlainYearMonth.from(t)); 39 | expect([...target].sort(compareDesc)).toEqual(target); 40 | }); 41 | test("PlainMonthDay", () => { 42 | expect(() => { 43 | compareDesc( 44 | // @ts-expect-error 45 | Temporal.PlainMonthDay.from("12-03"), 46 | Temporal.PlainMonthDay.from("12-04"), 47 | ); 48 | }).toThrow(); 49 | }); 50 | 51 | test("Typecheck", () => { 52 | expect(() => { 53 | // @ts-expect-error 54 | compareDesc(Temporal.Now.instant(), Temporal.Now.zonedDateTimeISO()); 55 | }).toThrow(); 56 | }); 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vremel 2 | 3 | [![npm](https://img.shields.io/npm/v/vremel)](https://www.npmjs.com/package/vremel) [![JSR](https://jsr.io/badges/@fabon/vremel)](https://jsr.io/@fabon/vremel) 4 | 5 | JavaScript date utility library for [Temporal API](https://tc39.es/proposal-temporal/docs/) inspired by [date-fns](https://date-fns.org/). 6 | 7 | - Contains only pure functions, supports tree-shaking by default. 8 | - Supports every types of Temporal API (`Instant`, `ZonedDateTime`, `PlainDate`...) with strict TypeScript definition. 9 | - Handles timezones and calendars strictly. 10 | - Works fine with any polyfills and native implementations. You don't have to even load a polyfill globally. 11 | 12 | ## Install 13 | 14 | ```shell 15 | npm install vremel 16 | # or from JSR 17 | deno add @fabon/vremel 18 | ``` 19 | 20 | This package is ESM-only. 21 | 22 | ## Usage 23 | 24 | ```typescript 25 | import { compareDesc } from "vremel"; 26 | import { isEqual } from "vremel/duration"; // utility functions for Temporal.Duration 27 | 28 | [ 29 | Temporal.PlainDate.from("2024-01-01"), 30 | Temporal.PlainDate.from("2024-02-01"), 31 | Temporal.PlainDate.from("2023-11-30"), 32 | ] 33 | .sort(compareDesc) 34 | .map((d) => d.toString()); // [ '2024-02-01', '2024-01-01', '2023-11-30' ] 35 | 36 | isEqual( 37 | Temporal.Duration.from({ hours: 3 }), 38 | Temporal.Duration.from({ hours: 3 }), 39 | ); // true 40 | ``` 41 | 42 | `vremel` works fine with any polyfills. Also it works even if `Temporal` doesn't exist in the global scope. 43 | 44 | ```typescript 45 | import { Temporal } from "temporal-polyfill"; 46 | // or 47 | import { Temporal } from "@js-temporal/polyfill"; 48 | import { isAfter } from "vremel"; 49 | 50 | isAfter( 51 | Temporal.PlainDate.from("2024-01-01"), 52 | Temporal.PlainDate.from("2024-02-01"), 53 | ); // false 54 | ``` 55 | 56 | ## Polyfill Support 57 | 58 | This package only supports latest `Temporal` polyfills following the latest spec: 59 | 60 | - `temporal-polyfill`: `0.3.0` or above 61 | - `@js-temporal/polyfill`: `0.5.0` or above 62 | 63 | ## Docs 64 | 65 | - [API docs](https://jsr.io/@fabon/vremel/doc) 66 | -------------------------------------------------------------------------------- /src/datetime/startOfMonth.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfMonth } from "./startOfMonth.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfMonth(Temporal.PlainDateTime.from("2024-01-23T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 8 | ); 9 | }); 10 | 11 | test("PlainDate", () => { 12 | expect(startOfMonth(Temporal.PlainDate.from("2024-01-23"))).toEqual( 13 | Temporal.PlainDate.from("2024-01-01"), 14 | ); 15 | }); 16 | 17 | test("PlainDate with non-ISO calendar", () => { 18 | expect( 19 | // 19 Adar I 5784 20 | startOfMonth(Temporal.PlainDate.from("2024-02-28[u-ca=hebrew]")), 21 | ).toEqual(Temporal.PlainDate.from("2024-02-10[u-ca=hebrew]")); 22 | }); 23 | 24 | test("ZonedDateTime without offset transition", () => { 25 | expect( 26 | startOfMonth(Temporal.ZonedDateTime.from("2024-03-21T01:23:45+09:00[Asia/Tokyo]")), 27 | ).toEqual(Temporal.ZonedDateTime.from("2024-03-01T00:00:00+09:00[Asia/Tokyo]")); 28 | }); 29 | 30 | test("ZonedDateTime and forward transition", () => { 31 | expect( 32 | startOfMonth(Temporal.ZonedDateTime.from("2004-01-01T12:00:00+10:00[Asia/Khandyga]")), 33 | ).toEqual(Temporal.ZonedDateTime.from("2004-01-01T01:00:00+10:00[Asia/Khandyga]")); 34 | expect( 35 | startOfMonth( 36 | Temporal.ZonedDateTime.from( 37 | // 17 Nisan 5761 38 | "2001-04-10T00:00:00+00:00[Atlantic/Azores][u-ca=hebrew]", 39 | ), 40 | ), 41 | ).toEqual( 42 | Temporal.ZonedDateTime.from( 43 | // 1 Nisan 5761 44 | "2001-03-25T01:00:00+00:00[Atlantic/Azores][u-ca=hebrew]", 45 | ), 46 | ); 47 | }); 48 | 49 | test("ZonedDateTime and backward transition", () => { 50 | expect( 51 | startOfMonth(Temporal.ZonedDateTime.from("2015-11-11T00:00:00-05:00[America/Havana]")), 52 | ).toEqual(Temporal.ZonedDateTime.from("2015-11-01T00:00:00-04:00[America/Havana]")); 53 | expect( 54 | startOfMonth( 55 | Temporal.ZonedDateTime.from( 56 | // 9 Heshvan 5763 57 | "2002-10-15T00:00:00+02:00[Asia/Jerusalem][u-ca=hebrew]", 58 | ), 59 | ), 60 | ).toEqual( 61 | Temporal.ZonedDateTime.from( 62 | // 1 Heshvan 5763 63 | "2002-10-07T00:00:00+03:00[Asia/Jerusalem][u-ca=hebrew]", 64 | ), 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /src/datetime/_equals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isInstant, 3 | isPlainDate, 4 | isPlainDateTime, 5 | isPlainMonthDay, 6 | isPlainTime, 7 | isPlainYearMonth, 8 | isZonedDateTime, 9 | } from "../type-utils.js"; 10 | import type { DateTimeType, Temporal } from "../types.js"; 11 | 12 | /** @internal */ 13 | export function isEqual(a: Temporal.Instant, b: Temporal.Instant): boolean; 14 | /** @internal */ 15 | export function isEqual(a: Temporal.ZonedDateTime, b: Temporal.ZonedDateTime): boolean; 16 | /** @internal */ 17 | export function isEqual(a: Temporal.PlainDate, b: Temporal.PlainDate): boolean; 18 | /** @internal */ 19 | export function isEqual(a: Temporal.PlainTime, b: Temporal.PlainTime): boolean; 20 | /** @internal */ 21 | export function isEqual(a: Temporal.PlainDateTime, b: Temporal.PlainDateTime): boolean; 22 | /** @internal */ 23 | export function isEqual(a: Temporal.PlainYearMonth, b: Temporal.PlainYearMonth): boolean; 24 | /** @internal */ 25 | export function isEqual(a: Temporal.PlainMonthDay, b: Temporal.PlainMonthDay): boolean; 26 | /** @internal */ 27 | export function isEqual(a: DateTimeType, b: DateTimeType): boolean; 28 | export function isEqual(a: DateTimeType, b: DateTimeType) { 29 | if (isInstant(a)) { 30 | if (!isInstant(b)) { 31 | throw new Error("Unmatched type"); 32 | } 33 | return a.equals(b); 34 | } 35 | if (isZonedDateTime(a)) { 36 | if (!isZonedDateTime(b)) { 37 | throw new Error("Unmatched type"); 38 | } 39 | return a.equals(b); 40 | } 41 | if (isPlainDate(a)) { 42 | if (!isPlainDate(b)) { 43 | throw new Error("Unmatched type"); 44 | } 45 | return a.equals(b); 46 | } 47 | if (isPlainTime(a)) { 48 | if (!isPlainTime(b)) { 49 | throw new Error("Unmatched type"); 50 | } 51 | return a.equals(b); 52 | } 53 | if (isPlainDateTime(a)) { 54 | if (!isPlainDateTime(b)) { 55 | throw new Error("Unmatched type"); 56 | } 57 | return a.equals(b); 58 | } 59 | if (isPlainYearMonth(a)) { 60 | if (!isPlainYearMonth(b)) { 61 | throw new Error("Unmatched type"); 62 | } 63 | return a.equals(b); 64 | } 65 | if (isPlainMonthDay(a)) { 66 | if (!isPlainMonthDay(b)) { 67 | throw new Error("Unmatched type"); 68 | } 69 | return a.equals(b); 70 | } 71 | throw new Error("Unknown type"); 72 | } 73 | -------------------------------------------------------------------------------- /src/datetime/startOfDay.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfDay } from "./startOfDay.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfDay(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 8 | ); 9 | }); 10 | 11 | test("ZonedDateTime without offset transition", () => { 12 | expect( 13 | startOfDay(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]")), 14 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]")); 15 | }); 16 | 17 | test("ZonedDateTime and backward transition", () => { 18 | expect( 19 | startOfDay(Temporal.ZonedDateTime.from("2024-10-27T23:00:00+00:00[Europe/London]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("2024-10-27T00:00:00+01:00[Europe/London]")); 21 | expect( 22 | startOfDay(Temporal.ZonedDateTime.from("2010-11-07T23:00:00-03:30[America/St_Johns]")), 23 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-07T00:00:00-02:30[America/St_Johns]")); 24 | }); 25 | 26 | test("ZonedDateTime and forward transition", () => { 27 | expect( 28 | startOfDay(Temporal.ZonedDateTime.from("2024-03-31T23:00:00+01:00[Europe/London]")), 29 | ).toEqual(Temporal.ZonedDateTime.from("2024-03-31T00:00:00+00:00[Europe/London]")); 30 | expect(startOfDay(Temporal.ZonedDateTime.from("2024-03-31T12:00:00+03:00[Asia/Beirut]"))).toEqual( 31 | Temporal.ZonedDateTime.from("2024-03-31T01:00:00+03:00[Asia/Beirut]"), 32 | ); 33 | expect( 34 | startOfDay(Temporal.ZonedDateTime.from("1919-03-31T12:00:00-04:00[America/Toronto]")), 35 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-31T00:30:00-04:00[America/Toronto]")); 36 | }); 37 | 38 | test("ZonedDateTime with `getTimeZoneTransition` edge case", () => { 39 | // temporary workaround for https://github.com/fullcalendar/temporal-polyfill/issues/73 40 | // skip the test for `temporal-polyfill` 41 | // TODO: remove the workaround when the bug is fixed 42 | let zdt; 43 | try { 44 | zdt = Temporal.ZonedDateTime.from("2000-10-08T01:00:00-03:00[America/Boa_Vista]"); 45 | } catch { 46 | return; 47 | } 48 | expect(startOfDay(zdt)).toEqual( 49 | Temporal.ZonedDateTime.from("2000-10-08T01:00:00-03:00[America/Boa_Vista]"), 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /.github/workflows/code_check.yml: -------------------------------------------------------------------------------- 1 | name: Code check 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 13 | with: 14 | persist-credentials: false 15 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 16 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 17 | with: 18 | node-version: 24 19 | cache: pnpm 20 | - run: pnpm install 21 | - name: Lint 22 | run: pnpm run lint 23 | typecheck: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 27 | with: 28 | persist-credentials: false 29 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 30 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 31 | with: 32 | node-version: 24 33 | cache: pnpm 34 | - run: pnpm install 35 | - name: Typecheck 36 | run: pnpm run typecheck 37 | build: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 41 | with: 42 | persist-credentials: false 43 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 44 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 45 | with: 46 | node-version: 24 47 | cache: pnpm 48 | - run: pnpm install 49 | - name: Build 50 | run: pnpm run build 51 | jsr: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 55 | with: 56 | persist-credentials: false 57 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 58 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 59 | with: 60 | node-version: 24 61 | cache: pnpm 62 | - run: pnpm install 63 | - run: pnpm dlx jsr publish --dry-run 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vremel", 3 | "version": "0.6.2", 4 | "description": "JavaScript date utility library for Temporal API", 5 | "type": "module", 6 | "exports": { 7 | ".": "./dist/index.js", 8 | "./duration": "./dist/duration/index.js" 9 | }, 10 | "sideEffects": false, 11 | "engines": { 12 | "node": ">=18" 13 | }, 14 | "files": [ 15 | "src/**/*.ts", 16 | "!src/**/*.test.ts", 17 | "!src/_test/**/*", 18 | "dist", 19 | "CHANGELOG.md" 20 | ], 21 | "scripts": { 22 | "build": "node script/build.ts && node script/check-build.ts", 23 | "test": "POLYFILL=@js-temporal/polyfill vitest", 24 | "test-firefox": "playwright install --with-deps firefox && vitest --browser.enabled=true", 25 | "test-ci": "POLYFILL=@js-temporal/polyfill vitest && POLYFILL=temporal-polyfill vitest", 26 | "typecheck": "tsc", 27 | "lint": "eslint . && oxfmt . --check", 28 | "repl": "node script/build.ts --dev && node script/repl.ts", 29 | "docs:generate": "deno doc --output=./_site --unstable-byonm --unstable-sloppy-imports --html src/index.ts src/duration/index.ts" 30 | }, 31 | "homepage": "https://github.com/fabon-f/vremel#readme", 32 | "bugs": { 33 | "url": "https://github.com/fabon-f/vremel/issues" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/fabon-f/vremel.git" 38 | }, 39 | "keywords": [ 40 | "Temporal" 41 | ], 42 | "author": "fabon (https://www.fabon.info/)", 43 | "license": "MIT", 44 | "devDependencies": { 45 | "@eslint/js": "^9.39.2", 46 | "@js-temporal/polyfill": "^0.5.1", 47 | "@tsconfig/strictest": "^2.0.8", 48 | "@types/node": "^25.0.3", 49 | "@typescript/native-preview": "latest", 50 | "@vitest/browser-playwright": "^4.0.16", 51 | "consola": "^3.4.2", 52 | "date-fns": "^4.1.0", 53 | "eslint": "^9.39.2", 54 | "eslint-plugin-simple-import-sort": "^12.1.1", 55 | "globals": "^16.5.0", 56 | "oxfmt": "0.19.0", 57 | "playwright": "^1.57.0", 58 | "temporal-polyfill": "0.3.0", 59 | "temporal-spec": "^0.3.0", 60 | "tinyexec": "^1.0.2", 61 | "typescript": "^5.9.3", 62 | "typescript-eslint": "8.50.0", 63 | "vite": "^7.3.0", 64 | "vitest": "^4.0.16" 65 | }, 66 | "dependencies": { 67 | "@date-fns/utc": "^2.1.1" 68 | }, 69 | "packageManager": "pnpm@10.25.0" 70 | } 71 | -------------------------------------------------------------------------------- /src/datetime/isAfter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isAfter } from "./isAfter.js"; 4 | 5 | test("Instant", () => { 6 | const a = Temporal.Instant.fromEpochMilliseconds(1720000000000); 7 | const b = Temporal.Instant.fromEpochMilliseconds(1700000000000); 8 | expect(isAfter(a, b)).toBe(true); 9 | expect(isAfter(b, a)).toBe(false); 10 | expect(isAfter(a, a)).toBe(false); 11 | }); 12 | test("ZonedDateTime", () => { 13 | const a = Temporal.ZonedDateTime.from("2024-01-01T00:00:00-05:00[America/Toronto]"); 14 | const b = Temporal.ZonedDateTime.from("2024-01-01T03:00:00+01:00[Europe/Paris]"); 15 | expect(isAfter(a, b)).toBe(true); 16 | expect(isAfter(b, a)).toBe(false); 17 | expect(isAfter(a, a)).toBe(false); 18 | }); 19 | test("PlainDate", () => { 20 | const a = Temporal.PlainDate.from("2024-01-02"); 21 | const b = Temporal.PlainDate.from("2024-01-01[u-ca=hebrew]"); 22 | expect(isAfter(a, b)).toBe(true); 23 | expect(isAfter(b, a)).toBe(false); 24 | expect(isAfter(a, a)).toBe(false); 25 | }); 26 | test("PlainTime", () => { 27 | const a = Temporal.PlainTime.from("23:45:00"); 28 | const b = Temporal.PlainTime.from("06:00:00"); 29 | expect(isAfter(a, b)).toBe(true); 30 | expect(isAfter(b, a)).toBe(false); 31 | expect(isAfter(a, a)).toBe(false); 32 | }); 33 | test("PlainDateTime", () => { 34 | const a = Temporal.PlainDateTime.from("2024-01-01T09:00:00+09:00[Asia/Tokyo]"); 35 | const b = Temporal.PlainDateTime.from("2024-01-01T03:00:00+01:00[Europe/Paris]"); 36 | expect(isAfter(a, b)).toBe(true); 37 | expect(isAfter(b, a)).toBe(false); 38 | expect(isAfter(a, a)).toBe(false); 39 | }); 40 | test("PlainYearMonth", () => { 41 | const a = Temporal.PlainYearMonth.from("2024-01"); 42 | const b = Temporal.PlainYearMonth.from("2023-12"); 43 | expect(isAfter(a, b)).toBe(true); 44 | expect(isAfter(b, a)).toBe(false); 45 | expect(isAfter(a, a)).toBe(false); 46 | }); 47 | test("PlainMonthDay", () => { 48 | expect(() => { 49 | isAfter( 50 | // @ts-expect-error 51 | Temporal.PlainMonthDay.from("12-03"), 52 | Temporal.PlainMonthDay.from("12-04"), 53 | ); 54 | }).toThrow(); 55 | }); 56 | 57 | test("Typecheck", () => { 58 | expect(() => { 59 | // @ts-expect-error 60 | isAfter(Temporal.Now.instant(), Temporal.Now.zonedDateTimeISO()); 61 | }).toThrow(); 62 | }); 63 | -------------------------------------------------------------------------------- /src/datetime/startOfHour.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfHour } from "./startOfHour.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfHour(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T12:00:00"), 8 | ); 9 | }); 10 | 11 | test("PlainTime", () => { 12 | expect(startOfHour(Temporal.PlainTime.from("12:34:56.789123456"))).toEqual( 13 | Temporal.PlainTime.from("12:00:00"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime without offset transition", () => { 18 | expect( 19 | startOfHour(Temporal.ZonedDateTime.from("2024-01-01T01:23:45.678901234+09:00[Asia/Tokyo]")), 20 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T01:00:00+09:00[Asia/Tokyo]")); 21 | }); 22 | 23 | test("ZonedDateTime and backward transition", () => { 24 | expect( 25 | startOfHour(Temporal.ZonedDateTime.from("2023-04-02T01:45:00+10:30[Australia/Lord_Howe]")), 26 | ).toEqual(Temporal.ZonedDateTime.from("2023-04-02T01:00:00+11:00[Australia/Lord_Howe]")); 27 | expect(startOfHour(Temporal.ZonedDateTime.from("2014-10-26T01:30:00+08:00[Asia/Chita]"))).toEqual( 28 | Temporal.ZonedDateTime.from("2014-10-26T01:00:00+10:00[Asia/Chita]"), 29 | ); 30 | expect( 31 | startOfHour(Temporal.ZonedDateTime.from("2010-11-07T00:30:00-03:30[America/St_Johns]")), 32 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-07T00:00:00-02:30[America/St_Johns]")); 33 | expect( 34 | startOfHour(Temporal.ZonedDateTime.from("1944-09-10T02:30:00-04:00[America/Barbados]")), 35 | ).toEqual(Temporal.ZonedDateTime.from("1944-09-10T02:00:00-03:30[America/Barbados]")); 36 | }); 37 | 38 | test("ZonedDateTime and forward transition", () => { 39 | expect( 40 | startOfHour(Temporal.ZonedDateTime.from("1984-10-01T00:45:00-03:00[America/Paramaribo]")), 41 | ).toEqual(Temporal.ZonedDateTime.from("1984-10-01T00:30:00-03:00[America/Paramaribo]")); 42 | expect( 43 | startOfHour(Temporal.ZonedDateTime.from("1919-03-31T00:45:00-04:00[America/Toronto]")), 44 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-31T00:30:00-04:00[America/Toronto]")); 45 | expect( 46 | startOfHour(Temporal.ZonedDateTime.from("1916-07-28T00:45:00+02:00[Europe/Athens]")), 47 | ).toEqual(Temporal.ZonedDateTime.from("1916-07-28T00:00:00+01:34:52[Europe/Athens]")); 48 | }); 49 | -------------------------------------------------------------------------------- /src/datetime/_compare.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getConstructor, 3 | getTypeName, 4 | isInstant, 5 | isPlainDate, 6 | isPlainDateTime, 7 | isPlainMonthDay, 8 | isPlainTime, 9 | isPlainYearMonth, 10 | isZonedDateTime, 11 | } from "../type-utils.js"; 12 | import type { ComparableDateTimeType, Temporal } from "../types.js"; 13 | 14 | /** @internal */ 15 | export function compare(a: Temporal.Instant, b: Temporal.Instant): Temporal.ComparisonResult; 16 | /** @internal */ 17 | export function compare( 18 | a: Temporal.ZonedDateTime, 19 | b: Temporal.ZonedDateTime, 20 | ): Temporal.ComparisonResult; 21 | /** @internal */ 22 | export function compare(a: Temporal.PlainDate, b: Temporal.PlainDate): Temporal.ComparisonResult; 23 | /** @internal */ 24 | export function compare(a: Temporal.PlainTime, b: Temporal.PlainTime): Temporal.ComparisonResult; 25 | /** @internal */ 26 | export function compare( 27 | a: Temporal.PlainDateTime, 28 | b: Temporal.PlainDateTime, 29 | ): Temporal.ComparisonResult; 30 | /** @internal */ 31 | export function compare( 32 | a: Temporal.PlainYearMonth, 33 | b: Temporal.PlainYearMonth, 34 | ): Temporal.ComparisonResult; 35 | /** @internal */ 36 | export function compare( 37 | a: ComparableDateTimeType, 38 | b: ComparableDateTimeType, 39 | ): Temporal.ComparisonResult; 40 | export function compare(a: ComparableDateTimeType, b: ComparableDateTimeType) { 41 | if (isInstant(a) && isInstant(b)) { 42 | return getConstructor(a).compare(a, b); 43 | } 44 | if (isZonedDateTime(a) && isZonedDateTime(b)) { 45 | return getConstructor(a).compare(a, b); 46 | } 47 | if (isPlainDate(a) && isPlainDate(b)) { 48 | return getConstructor(a).compare(a, b); 49 | } 50 | if (isPlainTime(a) && isPlainTime(b)) { 51 | return getConstructor(a).compare(a, b); 52 | } 53 | if (isPlainDateTime(a) && isPlainDateTime(b)) { 54 | return getConstructor(a).compare(a, b); 55 | } 56 | if (isPlainYearMonth(a) && isPlainYearMonth(b)) { 57 | if (a.calendarId !== b.calendarId) { 58 | throw new Error("Can't compare PlainYearMonth with different calendar"); 59 | } 60 | return getConstructor(a).compare(a, b); 61 | } 62 | if (isPlainMonthDay(a) || isPlainMonthDay(b)) { 63 | throw new Error("Can't compare PlainMonthDay"); 64 | } 65 | throw new Error(`Can't compare ${getTypeName(a)} and ${getTypeName(b)}`); 66 | } 67 | -------------------------------------------------------------------------------- /src/datetime/isBefore.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isBefore } from "./isBefore.js"; 4 | 5 | test("Instant", () => { 6 | const a = Temporal.Instant.fromEpochMilliseconds(1720000000000); 7 | const b = Temporal.Instant.fromEpochMilliseconds(1700000000000); 8 | expect(isBefore(a, b)).toBe(false); 9 | expect(isBefore(b, a)).toBe(true); 10 | expect(isBefore(a, a)).toBe(false); 11 | }); 12 | test("ZonedDateTime", () => { 13 | const a = Temporal.ZonedDateTime.from("2024-01-01T00:00:00-05:00[America/Toronto]"); 14 | const b = Temporal.ZonedDateTime.from("2024-01-01T03:00:00+01:00[Europe/Paris]"); 15 | expect(isBefore(a, b)).toBe(false); 16 | expect(isBefore(b, a)).toBe(true); 17 | expect(isBefore(a, a)).toBe(false); 18 | }); 19 | test("PlainDate", () => { 20 | const a = Temporal.PlainDate.from("2024-01-02"); 21 | const b = Temporal.PlainDate.from("2024-01-01[u-ca=hebrew]"); 22 | expect(isBefore(a, b)).toBe(false); 23 | expect(isBefore(b, a)).toBe(true); 24 | expect(isBefore(a, a)).toBe(false); 25 | }); 26 | test("PlainTime", () => { 27 | const a = Temporal.PlainTime.from("23:45:00"); 28 | const b = Temporal.PlainTime.from("06:00:00"); 29 | expect(isBefore(a, b)).toBe(false); 30 | expect(isBefore(b, a)).toBe(true); 31 | expect(isBefore(a, a)).toBe(false); 32 | }); 33 | test("PlainDateTime", () => { 34 | const a = Temporal.PlainDateTime.from("2024-01-01T09:00:00+09:00[Asia/Tokyo]"); 35 | const b = Temporal.PlainDateTime.from("2024-01-01T03:00:00+01:00[Europe/Paris]"); 36 | expect(isBefore(a, b)).toBe(false); 37 | expect(isBefore(b, a)).toBe(true); 38 | expect(isBefore(a, a)).toBe(false); 39 | }); 40 | test("PlainYearMonth", () => { 41 | const a = Temporal.PlainYearMonth.from("2024-01"); 42 | const b = Temporal.PlainYearMonth.from("2023-12"); 43 | expect(isBefore(a, b)).toBe(false); 44 | expect(isBefore(b, a)).toBe(true); 45 | expect(isBefore(a, a)).toBe(false); 46 | }); 47 | test("PlainMonthDay", () => { 48 | expect(() => { 49 | isBefore( 50 | // @ts-expect-error 51 | Temporal.PlainMonthDay.from("12-03"), 52 | Temporal.PlainMonthDay.from("12-04"), 53 | ); 54 | }).toThrow(); 55 | }); 56 | 57 | test("Typecheck", () => { 58 | expect(() => { 59 | // @ts-expect-error 60 | isBefore(Temporal.Now.instant(), Temporal.Now.zonedDateTimeISO()); 61 | }).toThrow(); 62 | }); 63 | -------------------------------------------------------------------------------- /src/datetime/formatRfc7231.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { formatRfc7231 } from "./formatRfc7231.js"; 4 | 5 | test("ZonedDateTime", () => { 6 | expect( 7 | formatRfc7231(Temporal.ZonedDateTime.from("2024-06-07T10:23:45+09:00[Asia/Tokyo]")), 8 | ).toEqual("Fri, 07 Jun 2024 01:23:45 GMT"); 9 | expect( 10 | formatRfc7231( 11 | Temporal.ZonedDateTime.from("2024-06-07T10:23:45+09:00[Asia/Tokyo][u-ca=hebrew]"), 12 | ), 13 | ).toEqual("Fri, 07 Jun 2024 01:23:45 GMT"); 14 | }); 15 | 16 | test("Instant", () => { 17 | expect(formatRfc7231(Temporal.Instant.from("2024-06-07T01:23:45Z"))).toEqual( 18 | "Fri, 07 Jun 2024 01:23:45 GMT", 19 | ); 20 | }); 21 | 22 | test("fractional seconds", () => { 23 | // units smaller than second is ignored 24 | expect(formatRfc7231(Temporal.Instant.from("2024-06-07T01:23:45.123456789Z"))).toEqual( 25 | "Fri, 07 Jun 2024 01:23:45 GMT", 26 | ); 27 | expect(formatRfc7231(Temporal.Instant.from("1968-06-07T01:23:45.123456789Z"))).toEqual( 28 | "Fri, 07 Jun 1968 01:23:45 GMT", 29 | ); 30 | }); 31 | 32 | test("valid range of year", () => { 33 | expect(formatRfc7231(Temporal.Instant.from("0000-01-01T00:00:00Z"))).toEqual( 34 | "Sat, 01 Jan 0000 00:00:00 GMT", 35 | ); 36 | expect(formatRfc7231(Temporal.ZonedDateTime.from("0000-01-01T00:00:00Z[UTC]"))).toEqual( 37 | "Sat, 01 Jan 0000 00:00:00 GMT", 38 | ); 39 | expect(formatRfc7231(Temporal.Instant.from("9999-12-31T00:00:00Z"))).toEqual( 40 | "Fri, 31 Dec 9999 00:00:00 GMT", 41 | ); 42 | expect(formatRfc7231(Temporal.ZonedDateTime.from("9999-12-31T00:00:00Z[UTC]"))).toEqual( 43 | "Fri, 31 Dec 9999 00:00:00 GMT", 44 | ); 45 | }); 46 | 47 | test("invalid range of year", () => { 48 | expect(() => { 49 | formatRfc7231(Temporal.Instant.from("-000001-12-31T23:59:59Z")); 50 | }).toThrow(); 51 | expect(() => { 52 | formatRfc7231(Temporal.ZonedDateTime.from("-000001-12-31T23:59:59Z[UTC]")); 53 | }).toThrow(); 54 | expect(() => { 55 | formatRfc7231(Temporal.Instant.from("-100000-01-01T00:00:00Z")); 56 | }).toThrow(); 57 | expect(() => { 58 | formatRfc7231(Temporal.ZonedDateTime.from("-100000-01-01T00:00:00Z[UTC]")); 59 | }).toThrow(); 60 | expect(() => { 61 | formatRfc7231(Temporal.Instant.from("+010000-01-01T00:00:00Z")); 62 | }).toThrow(); 63 | expect(() => { 64 | formatRfc7231(Temporal.ZonedDateTime.from("+010000-01-01T00:00:00Z[UTC]")); 65 | }).toThrow(); 66 | }); 67 | -------------------------------------------------------------------------------- /src/datetime/closestIndexTo.ts: -------------------------------------------------------------------------------- 1 | import { shortest } from "../duration/shortest.js"; 2 | import { 3 | isInstant, 4 | isInstantArray, 5 | isPlainDate, 6 | isPlainDateArray, 7 | isPlainDateTime, 8 | isPlainDateTimeArray, 9 | isPlainTime, 10 | isPlainTimeArray, 11 | isPlainYearMonth, 12 | isPlainYearMonthArray, 13 | isZonedDateTime, 14 | isZonedDateTimeArray, 15 | } from "../type-utils.js"; 16 | import type { ArrayOf, Temporal } from "../types.js"; 17 | 18 | function minIndex(array: number[]) { 19 | return array.indexOf(array.reduce((a, b) => Math.min(a, b))); 20 | } 21 | 22 | /** 23 | * Returns an index of the closest datetime object to the given datetime object from the passed array. 24 | * @param dateTimeToCompare the date to compare with 25 | * @param dateTimes array of datetime objects 26 | * @returns index of the closest datetime 27 | */ 28 | export function closestIndexTo< 29 | DateTime extends 30 | | Temporal.Instant 31 | | Temporal.ZonedDateTime 32 | | Temporal.PlainDate 33 | | Temporal.PlainTime 34 | | Temporal.PlainDateTime 35 | | Temporal.PlainYearMonth, 36 | >(dateTimeToCompare: DateTime, dateTimes: ArrayOf): number { 37 | if (isInstant(dateTimeToCompare) && isInstantArray(dateTimes)) { 38 | const diff = dateTimes.map((d) => dateTimeToCompare.until(d).abs()); 39 | return diff.indexOf(shortest(diff)); 40 | } 41 | if (isZonedDateTime(dateTimeToCompare) && isZonedDateTimeArray(dateTimes)) { 42 | return closestIndexTo( 43 | dateTimeToCompare.toInstant(), 44 | dateTimes.map((d) => d.toInstant()), 45 | ); 46 | } 47 | if (isPlainDate(dateTimeToCompare) && isPlainDateArray(dateTimes)) { 48 | const diff = dateTimes.map( 49 | (d) => dateTimeToCompare.until(d, { largestUnit: "day" }).abs().days, 50 | ); 51 | return minIndex(diff); 52 | } 53 | if (isPlainTime(dateTimeToCompare) && isPlainTimeArray(dateTimes)) { 54 | const diff = dateTimes.map((d) => dateTimeToCompare.until(d).abs()); 55 | return diff.indexOf(shortest(diff)); 56 | } 57 | if (isPlainDateTime(dateTimeToCompare) && isPlainDateTimeArray(dateTimes)) { 58 | const diff = dateTimes.map((d) => dateTimeToCompare.until(d, { largestUnit: "hour" }).abs()); 59 | return diff.indexOf(shortest(diff)); 60 | } 61 | if (isPlainYearMonth(dateTimeToCompare) && isPlainYearMonthArray(dateTimes)) { 62 | const diff = dateTimes.map( 63 | (d) => dateTimeToCompare.until(d, { largestUnit: "month" }).abs().months, 64 | ); 65 | return minIndex(diff); 66 | } 67 | throw new Error("Invalid arguments"); 68 | } 69 | -------------------------------------------------------------------------------- /src/datetime/endOfWeek.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { endOfTimeForZonedDateTime } from "./_endOfTimeForZonedDateTime.js"; 4 | 5 | export interface EndOfWeekOptions { 6 | /** 7 | * First day of the week. 8 | * For example, in ISO calendar Monday is `1`, Sunday is `7`. 9 | */ 10 | firstDayOfWeek: number; 11 | } 12 | 13 | function endOfWeekWithDayPrecision( 14 | dt: Temporal.PlainDate, 15 | firstDayOfWeek: number, 16 | ): Temporal.PlainDate; 17 | function endOfWeekWithDayPrecision( 18 | dt: Temporal.PlainDateTime, 19 | firstDayOfWeek: number, 20 | ): Temporal.PlainDateTime; 21 | function endOfWeekWithDayPrecision( 22 | dt: Temporal.PlainDate | Temporal.PlainDateTime, 23 | firstDayOfWeek: number, 24 | ) { 25 | return dt.add({ 26 | days: (firstDayOfWeek + dt.daysInWeek - dt.dayOfWeek - 1) % dt.daysInWeek, 27 | }); 28 | } 29 | 30 | /** 31 | * Returns the end of a week for the given datetime. 32 | * 'end of a week' is ambiguous and locale-dependent, 33 | * so `firstDayOfWeek` option is required. 34 | * This function supports a calendar with a fixed `daysInWeek`, 35 | * even if the week contains more or less than 7 days. 36 | * But it doesn't support a calendar which lacks a fixed number of days. 37 | * 38 | * @param dt datetime object which includes date info 39 | * @returns Temporal object which represents the end of a week 40 | */ 41 | export function endOfWeek< 42 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 43 | >(dt: DateTime, options: EndOfWeekOptions): DateTime { 44 | const firstDayOfWeek = options.firstDayOfWeek; 45 | if (!Number.isInteger(firstDayOfWeek) || firstDayOfWeek < 1 || firstDayOfWeek > dt.daysInWeek) { 46 | throw new Error(`${firstDayOfWeek} isn't a valid day of week`); 47 | } 48 | 49 | const timeArg = { 50 | hour: 23, 51 | minute: 59, 52 | second: 59, 53 | millisecond: 999, 54 | microsecond: 999, 55 | nanosecond: 999, 56 | }; 57 | 58 | if (isZonedDateTime(dt)) { 59 | const endOfWeek = endOfWeekWithDayPrecision(dt.toPlainDate(), firstDayOfWeek); 60 | return endOfTimeForZonedDateTime(dt, { 61 | year: endOfWeek.year, 62 | month: endOfWeek.month, 63 | day: endOfWeek.day, 64 | ...timeArg, 65 | }) as DateTime; 66 | } 67 | 68 | if (isPlainDate(dt)) { 69 | return endOfWeekWithDayPrecision(dt, firstDayOfWeek) as DateTime; 70 | } 71 | return endOfWeekWithDayPrecision(dt, firstDayOfWeek).with(timeArg) as DateTime; 72 | } 73 | -------------------------------------------------------------------------------- /src/datetime/_ldmrDatePattern.ts: -------------------------------------------------------------------------------- 1 | type Token = 2 | | { 3 | type: "literal"; 4 | value: string; 5 | } 6 | | { 7 | type: "field"; 8 | value: string; 9 | }; 10 | 11 | const regex = /''|'(?:''|[^'])+'|([a-zA-Z])\1*/g; 12 | 13 | const unbalancedSingleQuotesErrorMessage = 14 | "Unbalanced single quotes. Use single quotes for escaping and two single quotes to represent actual single quote."; 15 | 16 | function areSingleQuotesBalanced(format: string) { 17 | let count = 0; 18 | for (const char of format) { 19 | if (char === `'`) { 20 | count++; 21 | } 22 | } 23 | return count % 2 === 0; 24 | } 25 | 26 | function unescapeTwoSingleQuotes(format: string) { 27 | return format.replaceAll(`''`, `'`); 28 | } 29 | 30 | /** @internal */ 31 | export function replaceToken(pattern: string, replacer: (token: string) => string): string { 32 | if (!areSingleQuotesBalanced(pattern)) { 33 | throw new Error(unbalancedSingleQuotesErrorMessage); 34 | } 35 | return pattern.replaceAll(regex, (match) => { 36 | if (match === `''`) { 37 | return `'`; 38 | } 39 | if (match.startsWith(`'`) && match.endsWith(`'`)) { 40 | return unescapeTwoSingleQuotes(match.slice(1, match.length - 1)); 41 | } 42 | return replacer(match); 43 | }); 44 | } 45 | 46 | function pushLiteral(tokens: Token[], literal: string) { 47 | const previousToken = tokens[tokens.length - 1]; 48 | if (previousToken?.type === "literal") { 49 | previousToken.value += literal; 50 | } else { 51 | tokens.push({ 52 | type: "literal", 53 | value: literal, 54 | }); 55 | } 56 | } 57 | 58 | /** @internal */ 59 | export function tokenize(pattern: string): Token[] { 60 | if (!areSingleQuotesBalanced(pattern)) { 61 | throw new Error(unbalancedSingleQuotesErrorMessage); 62 | } 63 | let lastIndex = 0; 64 | const tokens: Token[] = []; 65 | for (const match of pattern.matchAll(regex)) { 66 | if (match.index > lastIndex) { 67 | pushLiteral(tokens, pattern.slice(lastIndex, match.index)); 68 | } 69 | const fragment = match[0]; 70 | if (fragment === `''`) { 71 | pushLiteral(tokens, `'`); 72 | } else if (fragment.startsWith(`'`) && fragment.endsWith(`'`)) { 73 | pushLiteral(tokens, unescapeTwoSingleQuotes(fragment.slice(1, fragment.length - 1))); 74 | } else { 75 | tokens.push({ 76 | type: "field", 77 | value: fragment, 78 | }); 79 | } 80 | lastIndex = match.index + fragment.length; 81 | } 82 | if (lastIndex < pattern.length) { 83 | // rest 84 | pushLiteral(tokens, pattern.slice(lastIndex)); 85 | } 86 | return tokens; 87 | } 88 | -------------------------------------------------------------------------------- /src/datetime/startOfWeek.ts: -------------------------------------------------------------------------------- 1 | import { isPlainDate, isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { startOfTimeForZonedDateTime } from "./_startOfTimeForZonedDateTime.js"; 4 | 5 | export interface StartOfWeekOptions { 6 | /** 7 | * First day of the week. 8 | * For example, in ISO calendar Monday is `1`, Sunday is `7`. 9 | */ 10 | firstDayOfWeek: number; 11 | } 12 | 13 | function startOfWeekWithDayPrecision( 14 | dt: Temporal.PlainDate, 15 | firstDayOfWeek: number, 16 | ): Temporal.PlainDate; 17 | function startOfWeekWithDayPrecision( 18 | dt: Temporal.PlainDateTime, 19 | firstDayOfWeek: number, 20 | ): Temporal.PlainDateTime; 21 | function startOfWeekWithDayPrecision( 22 | dt: Temporal.PlainDate | Temporal.PlainDateTime, 23 | firstDayOfWeek: number, 24 | ) { 25 | return dt.subtract({ 26 | days: (dt.dayOfWeek - firstDayOfWeek + dt.daysInWeek) % dt.daysInWeek, 27 | }); 28 | } 29 | 30 | /** 31 | * Returns the start of a week for the given datetime. 32 | * 'start of a week' is ambiguous and locale-dependent, 33 | * so `firstDayOfWeek` option is required. 34 | * This function supports a calendar with a fixed `daysInWeek`, 35 | * even if the week contains more or less than 7 days. 36 | * But it doesn't support a calendar which lacks a fixed number of days. 37 | * 38 | * @param dt datetime object which includes date info 39 | * @returns Temporal object which represents the start of a week 40 | */ 41 | export function startOfWeek< 42 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 43 | >(dt: DateTime, options: StartOfWeekOptions): DateTime { 44 | const firstDayOfWeek = options.firstDayOfWeek; 45 | if (!Number.isInteger(firstDayOfWeek) || firstDayOfWeek < 1 || firstDayOfWeek > dt.daysInWeek) { 46 | throw new Error(`${firstDayOfWeek} isn't a valid day of week`); 47 | } 48 | 49 | const timeArg = { 50 | hour: 0, 51 | minute: 0, 52 | second: 0, 53 | millisecond: 0, 54 | microsecond: 0, 55 | nanosecond: 0, 56 | }; 57 | 58 | if (isZonedDateTime(dt)) { 59 | const startOfWeek = startOfWeekWithDayPrecision(dt.toPlainDate(), firstDayOfWeek); 60 | return startOfTimeForZonedDateTime(dt, { 61 | year: startOfWeek.year, 62 | month: startOfWeek.month, 63 | day: startOfWeek.day, 64 | ...timeArg, 65 | }) as DateTime; 66 | } 67 | 68 | if (isPlainDate(dt)) { 69 | return startOfWeekWithDayPrecision(dt, firstDayOfWeek) as DateTime; 70 | } 71 | return startOfWeekWithDayPrecision(dt, firstDayOfWeek).with(timeArg) as DateTime; 72 | } 73 | -------------------------------------------------------------------------------- /src/datetime/withDayOfWeek.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { withDayOfWeek } from "./withDayOfWeek.js"; 4 | 5 | test.for([ 6 | ["2025-01-06", 3, 5, "2025-01-08"], 7 | ["2025-01-06", 5, 3, "2025-01-03"], 8 | ["2025-01-08", 1, 5, "2025-01-06"], 9 | ["2025-01-08", 5, 1, "2025-01-10"], 10 | ["2025-01-10", 1, 3, "2025-01-13"], 11 | ["2025-01-10", 3, 1, "2025-01-08"], 12 | ] as [string, number, number, string][])( 13 | "PlainDate (%s, dayOfWeek: %d, firstDayOfWeek: %d)", 14 | ([date, dayOfWeek, firstDayOfWeek, expected]) => { 15 | expect( 16 | withDayOfWeek(Temporal.PlainDate.from(date), dayOfWeek, { 17 | firstDayOfWeek, 18 | }), 19 | ).toEqual(Temporal.PlainDate.from(expected)); 20 | }, 21 | ); 22 | 23 | test("PlainDateTime", () => { 24 | expect( 25 | withDayOfWeek(Temporal.PlainDateTime.from("2025-01-01T12:30:00"), 5, { 26 | firstDayOfWeek: 1, 27 | }), 28 | ).toEqual(Temporal.PlainDateTime.from("2025-01-03T12:30:00")); 29 | }); 30 | 31 | test("ZonedDateTime without offset transition", () => { 32 | expect( 33 | withDayOfWeek(Temporal.ZonedDateTime.from("2025-01-01T12:00:00+09:00[Asia/Tokyo]"), 5, { 34 | firstDayOfWeek: 1, 35 | }), 36 | ).toEqual(Temporal.ZonedDateTime.from("2025-01-03T12:00:00+09:00[Asia/Tokyo]")); 37 | }); 38 | 39 | test("ZonedDateTime and forward transition", () => { 40 | expect( 41 | withDayOfWeek(Temporal.ZonedDateTime.from("2024-03-29T23:10:00-02:00[America/Nuuk]"), 6, { 42 | firstDayOfWeek: 1, 43 | }), 44 | ).toEqual(Temporal.ZonedDateTime.from("2024-03-31T00:10:00-01:00[America/Nuuk]")); 45 | expect( 46 | withDayOfWeek(Temporal.ZonedDateTime.from("2024-03-29T23:10:00-02:00[America/Nuuk]"), 6, { 47 | firstDayOfWeek: 1, 48 | disambiguation: "earlier", 49 | }), 50 | ).toEqual(Temporal.ZonedDateTime.from("2024-03-30T22:10:00-02:00[America/Nuuk]")); 51 | }); 52 | 53 | test("ZonedDateTime and backward transition", () => { 54 | expect( 55 | withDayOfWeek(Temporal.ZonedDateTime.from("2024-10-23T01:30:00+01:00[Europe/London]"), 7, { 56 | firstDayOfWeek: 1, 57 | }), 58 | ).toEqual(Temporal.ZonedDateTime.from("2024-10-27T01:30:00+01:00[Europe/London]")); 59 | expect( 60 | withDayOfWeek(Temporal.ZonedDateTime.from("2024-10-29T01:30:00+00:00[Europe/London]"), 7, { 61 | firstDayOfWeek: 7, 62 | }), 63 | ).toEqual(Temporal.ZonedDateTime.from("2024-10-27T01:30:00+00:00[Europe/London]")); 64 | expect( 65 | withDayOfWeek(Temporal.ZonedDateTime.from("2024-10-23T01:30:00+01:00[Europe/London]"), 7, { 66 | firstDayOfWeek: 1, 67 | disambiguation: "later", 68 | offset: "ignore", 69 | }), 70 | ).toEqual(Temporal.ZonedDateTime.from("2024-10-27T01:30:00+00:00[Europe/London]")); 71 | }); 72 | -------------------------------------------------------------------------------- /src/datetime/endOfYear.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfYear } from "./endOfYear.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfYear(Temporal.PlainDateTime.from("2024-02-23T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-12-31T23:59:59.999999999"), 8 | ); 9 | }); 10 | 11 | test("PlainDate", () => { 12 | expect(endOfYear(Temporal.PlainDate.from("2024-02-23"))).toEqual( 13 | Temporal.PlainDate.from("2024-12-31"), 14 | ); 15 | }); 16 | 17 | test("PlainYearMonth", () => { 18 | expect(endOfYear(Temporal.PlainYearMonth.from("2024-02"))).toEqual( 19 | Temporal.PlainYearMonth.from("2024-12"), 20 | ); 21 | }); 22 | 23 | test("PlainDate with non-ISO calendar", () => { 24 | // 19 Adar I 5784 -> 29 Elul 5784 25 | expect(endOfYear(Temporal.PlainDate.from("2024-02-28[u-ca=hebrew]"))).toEqual( 26 | Temporal.PlainDate.from("2024-10-02[u-ca=hebrew]"), 27 | ); 28 | }); 29 | 30 | test("PlainYearMonth with non-ISO calendar", () => { 31 | // Adar I 5784 -> Elul 5784 32 | expect(endOfYear(Temporal.PlainYearMonth.from("2024-02-28[u-ca=hebrew]"))).toEqual( 33 | Temporal.PlainYearMonth.from("2024-10-02[u-ca=hebrew]"), 34 | ); 35 | }); 36 | 37 | test("ZonedDateTime without offset transition", () => { 38 | expect(endOfYear(Temporal.ZonedDateTime.from("2024-03-21T01:23:45+09:00[Asia/Tokyo]"))).toEqual( 39 | Temporal.ZonedDateTime.from("2024-12-31T23:59:59.999999999+09:00[Asia/Tokyo]"), 40 | ); 41 | }); 42 | 43 | test("ZonedDateTime with forward transition", () => { 44 | expect( 45 | endOfYear(Temporal.ZonedDateTime.from("1994-12-29T00:00:00-10:00[Pacific/Kiritimati]")), 46 | ).toEqual(Temporal.ZonedDateTime.from("1994-12-30T23:59:59.999999999-10:00[Pacific/Kiritimati]")); 47 | expect( 48 | endOfYear( 49 | Temporal.ZonedDateTime.from( 50 | // 20 Aban 1332 51 | "1953-11-11T00:00:00+08:00[Asia/Macau][u-ca=persian]", 52 | ), 53 | ), 54 | ).toEqual( 55 | Temporal.ZonedDateTime.from( 56 | // 29 Esfand 1332 57 | "1954-03-20T22:59:59.999999999+08:00[Asia/Macau][u-ca=persian]", 58 | ), 59 | ); 60 | }); 61 | 62 | test("ZonedDateTime with backward transition", () => { 63 | expect( 64 | endOfYear(Temporal.ZonedDateTime.from("1996-12-31T23:00:00-05:00[America/Managua]")), 65 | ).toEqual(Temporal.ZonedDateTime.from("1996-12-31T23:59:59.999999999-06:00[America/Managua]")); 66 | expect( 67 | endOfYear( 68 | Temporal.ZonedDateTime.from( 69 | // 26 Sivan 5738 70 | "1978-07-01T00:00:00+03:00[Asia/Famagusta][u-ca=hebrew]", 71 | ), 72 | ), 73 | ).toEqual( 74 | Temporal.ZonedDateTime.from( 75 | // 29 Elul 5738 76 | "1978-10-01T23:59:59.999999999+02:00[Asia/Famagusta][u-ca=hebrew]", 77 | ), 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /src/datetime/startOfYear.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfYear } from "./startOfYear.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(startOfYear(Temporal.PlainDateTime.from("2024-02-23T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 8 | ); 9 | }); 10 | 11 | test("PlainDate", () => { 12 | expect(startOfYear(Temporal.PlainDate.from("2024-02-23"))).toEqual( 13 | Temporal.PlainDate.from("2024-01-01"), 14 | ); 15 | }); 16 | 17 | test("PlainYearMonth", () => { 18 | expect(startOfYear(Temporal.PlainYearMonth.from("2024-02"))).toEqual( 19 | Temporal.PlainYearMonth.from("2024-01"), 20 | ); 21 | }); 22 | 23 | test("PlainDate with non-ISO calendar", () => { 24 | // 19 Adar I 5784 -> 1 Tishrei 5784 25 | expect(startOfYear(Temporal.PlainDate.from("2024-02-28[u-ca=hebrew]"))).toEqual( 26 | Temporal.PlainDate.from("2023-09-16[u-ca=hebrew]"), 27 | ); 28 | }); 29 | 30 | test("PlainYearMonth with non-ISO calendar", () => { 31 | // Adar I 5784 -> Tishrei 5784 32 | expect(startOfYear(Temporal.PlainYearMonth.from("2024-02-28[u-ca=hebrew]"))).toEqual( 33 | Temporal.PlainYearMonth.from("2023-09-16[u-ca=hebrew]"), 34 | ); 35 | }); 36 | 37 | test("ZonedDateTime without offset transition", () => { 38 | expect(startOfYear(Temporal.ZonedDateTime.from("2024-03-21T01:23:45+09:00[Asia/Tokyo]"))).toEqual( 39 | Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]"), 40 | ); 41 | }); 42 | 43 | test("ZonedDateTime and backward transition", () => { 44 | expect( 45 | startOfYear(Temporal.ZonedDateTime.from("1922-06-30T00:00:00-07:00[America/Mexico_City]")), 46 | ).toEqual(Temporal.ZonedDateTime.from("1922-01-01T00:00:00-06:36:36[America/Mexico_City]")); 47 | expect( 48 | startOfYear( 49 | Temporal.ZonedDateTime.from( 50 | // Jumada I 14, 1337 AH 51 | "1919-02-15T00:00:00+00:00[Europe/Madrid][u-ca=islamic-civil]", 52 | ), 53 | ), 54 | ).toEqual( 55 | Temporal.ZonedDateTime.from( 56 | // Muharram 1, 1337 AH 57 | "1918-10-07T00:00:00+01:00[Europe/Madrid][u-ca=islamic-civil]", 58 | ), 59 | ); 60 | }); 61 | 62 | test("ZonedDateTime and forward transition", () => { 63 | expect( 64 | startOfYear(Temporal.ZonedDateTime.from("2004-02-01T12:00:00+10:00[Asia/Khandyga]")), 65 | ).toEqual(Temporal.ZonedDateTime.from("2004-01-01T01:00:00+10:00[Asia/Khandyga]")); 66 | expect( 67 | startOfYear( 68 | Temporal.ZonedDateTime.from( 69 | // 16 Tevet 5706 70 | "1945-12-20T02:30:00+11:30[Pacific/Nauru][u-ca=hebrew]", 71 | ), 72 | ), 73 | ).toEqual( 74 | Temporal.ZonedDateTime.from( 75 | // 1 Tishri 5706 76 | "1945-09-08T02:30:00+11:30[Pacific/Nauru][u-ca=hebrew]", 77 | ), 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /src/datetime/index.ts: -------------------------------------------------------------------------------- 1 | export type { GenericDateConstructor } from "../types.js"; 2 | export { 3 | areIntervalsOverlapping, 4 | type AreIntervalsOverlappingOptions, 5 | } from "./areIntervalsOverlapping.js"; 6 | export { clamp } from "./clamp.js"; 7 | export { closestIndexTo } from "./closestIndexTo.js"; 8 | export { closestTo } from "./closestTo.js"; 9 | export { compareAsc } from "./compareAsc.js"; 10 | export { compareDesc } from "./compareDesc.js"; 11 | export { earliest } from "./earliest.js"; 12 | export { endOfDay } from "./endOfDay.js"; 13 | export { endOfHour } from "./endOfHour.js"; 14 | export { endOfMinute } from "./endOfMinute.js"; 15 | export { endOfMonth } from "./endOfMonth.js"; 16 | export { endOfSecond } from "./endOfSecond.js"; 17 | export { endOfWeek, type EndOfWeekOptions } from "./endOfWeek.js"; 18 | export { endOfYear } from "./endOfYear.js"; 19 | export { epochMicroseconds } from "./epochMicroseconds.js"; 20 | export { epochSeconds } from "./epochSeconds.js"; 21 | export { formatRfc7231 } from "./formatRfc7231.js"; 22 | export { formatWithoutLocale, type FormatWithoutLocaleOptions } from "./formatWithoutLocale.js"; 23 | export { fromJulianDate } from "./fromJulianDate.js"; 24 | export { fromModifiedJulianDate } from "./fromModifiedJulianDate.js"; 25 | export { fromRfc2822 } from "./fromRfc2822.js"; 26 | export { fromRfc7231 } from "./fromRfc7231.js"; 27 | export { isAfter } from "./isAfter.js"; 28 | export { isBefore } from "./isBefore.js"; 29 | export { isSameWeek, type IsSameWeekOptions } from "./isSameWeek.js"; 30 | export { isWithinInterval } from "./isWithinInterval.js"; 31 | export { julianDate } from "./julianDate.js"; 32 | export { latest } from "./latest.js"; 33 | export { modifiedJulianDate } from "./modifiedJulianDate.js"; 34 | export { type LocaleDataForParser, parse, type ParseResult } from "./parse.js"; 35 | export { startOfDay } from "./startOfDay.js"; 36 | export { startOfHour } from "./startOfHour.js"; 37 | export { startOfMinute } from "./startOfMinute.js"; 38 | export { startOfMonth } from "./startOfMonth.js"; 39 | export { startOfSecond } from "./startOfSecond.js"; 40 | export { startOfWeek, type StartOfWeekOptions } from "./startOfWeek.js"; 41 | export { startOfYear } from "./startOfYear.js"; 42 | export { toDateFromClockTime } from "./toDateFromClockTime.js"; 43 | export { toDateFromExactTime } from "./toDateFromExactTime.js"; 44 | export { 45 | type PlainDateLike, 46 | type PlainDateTimeLike, 47 | type PlainMonthDayLike, 48 | type PlainYearMonthLike, 49 | toObject, 50 | type ZonedDateTimeLike, 51 | } from "./toObject.js"; 52 | export { toTemporalFromClockTime } from "./toTemporalFromClockTime.js"; 53 | export { withDayOfWeek, type WithDayOfWeekOptions } from "./withDayOfWeek.js"; 54 | -------------------------------------------------------------------------------- /src/datetime/closestTo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { closestTo } from "./closestTo.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1700000000, 1720000000, 1600000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | const dt = Temporal.Instant.fromEpochMilliseconds(1640000000 * 1000); 10 | expect(closestTo(dt, target)).toBe(target[2]); 11 | }); 12 | test("ZonedDateTime", () => { 13 | const target = [ 14 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 15 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 16 | "2024-01-01T00:00:00-05:00[America/Toronto]", 17 | ].map((t) => Temporal.ZonedDateTime.from(t)); 18 | const dt = Temporal.ZonedDateTime.from("2024-01-01T11:30:00+09:00[Asia/Tokyo]"); 19 | expect(closestTo(dt, target)).toBe(target[1]); 20 | }); 21 | test("PlainDate", () => { 22 | const target = ["2024-01-01", "2024-01-02", "2023-12-23"].map((t) => Temporal.PlainDate.from(t)); 23 | const dt = Temporal.PlainDate.from("2024-01-03"); 24 | expect(closestTo(dt, target)).toBe(target[1]); 25 | }); 26 | 27 | test("PlainTime", () => { 28 | const target = ["03:00:00", "06:00:00", "23:45:00"].map((t) => Temporal.PlainTime.from(t)); 29 | const dt = Temporal.PlainTime.from("18:00:00"); 30 | expect(closestTo(dt, target)).toBe(target[2]); 31 | }); 32 | test("PlainDateTime", () => { 33 | const target = [ 34 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 35 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 36 | "2024-01-01T00:00:00-05:00[America/Toronto]", 37 | ].map((t) => Temporal.PlainDateTime.from(t)); 38 | const dt = Temporal.PlainDateTime.from("2024-01-01T04:00:00+03:00[Europe/Moscow]"); 39 | expect(closestTo(dt, target)).toBe(target[1]); 40 | }); 41 | test("PlainYearMonth", () => { 42 | const target = ["2023-12", "2024-01", "2023-08"].map((t) => Temporal.PlainYearMonth.from(t)); 43 | const dt = Temporal.PlainYearMonth.from("2023-11"); 44 | expect(closestTo(dt, target)).toBe(target[0]); 45 | }); 46 | test("PlainYearMonth of non-ISO calendar", () => { 47 | const target = [ 48 | { year: 5784, monthCode: "M05L" }, 49 | { year: 5784, monthCode: "M08" }, 50 | { year: 5784, monthCode: "M09" }, 51 | ].map((d) => Temporal.PlainYearMonth.from({ ...d, calendar: "hebrew" })); 52 | const dt = Temporal.PlainYearMonth.from({ 53 | year: 5784, 54 | monthCode: "M06", 55 | calendar: "hebrew", 56 | }); 57 | expect(closestTo(dt, target)).toBe(target[0]); 58 | expect(() => { 59 | closestTo(Temporal.PlainYearMonth.from("2024-01"), target); 60 | }).toThrowError(RangeError); 61 | }); 62 | 63 | test("Typecheck", () => { 64 | expect(() => { 65 | closestTo( 66 | Temporal.Now.zonedDateTimeISO(), 67 | // @ts-expect-error 68 | [Temporal.Now.instant()], 69 | ); 70 | }).toThrow(); 71 | }); 72 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import { defineConfig, globalIgnores } from "eslint/config"; 3 | import simpleImportSort from "eslint-plugin-simple-import-sort"; 4 | import globals from "globals"; 5 | import tsEslint from "typescript-eslint"; 6 | 7 | const __dirname = import.meta.dirname; 8 | 9 | export default defineConfig([ 10 | { 11 | plugins: { 12 | "@typescript-eslint": tsEslint.plugin, 13 | "simple-import-sort": simpleImportSort, 14 | }, 15 | }, 16 | globalIgnores(["dist", "_site", "eslint.config.js", "src/temporal.d.ts"]), 17 | { 18 | languageOptions: { 19 | parser: tsEslint.parser, 20 | ecmaVersion: "latest", 21 | parserOptions: { 22 | projectService: true, 23 | tsconfigRootDir: __dirname, 24 | }, 25 | }, 26 | }, 27 | eslint.configs.recommended, 28 | tsEslint.configs.strictTypeChecked, 29 | tsEslint.configs.stylisticTypeChecked, 30 | { 31 | rules: { 32 | camelcase: "error", 33 | eqeqeq: "error", 34 | "no-console": "error", 35 | "no-eval": "error", 36 | "@typescript-eslint/consistent-type-exports": "error", 37 | "@typescript-eslint/consistent-type-imports": "error", 38 | "@typescript-eslint/default-param-last": "error", 39 | "@typescript-eslint/method-signature-style": "error", 40 | "@typescript-eslint/no-import-type-side-effects": "error", 41 | "@typescript-eslint/no-loop-func": "error", 42 | "@typescript-eslint/no-require-imports": "error", 43 | "@typescript-eslint/no-unsafe-unary-minus": "error", 44 | "@typescript-eslint/require-array-sort-compare": "error", 45 | "@typescript-eslint/restrict-template-expressions": [ 46 | "error", 47 | { 48 | allowNever: true, 49 | }, 50 | ], 51 | "@typescript-eslint/strict-boolean-expressions": "error", 52 | "@typescript-eslint/switch-exhaustiveness-check": "error", 53 | "simple-import-sort/imports": "error", 54 | "simple-import-sort/exports": "error", 55 | }, 56 | }, 57 | { 58 | files: ["src/**/*.test.ts", "src/_test/**/*.{ts,js}", "script/**/*.{ts,js}"], 59 | languageOptions: { 60 | globals: globals.node, 61 | }, 62 | rules: { 63 | "no-console": "off", 64 | "@typescript-eslint/ban-ts-comment": "off", 65 | "@typescript-eslint/no-empty-function": "off", 66 | "@typescript-eslint/no-explicit-any": "off", 67 | "@typescript-eslint/no-non-null-assertion": "off", 68 | "@typescript-eslint/no-unsafe-argument": "off", 69 | "@typescript-eslint/no-unsafe-assignment": "off", 70 | "@typescript-eslint/no-unsafe-call": "off", 71 | "@typescript-eslint/no-unsafe-member-access": "off", 72 | "@typescript-eslint/no-unsafe-return": "off", 73 | "@typescript-eslint/no-unused-vars": [ 74 | "error", 75 | { argsIgnorePattern: "^_", ignoreUsingDeclarations: true }, 76 | ], 77 | }, 78 | }, 79 | ]); 80 | -------------------------------------------------------------------------------- /src/datetime/endOfMonth.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfMonth } from "./endOfMonth.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect(endOfMonth(Temporal.PlainDateTime.from("2024-01-23T12:34:56.789123456"))).toEqual( 7 | Temporal.PlainDateTime.from("2024-01-31T23:59:59.999999999"), 8 | ); 9 | expect(endOfMonth(Temporal.PlainDateTime.from("2024-02-23T12:34:56.789123456"))).toEqual( 10 | Temporal.PlainDateTime.from("2024-02-29T23:59:59.999999999"), 11 | ); 12 | expect(endOfMonth(Temporal.PlainDateTime.from("2025-02-23T12:34:56.789123456"))).toEqual( 13 | Temporal.PlainDateTime.from("2025-02-28T23:59:59.999999999"), 14 | ); 15 | }); 16 | 17 | test("PlainDate", () => { 18 | expect(endOfMonth(Temporal.PlainDate.from("2024-01-23"))).toEqual( 19 | Temporal.PlainDate.from("2024-01-31"), 20 | ); 21 | expect(endOfMonth(Temporal.PlainDate.from("2024-02-23"))).toEqual( 22 | Temporal.PlainDate.from("2024-02-29"), 23 | ); 24 | expect(endOfMonth(Temporal.PlainDate.from("2025-02-23"))).toEqual( 25 | Temporal.PlainDate.from("2025-02-28"), 26 | ); 27 | }); 28 | 29 | test("PlainDate with non-ISO calendar", () => { 30 | expect( 31 | // 19 Adar I 5784 32 | endOfMonth(Temporal.PlainDate.from("2024-02-28[u-ca=hebrew]")), 33 | ).toEqual(Temporal.PlainDate.from("2024-03-10[u-ca=hebrew]")); 34 | }); 35 | 36 | test("ZonedDateTime without offset transition", () => { 37 | expect(endOfMonth(Temporal.ZonedDateTime.from("2024-03-21T01:23:45+09:00[Asia/Tokyo]"))).toEqual( 38 | Temporal.ZonedDateTime.from("2024-03-31T23:59:59.999999999+09:00[Asia/Tokyo]"), 39 | ); 40 | }); 41 | 42 | test("ZonedDateTime and forward transition", () => { 43 | expect( 44 | endOfMonth(Temporal.ZonedDateTime.from("1994-12-29T00:00:00-10:00[Pacific/Kiritimati]")), 45 | ).toEqual(Temporal.ZonedDateTime.from("1994-12-30T23:59:59.999999999-10:00[Pacific/Kiritimati]")); 46 | expect( 47 | endOfMonth( 48 | Temporal.ZonedDateTime.from( 49 | // 11 Adar 5785 50 | "2025-03-25T00:00:00-02:00[America/Nuuk][u-ca=hebrew]", 51 | ), 52 | ), 53 | ).toEqual( 54 | Temporal.ZonedDateTime.from( 55 | // 29 Adar 5785 56 | "2025-03-29T22:59:59.999999999-02:00[America/Nuuk][u-ca=hebrew]", 57 | ), 58 | ); 59 | }); 60 | 61 | test("ZonedDateTime and backward transition", () => { 62 | expect( 63 | endOfMonth(Temporal.ZonedDateTime.from("1996-12-31T23:00:00-05:00[America/Managua]")), 64 | ).toEqual(Temporal.ZonedDateTime.from("1996-12-31T23:59:59.999999999-06:00[America/Managua]")); 65 | expect( 66 | endOfMonth( 67 | Temporal.ZonedDateTime.from( 68 | // 19 Elul 5738 69 | "1978-09-21T00:00:00+03:00[Asia/Famagusta][u-ca=hebrew]", 70 | ), 71 | ), 72 | ).toEqual( 73 | Temporal.ZonedDateTime.from( 74 | // 29 Elul 5738 75 | "1978-10-01T23:59:59.999999999+02:00[Asia/Famagusta][u-ca=hebrew]", 76 | ), 77 | ); 78 | }); 79 | -------------------------------------------------------------------------------- /src/datetime/closestIndexTo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { closestIndexTo } from "./closestIndexTo.js"; 4 | 5 | test("Instant", () => { 6 | const target = [1700000000, 1720000000, 1600000000].map((t) => 7 | Temporal.Instant.fromEpochMilliseconds(t * 1000), 8 | ); 9 | const dt = Temporal.Instant.fromEpochMilliseconds(1640000000 * 1000); 10 | expect(closestIndexTo(dt, target)).toBe(2); 11 | }); 12 | test("Instants which differs few nanoseconds", () => { 13 | const target = [1700000000000000000n, 1700000000000000001n, 1700000000000000004n].map((t) => 14 | Temporal.Instant.fromEpochNanoseconds(t), 15 | ); 16 | const dt = Temporal.Instant.fromEpochNanoseconds(1700000000000000003n); 17 | expect(closestIndexTo(dt, target)).toBe(2); 18 | }); 19 | test("ZonedDateTime", () => { 20 | const target = [ 21 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 22 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 23 | "2024-01-01T00:00:00-05:00[America/Toronto]", 24 | ].map((t) => Temporal.ZonedDateTime.from(t)); 25 | const dt = Temporal.ZonedDateTime.from("2024-01-01T11:30:00+09:00[Asia/Tokyo]"); 26 | expect(closestIndexTo(dt, target)).toBe(1); 27 | }); 28 | test("PlainDate", () => { 29 | const target = ["2024-01-01", "2024-01-02", "2023-12-23"].map((t) => Temporal.PlainDate.from(t)); 30 | const dt = Temporal.PlainDate.from("2024-01-03"); 31 | expect(closestIndexTo(dt, target)).toBe(1); 32 | }); 33 | 34 | test("PlainTime", () => { 35 | const target = ["03:00:00", "06:00:00", "23:45:00"].map((t) => Temporal.PlainTime.from(t)); 36 | const dt = Temporal.PlainTime.from("18:00:00"); 37 | expect(closestIndexTo(dt, target)).toBe(2); 38 | }); 39 | test("PlainDateTime", () => { 40 | const target = [ 41 | "2024-01-01T09:00:00+09:00[Asia/Tokyo]", 42 | "2024-01-01T03:00:00+01:00[Europe/Paris]", 43 | "2024-01-01T00:00:00-05:00[America/Toronto]", 44 | ].map((t) => Temporal.PlainDateTime.from(t)); 45 | const dt = Temporal.PlainDateTime.from("2024-01-01T04:00:00+03:00[Europe/Moscow]"); 46 | expect(closestIndexTo(dt, target)).toBe(1); 47 | }); 48 | test("PlainYearMonth", () => { 49 | const target = ["2023-12", "2024-01", "2023-08"].map((t) => Temporal.PlainYearMonth.from(t)); 50 | const dt = Temporal.PlainYearMonth.from("2023-11"); 51 | expect(closestIndexTo(dt, target)).toBe(0); 52 | }); 53 | test("PlainYearMonth of non-ISO calendar", () => { 54 | const target = [ 55 | { year: 5784, monthCode: "M05L" }, 56 | { year: 5784, monthCode: "M08" }, 57 | { year: 5784, monthCode: "M09" }, 58 | ].map((d) => Temporal.PlainYearMonth.from({ ...d, calendar: "hebrew" })); 59 | const dt = Temporal.PlainYearMonth.from({ 60 | year: 5784, 61 | monthCode: "M06", 62 | calendar: "hebrew", 63 | }); 64 | expect(closestIndexTo(dt, target)).toBe(0); 65 | expect(() => { 66 | closestIndexTo(Temporal.PlainYearMonth.from("2024-01"), target); 67 | }).toThrowError(RangeError); 68 | }); 69 | 70 | test("Typecheck", () => { 71 | expect(() => { 72 | closestIndexTo( 73 | Temporal.Now.zonedDateTimeISO(), 74 | // @ts-expect-error 75 | [Temporal.Now.instant()], 76 | ); 77 | }).toThrow(); 78 | }); 79 | -------------------------------------------------------------------------------- /src/datetime/startOfWeek.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { startOfWeek } from "./startOfWeek.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect( 7 | startOfWeek(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"), { 8 | firstDayOfWeek: 1, 9 | }), 10 | ).toEqual(Temporal.PlainDateTime.from("2024-01-01T00:00:00")); 11 | expect( 12 | startOfWeek(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"), { 13 | firstDayOfWeek: 7, 14 | }), 15 | ).toEqual(Temporal.PlainDateTime.from("2023-12-31T00:00:00")); 16 | expect( 17 | startOfWeek(Temporal.PlainDateTime.from("2024-01-07T12:34:56.789123456"), { 18 | firstDayOfWeek: 1, 19 | }), 20 | ).toEqual(Temporal.PlainDateTime.from("2024-01-01T00:00:00")); 21 | }); 22 | 23 | test("PlainDate", () => { 24 | expect(startOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: 1 })).toEqual( 25 | Temporal.PlainDate.from("2024-01-01"), 26 | ); 27 | expect(startOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: 7 })).toEqual( 28 | Temporal.PlainDate.from("2023-12-31"), 29 | ); 30 | expect( 31 | startOfWeek(Temporal.PlainDate.from("2024-01-07"), { 32 | firstDayOfWeek: 1, 33 | }), 34 | ).toEqual(Temporal.PlainDate.from("2024-01-01")); 35 | }); 36 | 37 | test("invalid day of week", () => { 38 | expect(() => { 39 | startOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: 8 }); 40 | }).toThrowError(); 41 | expect(() => { 42 | startOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: -1 }); 43 | }).toThrowError(); 44 | }); 45 | 46 | test("ZonedDateTime without offset transition", () => { 47 | expect( 48 | startOfWeek(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]"), { 49 | firstDayOfWeek: 1, 50 | }), 51 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]")); 52 | expect( 53 | startOfWeek(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]"), { 54 | firstDayOfWeek: 7, 55 | }), 56 | ).toEqual(Temporal.ZonedDateTime.from("2023-12-31T00:00:00+09:00[Asia/Tokyo]")); 57 | expect( 58 | startOfWeek(Temporal.ZonedDateTime.from("2024-01-07T12:34:56.789123456+09:00[Asia/Tokyo]"), { 59 | firstDayOfWeek: 1, 60 | }), 61 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T00:00:00+09:00[Asia/Tokyo]")); 62 | }); 63 | 64 | test("ZonedDateTime and forward transition", () => { 65 | expect( 66 | startOfWeek(Temporal.ZonedDateTime.from("2012-01-01T00:00:00+14:00[Pacific/Apia]"), { 67 | firstDayOfWeek: 5, 68 | }), 69 | ).toEqual(Temporal.ZonedDateTime.from("2011-12-31T00:00:00+14:00[Pacific/Apia]")); 70 | expect( 71 | startOfWeek(Temporal.ZonedDateTime.from("1919-04-02T00:00:00-04:00[America/Toronto]"), { 72 | firstDayOfWeek: 1, 73 | }), 74 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-31T00:30:00-04:00[America/Toronto]")); 75 | }); 76 | 77 | test("ZonedDateTime and backward transition", () => { 78 | expect( 79 | startOfWeek(Temporal.ZonedDateTime.from("2010-11-09T00:00:00-03:30[America/St_Johns]"), { 80 | firstDayOfWeek: 7, 81 | }), 82 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-07T00:00:00-02:30[America/St_Johns]")); 83 | }); 84 | -------------------------------------------------------------------------------- /src/datetime/withDayOfWeek.ts: -------------------------------------------------------------------------------- 1 | import { isZonedDateTime } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | 4 | export interface WithDayOfWeekOptions { 5 | /** 6 | * First day of the week. 7 | * For example, in ISO calendar Monday is `1`, Sunday is `7`. 8 | */ 9 | firstDayOfWeek: number; 10 | /** 11 | * Same to `disambiguation` option in `Temporal.ZonedDateTime.prototype.with()`. 12 | * For other types it will be simply ignored. 13 | */ 14 | disambiguation?: "compatible" | "earlier" | "later" | "reject"; 15 | /** 16 | * Same to `offset` option in `Temporal.ZonedDateTime.prototype.with()`. 17 | * For other types it will be simply ignored. 18 | */ 19 | offset?: "use" | "prefer" | "ignore" | "reject"; 20 | } 21 | 22 | function withDayOfWeekForClockTime( 23 | dt: Temporal.PlainDate | Temporal.PlainDateTime, 24 | dayOfWeek: number, 25 | firstDayOfWeek: number, 26 | ): Temporal.PlainDate | Temporal.PlainDateTime { 27 | const current = (dt.dayOfWeek - firstDayOfWeek + dt.daysInWeek) % dt.daysInWeek; 28 | const target = (dayOfWeek - firstDayOfWeek + dt.daysInWeek) % dt.daysInWeek; 29 | return dt.add({ days: target - current }); 30 | } 31 | 32 | /** 33 | * Returns the datetime in the same week with specified day of a week. 34 | * 35 | * For `ZonedDateTime` this function behave like `Temporal.ZonedDateTime.prototype.with()`. 36 | * As well as `Temporal.ZonedDateTime.prototype.with()`, the result can be previous or next day of the desired day 37 | * depending on the `disambiguation` option for some edge cases. 38 | * 39 | * ```typescript 40 | * // In Greenland, 2024-03-30T23:10:00 doesn't exist due to DST 41 | * withDayOfWeek( 42 | * Temporal.ZonedDateTime.from("2024-03-29T23:10:00-02:00[America/Nuuk]"), 43 | * 6, // Saturday 44 | * { firstDayOfWeek: 1, disambiguation: "compatible" } 45 | * ); // 2024-03-31T00:10:00-01:00, Sunday! 46 | * // In Samoa, 2011-12-30 (Friday) is completely skipped due to an offset transition from `-10:00` to `+14:00` 47 | * withDayOfWeek( 48 | * Temporal.ZonedDateTime.from("2011-12-29T00:00:00-10:00[Pacific/Apia]"), 49 | * 5, // Friday 50 | * { firstDayOfWeek: 1, disambiguation: "compatible" } 51 | * ); // 2011-12-31T00:00:00+14:00, Saturday! 52 | * ``` 53 | * 54 | * 'same week' is ambiguous and locale-dependent, 55 | * so `firstDayOfWeek` option is required. 56 | * 57 | * This function supports a calendar with a fixed `daysInWeek`, 58 | * even if the week contains more or less than 7 days. 59 | * But it doesn't support a calendar which lacks a fixed number of days. 60 | */ 61 | export function withDayOfWeek< 62 | DateTime extends Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, 63 | >(dt: DateTime, dayOfWeek: number, options: WithDayOfWeekOptions): DateTime { 64 | const firstDayOfWeek = options.firstDayOfWeek; 65 | if (!Number.isInteger(firstDayOfWeek) || firstDayOfWeek < 1 || firstDayOfWeek > dt.daysInWeek) { 66 | throw new Error(`${firstDayOfWeek} isn't a valid day of week`); 67 | } 68 | if (isZonedDateTime(dt)) { 69 | const date = withDayOfWeekForClockTime(dt.toPlainDate(), dayOfWeek, firstDayOfWeek); 70 | return dt.with( 71 | { 72 | year: date.year, 73 | month: date.month, 74 | day: date.day, 75 | }, 76 | { disambiguation: options.disambiguation, offset: options.offset }, 77 | ) as DateTime; 78 | } 79 | return withDayOfWeekForClockTime(dt, dayOfWeek, firstDayOfWeek) as DateTime; 80 | } 81 | -------------------------------------------------------------------------------- /src/datetime/fromRfc7231.ts: -------------------------------------------------------------------------------- 1 | import { isInstantConstructor, isZonedDateTimeConstructor } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { formatDateIso } from "./_formatDateIso.js"; 4 | import { formatExactTimeIso } from "./_formatExactTimeIso.js"; 5 | import { formatHmsIso } from "./_formatHmsIso.js"; 6 | import { getDayOfWeekFromYmd } from "./_getDayOfWeekFromYmd.js"; 7 | import { getDayOfWeekNumberFromAbbreviation } from "./_getDayOfWeekNumberFromAbbreviation.js"; 8 | import { getMonthNumberFromAbbreviation } from "./_getMonthNumberFromAbbreviation.js"; 9 | import { isValidHms } from "./_isValidHms.js"; 10 | import { isValidYmd } from "./_isValidYmd.js"; 11 | 12 | const regex = /^([A-Za-z]{3}), (\d\d) ([A-Za-z]{3}) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/; 13 | 14 | function parse( 15 | date: string, 16 | ): [year: number, month: number, day: number, hour: number, minute: number, second: number] { 17 | const result = regex.exec(date); 18 | if (result === null) { 19 | throw new Error("Invalid format"); 20 | } 21 | const [, dayOfWeekName, day, monthName, year, hour, minute, second] = result; 22 | if ( 23 | dayOfWeekName === undefined || 24 | day === undefined || 25 | monthName === undefined || 26 | year === undefined || 27 | hour === undefined || 28 | minute === undefined || 29 | second === undefined 30 | ) { 31 | throw new Error("something wrong"); 32 | } 33 | const y = parseInt(year); 34 | const mo = getMonthNumberFromAbbreviation(monthName); 35 | const d = parseInt(day); 36 | const h = parseInt(hour); 37 | const mi = parseInt(minute); 38 | const s = parseInt(second); 39 | const dayOfWeek = getDayOfWeekNumberFromAbbreviation(dayOfWeekName); 40 | 41 | if (!isValidYmd(y, mo, d)) { 42 | throw new Error(`Invalid date: ${formatDateIso(y, mo, d)}`); 43 | } 44 | // leap second should occur only in 23:59:60 UTC 45 | if (!isValidHms(h, mi, s, true) || (s === 60 && (h !== 23 || mi !== 59))) { 46 | throw new Error(`Invalid time: ${formatHmsIso(h, mi, s)}`); 47 | } 48 | if (getDayOfWeekFromYmd(y, mo, d) !== dayOfWeek) { 49 | throw new Error(`Wrong day of week: ${dayOfWeekName}`); 50 | } 51 | return [y, mo, d, h, mi, s]; 52 | } 53 | 54 | /** 55 | * Creates Temporal object from datetime string in RFC 7231's format (HTTP date format) 56 | * such as `Mon, 01 Jan 2024 01:23:45 GMT`. 57 | * This function doesn't support obsoleted formats. 58 | * 59 | * @param date datetime string in RFC 7231's format 60 | * @param TemporalClass Temporal class (such as `Temporal.PlainDateTime` or `Temporal.Instant`) which will be returned 61 | * @returns an instance of Temporal class specified in `TemporalClass` argument 62 | */ 63 | export function fromRfc7231< 64 | TemporalClassType extends 65 | | typeof Temporal.Instant 66 | | typeof Temporal.ZonedDateTime 67 | | typeof Temporal.PlainDateTime, 68 | >(date: string, TemporalClass: TemporalClassType): InstanceType { 69 | const result = parse(date); 70 | const [year, month, day, hour, minute, second] = result; 71 | if (isInstantConstructor(TemporalClass)) { 72 | return TemporalClass.from( 73 | formatExactTimeIso(year, month, day, hour, minute, second, 0, "Z"), 74 | ) as InstanceType; 75 | } 76 | if (isZonedDateTimeConstructor(TemporalClass)) { 77 | return TemporalClass.from({ 78 | year, 79 | month, 80 | day, 81 | hour, 82 | minute, 83 | second, 84 | calendarId: "iso8601", 85 | timeZone: "UTC", 86 | }) as InstanceType; 87 | } 88 | return TemporalClass.from({ 89 | year, 90 | month, 91 | day, 92 | hour, 93 | minute, 94 | second, 95 | calendarId: "iso8601", 96 | }) as InstanceType; 97 | } 98 | -------------------------------------------------------------------------------- /src/datetime/clamp.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { clamp } from "./clamp.js"; 4 | 5 | test("Instant", () => { 6 | const i = { 7 | start: Temporal.Instant.from("2024-01-01T00:00:00Z"), 8 | end: Temporal.Instant.from("2024-01-04T00:00:00Z"), 9 | }; 10 | expect(clamp(Temporal.Instant.from("2023-12-31T00:00:00Z"), i)).toEqual(i.start); 11 | expect(clamp(Temporal.Instant.from("2024-01-02T00:00:00Z"), i)).toEqual( 12 | Temporal.Instant.from("2024-01-02T00:00:00Z"), 13 | ); 14 | expect(clamp(Temporal.Instant.from("2024-01-05T00:00:00Z"), i)).toEqual(i.end); 15 | expect(clamp(i.start, i)).toEqual(i.start); 16 | expect(clamp(i.end, i)).toEqual(i.end); 17 | }); 18 | 19 | test("ZonedDateTime", () => { 20 | const i = { 21 | start: Temporal.ZonedDateTime.from("2024-01-01T00:00:00Z[Europe/London]"), 22 | end: Temporal.ZonedDateTime.from("2024-01-04T00:00:00Z[Europe/London]"), 23 | }; 24 | expect(clamp(Temporal.ZonedDateTime.from("2023-12-31T00:00:00Z[Europe/London]"), i)).toEqual( 25 | i.start, 26 | ); 27 | expect(clamp(Temporal.ZonedDateTime.from("2024-01-02T00:00:00Z[Europe/London]"), i)).toEqual( 28 | Temporal.ZonedDateTime.from("2024-01-02T00:00:00Z[Europe/London]"), 29 | ); 30 | expect(clamp(Temporal.ZonedDateTime.from("2024-01-05T00:00:00Z[Europe/London]"), i)).toEqual( 31 | i.end, 32 | ); 33 | // if the given ZonedDateTime represents same exact time with `i.start` or `i.end`, 34 | // the given ZonedDateTime (not `i.start` nor `i.end`) should be returned 35 | expect(clamp(Temporal.ZonedDateTime.from("2024-01-01T09:00:00+09:00[Asia/Tokyo]"), i)).toEqual( 36 | Temporal.ZonedDateTime.from("2024-01-01T09:00:00+09:00[Asia/Tokyo]"), 37 | ); 38 | expect(clamp(i.start, i)).toEqual(i.start); 39 | expect(clamp(i.end, i)).toEqual(i.end); 40 | }); 41 | 42 | test("PlainDate", () => { 43 | const i = { 44 | start: Temporal.PlainDate.from("2024-01-01"), 45 | end: Temporal.PlainDate.from("2024-01-04"), 46 | }; 47 | expect(clamp(Temporal.PlainDate.from("2023-12-31"), i)).toEqual(i.start); 48 | expect(clamp(Temporal.PlainDate.from("2024-01-02"), i)).toEqual( 49 | Temporal.PlainDate.from("2024-01-02"), 50 | ); 51 | expect(clamp(Temporal.PlainDate.from("2024-01-05"), i)).toEqual(i.end); 52 | expect(clamp(i.start, i)).toEqual(i.start); 53 | expect(clamp(i.end, i)).toEqual(i.end); 54 | }); 55 | 56 | test("PlainTime", () => { 57 | const i = { 58 | start: Temporal.PlainTime.from("06:00:00"), 59 | end: Temporal.PlainTime.from("18:00:00"), 60 | }; 61 | expect(clamp(Temporal.PlainTime.from("03:00:00"), i)).toEqual(i.start); 62 | expect(clamp(Temporal.PlainTime.from("12:00:00"), i)).toEqual( 63 | Temporal.PlainTime.from("12:00:00"), 64 | ); 65 | expect(clamp(Temporal.PlainTime.from("21:00:00"), i)).toEqual(i.end); 66 | expect(clamp(i.start, i)).toEqual(i.start); 67 | expect(clamp(i.end, i)).toEqual(i.end); 68 | }); 69 | 70 | test("PlainDateTime", () => { 71 | const i = { 72 | start: Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 73 | end: Temporal.PlainDateTime.from("2024-01-04T00:00:00"), 74 | }; 75 | expect(clamp(Temporal.PlainDateTime.from("2023-12-31T00:00:00"), i)).toEqual(i.start); 76 | expect(clamp(Temporal.PlainDateTime.from("2024-01-02T00:00:00"), i)).toEqual( 77 | Temporal.PlainDateTime.from("2024-01-02T00:00:00"), 78 | ); 79 | expect(clamp(Temporal.PlainDateTime.from("2024-01-05T00:00:00"), i)).toEqual(i.end); 80 | expect(clamp(i.start, i)).toEqual(i.start); 81 | expect(clamp(i.end, i)).toEqual(i.end); 82 | }); 83 | 84 | test("PlainYearMonth", () => { 85 | const i = { 86 | start: Temporal.PlainYearMonth.from("2024-01"), 87 | end: Temporal.PlainYearMonth.from("2024-04"), 88 | }; 89 | expect(clamp(Temporal.PlainYearMonth.from("2023-12"), i)).toEqual(i.start); 90 | expect(clamp(Temporal.PlainYearMonth.from("2024-03"), i)).toEqual( 91 | Temporal.PlainYearMonth.from("2024-03"), 92 | ); 93 | expect(clamp(Temporal.PlainYearMonth.from("2024-05"), i)).toEqual(i.end); 94 | expect(clamp(i.start, i)).toEqual(i.start); 95 | expect(clamp(i.end, i)).toEqual(i.end); 96 | }); 97 | -------------------------------------------------------------------------------- /src/datetime/isWithinInterval.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { isWithinInterval } from "./isWithinInterval.js"; 4 | 5 | test("Instant", () => { 6 | const interval = { 7 | start: Temporal.Instant.from("2024-01-01T00:00:00Z"), 8 | end: Temporal.Instant.from("2024-01-03T00:00:00Z"), 9 | }; 10 | expect(isWithinInterval(Temporal.Instant.from("2024-01-02T00:00:00Z"), interval)).toEqual(true); 11 | expect(isWithinInterval(Temporal.Instant.from("2023-12-31T00:00:00Z"), interval)).toEqual(false); 12 | expect(isWithinInterval(Temporal.Instant.from("2024-01-04T00:00:00Z"), interval)).toEqual(false); 13 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 14 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 15 | }); 16 | 17 | test("ZonedDateTime", () => { 18 | const interval = { 19 | start: Temporal.ZonedDateTime.from("2024-01-01T00:00:00Z[Europe/London]"), 20 | end: Temporal.ZonedDateTime.from("2024-01-03T00:00:00Z[Europe/London]"), 21 | }; 22 | expect( 23 | isWithinInterval(Temporal.ZonedDateTime.from("2024-01-02T00:00:00Z[Europe/London]"), interval), 24 | ).toEqual(true); 25 | expect( 26 | isWithinInterval(Temporal.ZonedDateTime.from("2023-12-31T00:00:00Z[Europe/London]"), interval), 27 | ).toEqual(false); 28 | expect( 29 | isWithinInterval(Temporal.ZonedDateTime.from("2024-01-04T00:00:00Z[Europe/London]"), interval), 30 | ).toEqual(false); 31 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 32 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 33 | }); 34 | 35 | test("PlainDate", () => { 36 | const interval = { 37 | start: Temporal.PlainDate.from("2024-01-01"), 38 | end: Temporal.PlainDate.from("2024-01-03"), 39 | }; 40 | expect(isWithinInterval(Temporal.PlainDate.from("2024-01-02"), interval)).toEqual(true); 41 | expect(isWithinInterval(Temporal.PlainDate.from("2023-12-31"), interval)).toEqual(false); 42 | expect(isWithinInterval(Temporal.PlainDate.from("2024-01-04"), interval)).toEqual(false); 43 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 44 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 45 | }); 46 | 47 | test("PlainTime", () => { 48 | const interval = { 49 | start: Temporal.PlainTime.from("08:00:00"), 50 | end: Temporal.PlainTime.from("16:00:00"), 51 | }; 52 | expect(isWithinInterval(Temporal.PlainTime.from("12:00:00"), interval)).toEqual(true); 53 | expect(isWithinInterval(Temporal.PlainTime.from("04:00:00"), interval)).toEqual(false); 54 | expect(isWithinInterval(Temporal.PlainTime.from("20:00:00"), interval)).toEqual(false); 55 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 56 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 57 | }); 58 | 59 | test("PlainDateTime", () => { 60 | const interval = { 61 | start: Temporal.PlainDateTime.from("2024-01-01T00:00:00"), 62 | end: Temporal.PlainDateTime.from("2024-01-03T00:00:00"), 63 | }; 64 | expect(isWithinInterval(Temporal.PlainDateTime.from("2024-01-02T00:00:00"), interval)).toEqual( 65 | true, 66 | ); 67 | expect(isWithinInterval(Temporal.PlainDateTime.from("2023-12-31T00:00:00"), interval)).toEqual( 68 | false, 69 | ); 70 | expect(isWithinInterval(Temporal.PlainDateTime.from("2024-01-04T00:00:00"), interval)).toEqual( 71 | false, 72 | ); 73 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 74 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 75 | }); 76 | 77 | test("PlainYearMonth", () => { 78 | const interval = { 79 | start: Temporal.PlainYearMonth.from("2024-01"), 80 | end: Temporal.PlainYearMonth.from("2024-03"), 81 | }; 82 | expect(isWithinInterval(Temporal.PlainYearMonth.from("2024-02"), interval)).toEqual(true); 83 | expect(isWithinInterval(Temporal.PlainYearMonth.from("2023-12"), interval)).toEqual(false); 84 | expect(isWithinInterval(Temporal.PlainYearMonth.from("2024-04"), interval)).toEqual(false); 85 | expect(isWithinInterval(interval.start, interval)).toEqual(true); 86 | expect(isWithinInterval(interval.end, interval)).toEqual(true); 87 | }); 88 | -------------------------------------------------------------------------------- /src/datetime/fromRfc7231.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { fromRfc7231 } from "./fromRfc7231.js"; 4 | 5 | test("Instant", () => { 6 | expect(fromRfc7231("Fri, 07 Jun 2024 01:23:45 GMT", Temporal.Instant)).toEqual( 7 | Temporal.Instant.from("2024-06-07T01:23:45Z"), 8 | ); 9 | }); 10 | 11 | test("PlainDateTime", () => { 12 | expect(fromRfc7231("Fri, 07 Jun 2024 01:23:45 GMT", Temporal.PlainDateTime)).toEqual( 13 | Temporal.PlainDateTime.from("2024-06-07T01:23:45"), 14 | ); 15 | }); 16 | 17 | test("ZonedDateTime", () => { 18 | expect(fromRfc7231("Fri, 07 Jun 2024 01:23:45 GMT", Temporal.ZonedDateTime)).toEqual( 19 | Temporal.ZonedDateTime.from("2024-06-07T01:23:45+00:00[UTC]"), 20 | ); 21 | }); 22 | 23 | test("wrong format", () => { 24 | expect(() => { 25 | fromRfc7231("Fri, 7 Jun 2024 01:23:45 GMT", Temporal.Instant); 26 | }).toThrowError(); 27 | }); 28 | 29 | test("wrong day of week", () => { 30 | expect(() => { 31 | fromRfc7231("Tue, 07 Jun 2024 01:23:45 GMT", Temporal.Instant); 32 | }).toThrowError(/Tue/); 33 | expect(() => { 34 | fromRfc7231("Tue, 07 Jun 2024 01:23:45 GMT", Temporal.PlainDateTime); 35 | }).toThrowError(/Tue/); 36 | expect(() => { 37 | fromRfc7231("Tue, 07 Jun 2024 01:23:45 GMT", Temporal.ZonedDateTime); 38 | }).toThrowError(/Tue/); 39 | }); 40 | 41 | test("invalid day of week", () => { 42 | expect(() => { 43 | fromRfc7231("Mot, 07 Jun 2024 01:23:45 GMT", Temporal.Instant); 44 | }).toThrowError(/Mot/); 45 | expect(() => { 46 | fromRfc7231("Mot, 07 Jun 2024 01:23:45 GMT", Temporal.PlainDateTime); 47 | }).toThrowError(/Mot/); 48 | expect(() => { 49 | fromRfc7231("Mot, 07 Jun 2024 01:23:45 GMT", Temporal.ZonedDateTime); 50 | }).toThrowError(/Mot/); 51 | }); 52 | 53 | test("invalid month", () => { 54 | expect(() => { 55 | fromRfc7231("Mon, 07 Jut 2024 01:23:45 GMT", Temporal.Instant); 56 | }).toThrowError(/Jut/); 57 | expect(() => { 58 | fromRfc7231("Mon, 07 Jut 2024 01:23:45 GMT", Temporal.PlainDateTime); 59 | }).toThrowError(/Jut/); 60 | expect(() => { 61 | fromRfc7231("Mon, 07 Jut 2024 01:23:45 GMT", Temporal.ZonedDateTime); 62 | }).toThrowError(/Jut/); 63 | }); 64 | 65 | test("leap second", () => { 66 | const rfc7231 = "Fri, 07 Jun 2024 23:59:60 GMT"; 67 | const result = "2024-06-07T23:59:59+00:00[UTC]"; 68 | expect(fromRfc7231(rfc7231, Temporal.Instant)).toEqual(Temporal.Instant.from(result)); 69 | expect(fromRfc7231(rfc7231, Temporal.ZonedDateTime)).toEqual(Temporal.ZonedDateTime.from(result)); 70 | expect(fromRfc7231(rfc7231, Temporal.PlainDateTime)).toEqual(Temporal.PlainDateTime.from(result)); 71 | }); 72 | 73 | test("Invalid combination of year, month, and day", () => { 74 | expect(() => { 75 | fromRfc7231("Sat, 29 Feb 2025 00:00:00 GMT", Temporal.Instant); 76 | }).toThrow(); 77 | expect(() => { 78 | fromRfc7231("Sat, 29 Feb 2025 00:00:00 GMT", Temporal.PlainDateTime); 79 | }).toThrow(); 80 | expect(() => { 81 | fromRfc7231("Sat, 29 Feb 2025 00:00:00 GMT", Temporal.ZonedDateTime); 82 | }).toThrow(); 83 | }); 84 | 85 | test.for(["24:00:00", "23:58:60", "12:60:00"])("Invalid hour, minute, and second (%s)", (time) => { 86 | const rfc7231 = `Fri, 07 Jun 2024 ${time} GMT`; 87 | expect(() => { 88 | fromRfc7231(rfc7231, Temporal.Instant); 89 | }).toThrow(); 90 | expect(() => { 91 | fromRfc7231(rfc7231, Temporal.PlainDateTime); 92 | }).toThrow(); 93 | expect(() => { 94 | fromRfc7231(rfc7231, Temporal.ZonedDateTime); 95 | }).toThrow(); 96 | }); 97 | 98 | test.for([ 99 | ["Fri, 07 Jun 0824 01:23:45 GMT", "0824-06-07T01:23:45+00:00[UTC]"], 100 | ["Fri, 07 Jun 0024 01:23:45 GMT", "0024-06-07T01:23:45+00:00[UTC]"], 101 | ["Sat, 01 Jan 0000 01:23:45 GMT", "0000-01-01T01:23:45+00:00[UTC]"], 102 | ] as [string, string][])("when year is less than 1000", ([rfc7231, iso8601]) => { 103 | expect(fromRfc7231(rfc7231, Temporal.Instant)).toEqual(Temporal.Instant.from(iso8601)); 104 | expect(fromRfc7231(rfc7231, Temporal.PlainDateTime)).toEqual( 105 | Temporal.PlainDateTime.from(iso8601), 106 | ); 107 | expect(fromRfc7231(rfc7231, Temporal.ZonedDateTime)).toEqual( 108 | Temporal.ZonedDateTime.from(iso8601), 109 | ); 110 | }); 111 | -------------------------------------------------------------------------------- /src/datetime/endOfWeek.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { endOfWeek } from "./endOfWeek.js"; 4 | 5 | test("PlainDateTime", () => { 6 | expect( 7 | endOfWeek(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"), { 8 | firstDayOfWeek: 1, 9 | }), 10 | ).toEqual(Temporal.PlainDateTime.from("2024-01-07T23:59:59.999999999")); 11 | expect( 12 | endOfWeek(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"), { 13 | firstDayOfWeek: 2, 14 | }), 15 | ).toEqual(Temporal.PlainDateTime.from("2024-01-01T23:59:59.999999999")); 16 | expect( 17 | endOfWeek(Temporal.PlainDateTime.from("2024-01-02T12:34:56.789123456"), { 18 | firstDayOfWeek: 2, 19 | }), 20 | ).toEqual(Temporal.PlainDateTime.from("2024-01-08T23:59:59.999999999")); 21 | expect( 22 | endOfWeek(Temporal.PlainDateTime.from("2024-01-01T12:34:56.789123456"), { 23 | firstDayOfWeek: 7, 24 | }), 25 | ).toEqual(Temporal.PlainDateTime.from("2024-01-06T23:59:59.999999999")); 26 | expect( 27 | endOfWeek(Temporal.PlainDateTime.from("2024-01-07T12:34:56.789123456"), { 28 | firstDayOfWeek: 7, 29 | }), 30 | ).toEqual(Temporal.PlainDateTime.from("2024-01-13T23:59:59.999999999")); 31 | }); 32 | 33 | test("PlainDate", () => { 34 | expect( 35 | endOfWeek(Temporal.PlainDate.from("2024-01-01"), { 36 | firstDayOfWeek: 1, 37 | }), 38 | ).toEqual(Temporal.PlainDate.from("2024-01-07")); 39 | expect( 40 | endOfWeek(Temporal.PlainDate.from("2024-01-01"), { 41 | firstDayOfWeek: 2, 42 | }), 43 | ).toEqual(Temporal.PlainDate.from("2024-01-01")); 44 | expect( 45 | endOfWeek(Temporal.PlainDate.from("2024-01-02"), { 46 | firstDayOfWeek: 2, 47 | }), 48 | ).toEqual(Temporal.PlainDate.from("2024-01-08")); 49 | expect( 50 | endOfWeek(Temporal.PlainDate.from("2024-01-01"), { 51 | firstDayOfWeek: 7, 52 | }), 53 | ).toEqual(Temporal.PlainDate.from("2024-01-06")); 54 | expect( 55 | endOfWeek(Temporal.PlainDate.from("2024-01-07"), { 56 | firstDayOfWeek: 7, 57 | }), 58 | ).toEqual(Temporal.PlainDate.from("2024-01-13")); 59 | }); 60 | 61 | test("invalid day of week", () => { 62 | expect(() => { 63 | endOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: 8 }); 64 | }).toThrowError(); 65 | expect(() => { 66 | endOfWeek(Temporal.PlainDate.from("2024-01-01"), { firstDayOfWeek: -1 }); 67 | }).toThrowError(); 68 | }); 69 | 70 | test("ZonedDateTime without offset transition", () => { 71 | expect( 72 | endOfWeek(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]"), { 73 | firstDayOfWeek: 1, 74 | }), 75 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-07T23:59:59.999999999+09:00[Asia/Tokyo]")); 76 | expect( 77 | endOfWeek(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]"), { 78 | firstDayOfWeek: 2, 79 | }), 80 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-01T23:59:59.999999999+09:00[Asia/Tokyo]")); 81 | expect( 82 | endOfWeek(Temporal.ZonedDateTime.from("2024-01-02T12:34:56.789123456+09:00[Asia/Tokyo]"), { 83 | firstDayOfWeek: 2, 84 | }), 85 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-08T23:59:59.999999999+09:00[Asia/Tokyo]")); 86 | expect( 87 | endOfWeek(Temporal.ZonedDateTime.from("2024-01-01T12:34:56.789123456+09:00[Asia/Tokyo]"), { 88 | firstDayOfWeek: 7, 89 | }), 90 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-06T23:59:59.999999999+09:00[Asia/Tokyo]")); 91 | expect( 92 | endOfWeek(Temporal.ZonedDateTime.from("2024-01-07T12:34:56.789123456+09:00[Asia/Tokyo]"), { 93 | firstDayOfWeek: 7, 94 | }), 95 | ).toEqual(Temporal.ZonedDateTime.from("2024-01-13T23:59:59.999999999+09:00[Asia/Tokyo]")); 96 | }); 97 | 98 | test("ZonedDateTime and forward transition", () => { 99 | expect( 100 | endOfWeek(Temporal.ZonedDateTime.from("2011-12-27T00:00:00-10:00[Pacific/Apia]"), { 101 | firstDayOfWeek: 6, 102 | }), 103 | ).toEqual(Temporal.ZonedDateTime.from("2011-12-29T23:59:59.999999999-10:00[Pacific/Apia]")); 104 | expect( 105 | endOfWeek(Temporal.ZonedDateTime.from("1919-03-26T00:00:00-05:00[America/Toronto]"), { 106 | firstDayOfWeek: 1, 107 | }), 108 | ).toEqual(Temporal.ZonedDateTime.from("1919-03-30T23:29:59.999999999-05:00[America/Toronto]")); 109 | }); 110 | 111 | test("ZonedDateTime and backward transition", () => { 112 | expect( 113 | endOfWeek(Temporal.ZonedDateTime.from("2010-11-03T00:00:00-02:30[America/St_Johns]"), { 114 | firstDayOfWeek: 7, 115 | }), 116 | ).toEqual(Temporal.ZonedDateTime.from("2010-11-06T23:59:59.999999999-03:30[America/St_Johns]")); 117 | }); 118 | -------------------------------------------------------------------------------- /src/datetime/toDateFromClockTime.ts: -------------------------------------------------------------------------------- 1 | import { UTCDate } from "@date-fns/utc"; 2 | 3 | import { 4 | isPlainDate, 5 | isPlainMonthDay, 6 | isPlainTime, 7 | isPlainYearMonth, 8 | isZonedDateTime, 9 | } from "../type-utils.js"; 10 | import type { GenericDateConstructor, Temporal } from "../types.js"; 11 | import { createDateFromClockTime } from "./_createDateFromClockTime.js"; 12 | 13 | function parseIsoString(date: string) { 14 | const res = /^(\d{4,}|[+-]\d{6})-(\d{2})-(\d{2})/.exec(date); 15 | if (res === null) { 16 | throw new Error("Invalid format"); 17 | } 18 | const [, y, m, d] = res; 19 | if (y === undefined || m === undefined || d === undefined) { 20 | throw new Error("Invalid format"); 21 | } 22 | return { 23 | year: parseInt(y, 10), 24 | month: parseInt(m, 10), 25 | day: parseInt(d, 10), 26 | }; 27 | } 28 | 29 | // function to bypass an enigmatic TypeScript error "could be instantiated with a different subtype of constraint" 30 | function createDate( 31 | DateConstructor: GenericDateConstructor | undefined, 32 | year: number, 33 | month: number, 34 | day = 1, 35 | hour = 0, 36 | minute = 0, 37 | second = 0, 38 | millisecond = 0, 39 | ) { 40 | return DateConstructor 41 | ? createDateFromClockTime(DateConstructor, year, month, day, hour, minute, second, millisecond) 42 | : createDateFromClockTime(UTCDate, year, month, day, hour, minute, second, millisecond); 43 | } 44 | 45 | /** 46 | * Returns `Date` which represents clock (local) time of given temporal object, 47 | * dropping timezone and calendar information. 48 | * When you pass `ZonedDateTime`, clock time will be unchanged but exact time will change. 49 | * This function is useful when you want to use formatting functions of [date-fns](https://date-fns.org/). 50 | * 51 | * @param dateTime datetime object 52 | */ 53 | export function toDateFromClockTime( 54 | dateTime: 55 | | Temporal.ZonedDateTime 56 | | Temporal.PlainDate 57 | | Temporal.PlainTime 58 | | Temporal.PlainDateTime 59 | | Temporal.PlainYearMonth 60 | | Temporal.PlainMonthDay, 61 | ): UTCDate; 62 | /** 63 | * Returns `Date` which represents clock (local) time of given temporal object, 64 | * dropping timezone and calendar information. 65 | * When you pass `ZonedDateTime`, clock time will be unchanged but exact time will change. 66 | * This function is useful when you want to use formatting functions of [date-fns](https://date-fns.org/). 67 | * You can pass `DateConstructor` parameter to specify a constructor to build the date to return, 68 | * but passing JavaScript's `Date` is **strongly discouraged** because `Date` is a hotbed of timezone troubles. 69 | * 70 | * @example 71 | * ```typescript 72 | * import { UTCDate } from "@date-fns/utc"; 73 | * toDateFromClockTime(Temporal.Now.plainDateISO(), UTCDate); 74 | * ``` 75 | * 76 | * @param dateTime datetime object 77 | * @param DateConstructor constructor of return value, `UTCDate` from "@date-fns/utc" as default 78 | */ 79 | export function toDateFromClockTime( 80 | dateTime: 81 | | Temporal.ZonedDateTime 82 | | Temporal.PlainDate 83 | | Temporal.PlainTime 84 | | Temporal.PlainDateTime 85 | | Temporal.PlainYearMonth 86 | | Temporal.PlainMonthDay, 87 | DateConstructor: GenericDateConstructor, 88 | ): DateType; 89 | export function toDateFromClockTime( 90 | dateTime: 91 | | Temporal.ZonedDateTime 92 | | Temporal.PlainDate 93 | | Temporal.PlainTime 94 | | Temporal.PlainDateTime 95 | | Temporal.PlainYearMonth 96 | | Temporal.PlainMonthDay, 97 | DateConstructor?: GenericDateConstructor, 98 | ) { 99 | if (isPlainYearMonth(dateTime)) { 100 | const pd = dateTime.toPlainDate({ day: 1 }).withCalendar("iso8601"); 101 | return createDate(DateConstructor, pd.year, pd.month, pd.day); 102 | } 103 | if (isPlainMonthDay(dateTime)) { 104 | if (dateTime.calendarId === "iso8601") { 105 | const pd = dateTime.toPlainDate({ year: 1972 }); 106 | return createDate(DateConstructor, pd.year, pd.month, pd.day); 107 | } 108 | const { year, month, day } = parseIsoString(dateTime.toString()); 109 | return createDate(DateConstructor, year, month, day); 110 | } 111 | if (isPlainTime(dateTime)) { 112 | // Set default date to 2000-01-01 113 | return createDate( 114 | DateConstructor, 115 | 2000, 116 | 0, 117 | 1, 118 | dateTime.hour, 119 | dateTime.minute, 120 | dateTime.second, 121 | dateTime.millisecond, 122 | ); 123 | } 124 | const plainDateTime = isZonedDateTime(dateTime) 125 | ? dateTime.toPlainDateTime().withCalendar("iso8601") 126 | : isPlainDate(dateTime) 127 | ? dateTime.toPlainDateTime().withCalendar("iso8601") 128 | : dateTime.withCalendar("iso8601"); 129 | return createDate( 130 | DateConstructor, 131 | plainDateTime.year, 132 | plainDateTime.month, 133 | plainDateTime.day, 134 | plainDateTime.hour, 135 | plainDateTime.minute, 136 | plainDateTime.second, 137 | plainDateTime.millisecond, 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /src/datetime/toDateFromClockTime.test.ts: -------------------------------------------------------------------------------- 1 | import { UTCDate, UTCDateMini } from "@date-fns/utc"; 2 | import { lightFormat } from "date-fns"; 3 | import { expect, test } from "vitest"; 4 | 5 | import { modifyTimeZone } from "../_test/modifyTimeZone.js"; 6 | import { toDateFromClockTime } from "./toDateFromClockTime.js"; 7 | 8 | test("ZonedDateTime", () => { 9 | const date = toDateFromClockTime( 10 | Temporal.ZonedDateTime.from("2024-01-01T03:00:00-05:00[America/Toronto]"), 11 | ); 12 | expect(lightFormat(date, "yyyy-MM-dd HH:mm:ss")).toBe("2024-01-01 03:00:00"); 13 | }); 14 | test("PlainDate", () => { 15 | const date = toDateFromClockTime(Temporal.PlainDate.from("2024-01-01")); 16 | expect(lightFormat(date, "yyyy-MM-dd")).toBe("2024-01-01"); 17 | }); 18 | test("PlainTime", () => { 19 | const date = toDateFromClockTime(Temporal.PlainTime.from("04:00:00")); 20 | expect(lightFormat(date, "HH:mm:ss")).toBe("04:00:00"); 21 | }); 22 | test("PlainDateTime", () => { 23 | const date = toDateFromClockTime(Temporal.PlainDateTime.from("2024-01-01T03:00:00")); 24 | expect(lightFormat(date, "yyyy-MM-dd HH:mm:ss")).toBe("2024-01-01 03:00:00"); 25 | }); 26 | test("PlainYearMonth", () => { 27 | const date = toDateFromClockTime(Temporal.PlainYearMonth.from("2024-01")); 28 | expect(lightFormat(date, "yyyy-MM")).toBe("2024-01"); 29 | }); 30 | test("PlainMonthDay", () => { 31 | const date = toDateFromClockTime(Temporal.PlainMonthDay.from("02-29")); 32 | expect(lightFormat(date, "MM-dd")).toBe("02-29"); 33 | }); 34 | 35 | test("timezone", () => { 36 | using _modifier = modifyTimeZone("America/Chicago"); 37 | const date = toDateFromClockTime( 38 | // this datetime doesn't exist in USA due to DST 39 | Temporal.PlainDateTime.from("2023-03-12T02:30:00"), 40 | ); 41 | expect(lightFormat(date, "yyyy-MM-dd HH:mm:ss")).toBe("2023-03-12 02:30:00"); 42 | }); 43 | 44 | test("PlainMonthDay with non-ISO calendar", () => { 45 | const md = Temporal.PlainMonthDay.from({ 46 | monthCode: "M05L", 47 | day: 13, 48 | calendar: "hebrew", 49 | }); 50 | const md2 = Temporal.PlainDate.from(lightFormat(toDateFromClockTime(md), "yyyy-MM-dd")) 51 | .withCalendar("hebrew") 52 | .toPlainMonthDay(); 53 | expect(md).toEqual(md2); 54 | }); 55 | 56 | test("PlainMonthDay with a reference ISO year in the distant past or future", () => { 57 | // note: using `PlainMonthDay` constructor directly is highly discouraged, therefore this is an extreme edge case. 58 | expect(toDateFromClockTime(new Temporal.PlainMonthDay(1, 27, "hebrew", -3))).toStrictEqual( 59 | new UTCDate("-000003-01-27T00:00:00Z"), 60 | ); 61 | expect(toDateFromClockTime(new Temporal.PlainMonthDay(1, 1, "hebrew", -270000))).toStrictEqual( 62 | new UTCDate("-270000-01-01T00:00:00Z"), 63 | ); 64 | expect(toDateFromClockTime(new Temporal.PlainMonthDay(1, 1, "hebrew", 270000))).toStrictEqual( 65 | new UTCDate("+270000-01-01T00:00:00Z"), 66 | ); 67 | }); 68 | 69 | test("PlainYearMonth with non-ISO calendar", () => { 70 | const ym = Temporal.PlainYearMonth.from({ 71 | year: 5779, 72 | monthCode: "M05L", 73 | calendar: "hebrew", 74 | }); 75 | const ym2 = Temporal.PlainDate.from(lightFormat(toDateFromClockTime(ym), "yyyy-MM-dd")) 76 | .withCalendar("hebrew") 77 | .toPlainYearMonth(); 78 | expect(ym).toEqual(ym2); 79 | }); 80 | 81 | test("PlainDate with non-ISO calendar", () => { 82 | const pd = Temporal.PlainDate.from({ 83 | year: 5779, 84 | monthCode: "M05L", 85 | day: 13, 86 | calendar: "hebrew", 87 | }); 88 | const pd2 = Temporal.PlainDate.from( 89 | lightFormat(toDateFromClockTime(pd), "yyyy-MM-dd"), 90 | ).withCalendar("hebrew"); 91 | expect(pd).toEqual(pd2); 92 | }); 93 | 94 | test("PlainDateTime with non-ISO calendar", () => { 95 | const pdt = Temporal.PlainDate.from({ 96 | year: 5779, 97 | monthCode: "M05L", 98 | day: 13, 99 | calendar: "hebrew", 100 | }).toPlainDateTime(); 101 | const pdt2 = Temporal.PlainDateTime.from( 102 | lightFormat(toDateFromClockTime(pdt), "yyyy-MM-dd'T'HH:mm:ss"), 103 | ).withCalendar("hebrew"); 104 | expect(pdt).toEqual(pdt2); 105 | }); 106 | 107 | test("ZonedDateTime with non-ISO calendar", () => { 108 | const zdt = Temporal.PlainDate.from({ 109 | year: 5779, 110 | monthCode: "M05L", 111 | day: 13, 112 | calendar: "hebrew", 113 | }) 114 | .toPlainDateTime() 115 | .toZonedDateTime("Asia/Tokyo"); 116 | const pdt2 = Temporal.PlainDateTime.from( 117 | lightFormat(toDateFromClockTime(zdt), "yyyy-MM-dd'T'HH:mm:ss"), 118 | ).withCalendar("hebrew"); 119 | expect(zdt.toPlainDateTime()).toEqual(pdt2); 120 | }); 121 | 122 | test("date constructor type", () => { 123 | expect(toDateFromClockTime(Temporal.Now.plainDateISO(), UTCDateMini)).toBeInstanceOf(UTCDateMini); 124 | expect(toDateFromClockTime(Temporal.Now.plainDateISO())).toBeInstanceOf(UTCDate); 125 | }); 126 | 127 | test("2-digit year", () => { 128 | expect(toDateFromClockTime(Temporal.PlainDateTime.from("0050-01-01"))).toStrictEqual( 129 | new UTCDate("0050-01-01T00:00:00Z"), 130 | ); 131 | expect(toDateFromClockTime(Temporal.PlainDate.from("0050-01-01"))).toStrictEqual( 132 | new UTCDate("0050-01-01T00:00:00Z"), 133 | ); 134 | expect(toDateFromClockTime(Temporal.PlainYearMonth.from("0050-01-01"))).toStrictEqual( 135 | new UTCDate("0050-01-01T00:00:00Z"), 136 | ); 137 | }); 138 | -------------------------------------------------------------------------------- /src/datetime/areIntervalsOverlapping.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | 3 | import { areIntervalsOverlapping } from "./areIntervalsOverlapping.js"; 4 | 5 | test("Instant", () => { 6 | const t1 = Temporal.Instant.from("2024-01-01T00:00:00Z"); 7 | const t2 = Temporal.Instant.from("2024-01-02T00:00:00Z"); 8 | const t3 = Temporal.Instant.from("2024-01-03T00:00:00Z"); 9 | const t4 = Temporal.Instant.from("2024-01-04T00:00:00Z"); 10 | 11 | expect( 12 | areIntervalsOverlapping( 13 | { 14 | start: t1, 15 | end: t3, 16 | }, 17 | { 18 | start: t2, 19 | end: t4, 20 | }, 21 | ), 22 | ).toEqual(true); 23 | expect( 24 | areIntervalsOverlapping( 25 | { 26 | start: t1, 27 | end: t4, 28 | }, 29 | { 30 | start: t2, 31 | end: t3, 32 | }, 33 | ), 34 | ).toEqual(true); 35 | expect( 36 | areIntervalsOverlapping( 37 | { 38 | start: t1, 39 | end: t2, 40 | }, 41 | { 42 | start: t3, 43 | end: t4, 44 | }, 45 | ), 46 | ).toEqual(false); 47 | }); 48 | 49 | test("ZonedDateTime", () => { 50 | const t1 = Temporal.ZonedDateTime.from("2024-01-01T09:00:00+09:00[Asia/Tokyo]"); 51 | const t2 = Temporal.ZonedDateTime.from("2024-01-01T01:00:00+00:00[Europe/London]"); 52 | const t3 = Temporal.ZonedDateTime.from("2024-01-03T00:00:00+00:00[Europe/London]"); 53 | const t4 = Temporal.ZonedDateTime.from("2024-01-04T00:00:00+00:00[Europe/London]"); 54 | 55 | expect( 56 | areIntervalsOverlapping( 57 | { 58 | start: t1, 59 | end: t3, 60 | }, 61 | { 62 | start: t2, 63 | end: t4, 64 | }, 65 | ), 66 | ).toEqual(true); 67 | expect( 68 | areIntervalsOverlapping( 69 | { 70 | start: t1, 71 | end: t4, 72 | }, 73 | { 74 | start: t2, 75 | end: t3, 76 | }, 77 | ), 78 | ).toEqual(true); 79 | expect( 80 | areIntervalsOverlapping( 81 | { 82 | start: t1, 83 | end: t2, 84 | }, 85 | { 86 | start: t3, 87 | end: t4, 88 | }, 89 | ), 90 | ).toEqual(false); 91 | }); 92 | 93 | test("PlainDate", () => { 94 | const t1 = Temporal.PlainDate.from("2024-01-01"); 95 | const t2 = Temporal.PlainDate.from("2024-01-02"); 96 | const t3 = Temporal.PlainDate.from("2024-01-03"); 97 | const t4 = Temporal.PlainDate.from("2024-01-04"); 98 | 99 | expect( 100 | areIntervalsOverlapping( 101 | { 102 | start: t1, 103 | end: t3, 104 | }, 105 | { 106 | start: t2, 107 | end: t4, 108 | }, 109 | ), 110 | ).toEqual(true); 111 | expect( 112 | areIntervalsOverlapping( 113 | { 114 | start: t1, 115 | end: t4, 116 | }, 117 | { 118 | start: t2, 119 | end: t3, 120 | }, 121 | ), 122 | ).toEqual(true); 123 | expect( 124 | areIntervalsOverlapping( 125 | { 126 | start: t1, 127 | end: t2, 128 | }, 129 | { 130 | start: t3, 131 | end: t4, 132 | }, 133 | ), 134 | ).toEqual(false); 135 | }); 136 | 137 | test("PlainTime", () => { 138 | const t1 = Temporal.PlainTime.from("00:00:00"); 139 | const t2 = Temporal.PlainTime.from("01:00:00"); 140 | const t3 = Temporal.PlainTime.from("02:00:00"); 141 | const t4 = Temporal.PlainTime.from("03:00:00"); 142 | 143 | expect( 144 | areIntervalsOverlapping( 145 | { 146 | start: t1, 147 | end: t3, 148 | }, 149 | { 150 | start: t2, 151 | end: t4, 152 | }, 153 | ), 154 | ).toEqual(true); 155 | expect( 156 | areIntervalsOverlapping( 157 | { 158 | start: t1, 159 | end: t4, 160 | }, 161 | { 162 | start: t2, 163 | end: t3, 164 | }, 165 | ), 166 | ).toEqual(true); 167 | expect( 168 | areIntervalsOverlapping( 169 | { 170 | start: t1, 171 | end: t2, 172 | }, 173 | { 174 | start: t3, 175 | end: t4, 176 | }, 177 | ), 178 | ).toEqual(false); 179 | }); 180 | 181 | test("PlainDateTime", () => { 182 | const t1 = Temporal.PlainDateTime.from("2024-01-01T00:00:00"); 183 | const t2 = Temporal.PlainDateTime.from("2024-01-02T00:00:00"); 184 | const t3 = Temporal.PlainDateTime.from("2024-01-03T00:00:00"); 185 | const t4 = Temporal.PlainDateTime.from("2024-01-04T00:00:00"); 186 | 187 | expect( 188 | areIntervalsOverlapping( 189 | { 190 | start: t1, 191 | end: t3, 192 | }, 193 | { 194 | start: t2, 195 | end: t4, 196 | }, 197 | ), 198 | ).toEqual(true); 199 | expect( 200 | areIntervalsOverlapping( 201 | { 202 | start: t1, 203 | end: t4, 204 | }, 205 | { 206 | start: t2, 207 | end: t3, 208 | }, 209 | ), 210 | ).toEqual(true); 211 | expect( 212 | areIntervalsOverlapping( 213 | { 214 | start: t1, 215 | end: t2, 216 | }, 217 | { 218 | start: t3, 219 | end: t4, 220 | }, 221 | ), 222 | ).toEqual(false); 223 | }); 224 | 225 | test("PlainYearMonth", () => { 226 | const t1 = Temporal.PlainYearMonth.from("2024-01"); 227 | const t2 = Temporal.PlainYearMonth.from("2024-02"); 228 | const t3 = Temporal.PlainYearMonth.from("2024-03"); 229 | const t4 = Temporal.PlainYearMonth.from("2024-04"); 230 | 231 | expect( 232 | areIntervalsOverlapping( 233 | { 234 | start: t1, 235 | end: t3, 236 | }, 237 | { 238 | start: t2, 239 | end: t4, 240 | }, 241 | ), 242 | ).toEqual(true); 243 | expect( 244 | areIntervalsOverlapping( 245 | { 246 | start: t1, 247 | end: t4, 248 | }, 249 | { 250 | start: t2, 251 | end: t3, 252 | }, 253 | ), 254 | ).toEqual(true); 255 | expect( 256 | areIntervalsOverlapping( 257 | { 258 | start: t1, 259 | end: t2, 260 | }, 261 | { 262 | start: t3, 263 | end: t4, 264 | }, 265 | ), 266 | ).toEqual(false); 267 | }); 268 | 269 | test("`inclusive` option", () => { 270 | const i1 = { 271 | start: Temporal.Instant.from("2024-01-01T00:00:00Z"), 272 | end: Temporal.Instant.from("2024-01-02T00:00:00Z"), 273 | }; 274 | const i2 = { 275 | start: Temporal.Instant.from("2024-01-02T00:00:00Z"), 276 | end: Temporal.Instant.from("2024-01-03T00:00:00Z"), 277 | }; 278 | expect(areIntervalsOverlapping(i1, i2)).toEqual(true); 279 | expect(areIntervalsOverlapping(i1, i2, { inclusive: true })).toEqual(true); 280 | expect(areIntervalsOverlapping(i1, i2, { inclusive: false })).toEqual(false); 281 | }); 282 | -------------------------------------------------------------------------------- /src/datetime/fromRfc2822.ts: -------------------------------------------------------------------------------- 1 | import { isInstantConstructor, isPlainDateTimeConstructor } from "../type-utils.js"; 2 | import type { Temporal } from "../types.js"; 3 | import { createRecord } from "./_createRecord.js"; 4 | import { formatDateIso } from "./_formatDateIso.js"; 5 | import { formatExactTimeIso } from "./_formatExactTimeIso.js"; 6 | import { formatHmsIso } from "./_formatHmsIso.js"; 7 | import { getDayOfWeekFromYmd } from "./_getDayOfWeekFromYmd.js"; 8 | import { getDayOfWeekNumberFromAbbreviation } from "./_getDayOfWeekNumberFromAbbreviation.js"; 9 | import { getMonthNumberFromAbbreviation } from "./_getMonthNumberFromAbbreviation.js"; 10 | import { isValidHms } from "./_isValidHms.js"; 11 | import { isValidYmd } from "./_isValidYmd.js"; 12 | 13 | // spec: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3 https://datatracker.ietf.org/doc/html/rfc2822#section-4.3 14 | 15 | function removeComment(str: string) { 16 | const r = /(?= 50 ? 1900 + yearNum : 2000 + yearNum; 54 | } 55 | 56 | function getOffset(timeZone: string): string { 57 | if (["UT", "GMT", "z", "Z"].includes(timeZone)) { 58 | return "+00:00"; 59 | } 60 | if (timeZone === "-0000" || /^[A-IK-Za-ik-z]$/.test(timeZone)) { 61 | // according to the spec, military zone except 'Z' should be considered equivalent to "-0000", 62 | // which means the date-time contains no information about the local time zone 63 | throw new Error("No offset info"); 64 | } 65 | if (/^[+-]\d{4}$/.test(timeZone)) { 66 | return `${timeZone.slice(0, 3)}:${timeZone.slice(3)}`; 67 | } 68 | const table = createRecord({ 69 | EDT: "-04:00", 70 | EST: "-05:00", 71 | CDT: "-05:00", 72 | CST: "-06:00", 73 | MDT: "-06:00", 74 | MST: "-07:00", 75 | PDT: "-07:00", 76 | PST: "-08:00", 77 | }); 78 | if (table[timeZone] !== undefined) { 79 | return table[timeZone]; 80 | } 81 | throw new Error("Unknown time zone"); 82 | } 83 | 84 | function parse( 85 | date: string, 86 | ): [ 87 | year: number, 88 | month: number, 89 | day: number, 90 | hour: number, 91 | minute: number, 92 | second: number, 93 | dayOfWeek: string | undefined, 94 | timeZone: string, 95 | ] { 96 | const result = dateTimeFormatRegex.exec(date); 97 | if (result === null) { 98 | throw new Error(`Invalid date and time format: ${date}`); 99 | } 100 | const [, dayOfWeek, day, monthName, year, hour, minute, second = "00", timeZone] = result; 101 | if ( 102 | day === undefined || 103 | monthName === undefined || 104 | year === undefined || 105 | hour === undefined || 106 | minute === undefined || 107 | timeZone === undefined 108 | ) { 109 | throw new Error("something wrong"); 110 | } 111 | return [ 112 | fullYear(year), 113 | getMonthNumberFromAbbreviation(monthName), 114 | parseInt(day), 115 | parseInt(hour, 10), 116 | parseInt(minute, 10), 117 | parseInt(second), 118 | dayOfWeek, 119 | timeZone, 120 | ]; 121 | } 122 | 123 | /** 124 | * Creates Temporal object from datetime string in RFC 2822's format. 125 | * 126 | * @param date datetime string in RFC 2822's format 127 | * @param TemporalClass Temporal class (such as `Temporal.PlainDateTime` or `Temporal.Instant`) which will be returned 128 | * @returns an instance of Temporal class specified in `TemporalClass` argument 129 | */ 130 | export function fromRfc2822< 131 | TemporalClassType extends 132 | | typeof Temporal.Instant 133 | | typeof Temporal.ZonedDateTime 134 | | typeof Temporal.PlainDateTime, 135 | >(date: string, TemporalClass: TemporalClassType): InstanceType { 136 | const dateWithoutComment = date.includes("(") ? removeComment(date) : date; 137 | 138 | const [year, month, day, hour, minute, second, dayOfWeek, timeZone] = parse(dateWithoutComment); 139 | if (!isValidYmd(year, month, day)) { 140 | throw new Error(`Invalid date: ${formatDateIso(year, month, day)}`); 141 | } 142 | if (!isValidHms(hour, minute, second, true)) { 143 | throw new Error(`Invalid time: ${formatHmsIso(hour, minute, second)}`); 144 | } 145 | if ( 146 | dayOfWeek !== undefined && 147 | getDayOfWeekFromYmd(year, month, day) !== getDayOfWeekNumberFromAbbreviation(dayOfWeek) 148 | ) { 149 | throw new Error(`Wrong day of week: ${dayOfWeek}`); 150 | } 151 | if (!timeZoneFormatRegex.test(timeZone)) { 152 | throw new Error(`Invalid time zone: ${timeZone}`); 153 | } 154 | 155 | if (isPlainDateTimeConstructor(TemporalClass)) { 156 | return TemporalClass.from({ 157 | year, 158 | month, 159 | day, 160 | hour, 161 | minute, 162 | second, 163 | calendarId: "iso8601", 164 | }) as InstanceType; 165 | } 166 | 167 | const offsetIso = getOffset(timeZone); 168 | if (isInstantConstructor(TemporalClass)) { 169 | return TemporalClass.from( 170 | formatExactTimeIso(year, month, day, hour, minute, second, 0, offsetIso), 171 | ) as InstanceType; 172 | } 173 | return TemporalClass.from({ 174 | year, 175 | month, 176 | day, 177 | hour, 178 | minute, 179 | second, 180 | calendarId: "iso8601", 181 | timeZone: offsetIso, 182 | }) as InstanceType; 183 | } 184 | -------------------------------------------------------------------------------- /src/datetime/toObject.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPlainDate, 3 | isPlainDateTime, 4 | isPlainTime, 5 | isPlainYearMonth, 6 | isZonedDateTime, 7 | } from "../type-utils.js"; 8 | import type { Temporal } from "../types.js"; 9 | 10 | /** 11 | * Return value type of `toObject` for `Temporal.ZonedDateTime`. 12 | * It can be passed to `Temporal.ZonedDateTime.from` method. 13 | */ 14 | export interface ZonedDateTimeLike { 15 | era: string | undefined; 16 | eraYear: number | undefined; 17 | year: number; 18 | month: number; 19 | monthCode: string; 20 | day: number; 21 | hour: number; 22 | minute: number; 23 | second: number; 24 | millisecond: number; 25 | microsecond: number; 26 | nanosecond: number; 27 | offset: string; 28 | timeZone: string; 29 | calendar: string; 30 | } 31 | 32 | /** 33 | * Return value type of `toObject` for `Temporal.PlainDateTime`. 34 | * It can be passed to `Temporal.PlainDateTime.from` method. 35 | */ 36 | export interface PlainDateTimeLike { 37 | era: string | undefined; 38 | eraYear: number | undefined; 39 | year: number; 40 | month: number; 41 | monthCode: string; 42 | day: number; 43 | hour: number; 44 | minute: number; 45 | second: number; 46 | millisecond: number; 47 | microsecond: number; 48 | nanosecond: number; 49 | calendar: string; 50 | } 51 | 52 | /** 53 | * Return value type of `toObject` for `Temporal.PlainDate`. 54 | * It can be passed to `Temporal.PlainDate.from` method. 55 | */ 56 | export interface PlainDateLike { 57 | era: string | undefined; 58 | eraYear: number | undefined; 59 | year: number; 60 | month: number; 61 | monthCode: string; 62 | day: number; 63 | calendar: string; 64 | } 65 | 66 | /** 67 | * Return value type of `toObject` for `Temporal.PlainTime`. 68 | * It can be passed to `Temporal.PlainTime.from` method. 69 | */ 70 | export interface PlainTimeLike { 71 | hour: number; 72 | minute: number; 73 | second: number; 74 | millisecond: number; 75 | microsecond: number; 76 | nanosecond: number; 77 | } 78 | 79 | /** 80 | * Return value type of `toObject` for `Temporal.PlainYearMonth`. 81 | * It can be passed to `Temporal.PlainYearMonth.from` method. 82 | */ 83 | export interface PlainYearMonthLike { 84 | era: string | undefined; 85 | eraYear: number | undefined; 86 | year: number; 87 | month: number; 88 | monthCode: string; 89 | calendar: string; 90 | } 91 | 92 | /** 93 | * Return value type of `toObject` for `Temporal.PlainMonthDay`. 94 | * It can be passed to `Temporal.PlainMonthDay.from` method. 95 | */ 96 | export interface PlainMonthDayLike { 97 | monthCode: string; 98 | day: number; 99 | calendar: string; 100 | } 101 | 102 | /** 103 | * Returns a plain object which can passed to `Temporal.ZonedDateTime.from` to restore original `Temporal.ZonedDateTime`. 104 | * @param dt original `Temporal.ZonedDateTime` object 105 | */ 106 | export function toObject(dt: Temporal.ZonedDateTime): ZonedDateTimeLike; 107 | /** 108 | * Returns a plain object which can passed to `Temporal.PlainDateTime.from` to restore original `Temporal.PlainDateTime`. 109 | * @param dt original `Temporal.PlainDateTime` object 110 | */ 111 | export function toObject(dt: Temporal.PlainDateTime): PlainDateTimeLike; 112 | /** 113 | * Returns a plain object which can passed to `Temporal.PlainDate.from` to restore original `Temporal.PlainDate`. 114 | * @param dt original `Temporal.PlainDate` object 115 | */ 116 | export function toObject(dt: Temporal.PlainDate): PlainDateLike; 117 | /** 118 | * Returns a plain object which can passed to `Temporal.PlainTime.from` to restore original `Temporal.PlainTime`. 119 | * @param dt original `Temporal.PlainTime` object 120 | */ 121 | export function toObject(dt: Temporal.PlainTime): PlainTimeLike; 122 | /** 123 | * Returns a plain object which can passed to `Temporal.PlainYearMonth.from` to restore original `Temporal.PlainYearMonth`. 124 | * @param dt original `Temporal.PlainYearMonth` object 125 | */ 126 | export function toObject(dt: Temporal.PlainYearMonth): PlainYearMonthLike; 127 | /** 128 | * Returns a plain object which can passed to `Temporal.PlainMonthDay.from` to restore original `Temporal.PlainMonthDay`. 129 | * @param dt original `Temporal.PlainMonthDay` object 130 | */ 131 | export function toObject(dt: Temporal.PlainMonthDay): PlainMonthDayLike; 132 | export function toObject( 133 | dt: 134 | | Temporal.ZonedDateTime 135 | | Temporal.PlainDate 136 | | Temporal.PlainTime 137 | | Temporal.PlainDateTime 138 | | Temporal.PlainYearMonth 139 | | Temporal.PlainMonthDay, 140 | ) { 141 | if (isZonedDateTime(dt)) { 142 | const result: ZonedDateTimeLike = { 143 | era: dt.era, 144 | eraYear: dt.eraYear, 145 | year: dt.year, 146 | month: dt.month, 147 | monthCode: dt.monthCode, 148 | day: dt.day, 149 | hour: dt.hour, 150 | minute: dt.minute, 151 | second: dt.second, 152 | millisecond: dt.millisecond, 153 | microsecond: dt.microsecond, 154 | nanosecond: dt.nanosecond, 155 | offset: dt.offset, 156 | calendar: dt.calendarId, 157 | timeZone: dt.timeZoneId, 158 | }; 159 | return result; 160 | } 161 | if (isPlainDate(dt)) { 162 | const result: PlainDateLike = { 163 | era: dt.era, 164 | eraYear: dt.eraYear, 165 | year: dt.year, 166 | month: dt.month, 167 | monthCode: dt.monthCode, 168 | day: dt.day, 169 | calendar: dt.calendarId, 170 | }; 171 | return result; 172 | } 173 | if (isPlainDateTime(dt)) { 174 | const result: PlainDateTimeLike = { 175 | era: dt.era, 176 | eraYear: dt.eraYear, 177 | year: dt.year, 178 | month: dt.month, 179 | monthCode: dt.monthCode, 180 | day: dt.day, 181 | hour: dt.hour, 182 | minute: dt.minute, 183 | second: dt.second, 184 | millisecond: dt.millisecond, 185 | microsecond: dt.microsecond, 186 | nanosecond: dt.nanosecond, 187 | calendar: dt.calendarId, 188 | }; 189 | return result; 190 | } 191 | if (isPlainTime(dt)) { 192 | const result: PlainTimeLike = { 193 | hour: dt.hour, 194 | minute: dt.minute, 195 | second: dt.second, 196 | millisecond: dt.millisecond, 197 | microsecond: dt.microsecond, 198 | nanosecond: dt.nanosecond, 199 | }; 200 | return result; 201 | } 202 | if (isPlainYearMonth(dt)) { 203 | const result: PlainYearMonthLike = { 204 | era: dt.era, 205 | eraYear: dt.eraYear, 206 | year: dt.year, 207 | month: dt.month, 208 | monthCode: dt.monthCode, 209 | calendar: dt.calendarId, 210 | }; 211 | return result; 212 | } 213 | const result: PlainMonthDayLike = { 214 | monthCode: dt.monthCode, 215 | day: dt.day, 216 | calendar: dt.calendarId, 217 | }; 218 | return result; 219 | } 220 | -------------------------------------------------------------------------------- /src/type-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal as Temporal2 } from "@js-temporal/polyfill"; 2 | import { Temporal as Temporal1 } from "temporal-polyfill"; 3 | import { expect, test } from "vitest"; 4 | 5 | import { 6 | getConstructor, 7 | isInstant, 8 | isInstantConstructor, 9 | isPlainDate, 10 | isPlainDateConstructor, 11 | isPlainDateTime, 12 | isPlainDateTimeConstructor, 13 | isPlainMonthDay, 14 | isPlainMonthDayConstructor, 15 | isPlainTime, 16 | isPlainTimeConstructor, 17 | isPlainYearMonth, 18 | isPlainYearMonthConstructor, 19 | isZonedDateTime, 20 | isZonedDateTimeConstructor, 21 | } from "./type-utils.js"; 22 | import type { Temporal } from "./types.js"; 23 | 24 | function createTypes(temporal: any) { 25 | return [ 26 | temporal.Now.instant(), 27 | temporal.Now.zonedDateTimeISO(), 28 | temporal.Now.plainDateTimeISO(), 29 | temporal.Now.plainDateISO(), 30 | temporal.Now.plainTimeISO(), 31 | temporal.Now.plainDateISO().toPlainYearMonth(), 32 | temporal.Now.plainDateISO().toPlainMonthDay(), 33 | temporal.Duration.from({ seconds: 20 }), 34 | ]; 35 | } 36 | 37 | function constructors(temporal: any) { 38 | return [ 39 | temporal.Instant, 40 | temporal.ZonedDateTime, 41 | temporal.PlainDate, 42 | temporal.PlainTime, 43 | temporal.PlainDateTime, 44 | temporal.PlainYearMonth, 45 | temporal.PlainMonthDay, 46 | ]; 47 | } 48 | 49 | test("isInstant", () => { 50 | const expected = [true, false, false, false, false, false, false, false]; 51 | expect(createTypes(Temporal1).map((v) => isInstant(v))).toEqual(expected); 52 | expect(createTypes(Temporal2).map((v) => isInstant(v))).toEqual(expected); 53 | }); 54 | 55 | test("isZonedDateTime", () => { 56 | const expected = [false, true, false, false, false, false, false, false]; 57 | expect(createTypes(Temporal1).map((v) => isZonedDateTime(v))).toEqual(expected); 58 | expect(createTypes(Temporal2).map((v) => isZonedDateTime(v))).toEqual(expected); 59 | }); 60 | 61 | test("isPlainDateTime", () => { 62 | const expected = [false, false, true, false, false, false, false, false]; 63 | expect(createTypes(Temporal1).map((v) => isPlainDateTime(v))).toEqual(expected); 64 | expect(createTypes(Temporal2).map((v) => isPlainDateTime(v))).toEqual(expected); 65 | }); 66 | 67 | test("isPlainDate", () => { 68 | const expected = [false, false, false, true, false, false, false, false]; 69 | expect(createTypes(Temporal1).map((v) => isPlainDate(v))).toEqual(expected); 70 | expect(createTypes(Temporal2).map((v) => isPlainDate(v))).toEqual(expected); 71 | }); 72 | 73 | test("isPlainTime", () => { 74 | const expected = [false, false, false, false, true, false, false, false]; 75 | expect(createTypes(Temporal1).map((v) => isPlainTime(v))).toEqual(expected); 76 | expect(createTypes(Temporal2).map((v) => isPlainTime(v))).toEqual(expected); 77 | }); 78 | 79 | test("isPlainYearMonth", () => { 80 | const expected = [false, false, false, false, false, true, false, false]; 81 | expect(createTypes(Temporal1).map((v) => isPlainYearMonth(v))).toEqual(expected); 82 | expect(createTypes(Temporal2).map((v) => isPlainYearMonth(v))).toEqual(expected); 83 | }); 84 | 85 | test("isPlainMonthDay", () => { 86 | const expected = [false, false, false, false, false, false, true, false]; 87 | expect(createTypes(Temporal1).map((v) => isPlainMonthDay(v))).toEqual(expected); 88 | expect(createTypes(Temporal2).map((v) => isPlainMonthDay(v))).toEqual(expected); 89 | }); 90 | 91 | test("isInstantConstructor", () => { 92 | const expected = [true, false, false, false, false, false, false]; 93 | expect(constructors(Temporal1).map((c) => isInstantConstructor(c))).toEqual(expected); 94 | expect(constructors(Temporal2).map((c) => isInstantConstructor(c))).toEqual(expected); 95 | }); 96 | 97 | test("isZonedDateTimeConstructor", () => { 98 | const expected = [false, true, false, false, false, false, false]; 99 | expect(constructors(Temporal1).map((c) => isZonedDateTimeConstructor(c))).toEqual(expected); 100 | expect(constructors(Temporal2).map((c) => isZonedDateTimeConstructor(c))).toEqual(expected); 101 | }); 102 | 103 | test("isPlainDateConstructor", () => { 104 | const expected = [false, false, true, false, false, false, false]; 105 | expect(constructors(Temporal1).map((c) => isPlainDateConstructor(c))).toEqual(expected); 106 | expect(constructors(Temporal2).map((c) => isPlainDateConstructor(c))).toEqual(expected); 107 | }); 108 | 109 | test("isPlainTimeConstructor", () => { 110 | const expected = [false, false, false, true, false, false, false]; 111 | expect(constructors(Temporal1).map((c) => isPlainTimeConstructor(c))).toEqual(expected); 112 | expect(constructors(Temporal2).map((c) => isPlainTimeConstructor(c))).toEqual(expected); 113 | }); 114 | 115 | test("isPlainDateTimeConstructor", () => { 116 | const expected = [false, false, false, false, true, false, false]; 117 | expect(constructors(Temporal1).map((c) => isPlainDateTimeConstructor(c))).toEqual(expected); 118 | expect(constructors(Temporal2).map((c) => isPlainDateTimeConstructor(c))).toEqual(expected); 119 | }); 120 | 121 | test("isPlainYearMonthConstructor", () => { 122 | const expected = [false, false, false, false, false, true, false]; 123 | expect(constructors(Temporal1).map((c) => isPlainYearMonthConstructor(c))).toEqual(expected); 124 | expect(constructors(Temporal2).map((c) => isPlainYearMonthConstructor(c))).toEqual(expected); 125 | }); 126 | 127 | test("isPlainMonthDayConstructor", () => { 128 | const expected = [false, false, false, false, false, false, true]; 129 | expect(constructors(Temporal1).map((c) => isPlainMonthDayConstructor(c))).toEqual(expected); 130 | expect(constructors(Temporal2).map((c) => isPlainMonthDayConstructor(c))).toEqual(expected); 131 | }); 132 | 133 | test("getConstructor", () => { 134 | const createExpected = (Temporal: any) => [ 135 | Temporal.Instant, 136 | Temporal.ZonedDateTime, 137 | Temporal.PlainDateTime, 138 | Temporal.PlainDate, 139 | Temporal.PlainTime, 140 | Temporal.PlainYearMonth, 141 | Temporal.PlainMonthDay, 142 | Temporal.Duration, 143 | ]; 144 | expect(createTypes(Temporal1).map((v) => getConstructor(v))).toEqual(createExpected(Temporal1)); 145 | expect(createTypes(Temporal2).map((v) => getConstructor(v))).toEqual(createExpected(Temporal2)); 146 | }); 147 | 148 | test("type compatibility", () => { 149 | const testFunc = ( 150 | _1: Temporal.Instant, 151 | _2: Temporal.ZonedDateTime, 152 | _3: Temporal.PlainDateTime, 153 | _4: Temporal.PlainDate, 154 | _5: Temporal.PlainTime, 155 | _6: Temporal.PlainYearMonth, 156 | _7: Temporal.PlainMonthDay, 157 | _8: Temporal.Duration, 158 | ) => {}; 159 | testFunc( 160 | Temporal1.Now.instant(), 161 | Temporal1.Now.zonedDateTimeISO(), 162 | Temporal1.Now.plainDateTimeISO(), 163 | Temporal1.Now.plainDateISO(), 164 | Temporal1.Now.plainTimeISO(), 165 | Temporal1.Now.plainDateISO().toPlainYearMonth(), 166 | Temporal1.Now.plainDateISO().toPlainMonthDay(), 167 | Temporal1.Duration.from({ years: 2 }), 168 | ); 169 | testFunc( 170 | Temporal2.Now.instant(), 171 | Temporal2.Now.zonedDateTimeISO(), 172 | Temporal2.Now.plainDateTimeISO(), 173 | Temporal2.Now.plainDateISO(), 174 | Temporal2.Now.plainTimeISO(), 175 | Temporal2.Now.plainDateISO().toPlainYearMonth(), 176 | Temporal2.Now.plainDateISO().toPlainMonthDay(), 177 | Temporal1.Duration.from({ years: 2 }), 178 | ); 179 | }); 180 | --------------------------------------------------------------------------------