From 9084d2783a38f9fc1548afbccbd5cd3a4b7cfed1 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 25 Mar 2026 05:11:06 +0100 Subject: [PATCH] feat: port Dialog to acel, refactor alert-dialog, fix TS errors, add test pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Port Dialog component from uisv to acel (dialog.svelte.ts state machine) - Rewrite alert-dialog as thin wrapper over acel Dialog - Remove 4 redundant alert-dialog components (Trigger, Overlay, Title, Description) now re-exported directly from dialog/components/ - Add Dialog export to acel/index.ts - Fix double-namespace in button/index.ts - Fix SPA config: ssr=false, prerender=false, adapter-static fallback - Fix Snippet<[DialogContentSnippetProps]> children call with snippetProps - Fix glob/lib/date TS errors: import type for verbatimModuleSyntax (×45) - Fix TS7053 in manipulation.ts invertDuration and DateFormatter.ts - Fix TS2322 hourCycle type in comn_utils.ts and DateFormatter.ts - Recreate test pages: accordion, collapsible, alert-dialog, dialog (new), button (new) Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.json | 8 +- package-lock.json | 11 + package.json | 3 +- src/lib/glob/lib/comn_utils.ts | 2 +- src/lib/glob/lib/date/CalendarDate.ts | 406 +++++++++++++++ src/lib/glob/lib/date/DateFormatter.ts | 198 ++++++++ .../lib/date/calendars/BuddhistCalendar.ts | 64 +++ .../lib/date/calendars/EthiopicCalendar.ts | 204 ++++++++ .../lib/date/calendars/GregorianCalendar.ts | 144 ++++++ .../glob/lib/date/calendars/HebrewCalendar.ts | 211 ++++++++ .../glob/lib/date/calendars/IndianCalendar.ts | 129 +++++ .../lib/date/calendars/IslamicCalendar.ts | 233 +++++++++ .../lib/date/calendars/JapaneseCalendar.ts | 177 +++++++ .../lib/date/calendars/PersianCalendar.ts | 100 ++++ .../glob/lib/date/calendars/TaiwanCalendar.ts | 88 ++++ src/lib/glob/lib/date/conversion.ts | 316 ++++++++++++ src/lib/glob/lib/date/createCalendar.ts | 55 ++ src/lib/glob/lib/date/index.ts | 98 ++++ src/lib/glob/lib/date/manipulation.ts | 477 ++++++++++++++++++ src/lib/glob/lib/date/queries.ts | 373 ++++++++++++++ src/lib/glob/lib/date/string.ts | 299 +++++++++++ src/lib/glob/lib/date/types.ts | 161 ++++++ src/lib/glob/lib/date/utils.ts | 19 + src/lib/glob/lib/date/weekStartData.ts | 111 ++++ src/lib/glob/lib/dati_types.ts | 7 +- src/lib/glob/lib/index.ts | 2 + src/routes/layout.css | 310 ++++++++++++ src/routes/test/accordion/+page.svelte | 423 +++++++++++----- src/routes/test/alert-dialog/+page.svelte | 399 +++++++++++++++ src/routes/test/button/+page.svelte | 323 ++++++++++++ src/routes/test/collapsible/+page.svelte | 395 +++++++++++---- src/routes/test/dialog/+page.svelte | 433 ++++++++++++++++ src/routes/test/tabs/+page.svelte | 225 +++++++++ src/uix/acel/accordion/accordion.svelte.ts | 8 +- .../components/alert-dialog-action.svelte | 34 ++ .../components/alert-dialog-cancel.svelte | 36 ++ .../components/alert-dialog-content.svelte | 59 +++ .../components/alert-dialog.svelte | 27 + src/uix/acel/alert-dialog/exports.ts | 24 + src/uix/acel/alert-dialog/index.ts | 2 + src/uix/acel/alert-dialog/types.ts | 45 ++ src/uix/acel/app.d.ts | 28 + src/uix/acel/button/index.ts | 1 + .../acel/collapsible/collapsible.svelte.ts | 12 +- src/uix/acel/comn/config/acel-config.ts | 106 ++++ .../acel/comn/config/components/config.svelte | 14 + src/uix/acel/comn/config/exports.ts | 2 + src/uix/acel/comn/config/index.ts | 1 + src/uix/acel/comn/config/prop-resolvers.ts | 43 ++ src/uix/acel/comn/config/types.ts | 15 + src/uix/acel/comn/portal/index.ts | 2 + .../acel/comn/portal/portal-consumer.svelte | 9 + src/uix/acel/comn/portal/portal.svelte | 70 +++ src/uix/acel/comn/portal/types.ts | 24 + .../dialog/components/dialog-close.svelte | 37 ++ .../dialog/components/dialog-content.svelte | 61 +++ .../components/dialog-description.svelte | 34 ++ .../dialog/components/dialog-overlay.svelte | 37 ++ .../dialog/components/dialog-title.svelte | 36 ++ .../dialog/components/dialog-trigger.svelte | 36 ++ src/uix/acel/dialog/components/dialog.svelte | 27 + src/uix/acel/dialog/dialog.svelte.ts | 473 +++++++++++++++++ src/uix/acel/dialog/exports.ts | 19 + src/uix/acel/dialog/index.ts | 2 + src/uix/acel/dialog/types.ts | 102 ++++ src/uix/acel/index.ts | 5 + src/uix/acel/internal/types.ts | 12 +- src/uix/acel/internal/use-id.ts | 8 +- src/uix/acel/shared/date/types.ts | 10 +- .../acel/tabs/components/tabs-content.svelte | 36 ++ src/uix/acel/tabs/components/tabs-list.svelte | 34 ++ .../acel/tabs/components/tabs-trigger.svelte | 39 ++ src/uix/acel/tabs/components/tabs.svelte | 52 ++ src/uix/acel/tabs/exports.ts | 11 + src/uix/acel/tabs/index.ts | 1 + src/uix/acel/tabs/tabs.svelte.ts | 277 ++++++++++ src/uix/acel/tabs/types.ts | 89 ++++ src/uix/acel/utilities/box/index.ts | 3 + .../{ => events}/compose-handlers.ts | 4 +- src/uix/acel/utilities/index.ts | 8 +- src/uix/acel/utilities/props/merge-props.ts | 2 +- .../acel/utilities/{ => svelte}/attach-ref.ts | 2 +- .../{ => svelte}/use-on-change.svelte.ts | 2 +- .../{ => svelte}/use-ref-by-id.svelte.ts | 6 +- .../{watch => svelte}/watch.svelte.ts | 0 85 files changed, 8173 insertions(+), 258 deletions(-) create mode 100644 src/lib/glob/lib/date/CalendarDate.ts create mode 100644 src/lib/glob/lib/date/DateFormatter.ts create mode 100644 src/lib/glob/lib/date/calendars/BuddhistCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/EthiopicCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/GregorianCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/HebrewCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/IndianCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/IslamicCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/JapaneseCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/PersianCalendar.ts create mode 100644 src/lib/glob/lib/date/calendars/TaiwanCalendar.ts create mode 100644 src/lib/glob/lib/date/conversion.ts create mode 100644 src/lib/glob/lib/date/createCalendar.ts create mode 100644 src/lib/glob/lib/date/index.ts create mode 100644 src/lib/glob/lib/date/manipulation.ts create mode 100644 src/lib/glob/lib/date/queries.ts create mode 100644 src/lib/glob/lib/date/string.ts create mode 100644 src/lib/glob/lib/date/types.ts create mode 100644 src/lib/glob/lib/date/utils.ts create mode 100644 src/lib/glob/lib/date/weekStartData.ts create mode 100644 src/routes/test/alert-dialog/+page.svelte create mode 100644 src/routes/test/button/+page.svelte create mode 100644 src/routes/test/dialog/+page.svelte create mode 100644 src/routes/test/tabs/+page.svelte create mode 100644 src/uix/acel/alert-dialog/components/alert-dialog-action.svelte create mode 100644 src/uix/acel/alert-dialog/components/alert-dialog-cancel.svelte create mode 100644 src/uix/acel/alert-dialog/components/alert-dialog-content.svelte create mode 100644 src/uix/acel/alert-dialog/components/alert-dialog.svelte create mode 100644 src/uix/acel/alert-dialog/exports.ts create mode 100644 src/uix/acel/alert-dialog/index.ts create mode 100644 src/uix/acel/alert-dialog/types.ts create mode 100644 src/uix/acel/app.d.ts create mode 100644 src/uix/acel/comn/config/acel-config.ts create mode 100644 src/uix/acel/comn/config/components/config.svelte create mode 100644 src/uix/acel/comn/config/exports.ts create mode 100644 src/uix/acel/comn/config/index.ts create mode 100644 src/uix/acel/comn/config/prop-resolvers.ts create mode 100644 src/uix/acel/comn/config/types.ts create mode 100644 src/uix/acel/comn/portal/index.ts create mode 100644 src/uix/acel/comn/portal/portal-consumer.svelte create mode 100644 src/uix/acel/comn/portal/portal.svelte create mode 100644 src/uix/acel/comn/portal/types.ts create mode 100644 src/uix/acel/dialog/components/dialog-close.svelte create mode 100644 src/uix/acel/dialog/components/dialog-content.svelte create mode 100644 src/uix/acel/dialog/components/dialog-description.svelte create mode 100644 src/uix/acel/dialog/components/dialog-overlay.svelte create mode 100644 src/uix/acel/dialog/components/dialog-title.svelte create mode 100644 src/uix/acel/dialog/components/dialog-trigger.svelte create mode 100644 src/uix/acel/dialog/components/dialog.svelte create mode 100644 src/uix/acel/dialog/dialog.svelte.ts create mode 100644 src/uix/acel/dialog/exports.ts create mode 100644 src/uix/acel/dialog/index.ts create mode 100644 src/uix/acel/dialog/types.ts create mode 100644 src/uix/acel/tabs/components/tabs-content.svelte create mode 100644 src/uix/acel/tabs/components/tabs-list.svelte create mode 100644 src/uix/acel/tabs/components/tabs-trigger.svelte create mode 100644 src/uix/acel/tabs/components/tabs.svelte create mode 100644 src/uix/acel/tabs/exports.ts create mode 100644 src/uix/acel/tabs/index.ts create mode 100644 src/uix/acel/tabs/tabs.svelte.ts create mode 100644 src/uix/acel/tabs/types.ts create mode 100644 src/uix/acel/utilities/box/index.ts rename src/uix/acel/utilities/{ => events}/compose-handlers.ts (84%) rename src/uix/acel/utilities/{ => svelte}/attach-ref.ts (95%) rename src/uix/acel/utilities/{ => svelte}/use-on-change.svelte.ts (93%) rename src/uix/acel/utilities/{ => svelte}/use-ref-by-id.svelte.ts (86%) rename src/uix/acel/utilities/{watch => svelte}/watch.svelte.ts (100%) diff --git a/.claude/settings.json b/.claude/settings.json index 5707c58ad..b1fcd523a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,13 @@ { "permissions": { "allow": [ - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(npx svelte-check:*)", + "Bash(npx tsc:*)", + "Bash(grep -v \"^$\")", + "Bash(grep -n export.*Mutable g:/dev/svelte/vicen/src/lib/glob/lib/date/utils.ts)", + "Bash(grep -rn export.*Mutable g:/dev/svelte/vicen/src/lib/glob/lib/date/ --include=*.ts)" ] } } diff --git a/package-lock.json b/package-lock.json index b4630412a..9e2abe07c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "svelte": "^5.51.0", "svelte-check": "^4.4.2", "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.3.1", "vitest": "^4.1.0", @@ -2637,6 +2638,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.1.tgz", diff --git a/package.json b/package.json index 6cc0033a0..1cfb68ddf 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,16 @@ "svelte": "^5.51.0", "svelte-check": "^4.4.2", "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.3.1", "vitest": "^4.1.0", "vitest-browser-svelte": "^2.0.2" }, "dependencies": { - "csstype": "^3.2.3", "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", + "csstype": "^3.2.3", "esm-env": "^1.1.2", "runed": "0.37.1", "tabbable": "^6.2.0" diff --git a/src/lib/glob/lib/comn_utils.ts b/src/lib/glob/lib/comn_utils.ts index 87a8d17b7..e7b7604cd 100644 --- a/src/lib/glob/lib/comn_utils.ts +++ b/src/lib/glob/lib/comn_utils.ts @@ -471,7 +471,7 @@ function getCachedDTF(locale: string, options: Intl.DateTimeFormatOptions = {}): if (typeof options.hour12 === 'boolean' && hasBuggyHour12Behavior()) { options = { ...options }; const pref = hour12Prefs[String(options.hour12)]?.[locale.split('-')[0]]; - options.hourCycle = pref ?? (options.hour12 ? 'h12' : 'h23'); + options.hourCycle = (pref ?? (options.hour12 ? 'h12' : 'h23')) as Intl.DateTimeFormatOptions['hourCycle']; delete options.hour12; } diff --git a/src/lib/glob/lib/date/CalendarDate.ts b/src/lib/glob/lib/date/CalendarDate.ts new file mode 100644 index 000000000..2b43fb697 --- /dev/null +++ b/src/lib/glob/lib/date/CalendarDate.ts @@ -0,0 +1,406 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {add, addTime, addZoned, constrain, constrainTime, cycleDate, cycleTime, cycleZoned, set, setTime, setZoned, subtract, subtractTime, subtractZoned} from './manipulation'; +import type {AnyCalendarDate, AnyTime, Calendar, CycleOptions, CycleTimeOptions, DateDuration, DateField, DateFields, DateTimeDuration, Disambiguation, TimeDuration, TimeField, TimeFields} from './types'; +import {compareDate, compareTime} from './queries'; +import {dateTimeToString, dateToString, timeToString, zonedDateTimeToString} from './string'; +import {GregorianCalendar} from './calendars/GregorianCalendar'; +import {toCalendarDateTime, toDate, toZoned, zonedToDate} from './conversion'; + +function shiftArgs(args: any[]) { + let calendar: Calendar = typeof args[0] === 'object' + ? args.shift() + : new GregorianCalendar(); + + let era: string; + if (typeof args[0] === 'string') { + era = args.shift(); + } else { + let eras = calendar.getEras(); + era = eras[eras.length - 1]; + } + + let year = args.shift(); + let month = args.shift(); + let day = args.shift(); + + return [calendar, era, year, month, day]; +} + +/** A CalendarDate represents a date without any time components in a specific calendar system. */ +export class CalendarDate { + // This prevents TypeScript from allowing other types with the same fields to match. + // i.e. a ZonedDateTime should not be be passable to a parameter that expects CalendarDate. + // If that behavior is desired, use the AnyCalendarDate interface instead. + // @ts-ignore + #type; + /** The calendar system associated with this date, e.g. Gregorian. */ + public readonly calendar: Calendar; + /** The calendar era for this date, e.g. "BC" or "AD". */ + public readonly era: string; + /** The year of this date within the era. */ + public readonly year: number; + /** + * The month number within the year. Note that some calendar systems such as Hebrew + * may have a variable number of months per year. Therefore, month numbers may not + * always correspond to the same month names in different years. + */ + public readonly month: number; + /** The day number within the month. */ + public readonly day: number; + + constructor(year: number, month: number, day: number); + constructor(era: string, year: number, month: number, day: number); + constructor(calendar: Calendar, year: number, month: number, day: number); + constructor(calendar: Calendar, era: string, year: number, month: number, day: number); + constructor(...args: any[]) { + let [calendar, era, year, month, day] = shiftArgs(args); + this.calendar = calendar; + this.era = era; + this.year = year; + this.month = month; + this.day = day; + + constrain(this); + } + + /** Returns a copy of this date. */ + copy(): CalendarDate { + if (this.era) { + return new CalendarDate(this.calendar, this.era, this.year, this.month, this.day); + } else { + return new CalendarDate(this.calendar, this.year, this.month, this.day); + } + } + + /** Returns a new `CalendarDate` with the given duration added to it. */ + add(duration: DateDuration): CalendarDate { + return add(this, duration); + } + + /** Returns a new `CalendarDate` with the given duration subtracted from it. */ + subtract(duration: DateDuration): CalendarDate { + return subtract(this, duration); + } + + /** Returns a new `CalendarDate` with the given fields set to the provided values. Other fields will be constrained accordingly. */ + set(fields: DateFields): CalendarDate { + return set(this, fields); + } + + /** + * Returns a new `CalendarDate` with the given field adjusted by a specified amount. + * When the resulting value reaches the limits of the field, it wraps around. + */ + cycle(field: DateField, amount: number, options?: CycleOptions): CalendarDate { + return cycleDate(this, field, amount, options); + } + + /** Converts the date to a native JavaScript Date object, with the time set to midnight in the given time zone. */ + toDate(timeZone: string): Date { + return toDate(this, timeZone); + } + + /** Converts the date to an ISO 8601 formatted string. */ + toString(): string { + return dateToString(this); + } + + /** Compares this date with another. A negative result indicates that this date is before the given one, and a positive date indicates that it is after. */ + compare(b: AnyCalendarDate): number { + return compareDate(this, b); + } +} + +/** A Time represents a clock time without any date components. */ +export class Time { + // This prevents TypeScript from allowing other types with the same fields to match. + // @ts-ignore + #type; + /** The hour, numbered from 0 to 23. */ + public readonly hour: number; + /** The minute in the hour. */ + public readonly minute: number; + /** The second in the minute. */ + public readonly second: number; + /** The millisecond in the second. */ + public readonly millisecond: number; + + constructor( + hour: number = 0, + minute: number = 0, + second: number = 0, + millisecond: number = 0 + ) { + this.hour = hour; + this.minute = minute; + this.second = second; + this.millisecond = millisecond; + constrainTime(this); + } + + /** Returns a copy of this time. */ + copy(): Time { + return new Time(this.hour, this.minute, this.second, this.millisecond); + } + + /** Returns a new `Time` with the given duration added to it. */ + add(duration: TimeDuration): Time { + return addTime(this, duration); + } + + /** Returns a new `Time` with the given duration subtracted from it. */ + subtract(duration: TimeDuration): Time { + return subtractTime(this, duration); + } + + /** Returns a new `Time` with the given fields set to the provided values. Other fields will be constrained accordingly. */ + set(fields: TimeFields): Time { + return setTime(this, fields); + } + + /** + * Returns a new `Time` with the given field adjusted by a specified amount. + * When the resulting value reaches the limits of the field, it wraps around. + */ + cycle(field: TimeField, amount: number, options?: CycleTimeOptions): Time { + return cycleTime(this, field, amount, options); + } + + /** Converts the time to an ISO 8601 formatted string. */ + toString(): string { + return timeToString(this); + } + + /** Compares this time with another. A negative result indicates that this time is before the given one, and a positive time indicates that it is after. */ + compare(b: AnyTime): number { + return compareTime(this, b); + } +} + +/** A CalendarDateTime represents a date and time without a time zone, in a specific calendar system. */ +export class CalendarDateTime { + // This prevents TypeScript from allowing other types with the same fields to match. + // @ts-ignore + #type; + /** The calendar system associated with this date, e.g. Gregorian. */ + public readonly calendar: Calendar; + /** The calendar era for this date, e.g. "BC" or "AD". */ + public readonly era: string; + /** The year of this date within the era. */ + public readonly year: number; + /** + * The month number within the year. Note that some calendar systems such as Hebrew + * may have a variable number of months per year. Therefore, month numbers may not + * always correspond to the same month names in different years. + */ + public readonly month: number; + /** The day number within the month. */ + public readonly day: number; + /** The hour in the day, numbered from 0 to 23. */ + public readonly hour: number; + /** The minute in the hour. */ + public readonly minute: number; + /** The second in the minute. */ + public readonly second: number; + /** The millisecond in the second. */ + public readonly millisecond: number; + + constructor(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(era: string, year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(calendar: Calendar, year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(calendar: Calendar, era: string, year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(...args: any[]) { + let [calendar, era, year, month, day] = shiftArgs(args); + this.calendar = calendar; + this.era = era; + this.year = year; + this.month = month; + this.day = day; + this.hour = args.shift() || 0; + this.minute = args.shift() || 0; + this.second = args.shift() || 0; + this.millisecond = args.shift() || 0; + + constrain(this); + } + + /** Returns a copy of this date. */ + copy(): CalendarDateTime { + if (this.era) { + return new CalendarDateTime(this.calendar, this.era, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond); + } else { + return new CalendarDateTime(this.calendar, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond); + } + } + + /** Returns a new `CalendarDateTime` with the given duration added to it. */ + add(duration: DateTimeDuration): CalendarDateTime { + return add(this, duration); + } + + /** Returns a new `CalendarDateTime` with the given duration subtracted from it. */ + subtract(duration: DateTimeDuration): CalendarDateTime { + return subtract(this, duration); + } + + /** Returns a new `CalendarDateTime` with the given fields set to the provided values. Other fields will be constrained accordingly. */ + set(fields: DateFields & TimeFields): CalendarDateTime { + return set(setTime(this, fields), fields); + } + + /** + * Returns a new `CalendarDateTime` with the given field adjusted by a specified amount. + * When the resulting value reaches the limits of the field, it wraps around. + */ + cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions): CalendarDateTime { + switch (field) { + case 'era': + case 'year': + case 'month': + case 'day': + return cycleDate(this, field, amount, options); + default: + return cycleTime(this, field, amount, options); + } + } + + /** Converts the date to a native JavaScript Date object in the given time zone. */ + toDate(timeZone: string, disambiguation?: Disambiguation): Date { + return toDate(this, timeZone, disambiguation); + } + + /** Converts the date to an ISO 8601 formatted string. */ + toString(): string { + return dateTimeToString(this); + } + + /** Compares this date with another. A negative result indicates that this date is before the given one, and a positive date indicates that it is after. */ + compare(b: CalendarDate | CalendarDateTime | ZonedDateTime): number { + let res = compareDate(this, b); + if (res === 0) { + return compareTime(this, toCalendarDateTime(b)); + } + + return res; + } +} + +/** A ZonedDateTime represents a date and time in a specific time zone and calendar system. */ +export class ZonedDateTime { + // This prevents TypeScript from allowing other types with the same fields to match. + // @ts-ignore + #type; + /** The calendar system associated with this date, e.g. Gregorian. */ + public readonly calendar: Calendar; + /** The calendar era for this date, e.g. "BC" or "AD". */ + public readonly era: string; + /** The year of this date within the era. */ + public readonly year: number; + /** + * The month number within the year. Note that some calendar systems such as Hebrew + * may have a variable number of months per year. Therefore, month numbers may not + * always correspond to the same month names in different years. + */ + public readonly month: number; + /** The day number within the month. */ + public readonly day: number; + /** The hour in the day, numbered from 0 to 23. */ + public readonly hour: number; + /** The minute in the hour. */ + public readonly minute: number; + /** The second in the minute. */ + public readonly second: number; + /** The millisecond in the second. */ + public readonly millisecond: number; + /** The IANA time zone identifier that this date and time is represented in. */ + public readonly timeZone: string; + /** The UTC offset for this time, in milliseconds. */ + public readonly offset: number; + + constructor(year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(era: string, year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(calendar: Calendar, year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(calendar: Calendar, era: string, year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number); + constructor(...args: any[]) { + let [calendar, era, year, month, day] = shiftArgs(args); + let timeZone = args.shift(); + let offset = args.shift(); + this.calendar = calendar; + this.era = era; + this.year = year; + this.month = month; + this.day = day; + this.timeZone = timeZone; + this.offset = offset; + this.hour = args.shift() || 0; + this.minute = args.shift() || 0; + this.second = args.shift() || 0; + this.millisecond = args.shift() || 0; + + constrain(this); + } + + /** Returns a copy of this date. */ + copy(): ZonedDateTime { + if (this.era) { + return new ZonedDateTime(this.calendar, this.era, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond); + } else { + return new ZonedDateTime(this.calendar, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond); + } + } + + /** Returns a new `ZonedDateTime` with the given duration added to it. */ + add(duration: DateTimeDuration): ZonedDateTime { + return addZoned(this, duration); + } + + /** Returns a new `ZonedDateTime` with the given duration subtracted from it. */ + subtract(duration: DateTimeDuration): ZonedDateTime { + return subtractZoned(this, duration); + } + + /** Returns a new `ZonedDateTime` with the given fields set to the provided values. Other fields will be constrained accordingly. */ + set(fields: DateFields & TimeFields, disambiguation?: Disambiguation): ZonedDateTime { + return setZoned(this, fields, disambiguation); + } + + /** + * Returns a new `ZonedDateTime` with the given field adjusted by a specified amount. + * When the resulting value reaches the limits of the field, it wraps around. + */ + cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions): ZonedDateTime { + return cycleZoned(this, field, amount, options); + } + + /** Converts the date to a native JavaScript Date object. */ + toDate(): Date { + return zonedToDate(this); + } + + /** Converts the date to an ISO 8601 formatted string, including the UTC offset and time zone identifier. */ + toString(): string { + return zonedDateTimeToString(this); + } + + /** Converts the date to an ISO 8601 formatted string in UTC. */ + toAbsoluteString(): string { + return this.toDate().toISOString(); + } + + /** Compares this date with another. A negative result indicates that this date is before the given one, and a positive date indicates that it is after. */ + compare(b: CalendarDate | CalendarDateTime | ZonedDateTime): number { + // TODO: Is this a bad idea?? + return this.toDate().getTime() - toZoned(b, this.timeZone).toDate().getTime(); + } +} diff --git a/src/lib/glob/lib/date/DateFormatter.ts b/src/lib/glob/lib/date/DateFormatter.ts new file mode 100644 index 000000000..5a27171c3 --- /dev/null +++ b/src/lib/glob/lib/date/DateFormatter.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +let formatterCache = new Map(); + +interface DateRangeFormatPart extends Intl.DateTimeFormatPart { + source: 'startRange' | 'endRange' | 'shared' +} + +/** A wrapper around Intl.DateTimeFormat that fixes various browser bugs, and polyfills new features. */ +export class DateFormatter implements Intl.DateTimeFormat { + private formatter: Intl.DateTimeFormat; + private options: Intl.DateTimeFormatOptions; + private resolvedHourCycle: Intl.DateTimeFormatOptions['hourCycle']; + + constructor(locale: string, options: Intl.DateTimeFormatOptions = {}) { + this.formatter = getCachedDateFormatter(locale, options); + this.options = options; + } + + /** Formats a date as a string according to the locale and format options passed to the constructor. */ + format(value: Date): string { + return this.formatter.format(value); + } + + /** Formats a date to an array of parts such as separators, numbers, punctuation, and more. */ + formatToParts(value: Date): Intl.DateTimeFormatPart[] { + return this.formatter.formatToParts(value); + } + + /** Formats a date range as a string. */ + formatRange(start: Date, end: Date): string { + // @ts-ignore + if (typeof this.formatter.formatRange === 'function') { + // @ts-ignore + return this.formatter.formatRange(start, end); + } + + if (end < start) { + throw new RangeError('End date must be >= start date'); + } + + // Very basic fallback for old browsers. + return `${this.formatter.format(start)} – ${this.formatter.format(end)}`; + } + + /** Formats a date range as an array of parts. */ + formatRangeToParts(start: Date, end: Date): DateRangeFormatPart[] { + // @ts-ignore + if (typeof this.formatter.formatRangeToParts === 'function') { + // @ts-ignore + return this.formatter.formatRangeToParts(start, end); + } + + if (end < start) { + throw new RangeError('End date must be >= start date'); + } + + let startParts = this.formatter.formatToParts(start); + let endParts = this.formatter.formatToParts(end); + return [ + ...startParts.map(p => ({...p, source: 'startRange'} as DateRangeFormatPart)), + {type: 'literal', value: ' – ', source: 'shared'}, + ...endParts.map(p => ({...p, source: 'endRange'} as DateRangeFormatPart)) + ]; + } + + /** Returns the resolved formatting options based on the values passed to the constructor. */ + resolvedOptions(): Intl.ResolvedDateTimeFormatOptions { + let resolvedOptions = this.formatter.resolvedOptions(); + if (hasBuggyResolvedHourCycle()) { + if (!this.resolvedHourCycle) { + this.resolvedHourCycle = getResolvedHourCycle(resolvedOptions.locale, this.options); + } + resolvedOptions.hourCycle = this.resolvedHourCycle; + resolvedOptions.hour12 = this.resolvedHourCycle === 'h11' || this.resolvedHourCycle === 'h12'; + } + + // Safari uses a different name for the Ethiopic (Amete Alem) calendar. + // https://bugs.webkit.org/show_bug.cgi?id=241564 + if (resolvedOptions.calendar === 'ethiopic-amete-alem') { + resolvedOptions.calendar = 'ethioaa'; + } + + return resolvedOptions; + } +} + +// There are multiple bugs involving the hour12 and hourCycle options in various browser engines. +// - Chrome [1] (and the ECMA 402 spec [2]) resolve hour12: false in English and other locales to h24 (24:00 - 23:59) +// rather than h23 (00:00 - 23:59). Same can happen with hour12: true in French, which Chrome resolves to h11 (00:00 - 11:59) +// rather than h12 (12:00 - 11:59). +// - WebKit returns an incorrect hourCycle resolved option in the French locale due to incorrect parsing of 'h' literal +// in the resolved pattern. It also formats incorrectly when specifying the hourCycle option for the same reason. [3] +// [1] https://bugs.chromium.org/p/chromium/issues/detail?id=1045791 +// [2] https://github.com/tc39/ecma402/issues/402 +// [3] https://bugs.webkit.org/show_bug.cgi?id=229313 + +// https://github.com/unicode-org/cldr/blob/018b55eff7ceb389c7e3fc44e2f657eae3b10b38/common/supplemental/supplementalData.xml#L4774-L4802 +const hour12Preferences = { + true: { + // Only Japanese uses the h11 style for 12 hour time. All others use h12. + ja: 'h11' + }, + false: { + // All locales use h23 for 24 hour time. None use h24. + } +}; + +function getCachedDateFormatter(locale: string, options: Intl.DateTimeFormatOptions = {}): Intl.DateTimeFormat { + // Work around buggy hour12 behavior in Chrome / ECMA 402 spec by using hourCycle instead. + // Only apply the workaround if the issue is detected, because the hourCycle option is buggy in Safari. + if (typeof options.hour12 === 'boolean' && hasBuggyHour12Behavior()) { + options = {...options}; + let pref = (hour12Preferences[String(options.hour12) as keyof typeof hour12Preferences] as Record)[locale.split('-')[0]]; + let defaultHourCycle = options.hour12 ? 'h12' : 'h23'; + options.hourCycle = (pref ?? defaultHourCycle) as Intl.DateTimeFormatOptions['hourCycle']; + delete options.hour12; + } + + let cacheKey = locale + (options ? Object.entries(options).sort((a, b) => a[0] < b[0] ? -1 : 1).join() : ''); + if (formatterCache.has(cacheKey)) { + return formatterCache.get(cacheKey)!; + } + + let numberFormatter = new Intl.DateTimeFormat(locale, options); + formatterCache.set(cacheKey, numberFormatter); + return numberFormatter; +} + +let _hasBuggyHour12Behavior: boolean | null = null; +function hasBuggyHour12Behavior() { + if (_hasBuggyHour12Behavior == null) { + _hasBuggyHour12Behavior = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + hour12: false + }).format(new Date(2020, 2, 3, 0)) === '24'; + } + + return _hasBuggyHour12Behavior; +} + +let _hasBuggyResolvedHourCycle: boolean | null = null; +function hasBuggyResolvedHourCycle() { + if (_hasBuggyResolvedHourCycle == null) { + _hasBuggyResolvedHourCycle = new Intl.DateTimeFormat('fr', { + hour: 'numeric', + hour12: false + }).resolvedOptions().hourCycle === 'h12'; + } + + return _hasBuggyResolvedHourCycle; +} + +function getResolvedHourCycle(locale: string, options: Intl.DateTimeFormatOptions) { + if (!options.timeStyle && !options.hour) { + return undefined; + } + + // Work around buggy results in resolved hourCycle and hour12 options in WebKit. + // Format the minimum possible hour and maximum possible hour in a day and parse the results. + locale = locale.replace(/(-u-)?-nu-[a-zA-Z0-9]+/, ''); + locale += (locale.includes('-u-') ? '' : '-u') + '-nu-latn'; + let formatter = getCachedDateFormatter(locale, { + ...options, + timeZone: undefined // use local timezone + }); + + let min = parseInt(formatter.formatToParts(new Date(2020, 2, 3, 0)).find(p => p.type === 'hour')!.value, 10); + let max = parseInt(formatter.formatToParts(new Date(2020, 2, 3, 23)).find(p => p.type === 'hour')!.value, 10); + + if (min === 0 && max === 23) { + return 'h23'; + } + + if (min === 24 && max === 23) { + return 'h24'; + } + + if (min === 0 && max === 11) { + return 'h11'; + } + + if (min === 12 && max === 11) { + return 'h12'; + } + + throw new Error('Unexpected hour cycle result'); +} diff --git a/src/lib/glob/lib/date/calendars/BuddhistCalendar.ts b/src/lib/glob/lib/date/calendars/BuddhistCalendar.ts new file mode 100644 index 000000000..6198a239a --- /dev/null +++ b/src/lib/glob/lib/date/calendars/BuddhistCalendar.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar'; + +const BUDDHIST_ERA_START = -543; + +/** + * The Buddhist calendar is the same as the Gregorian calendar, but counts years + * starting from the birth of Buddha in 543 BC (Gregorian). It supports only one + * era, identified as 'BE'. + */ +export class BuddhistCalendar extends GregorianCalendar { + identifier: CalendarIdentifier = 'buddhist'; + + fromJulianDay(jd: number): CalendarDate { + let gregorianDate = super.fromJulianDay(jd); + let year = getExtendedYear(gregorianDate.era, gregorianDate.year); + return new CalendarDate( + this, + year - BUDDHIST_ERA_START, + gregorianDate.month, + gregorianDate.day + ); + } + + toJulianDay(date: AnyCalendarDate): number { + return super.toJulianDay(toGregorian(date)); + } + + getEras(): string[] { + return ['BE']; + } + + getDaysInMonth(date: AnyCalendarDate): number { + return super.getDaysInMonth(toGregorian(date)); + } + + balanceDate(): void {} +} + +function toGregorian(date: AnyCalendarDate) { + let [era, year] = fromExtendedYear(date.year + BUDDHIST_ERA_START); + return new CalendarDate( + era, + year, + date.month, + date.day + ); +} diff --git a/src/lib/glob/lib/date/calendars/EthiopicCalendar.ts b/src/lib/glob/lib/date/calendars/EthiopicCalendar.ts new file mode 100644 index 000000000..f5bea84c6 --- /dev/null +++ b/src/lib/glob/lib/date/calendars/EthiopicCalendar.ts @@ -0,0 +1,204 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import type {Mutable} from '../utils'; + +const ETHIOPIC_EPOCH = 1723856; +const COPTIC_EPOCH = 1824665; + +// The delta between Amete Alem 1 and Amete Mihret 1 +// AA 5501 = AM 1 +const AMETE_MIHRET_DELTA = 5500; + +function ceToJulianDay(epoch: number, year: number, month: number, day: number): number { + return ( + epoch // difference from Julian epoch to 1,1,1 + + 365 * year // number of days from years + + Math.floor(year / 4) // extra day of leap year + + 30 * (month - 1) // number of days from months (1 based) + + day - 1 // number of days for present month (1 based) + ); +} + +function julianDayToCE(epoch: number, jd: number) { + let year = Math.floor((4 * (jd - epoch)) / 1461); + let month = 1 + Math.floor((jd - ceToJulianDay(epoch, year, 1, 1)) / 30); + let day = jd + 1 - ceToJulianDay(epoch, year, month, 1); + return [year, month, day]; +} + +function getLeapDay(year: number) { + return Math.floor((year % 4) / 3); +} + +function getDaysInMonth(year: number, month: number) { + // The Ethiopian and Coptic calendars have 13 months, 12 of 30 days each and + // an intercalary month at the end of the year of 5 or 6 days, depending whether + // the year is a leap year or not. The Leap Year follows the same rules as the + // Julian Calendar so that the extra month always has six days in the year before + // a Julian Leap Year. + if (month % 13 !== 0) { + // not intercalary month + return 30; + } else { + // intercalary month 5 days + possible leap day + return getLeapDay(year) + 5; + } +} + +/** + * The Ethiopic calendar system is the official calendar used in Ethiopia. + * It includes 12 months of 30 days each, plus 5 or 6 intercalary days depending + * on whether it is a leap year. Two eras are supported: 'AA' and 'AM'. + */ +export class EthiopicCalendar implements Calendar { + identifier: CalendarIdentifier = 'ethiopic'; + + fromJulianDay(jd: number): CalendarDate { + let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd); + let era = 'AM'; + if (year <= 0) { + era = 'AA'; + year += AMETE_MIHRET_DELTA; + } + + return new CalendarDate(this, era, year, month, day); + } + + toJulianDay(date: AnyCalendarDate): number { + let year = date.year; + if (date.era === 'AA') { + year -= AMETE_MIHRET_DELTA; + } + + return ceToJulianDay(ETHIOPIC_EPOCH, year, date.month, date.day); + } + + getDaysInMonth(date: AnyCalendarDate): number { + return getDaysInMonth(date.year, date.month); + } + + getMonthsInYear(): number { + return 13; + } + + getDaysInYear(date: AnyCalendarDate): number { + return 365 + getLeapDay(date.year); + } + + getMaximumMonthsInYear(): number { + return 13; + } + + getMaximumDaysInMonth(): number { + return 30; + } + + getYearsInEra(date: AnyCalendarDate): number { + // 9999-12-31 gregorian is 9992-20-02 ethiopic. + // Round down to 9991 for the last full year. + // AA 9999-01-01 ethiopic is 4506-09-30 gregorian. + return date.era === 'AA' ? 9999 : 9991; + } + + getEras(): string[] { + return ['AA', 'AM']; + } +} + +/** + * The Ethiopic (Amete Alem) calendar is the same as the modern Ethiopic calendar, + * except years were measured from a different epoch. Only one era is supported: 'AA'. + */ +export class EthiopicAmeteAlemCalendar extends EthiopicCalendar { + identifier: CalendarIdentifier = 'ethioaa'; // also known as 'ethiopic-amete-alem' in ICU + + fromJulianDay(jd: number): CalendarDate { + let [year, month, day] = julianDayToCE(ETHIOPIC_EPOCH, jd); + year += AMETE_MIHRET_DELTA; + return new CalendarDate(this, 'AA', year, month, day); + } + + getEras(): string[] { + return ['AA']; + } + + getYearsInEra(): number { + // 9999-13-04 ethioaa is the maximum date, which is equivalent to 4506-09-29 gregorian. + return 9999; + } +} + +/** + * The Coptic calendar is similar to the Ethiopic calendar. + * It includes 12 months of 30 days each, plus 5 or 6 intercalary days depending + * on whether it is a leap year. Two eras are supported: 'BCE' and 'CE'. + */ +export class CopticCalendar extends EthiopicCalendar { + identifier: CalendarIdentifier = 'coptic'; + + fromJulianDay(jd: number): CalendarDate { + let [year, month, day] = julianDayToCE(COPTIC_EPOCH, jd); + let era = 'CE'; + if (year <= 0) { + era = 'BCE'; + year = 1 - year; + } + + return new CalendarDate(this, era, year, month, day); + } + + toJulianDay(date: AnyCalendarDate): number { + let year = date.year; + if (date.era === 'BCE') { + year = 1 - year; + } + + return ceToJulianDay(COPTIC_EPOCH, year, date.month, date.day); + } + + getDaysInMonth(date: AnyCalendarDate): number { + let year = date.year; + if (date.era === 'BCE') { + year = 1 - year; + } + + return getDaysInMonth(year, date.month); + } + + isInverseEra(date: AnyCalendarDate): boolean { + return date.era === 'BCE'; + } + + balanceDate(date: Mutable): void { + if (date.year <= 0) { + date.era = date.era === 'BCE' ? 'CE' : 'BCE'; + date.year = 1 - date.year; + } + } + + getEras(): string[] { + return ['BCE', 'CE']; + } + + getYearsInEra(date: AnyCalendarDate): number { + // 9999-12-30 gregorian is 9716-02-20 coptic. + // Round down to 9715 for the last full year. + // BCE 9999-01-01 coptic is BC 9716-06-15 gregorian. + return date.era === 'BCE' ? 9999 : 9715; + } +} diff --git a/src/lib/glob/lib/date/calendars/GregorianCalendar.ts b/src/lib/glob/lib/date/calendars/GregorianCalendar.ts new file mode 100644 index 000000000..46c422eb3 --- /dev/null +++ b/src/lib/glob/lib/date/calendars/GregorianCalendar.ts @@ -0,0 +1,144 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {mod} from '../utils'; +import type {Mutable} from '../utils'; + +const EPOCH = 1721426; // 001/01/03 Julian C.E. +export function gregorianToJulianDay(era: string, year: number, month: number, day: number): number { + year = getExtendedYear(era, year); + + let y1 = year - 1; + let monthOffset = -2; + if (month <= 2) { + monthOffset = 0; + } else if (isLeapYear(year)) { + monthOffset = -1; + } + + return ( + EPOCH - + 1 + + 365 * y1 + + Math.floor(y1 / 4) - + Math.floor(y1 / 100) + + Math.floor(y1 / 400) + + Math.floor((367 * month - 362) / 12 + monthOffset + day) + ); +} + +export function isLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + +export function getExtendedYear(era: string, year: number): number { + return era === 'BC' ? 1 - year : year; +} + +export function fromExtendedYear(year: number): [string, number] { + let era = 'AD'; + if (year <= 0) { + era = 'BC'; + year = 1 - year; + } + + return [era, year]; +} + +const daysInMonth = { + standard: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + leapyear: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] +}; + +/** + * The Gregorian calendar is the most commonly used calendar system in the world. It supports two eras: BC, and AD. + * Years always contain 12 months, and 365 or 366 days depending on whether it is a leap year. + */ +export class GregorianCalendar implements Calendar { + identifier: CalendarIdentifier = 'gregory'; + + fromJulianDay(jd: number): CalendarDate { + let jd0 = jd; + let depoch = jd0 - EPOCH; + let quadricent = Math.floor(depoch / 146097); + let dqc = mod(depoch, 146097); + let cent = Math.floor(dqc / 36524); + let dcent = mod(dqc, 36524); + let quad = Math.floor(dcent / 1461); + let dquad = mod(dcent, 1461); + let yindex = Math.floor(dquad / 365); + + let extendedYear = quadricent * 400 + cent * 100 + quad * 4 + yindex + (cent !== 4 && yindex !== 4 ? 1 : 0); + let [era, year] = fromExtendedYear(extendedYear); + let yearDay = jd0 - gregorianToJulianDay(era, year, 1, 1); + let leapAdj = 2; + if (jd0 < gregorianToJulianDay(era, year, 3, 1)) { + leapAdj = 0; + } else if (isLeapYear(year)) { + leapAdj = 1; + } + let month = Math.floor(((yearDay + leapAdj) * 12 + 373) / 367); + let day = jd0 - gregorianToJulianDay(era, year, month, 1) + 1; + + return new CalendarDate(era, year, month, day); + } + + toJulianDay(date: AnyCalendarDate): number { + return gregorianToJulianDay(date.era, date.year, date.month, date.day); + } + + getDaysInMonth(date: AnyCalendarDate): number { + return daysInMonth[isLeapYear(date.year) ? 'leapyear' : 'standard'][date.month - 1]; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getMonthsInYear(date: AnyCalendarDate): number { + return 12; + } + + getDaysInYear(date: AnyCalendarDate): number { + return isLeapYear(date.year) ? 366 : 365; + } + + getMaximumMonthsInYear(): number { + return 12; + } + + getMaximumDaysInMonth(): number { + return 31; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getYearsInEra(date: AnyCalendarDate): number { + return 9999; + } + + getEras(): string[] { + return ['BC', 'AD']; + } + + isInverseEra(date: AnyCalendarDate): boolean { + return date.era === 'BC'; + } + + balanceDate(date: Mutable): void { + if (date.year <= 0) { + date.era = date.era === 'BC' ? 'AD' : 'BC'; + date.year = 1 - date.year; + } + } +} diff --git a/src/lib/glob/lib/date/calendars/HebrewCalendar.ts b/src/lib/glob/lib/date/calendars/HebrewCalendar.ts new file mode 100644 index 000000000..366200c6e --- /dev/null +++ b/src/lib/glob/lib/date/calendars/HebrewCalendar.ts @@ -0,0 +1,211 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {mod} from '../utils'; +import type {Mutable} from '../utils'; + +const HEBREW_EPOCH = 347997; + +// Hebrew date calculations are performed in terms of days, hours, and +// "parts" (or halakim), which are 1/1080 of an hour, or 3 1/3 seconds. +const HOUR_PARTS = 1080; +const DAY_PARTS = 24 * HOUR_PARTS; + +// An approximate value for the length of a lunar month. +// It is used to calculate the approximate year and month of a given +// absolute date. +const MONTH_DAYS = 29; +const MONTH_FRACT = 12 * HOUR_PARTS + 793; +const MONTH_PARTS = MONTH_DAYS * DAY_PARTS + MONTH_FRACT; + +function isLeapYear(year: number) { + return mod(year * 7 + 1, 19) < 7; +} + +// Test for delay of start of new year and to avoid +// Sunday, Wednesday, and Friday as start of the new year. +function hebrewDelay1(year: number) { + let months = Math.floor((235 * year - 234) / 19); + let parts = 12084 + 13753 * months; + let day = months * 29 + Math.floor(parts / 25920); + + if (mod(3 * (day + 1), 7) < 3) { + day += 1; + } + + return day; +} + +// Check for delay in start of new year due to length of adjacent years +function hebrewDelay2(year: number) { + let last = hebrewDelay1(year - 1); + let present = hebrewDelay1(year); + let next = hebrewDelay1(year + 1); + + if (next - present === 356) { + return 2; + } + + if (present - last === 382) { + return 1; + } + + return 0; +} + +function startOfYear(year: number) { + return hebrewDelay1(year) + hebrewDelay2(year); +} + +function getDaysInYear(year: number) { + return startOfYear(year + 1) - startOfYear(year); +} + +function getYearType(year: number) { + let yearLength = getDaysInYear(year); + + if (yearLength > 380) { + yearLength -= 30; // Subtract length of leap month. + } + + switch (yearLength) { + case 353: + return 0; // deficient + case 354: + return 1; // normal + case 355: + return 2; // complete + } +} + +function getDaysInMonth(year: number, month: number): number { + // Normalize month numbers from 1 - 13, even on non-leap years + if (month >= 6 && !isLeapYear(year)) { + month++; + } + + // First of all, dispose of fixed-length 29 day months + if (month === 4 || month === 7 || month === 9 || month === 11 || month === 13) { + return 29; + } + + let yearType = getYearType(year); + + // If it's Heshvan, days depend on length of year + if (month === 2) { + return yearType === 2 ? 30 : 29; + } + + // Similarly, Kislev varies with the length of year + if (month === 3) { + return yearType === 0 ? 29 : 30; + } + + // Adar I only exists in leap years + if (month === 6) { + return isLeapYear(year) ? 30 : 0; + } + + return 30; +} + +/** + * The Hebrew calendar is used in Israel and around the world by the Jewish faith. + * Years include either 12 or 13 months depending on whether it is a leap year. + * In leap years, an extra month is inserted at month 6. + */ +export class HebrewCalendar implements Calendar { + identifier: CalendarIdentifier = 'hebrew'; + + fromJulianDay(jd: number): CalendarDate { + let d = jd - HEBREW_EPOCH; + let m = (d * DAY_PARTS) / MONTH_PARTS; // Months (approx) + let year = Math.floor((19 * m + 234) / 235) + 1; // Years (approx) + let ys = startOfYear(year); // 1st day of year + let dayOfYear = Math.floor(d - ys); + + // Because of the postponement rules, it's possible to guess wrong. Fix it. + while (dayOfYear < 1) { + year--; + ys = startOfYear(year); + dayOfYear = Math.floor(d - ys); + } + + // Now figure out which month we're in, and the date within that month + let month = 1; + let monthStart = 0; + while (monthStart < dayOfYear) { + monthStart += getDaysInMonth(year, month); + month++; + } + + month--; + monthStart -= getDaysInMonth(year, month); + + let day = dayOfYear - monthStart; + return new CalendarDate(this, year, month, day); + } + + toJulianDay(date: AnyCalendarDate): number { + let jd = startOfYear(date.year); + for (let month = 1; month < date.month; month++) { + jd += getDaysInMonth(date.year, month); + } + + return jd + date.day + HEBREW_EPOCH; + } + + getDaysInMonth(date: AnyCalendarDate): number { + return getDaysInMonth(date.year, date.month); + } + + getMonthsInYear(date: AnyCalendarDate): number { + return isLeapYear(date.year) ? 13 : 12; + } + + getDaysInYear(date: AnyCalendarDate): number { + return getDaysInYear(date.year); + } + + getMaximumMonthsInYear(): number { + return 13; + } + + getMaximumDaysInMonth(): number { + return 30; + } + + getYearsInEra(): number { + // 6239 gregorian + return 9999; + } + + getEras(): string[] { + return ['AM']; + } + + balanceYearMonth(date: Mutable, previousDate: AnyCalendarDate): void { + // Keep date in the same month when switching between leap years and non leap years + if (previousDate.year !== date.year) { + if (isLeapYear(previousDate.year) && !isLeapYear(date.year) && previousDate.month > 6) { + date.month--; + } else if (!isLeapYear(previousDate.year) && isLeapYear(date.year) && previousDate.month > 6) { + date.month++; + } + } + } +} diff --git a/src/lib/glob/lib/date/calendars/IndianCalendar.ts b/src/lib/glob/lib/date/calendars/IndianCalendar.ts new file mode 100644 index 000000000..faae008a8 --- /dev/null +++ b/src/lib/glob/lib/date/calendars/IndianCalendar.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {fromExtendedYear, GregorianCalendar, gregorianToJulianDay, isLeapYear} from './GregorianCalendar'; + +// Starts in 78 AD, +const INDIAN_ERA_START = 78; + +// The Indian year starts 80 days later than the Gregorian year. +const INDIAN_YEAR_START = 80; + +/** + * The Indian National Calendar is similar to the Gregorian calendar, but with + * years numbered since the Saka era in 78 AD (Gregorian). There are 12 months + * in each year, with either 30 or 31 days. Only one era identifier is supported: 'saka'. + */ +export class IndianCalendar extends GregorianCalendar { + identifier: CalendarIdentifier = 'indian'; + + fromJulianDay(jd: number): CalendarDate { + // Gregorian date for Julian day + let date = super.fromJulianDay(jd); + + // Year in Saka era + let indianYear = date.year - INDIAN_ERA_START; + + // Day number in Gregorian year (starting from 0) + let yDay = jd - gregorianToJulianDay(date.era, date.year, 1, 1); + + let leapMonth: number; + if (yDay < INDIAN_YEAR_START) { + // Day is at the end of the preceding Saka year + indianYear--; + + // Days in leapMonth this year, previous Gregorian year + leapMonth = isLeapYear(date.year - 1) ? 31 : 30; + yDay += leapMonth + (31 * 5) + (30 * 3) + 10; + } else { + // Days in leapMonth this year + leapMonth = isLeapYear(date.year) ? 31 : 30; + yDay -= INDIAN_YEAR_START; + } + + let indianMonth: number; + let indianDay: number; + if (yDay < leapMonth) { + indianMonth = 1; + indianDay = yDay + 1; + } else { + let mDay = yDay - leapMonth; + if (mDay < (31 * 5)) { + indianMonth = Math.floor(mDay / 31) + 2; + indianDay = (mDay % 31) + 1; + } else { + mDay -= 31 * 5; + indianMonth = Math.floor(mDay / 30) + 7; + indianDay = (mDay % 30) + 1; + } + } + + return new CalendarDate(this, indianYear, indianMonth, indianDay); + } + + toJulianDay(date: AnyCalendarDate): number { + let extendedYear = date.year + INDIAN_ERA_START; + let [era, year] = fromExtendedYear(extendedYear); + + let leapMonth: number; + let jd: number; + if (isLeapYear(year)) { + leapMonth = 31; + jd = gregorianToJulianDay(era, year, 3, 21); + } else { + leapMonth = 30; + jd = gregorianToJulianDay(era, year, 3, 22); + } + + if (date.month === 1) { + return jd + date.day - 1; + } + + jd += leapMonth + Math.min(date.month - 2, 5) * 31; + + if (date.month >= 8) { + jd += (date.month - 7) * 30; + } + + jd += date.day - 1; + return jd; + } + + getDaysInMonth(date: AnyCalendarDate): number { + if (date.month === 1 && isLeapYear(date.year + INDIAN_ERA_START)) { + return 31; + } + + if (date.month >= 2 && date.month <= 6) { + return 31; + } + + return 30; + } + + getYearsInEra(): number { + // 9999-12-31 gregorian is 9920-10-10 indian. + // Round down to 9919 for the last full year. + return 9919; + } + + getEras(): string[] { + return ['saka']; + } + + balanceDate(): void {} +} diff --git a/src/lib/glob/lib/date/calendars/IslamicCalendar.ts b/src/lib/glob/lib/date/calendars/IslamicCalendar.ts new file mode 100644 index 000000000..e6f5630ad --- /dev/null +++ b/src/lib/glob/lib/date/calendars/IslamicCalendar.ts @@ -0,0 +1,233 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; + +const CIVIL_EPOC = 1948440; // CE 622 July 16 Friday (Julian calendar) / CE 622 July 19 (Gregorian calendar) +const ASTRONOMICAL_EPOC = 1948439; // CE 622 July 15 Thursday (Julian calendar) +const UMALQURA_YEAR_START = 1300; +const UMALQURA_YEAR_END = 1600; +const UMALQURA_START_DAYS = 460322; + +function islamicToJulianDay(epoch: number, year: number, month: number, day: number): number { + return day + + Math.ceil(29.5 * (month - 1)) + + (year - 1) * 354 + + Math.floor((3 + 11 * year) / 30) + + epoch - 1; +} + +function julianDayToIslamic(calendar: Calendar, epoch: number, jd: number) { + let year = Math.floor((30 * (jd - epoch) + 10646) / 10631); + let month = Math.min(12, Math.ceil((jd - (29 + islamicToJulianDay(epoch, year, 1, 1))) / 29.5) + 1); + let day = jd - islamicToJulianDay(epoch, year, month, 1) + 1; + + return new CalendarDate(calendar, year, month, day); +} + +function isLeapYear(year: number): boolean { + return (14 + 11 * year) % 30 < 11; +} + +/** + * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab world. + * The civil variant uses simple arithmetic rules rather than astronomical calculations to approximate + * the traditional calendar, which is based on sighting of the crescent moon. It uses Friday, July 16 622 CE (Julian) as the epoch. + * Each year has 12 months, with either 354 or 355 days depending on whether it is a leap year. + * Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types). + */ +export class IslamicCivilCalendar implements Calendar { + identifier: CalendarIdentifier = 'islamic-civil'; + + fromJulianDay(jd: number): CalendarDate { + return julianDayToIslamic(this, CIVIL_EPOC, jd); + } + + toJulianDay(date: AnyCalendarDate): number { + return islamicToJulianDay(CIVIL_EPOC, date.year, date.month, date.day); + } + + getDaysInMonth(date: AnyCalendarDate): number { + let length = 29 + date.month % 2; + if (date.month === 12 && isLeapYear(date.year)) { + length++; + } + + return length; + } + + getMonthsInYear(): number { + return 12; + } + + getDaysInYear(date: AnyCalendarDate): number { + return isLeapYear(date.year) ? 355 : 354; + } + + getMaximumMonthsInYear(): number { + return 12; + } + + getMaximumDaysInMonth(): number { + return 30; + } + + getYearsInEra(): number { + // 9999 gregorian + return 9665; + } + + getEras(): string[] { + return ['AH']; + } +} + +/** + * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab world. + * The tabular variant uses simple arithmetic rules rather than astronomical calculations to approximate + * the traditional calendar, which is based on sighting of the crescent moon. It uses Thursday, July 15 622 CE (Julian) as the epoch. + * Each year has 12 months, with either 354 or 355 days depending on whether it is a leap year. + * Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types). + */ +export class IslamicTabularCalendar extends IslamicCivilCalendar { + identifier: CalendarIdentifier = 'islamic-tbla'; + + fromJulianDay(jd: number): CalendarDate { + return julianDayToIslamic(this, ASTRONOMICAL_EPOC, jd); + } + + toJulianDay(date: AnyCalendarDate): number { + return islamicToJulianDay(ASTRONOMICAL_EPOC, date.year, date.month, date.day); + } +} + +// Generated by scripts/generate-umalqura.js +const UMALQURA_DATA = 'qgpUDckO1AbqBmwDrQpVBakGkgepC9QF2gpcBS0NlQZKB1QLagutBa4ETwoXBYsGpQbVCtYCWwmdBE0KJg2VDawFtgm6AlsKKwWVCsoG6Qr0AnYJtgJWCcoKpAvSC9kF3AJtCU0FpQpSC6ULtAW2CVcFlwJLBaMGUgdlC2oFqworBZUMSg2lDcoF1gpXCasESwmlClILagt1BXYCtwhbBFUFqQW0BdoJ3QRuAjYJqgpUDbIN1QXaAlsJqwRVCkkLZAtxC7QFtQpVCiUNkg7JDtQG6QprCasEkwpJDaQNsg25CroEWworBZUKKgtVC1wFvQQ9Ah0JlQpKC1oLbQW2AjsJmwRVBqkGVAdqC2wFrQpVBSkLkgupC9QF2gpaBasKlQVJB2QHqgu1BbYCVgpNDiULUgtqC60FrgIvCZcESwalBqwG1gpdBZ0ETQoWDZUNqgW1BdoCWwmtBJUFygbkBuoK9QS2AlYJqgpUC9IL2QXqAm0JrQSVCkoLpQuyBbUJ1gSXCkcFkwZJB1ULagVrCisFiwpGDaMNygXWCtsEawJLCaUKUgtpC3UFdgG3CFsCKwVlBbQF2gntBG0BtgimClINqQ3UBdoKWwmrBFMGKQdiB6kLsgW1ClUFJQuSDckO0gbpCmsFqwRVCikNVA2qDbUJugQ7CpsETQqqCtUK2gJdCV4ELgqaDFUNsga5BroEXQotBZUKUguoC7QLuQXaAloJSgukDdEO6AZqC20FNQWVBkoNqA3UDdoGWwWdAisGFQtKC5ULqgWuCi4JjwwnBZUGqgbWCl0FnQI='; +let UMALQURA_MONTHLENGTH: Uint16Array; +let UMALQURA_YEAR_START_TABLE: Uint32Array; + +function umalquraYearStart(year: number): number { + return UMALQURA_START_DAYS + UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START]; +} + +function umalquraMonthLength(year: number, month: number): number { + let idx = (year - UMALQURA_YEAR_START); + let mask = (0x01 << (11 - (month - 1))); + if ((UMALQURA_MONTHLENGTH[idx] & mask) === 0) { + return 29; + } else { + return 30; + } +} + +function umalquraMonthStart(year: number, month: number): number { + let day = umalquraYearStart(year); + for (let i = 1; i < month; i++) { + day += umalquraMonthLength(year, i); + } + return day; +} + +function umalquraYearLength(year: number): number { + return UMALQURA_YEAR_START_TABLE[year + 1 - UMALQURA_YEAR_START] - UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START]; +} + +/** + * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab world. + * The Umalqura variant is primarily used in Saudi Arabia. It is a lunar calendar, based on astronomical + * calculations that predict the sighting of a crescent moon. Month and year lengths vary between years + * depending on these calculations. + * Learn more about the available Islamic calendars [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types). + */ +export class IslamicUmalquraCalendar extends IslamicCivilCalendar { + identifier: CalendarIdentifier = 'islamic-umalqura'; + + constructor() { + super(); + if (!UMALQURA_MONTHLENGTH) { + UMALQURA_MONTHLENGTH = new Uint16Array(Uint8Array.from(atob(UMALQURA_DATA), c => c.charCodeAt(0)).buffer); + } + + if (!UMALQURA_YEAR_START_TABLE) { + UMALQURA_YEAR_START_TABLE = new Uint32Array(UMALQURA_YEAR_END - UMALQURA_YEAR_START + 1); + + let yearStart = 0; + for (let year = UMALQURA_YEAR_START; year <= UMALQURA_YEAR_END; year++) { + UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START] = yearStart; + for (let i = 1; i <= 12; i++) { + yearStart += umalquraMonthLength(year, i); + } + } + } + } + + fromJulianDay(jd: number): CalendarDate { + let days = jd - CIVIL_EPOC; + let startDays = umalquraYearStart(UMALQURA_YEAR_START); + let endDays = umalquraYearStart(UMALQURA_YEAR_END); + if (days < startDays || days > endDays) { + return super.fromJulianDay(jd); + } else { + let y = UMALQURA_YEAR_START - 1; + let m = 1; + let d = 1; + while (d > 0) { + y++; + d = days - umalquraYearStart(y) + 1; + let yearLength = umalquraYearLength(y); + if (d === yearLength) { + m = 12; + break; + } else if (d < yearLength) { + let monthLength = umalquraMonthLength(y, m); + m = 1; + while (d > monthLength) { + d -= monthLength; + m++; + monthLength = umalquraMonthLength(y, m); + } + break; + } + } + + return new CalendarDate(this, y, m, (days - umalquraMonthStart(y, m) + 1)); + } + } + + toJulianDay(date: AnyCalendarDate): number { + if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) { + return super.toJulianDay(date); + } + + return CIVIL_EPOC + umalquraMonthStart(date.year, date.month) + (date.day - 1); + } + + getDaysInMonth(date: AnyCalendarDate): number { + if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) { + return super.getDaysInMonth(date); + } + + return umalquraMonthLength(date.year, date.month); + } + + getDaysInYear(date: AnyCalendarDate): number { + if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) { + return super.getDaysInYear(date); + } + + return umalquraYearLength(date.year); + } +} diff --git a/src/lib/glob/lib/date/calendars/JapaneseCalendar.ts b/src/lib/glob/lib/date/calendars/JapaneseCalendar.ts new file mode 100644 index 000000000..b27333110 --- /dev/null +++ b/src/lib/glob/lib/date/calendars/JapaneseCalendar.ts @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from the TC39 Temporal proposal. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {GregorianCalendar} from './GregorianCalendar'; +import type {Mutable} from '../utils'; + +const ERA_START_DATES = [[1868, 9, 8], [1912, 7, 30], [1926, 12, 25], [1989, 1, 8], [2019, 5, 1]]; +const ERA_END_DATES = [[1912, 7, 29], [1926, 12, 24], [1989, 1, 7], [2019, 4, 30]]; +const ERA_ADDENDS = [1867, 1911, 1925, 1988, 2018]; +const ERA_NAMES = ['meiji', 'taisho', 'showa', 'heisei', 'reiwa']; + +function findEraFromGregorianDate(date: AnyCalendarDate) { + const idx = ERA_START_DATES.findIndex(([year, month, day]) => { + if (date.year < year) { + return true; + } + + if (date.year === year && date.month < month) { + return true; + } + + if (date.year === year && date.month === month && date.day < day) { + return true; + } + + return false; + }); + + if (idx === -1) { + return ERA_START_DATES.length - 1; + } + + if (idx === 0) { + return 0; + } + + return idx - 1; +} + +function toGregorian(date: AnyCalendarDate) { + let eraAddend = ERA_ADDENDS[ERA_NAMES.indexOf(date.era)]; + if (!eraAddend) { + throw new Error('Unknown era: ' + date.era); + } + + return new CalendarDate( + date.year + eraAddend, + date.month, + date.day + ); +} + +/** + * The Japanese calendar is based on the Gregorian calendar, but with eras for the reign of each Japanese emperor. + * Whenever a new emperor ascends to the throne, a new era begins and the year starts again from 1. + * Note that eras before 1868 (Gregorian) are not currently supported by this implementation. + */ +export class JapaneseCalendar extends GregorianCalendar { + identifier: CalendarIdentifier = 'japanese'; + + fromJulianDay(jd: number): CalendarDate { + let date = super.fromJulianDay(jd); + let era = findEraFromGregorianDate(date); + + return new CalendarDate( + this, + ERA_NAMES[era], + date.year - ERA_ADDENDS[era], + date.month, + date.day + ); + } + + toJulianDay(date: AnyCalendarDate): number { + return super.toJulianDay(toGregorian(date)); + } + + balanceDate(date: Mutable): void { + let gregorianDate = toGregorian(date); + let era = findEraFromGregorianDate(gregorianDate); + + if (ERA_NAMES[era] !== date.era) { + date.era = ERA_NAMES[era]; + date.year = gregorianDate.year - ERA_ADDENDS[era]; + } + + // Constrain in case we went before the first supported era. + this.constrainDate(date); + } + + constrainDate(date: Mutable): void { + let idx = ERA_NAMES.indexOf(date.era); + let end = ERA_END_DATES[idx]; + if (end != null) { + let [endYear, endMonth, endDay] = end; + + // Constrain the year to the maximum possible value in the era. + // Then constrain the month and day fields within that. + let maxYear = endYear - ERA_ADDENDS[idx]; + date.year = Math.max(1, Math.min(maxYear, date.year)); + if (date.year === maxYear) { + date.month = Math.min(endMonth, date.month); + + if (date.month === endMonth) { + date.day = Math.min(endDay, date.day); + } + } + } + + if (date.year === 1 && idx >= 0) { + let [, startMonth, startDay] = ERA_START_DATES[idx]; + date.month = Math.max(startMonth, date.month); + + if (date.month === startMonth) { + date.day = Math.max(startDay, date.day); + } + } + } + + getEras(): string[] { + return ERA_NAMES; + } + + getYearsInEra(date: AnyCalendarDate): number { + // Get the number of years in the era, taking into account the date's month and day fields. + let era = ERA_NAMES.indexOf(date.era); + let cur = ERA_START_DATES[era]; + let next = ERA_START_DATES[era + 1]; + if (next == null) { + // 9999 gregorian is the maximum year allowed. + return 9999 - cur[0] + 1; + } + + let years = next[0] - cur[0]; + + if (date.month < next[1] || (date.month === next[1] && date.day < next[2])) { + years++; + } + + return years; + } + + getDaysInMonth(date: AnyCalendarDate): number { + return super.getDaysInMonth(toGregorian(date)); + } + + getMinimumMonthInYear(date: AnyCalendarDate): number { + let start = getMinimums(date); + return start ? start[1] : 1; + } + + getMinimumDayInMonth(date: AnyCalendarDate): number { + let start = getMinimums(date); + return start && date.month === start[1] ? start[2] : 1; + } +} + +function getMinimums(date: AnyCalendarDate) { + if (date.year === 1) { + let idx = ERA_NAMES.indexOf(date.era); + return ERA_START_DATES[idx]; + } +} diff --git a/src/lib/glob/lib/date/calendars/PersianCalendar.ts b/src/lib/glob/lib/date/calendars/PersianCalendar.ts new file mode 100644 index 000000000..cb6e22add --- /dev/null +++ b/src/lib/glob/lib/date/calendars/PersianCalendar.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {mod} from '../utils'; + +const PERSIAN_EPOCH = 1948320; + +// Number of days from the start of the year to the start of each month. +const MONTH_START = [ + 0, // Farvardin + 31, // Ordibehesht + 62, // Khordad + 93, // Tir + 124, // Mordad + 155, // Shahrivar + 186, // Mehr + 216, // Aban + 246, // Azar + 276, // Dey + 306, // Bahman + 336 // Esfand +]; + +/** + * The Persian calendar is the main calendar used in Iran and Afghanistan. It has 12 months + * in each year, the first 6 of which have 31 days, and the next 5 have 30 days. The 12th month + * has either 29 or 30 days depending on whether it is a leap year. The Persian year starts + * around the March equinox. + */ +export class PersianCalendar implements Calendar { + identifier: CalendarIdentifier = 'persian'; + + fromJulianDay(jd: number): CalendarDate { + let daysSinceEpoch = jd - PERSIAN_EPOCH; + let year = 1 + Math.floor((33 * daysSinceEpoch + 3) / 12053); + let farvardin1 = 365 * (year - 1) + Math.floor((8 * year + 21) / 33); + let dayOfYear = daysSinceEpoch - farvardin1; + let month = dayOfYear < 216 + ? Math.floor(dayOfYear / 31) + : Math.floor((dayOfYear - 6) / 30); + let day = dayOfYear - MONTH_START[month] + 1; + return new CalendarDate(this, year, month + 1, day); + } + + toJulianDay(date: AnyCalendarDate): number { + let jd = PERSIAN_EPOCH - 1 + 365 * (date.year - 1) + Math.floor((8 * date.year + 21) / 33); + jd += MONTH_START[date.month - 1]; + jd += date.day; + return jd; + } + + getMonthsInYear(): number { + return 12; + } + + getDaysInMonth(date: AnyCalendarDate): number { + if (date.month <= 6) { + return 31; + } + + if (date.month <= 11) { + return 30; + } + + let isLeapYear = mod(25 * date.year + 11, 33) < 8; + return isLeapYear ? 30 : 29; + } + + getMaximumMonthsInYear(): number { + return 12; + } + + getMaximumDaysInMonth(): number { + return 31; + } + + getEras(): string[] { + return ['AP']; + } + + getYearsInEra(): number { + // 9378-10-10 persian is 9999-12-31 gregorian. + // Round down to 9377 to set the maximum full year. + return 9377; + } +} diff --git a/src/lib/glob/lib/date/calendars/TaiwanCalendar.ts b/src/lib/glob/lib/date/calendars/TaiwanCalendar.ts new file mode 100644 index 000000000..e0eaaab22 --- /dev/null +++ b/src/lib/glob/lib/date/calendars/TaiwanCalendar.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from ICU. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, CalendarIdentifier} from '../types'; +import {CalendarDate} from '../CalendarDate'; +import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar'; +import type {Mutable} from '../utils'; + +const TAIWAN_ERA_START = 1911; + +function gregorianYear(date: AnyCalendarDate) { + return date.era === 'minguo' + ? date.year + TAIWAN_ERA_START + : 1 - date.year + TAIWAN_ERA_START; +} + +function gregorianToTaiwan(year: number): [string, number] { + let y = year - TAIWAN_ERA_START; + if (y > 0) { + return ['minguo', y]; + } else { + return ['before_minguo', 1 - y]; + } +} + +/** + * The Taiwanese calendar is the same as the Gregorian calendar, but years + * are numbered starting from 1912 (Gregorian). Two eras are supported: + * 'before_minguo' and 'minguo'. + */ +export class TaiwanCalendar extends GregorianCalendar { + identifier: CalendarIdentifier = 'roc'; // Republic of China + + fromJulianDay(jd: number): CalendarDate { + let date = super.fromJulianDay(jd); + let extendedYear = getExtendedYear(date.era, date.year); + let [era, year] = gregorianToTaiwan(extendedYear); + return new CalendarDate(this, era, year, date.month, date.day); + } + + toJulianDay(date: AnyCalendarDate): number { + return super.toJulianDay(toGregorian(date)); + } + + getEras(): string[] { + return ['before_minguo', 'minguo']; + } + + balanceDate(date: Mutable): void { + let [era, year] = gregorianToTaiwan(gregorianYear(date)); + date.era = era; + date.year = year; + } + + isInverseEra(date: AnyCalendarDate): boolean { + return date.era === 'before_minguo'; + } + + getDaysInMonth(date: AnyCalendarDate): number { + return super.getDaysInMonth(toGregorian(date)); + } + + getYearsInEra(date: AnyCalendarDate): number { + return date.era === 'before_minguo' ? 9999 : 9999 - TAIWAN_ERA_START; + } +} + +function toGregorian(date: AnyCalendarDate) { + let [era, year] = fromExtendedYear(gregorianYear(date)); + return new CalendarDate( + era, + year, + date.month, + date.day + ); +} diff --git a/src/lib/glob/lib/date/conversion.ts b/src/lib/glob/lib/date/conversion.ts new file mode 100644 index 000000000..840689182 --- /dev/null +++ b/src/lib/glob/lib/date/conversion.ts @@ -0,0 +1,316 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Portions of the code in this file are based on code from the TC39 Temporal proposal. +// Original licensing can be found in the NOTICE file in the root directory of this source tree. + +import type {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, DateFields, Disambiguation, TimeFields} from './types'; +import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; +import {constrain} from './manipulation'; +import {getExtendedYear, GregorianCalendar} from './calendars/GregorianCalendar'; +import {getLocalTimeZone, isEqualCalendar, isLocalTimeZoneOverridden} from './queries'; +import type {Mutable} from './utils'; + +export function epochFromDate(date: AnyDateTime): number { + date = toCalendar(date, new GregorianCalendar()); + let year = getExtendedYear(date.era, date.year); + return epochFromParts(year, date.month, date.day, date.hour, date.minute, date.second, date.millisecond); +} + +function epochFromParts(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number): number { + // Note: Date.UTC() interprets one and two-digit years as being in the + // 20th century, so don't use it + let date = new Date(); + date.setUTCHours(hour, minute, second, millisecond); + date.setUTCFullYear(year, month - 1, day); + return date.getTime(); +} + +export function getTimeZoneOffset(ms: number, timeZone: string): number { + // Fast path for UTC. + if (timeZone === 'UTC') { + return 0; + } + + // Fast path: for local timezone after 1970, use native Date. + // Skip this fast path if the local timezone was explicitly overridden via setLocalTimeZone, + // since native Date always uses the browser's timezone, not the overridden one. + if (ms > 0 && timeZone === getLocalTimeZone() && !isLocalTimeZoneOverridden()) { + return new Date(ms).getTimezoneOffset() * -60 * 1000; + } + + let {year, month, day, hour, minute, second} = getTimeZoneParts(ms, timeZone); + let utc = epochFromParts(year, month, day, hour, minute, second, 0); + return utc - Math.floor(ms / 1000) * 1000; +} + +const formattersByTimeZone = new Map(); + +function getTimeZoneParts(ms: number, timeZone: string) { + let formatter = formattersByTimeZone.get(timeZone); + if (!formatter) { + formatter = new Intl.DateTimeFormat('en-US', { + timeZone, + hour12: false, + era: 'short', + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric' + }); + + formattersByTimeZone.set(timeZone, formatter); + } + + let parts = formatter.formatToParts(new Date(ms)); + let namedParts: {[name: string]: string} = {}; + for (let part of parts) { + if (part.type !== 'literal') { + namedParts[part.type] = part.value; + } + } + + + return { + // Firefox returns B instead of BC... https://bugzilla.mozilla.org/show_bug.cgi?id=1752253 + year: namedParts.era === 'BC' || namedParts.era === 'B' ? -namedParts.year + 1 : +namedParts.year, + month: +namedParts.month, + day: +namedParts.day, + hour: namedParts.hour === '24' ? 0 : +namedParts.hour, // bugs.chromium.org/p/chromium/issues/detail?id=1045791 + minute: +namedParts.minute, + second: +namedParts.second + }; +} + +const DAYMILLIS = 86400000; + +export function possibleAbsolutes(date: CalendarDateTime, timeZone: string): number[] { + let ms = epochFromDate(date); + let earlier = ms - getTimeZoneOffset(ms - DAYMILLIS, timeZone); + let later = ms - getTimeZoneOffset(ms + DAYMILLIS, timeZone); + return getValidWallTimes(date, timeZone, earlier, later); +} + +function getValidWallTimes(date: CalendarDateTime, timeZone: string, earlier: number, later: number): number[] { + let found = earlier === later ? [earlier] : [earlier, later]; + return found.filter(absolute => isValidWallTime(date, timeZone, absolute)); +} + +function isValidWallTime(date: CalendarDateTime, timeZone: string, absolute: number) { + let parts = getTimeZoneParts(absolute, timeZone); + return date.year === parts.year + && date.month === parts.month + && date.day === parts.day + && date.hour === parts.hour + && date.minute === parts.minute + && date.second === parts.second; +} + +export function toAbsolute(date: CalendarDate | CalendarDateTime, timeZone: string, disambiguation: Disambiguation = 'compatible'): number { + let dateTime = toCalendarDateTime(date); + + // Fast path: if the time zone is UTC, use native Date. + if (timeZone === 'UTC') { + return epochFromDate(dateTime); + } + + // Fast path: if the time zone is the local timezone and disambiguation is compatible, use native Date. + // Skip this fast path if the local timezone was explicitly overridden via setLocalTimeZone, + // since native Date always uses the browser's timezone, not the overridden one. + if (timeZone === getLocalTimeZone() && disambiguation === 'compatible' && !isLocalTimeZoneOverridden()) { + dateTime = toCalendar(dateTime, new GregorianCalendar()); + + // Don't use Date constructor here because two-digit years are interpreted in the 20th century. + let date = new Date(); + let year = getExtendedYear(dateTime.era, dateTime.year); + date.setFullYear(year, dateTime.month - 1, dateTime.day); + date.setHours(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond); + return date.getTime(); + } + + let ms = epochFromDate(dateTime); + let offsetBefore = getTimeZoneOffset(ms - DAYMILLIS, timeZone); + let offsetAfter = getTimeZoneOffset(ms + DAYMILLIS, timeZone); + let valid = getValidWallTimes(dateTime, timeZone, ms - offsetBefore, ms - offsetAfter); + + if (valid.length === 1) { + return valid[0]; + } + + if (valid.length > 1) { + switch (disambiguation) { + // 'compatible' means 'earlier' for "fall back" transitions + case 'compatible': + case 'earlier': + return valid[0]; + case 'later': + return valid[valid.length - 1]; + case 'reject': + throw new RangeError('Multiple possible absolute times found'); + } + } + + switch (disambiguation) { + case 'earlier': + return Math.min(ms - offsetBefore, ms - offsetAfter); + // 'compatible' means 'later' for "spring forward" transitions + case 'compatible': + case 'later': + return Math.max(ms - offsetBefore, ms - offsetAfter); + case 'reject': + throw new RangeError('No such absolute time found'); + } +} + +export function toDate(dateTime: CalendarDate | CalendarDateTime, timeZone: string, disambiguation: Disambiguation = 'compatible'): Date { + return new Date(toAbsolute(dateTime, timeZone, disambiguation)); +} + +/** + * Takes a Unix epoch (milliseconds since 1970) and converts it to the provided time zone. + */ +export function fromAbsolute(ms: number, timeZone: string): ZonedDateTime { + let offset = getTimeZoneOffset(ms, timeZone); + let date = new Date(ms + offset); + let year = date.getUTCFullYear(); + let month = date.getUTCMonth() + 1; + let day = date.getUTCDate(); + let hour = date.getUTCHours(); + let minute = date.getUTCMinutes(); + let second = date.getUTCSeconds(); + let millisecond = date.getUTCMilliseconds(); + + return new ZonedDateTime(year < 1 ? 'BC' : 'AD', year < 1 ? -year + 1 : year, month, day, timeZone, offset, hour, minute, second, millisecond); +} + +/** + * Takes a `Date` object and converts it to the provided time zone. + */ +export function fromDate(date: Date, timeZone: string): ZonedDateTime { + return fromAbsolute(date.getTime(), timeZone); +} + +/** + * Takes a `Date` object and converts it to the time zone identifier for the current user. + */ +export function fromDateToLocal(date: Date): ZonedDateTime { + return fromDate(date, getLocalTimeZone()); +} + +/** Converts a value with date components such as a `CalendarDateTime` or `ZonedDateTime` into a `CalendarDate`. */ +export function toCalendarDate(dateTime: AnyCalendarDate): CalendarDate { + return new CalendarDate(dateTime.calendar, dateTime.era, dateTime.year, dateTime.month, dateTime.day); +} + +export function toDateFields(date: AnyCalendarDate): DateFields { + return { + era: date.era, + year: date.year, + month: date.month, + day: date.day + }; +} + +export function toTimeFields(date: AnyTime): TimeFields { + return { + hour: date.hour, + minute: date.minute, + second: date.second, + millisecond: date.millisecond + }; +} + +/** + * Converts a date value to a `CalendarDateTime`. An optional `Time` value can be passed to set the time + * of the resulting value, otherwise it will default to midnight. + */ +export function toCalendarDateTime(date: CalendarDate | CalendarDateTime | ZonedDateTime, time?: AnyTime): CalendarDateTime { + let hour = 0, minute = 0, second = 0, millisecond = 0; + if ('timeZone' in date) { + ({hour, minute, second, millisecond} = date); + } else if ('hour' in date && !time) { + return date; + } + + if (time) { + ({hour, minute, second, millisecond} = time); + } + + return new CalendarDateTime( + date.calendar, + date.era, + date.year, + date.month, + date.day, + hour, + minute, + second, + millisecond + ); +} + +/** Extracts the time components from a value containing a date and time. */ +export function toTime(dateTime: CalendarDateTime | ZonedDateTime): Time { + return new Time(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond); +} + +/** Converts a date from one calendar system to another. */ +export function toCalendar(date: T, calendar: Calendar): T { + if (isEqualCalendar(date.calendar, calendar)) { + return date; + } + + let calendarDate = calendar.fromJulianDay(date.calendar.toJulianDay(date)); + let copy: Mutable = date.copy(); + copy.calendar = calendar; + copy.era = calendarDate.era; + copy.year = calendarDate.year; + copy.month = calendarDate.month; + copy.day = calendarDate.day; + constrain(copy); + return copy; +} + +/** + * Converts a date value to a `ZonedDateTime` in the provided time zone. The `disambiguation` option can be set + * to control how values that fall on daylight saving time changes are interpreted. + */ +export function toZoned(date: CalendarDate | CalendarDateTime | ZonedDateTime, timeZone: string, disambiguation?: Disambiguation): ZonedDateTime { + if (date instanceof ZonedDateTime) { + if (date.timeZone === timeZone) { + return date; + } + + return toTimeZone(date, timeZone); + } + + let ms = toAbsolute(date, timeZone, disambiguation); + return fromAbsolute(ms, timeZone); +} + +export function zonedToDate(date: ZonedDateTime): Date { + let ms = epochFromDate(date) - date.offset; + return new Date(ms); +} + +/** Converts a `ZonedDateTime` from one time zone to another. */ +export function toTimeZone(date: ZonedDateTime, timeZone: string): ZonedDateTime { + let ms = epochFromDate(date) - date.offset; + return toCalendar(fromAbsolute(ms, timeZone), date.calendar); +} + +/** Converts the given `ZonedDateTime` into the user's local time zone. */ +export function toLocalTimeZone(date: ZonedDateTime): ZonedDateTime { + return toTimeZone(date, getLocalTimeZone()); +} diff --git a/src/lib/glob/lib/date/createCalendar.ts b/src/lib/glob/lib/date/createCalendar.ts new file mode 100644 index 000000000..18d0fc8e9 --- /dev/null +++ b/src/lib/glob/lib/date/createCalendar.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {BuddhistCalendar} from './calendars/BuddhistCalendar'; +import type {Calendar, CalendarIdentifier} from './types'; +import {CopticCalendar, EthiopicAmeteAlemCalendar, EthiopicCalendar} from './calendars/EthiopicCalendar'; +import {GregorianCalendar} from './calendars/GregorianCalendar'; +import {HebrewCalendar} from './calendars/HebrewCalendar'; +import {IndianCalendar} from './calendars/IndianCalendar'; +import {IslamicCivilCalendar, IslamicTabularCalendar, IslamicUmalquraCalendar} from './calendars/IslamicCalendar'; +import {JapaneseCalendar} from './calendars/JapaneseCalendar'; +import {PersianCalendar} from './calendars/PersianCalendar'; +import {TaiwanCalendar} from './calendars/TaiwanCalendar'; + +/** Creates a `Calendar` instance from a Unicode calendar identifier string. */ +export function createCalendar(name: CalendarIdentifier): Calendar { + switch (name) { + case 'buddhist': + return new BuddhistCalendar(); + case 'ethiopic': + return new EthiopicCalendar(); + case 'ethioaa': + return new EthiopicAmeteAlemCalendar(); + case 'coptic': + return new CopticCalendar(); + case 'hebrew': + return new HebrewCalendar(); + case 'indian': + return new IndianCalendar(); + case 'islamic-civil': + return new IslamicCivilCalendar(); + case 'islamic-tbla': + return new IslamicTabularCalendar(); + case 'islamic-umalqura': + return new IslamicUmalquraCalendar(); + case 'japanese': + return new JapaneseCalendar(); + case 'persian': + return new PersianCalendar(); + case 'roc': + return new TaiwanCalendar(); + case 'gregory': + default: + return new GregorianCalendar(); + } +} diff --git a/src/lib/glob/lib/date/index.ts b/src/lib/glob/lib/date/index.ts new file mode 100644 index 000000000..3718562f3 --- /dev/null +++ b/src/lib/glob/lib/date/index.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export type { + AnyCalendarDate, + AnyTime, + AnyDateTime, + Calendar, + CalendarIdentifier, + DateDuration, + TimeDuration, + DateTimeDuration, + DateFields, + TimeFields, + DateField, + TimeField, + Disambiguation, + CycleOptions, + CycleTimeOptions +} from './types'; + +export type { DateValue } from './queries'; + + + +export {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; +export {GregorianCalendar} from './calendars/GregorianCalendar'; +export {JapaneseCalendar} from './calendars/JapaneseCalendar'; +export {BuddhistCalendar} from './calendars/BuddhistCalendar'; +export {TaiwanCalendar} from './calendars/TaiwanCalendar'; +export {PersianCalendar} from './calendars/PersianCalendar'; +export {IndianCalendar} from './calendars/IndianCalendar'; +export {IslamicCivilCalendar, IslamicTabularCalendar, IslamicUmalquraCalendar} from './calendars/IslamicCalendar'; +export {HebrewCalendar} from './calendars/HebrewCalendar'; +export {EthiopicCalendar, EthiopicAmeteAlemCalendar, CopticCalendar} from './calendars/EthiopicCalendar'; +export {createCalendar} from './createCalendar'; +export { + toCalendarDate, + toCalendarDateTime, + toTime, + toCalendar, + toZoned, + toTimeZone, + toLocalTimeZone, + fromDate, + fromDateToLocal, + fromAbsolute +} from './conversion'; +export { + isSameDay, + isSameMonth, + isSameYear, + isEqualDay, + isEqualMonth, + isEqualYear, + isToday, + getDayOfWeek, + now, + today, + getHoursInDay, + getLocalTimeZone, + setLocalTimeZone, + resetLocalTimeZone, + startOfMonth, + startOfWeek, + startOfYear, + endOfMonth, + endOfWeek, + endOfYear, + getMinimumMonthInYear, + getMinimumDayInMonth, + getWeeksInMonth, + minDate, + maxDate, + isWeekend, + isWeekday, + isEqualCalendar, + +} from './queries'; +export { + parseDate, + parseDateTime, + parseTime, + parseAbsolute, + parseAbsoluteToLocal, + parseZonedDateTime, + parseDuration +} from './string'; +export {DateFormatter} from './DateFormatter'; diff --git a/src/lib/glob/lib/date/manipulation.ts b/src/lib/glob/lib/date/manipulation.ts new file mode 100644 index 000000000..c773d643d --- /dev/null +++ b/src/lib/glob/lib/date/manipulation.ts @@ -0,0 +1,477 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import type {AnyCalendarDate, AnyDateTime, AnyTime, CycleOptions, CycleTimeOptions, DateDuration, DateField, DateFields, DateTimeDuration, Disambiguation, TimeDuration, TimeField, TimeFields} from './types'; +import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate'; +import {epochFromDate, fromAbsolute, toAbsolute, toCalendar, toCalendarDateTime} from './conversion'; +import {GregorianCalendar} from './calendars/GregorianCalendar'; +import type {Mutable} from './utils'; + +const ONE_HOUR = 3600000; + +export function add(date: CalendarDateTime, duration: DateTimeDuration): CalendarDateTime; +export function add(date: CalendarDate, duration: DateDuration): CalendarDate; +export function add(date: CalendarDate | CalendarDateTime, duration: DateTimeDuration): CalendarDate | CalendarDateTime; +export function add(date: CalendarDate | CalendarDateTime, duration: DateTimeDuration): Mutable { + let mutableDate: Mutable = date.copy(); + let days = 'hour' in mutableDate ? addTimeFields(mutableDate, duration) : 0; + + addYears(mutableDate, duration.years || 0); + if (mutableDate.calendar.balanceYearMonth) { + mutableDate.calendar.balanceYearMonth(mutableDate, date); + } + + mutableDate.month += duration.months || 0; + + balanceYearMonth(mutableDate); + constrainMonthDay(mutableDate); + + mutableDate.day += (duration.weeks || 0) * 7; + mutableDate.day += duration.days || 0; + mutableDate.day += days; + + balanceDay(mutableDate); + + if (mutableDate.calendar.balanceDate) { + mutableDate.calendar.balanceDate(mutableDate); + } + + // Constrain in case adding ended up with a date outside the valid range for the calendar system. + // The behavior here is slightly different than when constraining in the `set` function in that + // we adjust smaller fields to their minimum/maximum values rather than constraining each field + // individually. This matches the general behavior of `add` vs `set` regarding how fields are balanced. + if (mutableDate.year < 1) { + mutableDate.year = 1; + mutableDate.month = 1; + mutableDate.day = 1; + } + + let maxYear = mutableDate.calendar.getYearsInEra(mutableDate); + if (mutableDate.year > maxYear) { + let isInverseEra = mutableDate.calendar.isInverseEra?.(mutableDate); + mutableDate.year = maxYear; + mutableDate.month = isInverseEra ? 1 : mutableDate.calendar.getMonthsInYear(mutableDate); + mutableDate.day = isInverseEra ? 1 : mutableDate.calendar.getDaysInMonth(mutableDate); + } + + if (mutableDate.month < 1) { + mutableDate.month = 1; + mutableDate.day = 1; + } + + let maxMonth = mutableDate.calendar.getMonthsInYear(mutableDate); + if (mutableDate.month > maxMonth) { + mutableDate.month = maxMonth; + mutableDate.day = mutableDate.calendar.getDaysInMonth(mutableDate); + } + + mutableDate.day = Math.max(1, Math.min(mutableDate.calendar.getDaysInMonth(mutableDate), mutableDate.day)); + return mutableDate; +} + +function addYears(date: Mutable, years: number) { + if (date.calendar.isInverseEra?.(date)) { + years = -years; + } + + date.year += years; +} + +function balanceYearMonth(date: Mutable) { + while (date.month < 1) { + addYears(date, -1); + date.month += date.calendar.getMonthsInYear(date); + } + + let monthsInYear = 0; + while (date.month > (monthsInYear = date.calendar.getMonthsInYear(date))) { + date.month -= monthsInYear; + addYears(date, 1); + } +} + +function balanceDay(date: Mutable) { + while (date.day < 1) { + date.month--; + balanceYearMonth(date); + date.day += date.calendar.getDaysInMonth(date); + } + + while (date.day > date.calendar.getDaysInMonth(date)) { + date.day -= date.calendar.getDaysInMonth(date); + date.month++; + balanceYearMonth(date); + } +} + +function constrainMonthDay(date: Mutable) { + date.month = Math.max(1, Math.min(date.calendar.getMonthsInYear(date), date.month)); + date.day = Math.max(1, Math.min(date.calendar.getDaysInMonth(date), date.day)); +} + +export function constrain(date: Mutable): void { + if (date.calendar.constrainDate) { + date.calendar.constrainDate(date); + } + + date.year = Math.max(1, Math.min(date.calendar.getYearsInEra(date), date.year)); + constrainMonthDay(date); +} + +export function invertDuration(duration: DateTimeDuration): DateTimeDuration { + let inverseDuration: Record = {}; + for (let key in duration) { + const k = key as keyof DateTimeDuration; + if (typeof duration[k] === 'number') { + inverseDuration[key] = -(duration[k] as number); + } + } + + return inverseDuration as DateTimeDuration; +} + +export function subtract(date: CalendarDateTime, duration: DateTimeDuration): CalendarDateTime; +export function subtract(date: CalendarDate, duration: DateDuration): CalendarDate; +export function subtract(date: CalendarDate | CalendarDateTime, duration: DateTimeDuration): CalendarDate | CalendarDateTime { + return add(date, invertDuration(duration)); +} + +export function set(date: CalendarDateTime, fields: DateFields): CalendarDateTime; +export function set(date: CalendarDate, fields: DateFields): CalendarDate; +export function set(date: CalendarDate | CalendarDateTime, fields: DateFields): Mutable { + let mutableDate: Mutable = date.copy(); + + if (fields.era != null) { + mutableDate.era = fields.era; + } + + if (fields.year != null) { + mutableDate.year = fields.year; + } + + if (fields.month != null) { + mutableDate.month = fields.month; + } + + if (fields.day != null) { + mutableDate.day = fields.day; + } + + constrain(mutableDate); + return mutableDate; +} + +export function setTime(value: CalendarDateTime, fields: TimeFields): CalendarDateTime; +export function setTime(value: Time, fields: TimeFields): Time; +export function setTime(value: Time | CalendarDateTime, fields: TimeFields): Mutable