This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
+406
View File
@@ -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 {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();
}
}
+198
View File
@@ -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<string, Intl.DateTimeFormat>();
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)][locale.split('-')[0]];
let defaultHourCycle = options.hour12 ? 'h12' : 'h23';
options.hourCycle = pref ?? defaultHourCycle;
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');
}
+64
View File
@@ -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 {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
);
}
+204
View File
@@ -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 {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {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<AnyCalendarDate>): 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;
}
}
+143
View File
@@ -0,0 +1,143 @@
/*
* 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 {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {mod, 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<AnyCalendarDate>): void {
if (date.year <= 0) {
date.era = date.era === 'BC' ? 'AD' : 'BC';
date.year = 1 - date.year;
}
}
}
+210
View File
@@ -0,0 +1,210 @@
/*
* 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 {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {mod, 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<AnyCalendarDate>, 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++;
}
}
}
}
+129
View File
@@ -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 {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 {}
}
+233
View File
@@ -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 {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);
}
}
+177
View File
@@ -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 {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {GregorianCalendar} from './GregorianCalendar';
import {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<AnyCalendarDate>): 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<AnyCalendarDate>): 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];
}
}
+100
View File
@@ -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 {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;
}
}
+88
View File
@@ -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 {AnyCalendarDate, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';
import {fromExtendedYear, getExtendedYear, GregorianCalendar} from './GregorianCalendar';
import {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<AnyCalendarDate>): 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
);
}
+309
View File
@@ -0,0 +1,309 @@
/*
* 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 {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} from './queries';
import {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.
if (ms > 0 && timeZone === getLocalTimeZone()) {
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<string, Intl.DateTimeFormat>();
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.
if (timeZone === getLocalTimeZone() && disambiguation === 'compatible') {
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);
}
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<T extends AnyCalendarDate>(date: T, calendar: Calendar): T {
if (isEqualCalendar(date.calendar, calendar)) {
return date;
}
let calendarDate = calendar.fromJulianDay(date.calendar.toJulianDay(date));
let copy: Mutable<T> = 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());
}
+55
View File
@@ -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 {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();
}
}
+92
View File
@@ -0,0 +1,92 @@
/*
* 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 {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,
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';
+476
View File
@@ -0,0 +1,476 @@
/*
* 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 {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 {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<AnyCalendarDate | AnyDateTime> {
let mutableDate: Mutable<AnyCalendarDate | AnyDateTime> = 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<AnyCalendarDate>, years: number) {
if (date.calendar.isInverseEra?.(date)) {
years = -years;
}
date.year += years;
}
function balanceYearMonth(date: Mutable<AnyCalendarDate>) {
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<AnyCalendarDate>) {
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<AnyCalendarDate>) {
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<AnyCalendarDate>): 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 = {};
for (let key in duration) {
if (typeof duration[key] === 'number') {
inverseDuration[key] = -duration[key];
}
}
return inverseDuration;
}
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<AnyCalendarDate> {
let mutableDate: Mutable<AnyCalendarDate> = 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<Time | CalendarDateTime> {
let mutableValue: Mutable<Time | CalendarDateTime> = value.copy();
if (fields.hour != null) {
mutableValue.hour = fields.hour;
}
if (fields.minute != null) {
mutableValue.minute = fields.minute;
}
if (fields.second != null) {
mutableValue.second = fields.second;
}
if (fields.millisecond != null) {
mutableValue.millisecond = fields.millisecond;
}
constrainTime(mutableValue);
return mutableValue;
}
function balanceTime(time: Mutable<AnyTime>): number {
time.second += Math.floor(time.millisecond / 1000);
time.millisecond = nonNegativeMod(time.millisecond, 1000);
time.minute += Math.floor(time.second / 60);
time.second = nonNegativeMod(time.second, 60);
time.hour += Math.floor(time.minute / 60);
time.minute = nonNegativeMod(time.minute, 60);
let days = Math.floor(time.hour / 24);
time.hour = nonNegativeMod(time.hour, 24);
return days;
}
export function constrainTime(time: Mutable<AnyTime>): void {
time.millisecond = Math.max(0, Math.min(time.millisecond, 1000));
time.second = Math.max(0, Math.min(time.second, 59));
time.minute = Math.max(0, Math.min(time.minute, 59));
time.hour = Math.max(0, Math.min(time.hour, 23));
}
function nonNegativeMod(a: number, b: number) {
let result = a % b;
if (result < 0) {
result += b;
}
return result;
}
function addTimeFields(time: Mutable<AnyTime>, duration: TimeDuration): number {
time.hour += duration.hours || 0;
time.minute += duration.minutes || 0;
time.second += duration.seconds || 0;
time.millisecond += duration.milliseconds || 0;
return balanceTime(time);
}
export function addTime(time: Time, duration: TimeDuration): Time {
let res = time.copy();
addTimeFields(res, duration);
return res;
}
export function subtractTime(time: Time, duration: TimeDuration): Time {
return addTime(time, invertDuration(duration));
}
export function cycleDate(value: CalendarDateTime, field: DateField, amount: number, options?: CycleOptions): CalendarDateTime;
export function cycleDate(value: CalendarDate, field: DateField, amount: number, options?: CycleOptions): CalendarDate;
export function cycleDate(value: CalendarDate | CalendarDateTime, field: DateField, amount: number, options?: CycleOptions): Mutable<CalendarDate | CalendarDateTime> {
let mutable: Mutable<CalendarDate | CalendarDateTime> = value.copy();
switch (field) {
case 'era': {
let eras = value.calendar.getEras();
let eraIndex = eras.indexOf(value.era);
if (eraIndex < 0) {
throw new Error('Invalid era: ' + value.era);
}
eraIndex = cycleValue(eraIndex, amount, 0, eras.length - 1, options?.round);
mutable.era = eras[eraIndex];
// Constrain the year and other fields within the era, so the era doesn't change when we balance below.
constrain(mutable);
break;
}
case 'year': {
if (mutable.calendar.isInverseEra?.(mutable)) {
amount = -amount;
}
// The year field should not cycle within the era as that can cause weird behavior affecting other fields.
// We need to also allow values < 1 so that decrementing goes to the previous era. If we get -Infinity back
// we know we wrapped around after reaching 9999 (the maximum), so set the year back to 1.
mutable.year = cycleValue(value.year, amount, -Infinity, 9999, options?.round);
if (mutable.year === -Infinity) {
mutable.year = 1;
}
if (mutable.calendar.balanceYearMonth) {
mutable.calendar.balanceYearMonth(mutable, value);
}
break;
}
case 'month':
mutable.month = cycleValue(value.month, amount, 1, value.calendar.getMonthsInYear(value), options?.round);
break;
case 'day':
mutable.day = cycleValue(value.day, amount, 1, value.calendar.getDaysInMonth(value), options?.round);
break;
default:
throw new Error('Unsupported field ' + field);
}
if (value.calendar.balanceDate) {
value.calendar.balanceDate(mutable);
}
constrain(mutable);
return mutable;
}
export function cycleTime(value: CalendarDateTime, field: TimeField, amount: number, options?: CycleTimeOptions): CalendarDateTime;
export function cycleTime(value: Time, field: TimeField, amount: number, options?: CycleTimeOptions): Time;
export function cycleTime(value: Time | CalendarDateTime, field: TimeField, amount: number, options?: CycleTimeOptions): Mutable<Time | CalendarDateTime> {
let mutable: Mutable<Time | CalendarDateTime> = value.copy();
switch (field) {
case 'hour': {
let hours = value.hour;
let min = 0;
let max = 23;
if (options?.hourCycle === 12) {
let isPM = hours >= 12;
min = isPM ? 12 : 0;
max = isPM ? 23 : 11;
}
mutable.hour = cycleValue(hours, amount, min, max, options?.round);
break;
}
case 'minute':
mutable.minute = cycleValue(value.minute, amount, 0, 59, options?.round);
break;
case 'second':
mutable.second = cycleValue(value.second, amount, 0, 59, options?.round);
break;
case 'millisecond':
mutable.millisecond = cycleValue(value.millisecond, amount, 0, 999, options?.round);
break;
default:
throw new Error('Unsupported field ' + field);
}
return mutable;
}
function cycleValue(value: number, amount: number, min: number, max: number, round = false) {
if (round) {
value += Math.sign(amount);
if (value < min) {
value = max;
}
let div = Math.abs(amount);
if (amount > 0) {
value = Math.ceil(value / div) * div;
} else {
value = Math.floor(value / div) * div;
}
if (value > max) {
value = min;
}
} else {
value += amount;
if (value < min) {
value = max - (min - value - 1);
} else if (value > max) {
value = min + (value - max - 1);
}
}
return value;
}
export function addZoned(dateTime: ZonedDateTime, duration: DateTimeDuration): ZonedDateTime {
let ms: number;
if ((duration.years != null && duration.years !== 0) || (duration.months != null && duration.months !== 0) || (duration.weeks != null && duration.weeks !== 0) || (duration.days != null && duration.days !== 0)) {
let res = add(toCalendarDateTime(dateTime), {
years: duration.years,
months: duration.months,
weeks: duration.weeks,
days: duration.days
});
// Changing the date may change the timezone offset, so we need to recompute
// using the 'compatible' disambiguation.
ms = toAbsolute(res, dateTime.timeZone);
} else {
// Otherwise, preserve the offset of the original date.
ms = epochFromDate(dateTime) - dateTime.offset;
}
// Perform time manipulation in milliseconds rather than on the original time fields to account for DST.
// For example, adding one hour during a DST transition may result in the hour field staying the same or
// skipping an hour. This results in the offset field changing value instead of the specified field.
ms += duration.milliseconds || 0;
ms += (duration.seconds || 0) * 1000;
ms += (duration.minutes || 0) * 60 * 1000;
ms += (duration.hours || 0) * 60 * 60 * 1000;
let res = fromAbsolute(ms, dateTime.timeZone);
return toCalendar(res, dateTime.calendar);
}
export function subtractZoned(dateTime: ZonedDateTime, duration: DateTimeDuration): ZonedDateTime {
return addZoned(dateTime, invertDuration(duration));
}
export function cycleZoned(dateTime: ZonedDateTime, field: DateField | TimeField, amount: number, options?: CycleTimeOptions): ZonedDateTime {
// For date fields, we want the time to remain consistent and the UTC offset to potentially change to account for DST changes.
// For time fields, we want the time to change by the amount given. This may result in the hour field staying the same, but the UTC
// offset changing in the case of a backward DST transition, or skipping an hour in the case of a forward DST transition.
switch (field) {
case 'hour': {
let min = 0;
let max = 23;
if (options?.hourCycle === 12) {
let isPM = dateTime.hour >= 12;
min = isPM ? 12 : 0;
max = isPM ? 23 : 11;
}
// The minimum and maximum hour may be affected by daylight saving time.
// For example, it might jump forward at midnight, and skip 1am.
// Or it might end at midnight and repeat the 11pm hour. To handle this, we get
// the possible absolute times for the min and max, and find the maximum range
// that is within the current day.
let plainDateTime = toCalendarDateTime(dateTime);
let minDate = toCalendar(setTime(plainDateTime, {hour: min}), new GregorianCalendar());
let minAbsolute = [toAbsolute(minDate, dateTime.timeZone, 'earlier'), toAbsolute(minDate, dateTime.timeZone, 'later')]
.filter(ms => fromAbsolute(ms, dateTime.timeZone).day === minDate.day)[0];
let maxDate = toCalendar(setTime(plainDateTime, {hour: max}), new GregorianCalendar());
let maxAbsolute = [toAbsolute(maxDate, dateTime.timeZone, 'earlier'), toAbsolute(maxDate, dateTime.timeZone, 'later')]
.filter(ms => fromAbsolute(ms, dateTime.timeZone).day === maxDate.day).pop()!;
// Since hours may repeat, we need to operate on the absolute time in milliseconds.
// This is done in hours from the Unix epoch so that cycleValue works correctly,
// and then converted back to milliseconds.
let ms = epochFromDate(dateTime) - dateTime.offset;
let hours = Math.floor(ms / ONE_HOUR);
let remainder = ms % ONE_HOUR;
ms = cycleValue(
hours,
amount,
Math.floor(minAbsolute / ONE_HOUR),
Math.floor(maxAbsolute / ONE_HOUR),
options?.round
) * ONE_HOUR + remainder;
// Now compute the new timezone offset, and convert the absolute time back to local time.
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
}
case 'minute':
case 'second':
case 'millisecond':
// @ts-ignore
return cycleTime(dateTime, field, amount, options);
case 'era':
case 'year':
case 'month':
case 'day': {
let res = cycleDate(toCalendarDateTime(dateTime), field, amount, options);
let ms = toAbsolute(res, dateTime.timeZone);
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
}
default:
throw new Error('Unsupported field ' + field);
}
}
export function setZoned(dateTime: ZonedDateTime, fields: DateFields & TimeFields, disambiguation?: Disambiguation): ZonedDateTime {
// Set the date/time fields, and recompute the UTC offset to account for DST changes.
// We also need to validate by converting back to a local time in case hours are skipped during forward DST transitions.
let plainDateTime = toCalendarDateTime(dateTime);
let res = setTime(set(plainDateTime, fields), fields);
// If the resulting plain date time values are equal, return the original time.
// We don't want to change the offset when setting the time to the same value.
if (res.compare(plainDateTime) === 0) {
return dateTime;
}
let ms = toAbsolute(res, dateTime.timeZone, disambiguation);
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
}
+365
View File
@@ -0,0 +1,365 @@
/*
* 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 {AnyCalendarDate, AnyTime, Calendar} from './types';
import {CalendarDate, CalendarDateTime, ZonedDateTime} from './CalendarDate';
import {fromAbsolute, toAbsolute, toCalendar, toCalendarDate} from './conversion';
import {weekStartData} from './weekStartData';
type DateValue = CalendarDate | CalendarDateTime | ZonedDateTime;
/** Returns whether the given dates occur on the same day, regardless of the time or calendar system. */
export function isSameDay(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
return a.era === b.era && a.year === b.year && a.month === b.month && a.day === b.day;
}
/** Returns whether the given dates occur in the same month, using the calendar system of the first date. */
export function isSameMonth(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
// In the Japanese calendar, months can span multiple eras/years, so only compare the first of the month.
a = startOfMonth(a);
b = startOfMonth(b);
return a.era === b.era && a.year === b.year && a.month === b.month;
}
/** Returns whether the given dates occur in the same year, using the calendar system of the first date. */
export function isSameYear(a: DateValue, b: DateValue): boolean {
b = toCalendar(b, a.calendar);
a = startOfYear(a);
b = startOfYear(b);
return a.era === b.era && a.year === b.year;
}
/** Returns whether the given dates occur on the same day, and are of the same calendar system. */
export function isEqualDay(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameDay(a, b);
}
/** Returns whether the given dates occur in the same month, and are of the same calendar system. */
export function isEqualMonth(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameMonth(a, b);
}
/** Returns whether the given dates occur in the same year, and are of the same calendar system. */
export function isEqualYear(a: DateValue, b: DateValue): boolean {
return isEqualCalendar(a.calendar, b.calendar) && isSameYear(a, b);
}
/** Returns whether two calendars are the same. */
export function isEqualCalendar(a: Calendar, b: Calendar): boolean {
return a.isEqual?.(b) ?? b.isEqual?.(a) ?? a.identifier === b.identifier;
}
/** Returns whether the date is today in the given time zone. */
export function isToday(date: DateValue, timeZone: string): boolean {
return isSameDay(date, today(timeZone));
}
const DAY_MAP = {
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6
};
type DayOfWeek = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';
/**
* Returns the day of week for the given date and locale. Days are numbered from zero to six,
* where zero is the first day of the week in the given locale. For example, in the United States,
* the first day of the week is Sunday, but in France it is Monday.
*/
export function getDayOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let julian = date.calendar.toJulianDay(date);
// If julian is negative, then julian % 7 will be negative, so we adjust
// accordingly. Julian day 0 is Monday.
let weekStart = firstDayOfWeek ? DAY_MAP[firstDayOfWeek] : getWeekStart(locale);
let dayOfWeek = Math.ceil(julian + 1 - weekStart) % 7;
if (dayOfWeek < 0) {
dayOfWeek += 7;
}
return dayOfWeek;
}
/** Returns the current time in the given time zone. */
export function now(timeZone: string): ZonedDateTime {
return fromAbsolute(Date.now(), timeZone);
}
/** Returns today's date in the given time zone. */
export function today(timeZone: string): CalendarDate {
return toCalendarDate(now(timeZone));
}
export function compareDate(a: AnyCalendarDate, b: AnyCalendarDate): number {
return a.calendar.toJulianDay(a) - b.calendar.toJulianDay(b);
}
export function compareTime(a: AnyTime, b: AnyTime): number {
return timeToMs(a) - timeToMs(b);
}
function timeToMs(a: AnyTime): number {
return a.hour * 60 * 60 * 1000 + a.minute * 60 * 1000 + a.second * 1000 + a.millisecond;
}
/**
* Returns the number of hours in the given date and time zone.
* Usually this is 24, but it could be 23 or 25 if the date is on a daylight saving transition.
*/
export function getHoursInDay(a: CalendarDate, timeZone: string): number {
let ms = toAbsolute(a, timeZone);
let tomorrow = a.add({days: 1});
let tomorrowMs = toAbsolute(tomorrow, timeZone);
return (tomorrowMs - ms) / 3600000;
}
let localTimeZone: string | null = null;
/** Returns the time zone identifier for the current user. */
export function getLocalTimeZone(): string {
if (localTimeZone == null) {
localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
}
return localTimeZone!;
}
/** Sets the time zone identifier for the current user. */
export function setLocalTimeZone(timeZone: string): void {
localTimeZone = timeZone;
}
/** Resets the time zone identifier for the current user. */
export function resetLocalTimeZone(): void {
localTimeZone = null;
}
/** Returns the first date of the month for the given date. */
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;
export function startOfMonth(date: CalendarDate): CalendarDate;
export function startOfMonth(date: DateValue): DateValue;
export function startOfMonth(date: DateValue): DateValue {
// Use `subtract` instead of `set` so we don't get constrained in an era.
return date.subtract({days: date.day - 1});
}
/** Returns the last date of the month for the given date. */
export function endOfMonth(date: ZonedDateTime): ZonedDateTime;
export function endOfMonth(date: CalendarDateTime): CalendarDateTime;
export function endOfMonth(date: CalendarDate): CalendarDate;
export function endOfMonth(date: DateValue): DateValue;
export function endOfMonth(date: DateValue): DateValue {
return date.add({days: date.calendar.getDaysInMonth(date) - date.day});
}
/** Returns the first day of the year for the given date. */
export function startOfYear(date: ZonedDateTime): ZonedDateTime;
export function startOfYear(date: CalendarDateTime): CalendarDateTime;
export function startOfYear(date: CalendarDate): CalendarDate;
export function startOfYear(date: DateValue): DateValue;
export function startOfYear(date: DateValue): DateValue {
return startOfMonth(date.subtract({months: date.month - 1}));
}
/** Returns the last day of the year for the given date. */
export function endOfYear(date: ZonedDateTime): ZonedDateTime;
export function endOfYear(date: CalendarDateTime): CalendarDateTime;
export function endOfYear(date: CalendarDate): CalendarDate;
export function endOfYear(date: DateValue): DateValue;
export function endOfYear(date: DateValue): DateValue {
return endOfMonth(date.add({months: date.calendar.getMonthsInYear(date) - date.month}));
}
export function getMinimumMonthInYear(date: AnyCalendarDate): number {
if (date.calendar.getMinimumMonthInYear) {
return date.calendar.getMinimumMonthInYear(date);
}
return 1;
}
export function getMinimumDayInMonth(date: AnyCalendarDate): number {
if (date.calendar.getMinimumDayInMonth) {
return date.calendar.getMinimumDayInMonth(date);
}
return 1;
}
/** Returns the first date of the week for the given date and locale. */
export function startOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function startOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function startOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function startOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
let dayOfWeek = getDayOfWeek(date, locale, firstDayOfWeek);
return date.subtract({days: dayOfWeek});
}
/** Returns the last date of the week for the given date and locale. */
export function endOfWeek(date: ZonedDateTime, locale: string, firstDayOfWeek?: DayOfWeek): ZonedDateTime;
export function endOfWeek(date: CalendarDateTime, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDateTime;
export function endOfWeek(date: CalendarDate, locale: string, firstDayOfWeek?: DayOfWeek): CalendarDate;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue;
export function endOfWeek(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): DateValue {
return startOfWeek(date, locale, firstDayOfWeek).add({days: 6});
}
const cachedRegions = new Map<string, string>();
const cachedWeekInfo = new Map<string, {firstDay: number}>();
function getRegion(locale: string): string | undefined {
// If the Intl.Locale API is available, use it to get the region for the locale.
// @ts-ignore
if (Intl.Locale) {
// Constructing an Intl.Locale is expensive, so cache the result.
let region = cachedRegions.get(locale);
if (!region) {
// @ts-ignore
region = new Intl.Locale(locale).maximize().region;
if (region) {
cachedRegions.set(locale, region);
}
}
return region;
}
// If not, just try splitting the string.
// If the second part of the locale string is 'u',
// then this is a unicode extension, so ignore it.
// Otherwise, it should be the region.
let part = locale.split('-')[1];
return part === 'u' ? undefined : part;
}
function getWeekStart(locale: string): number {
// TODO: use Intl.Locale for this once browsers support the weekInfo property
// https://github.com/tc39/proposal-intl-locale-info
let weekInfo = cachedWeekInfo.get(locale);
if (!weekInfo) {
if (Intl.Locale) {
// @ts-ignore
let localeInst = new Intl.Locale(locale);
if ('getWeekInfo' in localeInst) {
// @ts-expect-error
weekInfo = localeInst.getWeekInfo();
if (weekInfo) {
cachedWeekInfo.set(locale, weekInfo);
return weekInfo.firstDay;
}
}
}
let region = getRegion(locale);
if (locale.includes('-fw-')) {
// pull the value for the attribute fw from strings such as en-US-u-ca-iso8601-fw-tue or en-US-u-ca-iso8601-fw-mon-nu-thai
// where the fw attribute could be followed by another unicode locale extension or not
let day = locale.split('-fw-')[1].split('-')[0];
if (day === 'mon') {
weekInfo = {firstDay: 1};
} else if (day === 'tue') {
weekInfo = {firstDay: 2};
} else if (day === 'wed') {
weekInfo = {firstDay: 3};
} else if (day === 'thu') {
weekInfo = {firstDay: 4};
} else if (day === 'fri') {
weekInfo = {firstDay: 5};
} else if (day === 'sat') {
weekInfo = {firstDay: 6};
} else {
weekInfo = {firstDay: 0};
}
} else if (locale.includes('-ca-iso8601')) {
weekInfo = {firstDay: 1};
} else {
weekInfo = {firstDay: region ? weekStartData[region] || 0 : 0};
}
cachedWeekInfo.set(locale, weekInfo);
}
return weekInfo.firstDay;
}
/** Returns the number of weeks in the given month and locale. */
export function getWeeksInMonth(date: DateValue, locale: string, firstDayOfWeek?: DayOfWeek): number {
let days = date.calendar.getDaysInMonth(date);
return Math.ceil((getDayOfWeek(startOfMonth(date), locale, firstDayOfWeek) + days) / 7);
}
/** Returns the lesser of the two provider dates. */
export function minDate<A extends DateValue, B extends DateValue>(a?: A | null, b?: B | null): A | B | null | undefined {
if (a && b) {
return a.compare(b) <= 0 ? a : b;
}
return a || b;
}
/** Returns the greater of the two provider dates. */
export function maxDate<A extends DateValue, B extends DateValue>(a?: A | null, b?: B | null): A | B | null | undefined {
if (a && b) {
return a.compare(b) >= 0 ? a : b;
}
return a || b;
}
const WEEKEND_DATA = {
AF: [4, 5],
AE: [5, 6],
BH: [5, 6],
DZ: [5, 6],
EG: [5, 6],
IL: [5, 6],
IQ: [5, 6],
IR: [5, 5],
JO: [5, 6],
KW: [5, 6],
LY: [5, 6],
OM: [5, 6],
QA: [5, 6],
SA: [5, 6],
SD: [5, 6],
SY: [5, 6],
YE: [5, 6]
};
/** Returns whether the given date is on a weekend in the given locale. */
export function isWeekend(date: DateValue, locale: string): boolean {
let julian = date.calendar.toJulianDay(date);
// If julian is negative, then julian % 7 will be negative, so we adjust
// accordingly. Julian day 0 is Monday.
let dayOfWeek = Math.ceil(julian + 1) % 7;
if (dayOfWeek < 0) {
dayOfWeek += 7;
}
let region = getRegion(locale);
// Use Intl.Locale for this once weekInfo is supported.
// https://github.com/tc39/proposal-intl-locale-info
let [start, end] = WEEKEND_DATA[region!] || [6, 0];
return dayOfWeek === start || dayOfWeek === end;
}
/** Returns whether the given date is on a weekday in the given locale. */
export function isWeekday(date: DateValue, locale: string): boolean {
return !isWeekend(date, locale);
}
+299
View File
@@ -0,0 +1,299 @@
/*
* 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 {AnyDateTime, DateTimeDuration, Disambiguation} from './types';
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate';
import {epochFromDate, fromAbsolute, possibleAbsolutes, toAbsolute, toCalendar, toCalendarDateTime, toTimeZone} from './conversion';
import {getLocalTimeZone} from './queries';
import {GregorianCalendar} from './calendars/GregorianCalendar';
import {Mutable} from './utils';
const TIME_RE = /^(\d{2})(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?$/;
const DATE_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})$/;
const DATE_TIME_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?$/;
const ZONED_DATE_TIME_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:([+-]\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?\[(.*?)\]$/;
const ABSOLUTE_RE = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:T(\d{2}))?(?::(\d{2}))?(?::(\d{2}))?(\.\d+)?(?:(?:([+-]\d{2})(?::?(\d{2}))?)|Z)$/;
const DATE_TIME_DURATION_RE =
/^((?<negative>-)|\+)?P((?<years>\d*)Y)?((?<months>\d*)M)?((?<weeks>\d*)W)?((?<days>\d*)D)?((?<time>T)((?<hours>\d*[.,]?\d{1,9})H)?((?<minutes>\d*[.,]?\d{1,9})M)?((?<seconds>\d*[.,]?\d{1,9})S)?)?$/;
const requiredDurationTimeGroups = ['hours', 'minutes', 'seconds'];
const requiredDurationGroups = ['years', 'months', 'weeks', 'days', ...requiredDurationTimeGroups];
/** Parses an ISO 8601 time string. */
export function parseTime(value: string): Time {
let m = value.match(TIME_RE);
if (!m) {
throw new Error('Invalid ISO 8601 time string: ' + value);
}
return new Time(
parseNumber(m[1], 0, 23),
m[2] ? parseNumber(m[2], 0, 59) : 0,
m[3] ? parseNumber(m[3], 0, 59) : 0,
m[4] ? parseNumber(m[4], 0, Infinity) * 1000 : 0
);
}
/** Parses an ISO 8601 date string, with no time components. */
export function parseDate(value: string): CalendarDate {
let m = value.match(DATE_RE);
if (!m) {
if (ABSOLUTE_RE.test(value)) {
throw new Error(`Invalid ISO 8601 date string: ${value}. Use parseAbsolute() instead.`);
}
throw new Error('Invalid ISO 8601 date string: ' + value);
}
let date: Mutable<CalendarDate> = new CalendarDate(
parseNumber(m[1], 0, 9999),
parseNumber(m[2], 1, 12),
1
);
date.day = parseNumber(m[3], 1, date.calendar.getDaysInMonth(date));
return date as CalendarDate;
}
/** Parses an ISO 8601 date and time string, with no time zone. */
export function parseDateTime(value: string): CalendarDateTime {
let m = value.match(DATE_TIME_RE);
if (!m) {
if (ABSOLUTE_RE.test(value)) {
throw new Error(`Invalid ISO 8601 date time string: ${value}. Use parseAbsolute() instead.`);
}
throw new Error('Invalid ISO 8601 date time string: ' + value);
}
let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';
let date: Mutable<CalendarDateTime> = new CalendarDateTime(
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
m[4] ? parseNumber(m[4], 0, 23) : 0,
m[5] ? parseNumber(m[5], 0, 59) : 0,
m[6] ? parseNumber(m[6], 0, 59) : 0,
m[7] ? parseNumber(m[7], 0, Infinity) * 1000 : 0
);
date.day = parseNumber(m[3], 0, date.calendar.getDaysInMonth(date));
return date as CalendarDateTime;
}
/**
* Parses an ISO 8601 date and time string with a time zone extension and optional UTC offset
* (e.g. "2021-11-07T00:45[America/Los_Angeles]" or "2021-11-07T00:45-07:00[America/Los_Angeles]").
* Ambiguous times due to daylight saving time transitions are resolved according to the `disambiguation`
* parameter.
*/
export function parseZonedDateTime(value: string, disambiguation?: Disambiguation): ZonedDateTime {
let m = value.match(ZONED_DATE_TIME_RE);
if (!m) {
throw new Error('Invalid ISO 8601 date time string: ' + value);
}
let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';
let date: Mutable<ZonedDateTime> = new ZonedDateTime(
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
m[11],
0,
m[4] ? parseNumber(m[4], 0, 23) : 0,
m[5] ? parseNumber(m[5], 0, 59) : 0,
m[6] ? parseNumber(m[6], 0, 59) : 0,
m[7] ? parseNumber(m[7], 0, Infinity) * 1000 : 0
);
date.day = parseNumber(m[3], 0, date.calendar.getDaysInMonth(date));
let plainDateTime = toCalendarDateTime(date as ZonedDateTime);
let ms: number;
if (m[8]) {
let hourOffset = parseNumber(m[8], -23, 23);
date.offset = Math.sign(hourOffset) * (Math.abs(hourOffset) * 60 * 60 * 1000 + parseNumber(m[9] ?? '0', 0, 59) * 60 * 1000 + parseNumber(m[10] ?? '0', 0, 59) * 1000);
ms = epochFromDate(date as ZonedDateTime) - date.offset;
// Validate offset against parsed date.
let absolutes = possibleAbsolutes(plainDateTime, date.timeZone);
if (!absolutes.includes(ms)) {
throw new Error(`Offset ${offsetToString(date.offset)} is invalid for ${dateTimeToString(date)} in ${date.timeZone}`);
}
} else {
// Convert to absolute and back to fix invalid times due to DST.
ms = toAbsolute(toCalendarDateTime(plainDateTime), date.timeZone, disambiguation);
}
return fromAbsolute(ms, date.timeZone);
}
/**
* Parses an ISO 8601 date and time string with a UTC offset (e.g. "2021-11-07T07:45:00Z"
* or "2021-11-07T07:45:00-07:00"). The result is converted to the provided time zone.
*/
export function parseAbsolute(value: string, timeZone: string): ZonedDateTime {
let m = value.match(ABSOLUTE_RE);
if (!m) {
throw new Error('Invalid ISO 8601 date time string: ' + value);
}
let year = parseNumber(m[1], -9999, 9999);
let era = year < 1 ? 'BC' : 'AD';
let date: Mutable<ZonedDateTime> = new ZonedDateTime(
era,
year < 1 ? -year + 1 : year,
parseNumber(m[2], 1, 12),
1,
timeZone,
0,
m[4] ? parseNumber(m[4], 0, 23) : 0,
m[5] ? parseNumber(m[5], 0, 59) : 0,
m[6] ? parseNumber(m[6], 0, 59) : 0,
m[7] ? parseNumber(m[7], 0, Infinity) * 1000 : 0
);
date.day = parseNumber(m[3], 0, date.calendar.getDaysInMonth(date));
if (m[8]) {
date.offset = parseNumber(m[8], -23, 23) * 60 * 60 * 1000 + parseNumber(m[9] ?? '0', 0, 59) * 60 * 1000;
}
return toTimeZone(date as ZonedDateTime, timeZone);
}
/**
* Parses an ISO 8601 date and time string with a UTC offset (e.g. "2021-11-07T07:45:00Z"
* or "2021-11-07T07:45:00-07:00"). The result is converted to the user's local time zone.
*/
export function parseAbsoluteToLocal(value: string): ZonedDateTime {
return parseAbsolute(value, getLocalTimeZone());
}
function parseNumber(value: string, min: number, max: number) {
let val = Number(value);
if (val < min || val > max) {
throw new RangeError(`Value out of range: ${min} <= ${val} <= ${max}`);
}
return val;
}
export function timeToString(time: Time): string {
return `${String(time.hour).padStart(2, '0')}:${String(time.minute).padStart(2, '0')}:${String(time.second).padStart(2, '0')}${time.millisecond ? String(time.millisecond / 1000).slice(1) : ''}`;
}
export function dateToString(date: CalendarDate): string {
let gregorianDate = toCalendar(date, new GregorianCalendar());
let year: string;
if (gregorianDate.era === 'BC') {
year = gregorianDate.year === 1
? '0000'
: '-' + String(Math.abs(1 - gregorianDate.year)).padStart(6, '00');
} else {
year = String(gregorianDate.year).padStart(4, '0');
}
return `${year}-${String(gregorianDate.month).padStart(2, '0')}-${String(gregorianDate.day).padStart(2, '0')}`;
}
export function dateTimeToString(date: AnyDateTime): string {
// @ts-ignore
return `${dateToString(date)}T${timeToString(date)}`;
}
function offsetToString(offset: number) {
let sign = Math.sign(offset) < 0 ? '-' : '+';
offset = Math.abs(offset);
let offsetHours = Math.floor(offset / (60 * 60 * 1000));
let offsetMinutes = Math.floor((offset % (60 * 60 * 1000)) / (60 * 1000));
let offsetSeconds = Math.floor((offset % (60 * 60 * 1000)) % (60 * 1000) / 1000);
let stringOffset = `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}`;
if (offsetSeconds !== 0) {
stringOffset += `:${String(offsetSeconds).padStart(2, '0')}`;
}
return stringOffset;
}
export function zonedDateTimeToString(date: ZonedDateTime): string {
return `${dateTimeToString(date)}${offsetToString(date.offset)}[${date.timeZone}]`;
}
/**
* Parses an ISO 8601 duration string (e.g. "P3Y6M6W4DT12H30M5S").
* @param value An ISO 8601 duration string.
* @returns A DateTimeDuration object.
*/
export function parseDuration(value: string): Required<DateTimeDuration> {
const match = value.match(DATE_TIME_DURATION_RE);
if (!match) {
throw new Error(`Invalid ISO 8601 Duration string: ${value}`);
}
const parseDurationGroup = (
group: string | undefined,
isNegative: boolean
): number => {
if (!group) {
return 0;
}
try {
const sign = isNegative ? -1 : 1;
return sign * Number(group.replace(',', '.'));
} catch {
throw new Error(`Invalid ISO 8601 Duration string: ${value}`);
}
};
const isNegative = !!match.groups?.negative;
const hasRequiredGroups = requiredDurationGroups.some(group => match.groups?.[group]);
if (!hasRequiredGroups) {
throw new Error(`Invalid ISO 8601 Duration string: ${value}`);
}
const durationStringIncludesTime = match.groups?.time;
if (durationStringIncludesTime) {
const hasRequiredDurationTimeGroups = requiredDurationTimeGroups.some(group => match.groups?.[group]);
if (!hasRequiredDurationTimeGroups) {
throw new Error(`Invalid ISO 8601 Duration string: ${value}`);
}
}
const duration: Mutable<DateTimeDuration> = {
years: parseDurationGroup(match.groups?.years, isNegative),
months: parseDurationGroup(match.groups?.months, isNegative),
weeks: parseDurationGroup(match.groups?.weeks, isNegative),
days: parseDurationGroup(match.groups?.days, isNegative),
hours: parseDurationGroup(match.groups?.hours, isNegative),
minutes: parseDurationGroup(match.groups?.minutes, isNegative),
seconds: parseDurationGroup(match.groups?.seconds, isNegative)
};
if (duration.hours !== undefined && ((duration.hours % 1) !== 0) && (duration.minutes || duration.seconds)) {
throw new Error(`Invalid ISO 8601 Duration string: ${value} - only the smallest unit can be fractional`);
}
if (duration.minutes !== undefined && ((duration.minutes % 1) !== 0) && duration.seconds) {
throw new Error(`Invalid ISO 8601 Duration string: ${value} - only the smallest unit can be fractional`);
}
return duration as Required<DateTimeDuration>;
}
+161
View File
@@ -0,0 +1,161 @@
/*
* 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 {CalendarDate} from './CalendarDate';
/** An interface that is compatible with any object with date fields. */
export interface AnyCalendarDate {
readonly calendar: Calendar,
readonly era: string,
readonly year: number,
readonly month: number,
readonly day: number,
copy(): this
}
/** An interface that is compatible with any object with time fields. */
export interface AnyTime {
readonly hour: number,
readonly minute: number,
readonly second: number,
readonly millisecond: number,
copy(): this
}
/** An interface that is compatible with any object with both date and time fields. */
export interface AnyDateTime extends AnyCalendarDate, AnyTime {}
export type CalendarIdentifier = 'gregory' | 'buddhist' | 'chinese' | 'coptic' | 'dangi' | 'ethioaa' | 'ethiopic' | 'hebrew' | 'indian' | 'islamic' | 'islamic-umalqura' | 'islamic-tbla' | 'islamic-civil' | 'islamic-rgsa' | 'iso8601' | 'japanese' | 'persian' | 'roc';
/**
* The Calendar interface represents a calendar system, including information
* about how days, months, years, and eras are organized, and methods to perform
* arithmetic on dates.
*/
export interface Calendar {
/**
* A string identifier for the calendar, as defined by Unicode CLDR.
* See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf#supported_calendar_types).
*/
identifier: CalendarIdentifier,
/** Creates a CalendarDate in this calendar from the given Julian day number. */
fromJulianDay(jd: number): CalendarDate,
/** Converts a date in this calendar to a Julian day number. */
toJulianDay(date: AnyCalendarDate): number,
/** Returns the number of days in the month of the given date. */
getDaysInMonth(date: AnyCalendarDate): number,
/** Returns the number of months in the year of the given date. */
getMonthsInYear(date: AnyCalendarDate): number,
/** Returns the number of years in the era of the given date. */
getYearsInEra(date: AnyCalendarDate): number,
/** Returns a list of era identifiers for the calendar. */
getEras(): string[],
/**
* Returns the minimum month number of the given date's year.
* Normally, this is 1, but in some calendars such as the Japanese,
* eras may begin in the middle of a year.
*/
getMinimumMonthInYear?(date: AnyCalendarDate): number,
/**
* Returns the minimum day number of the given date's month.
* Normally, this is 1, but in some calendars such as the Japanese,
* eras may begin in the middle of a month.
*/
getMinimumDayInMonth?(date: AnyCalendarDate): number,
/** Returns the maximum months across all years. */
getMaximumMonthsInYear(): number,
/** Returns the maximum days across all months. */
getMaximumDaysInMonth(): number,
/**
* Returns a date that is the first day of the month for the given date.
* This is used to determine the month that the given date falls in, if
* the calendar has months that do not align with the standard calendar months
* (e.g. fiscal calendars).
*/
getFormattableMonth?(date: AnyCalendarDate): CalendarDate,
/** Returns whether the given calendar is the same as this calendar. */
isEqual?(calendar: Calendar): boolean,
/** @private */
balanceDate?(date: AnyCalendarDate): void,
/** @private */
balanceYearMonth?(date: AnyCalendarDate, previousDate: AnyCalendarDate): void,
/** @private */
constrainDate?(date: AnyCalendarDate): void,
/** @private */
isInverseEra?(date: AnyCalendarDate): boolean
}
/** Represents an amount of time in calendar-specific units, for use when performing arithmetic. */
export interface DateDuration {
/** The number of years to add or subtract. */
years?: number,
/** The number of months to add or subtract. */
months?: number,
/** The number of weeks to add or subtract. */
weeks?: number,
/** The number of days to add or subtract. */
days?: number
}
/** Represents an amount of time, for use whe performing arithmetic. */
export interface TimeDuration {
/** The number of hours to add or subtract. */
hours?: number,
/** The number of minutes to add or subtract. */
minutes?: number,
/** The number of seconds to add or subtract. */
seconds?: number,
/** The number of milliseconds to add or subtract. */
milliseconds?: number
}
/** Represents an amount of time with both date and time components, for use when performing arithmetic. */
export interface DateTimeDuration extends DateDuration, TimeDuration {}
export interface DateFields {
era?: string,
year?: number,
month?: number,
day?: number
}
export interface TimeFields {
hour?: number,
minute?: number,
second?: number,
millisecond?: number
}
export type DateField = keyof DateFields;
export type TimeField = keyof TimeFields;
export type Disambiguation = 'compatible' | 'earlier' | 'later' | 'reject';
export interface CycleOptions {
/** Whether to round the field value to the nearest interval of the amount. */
round?: boolean
}
export interface CycleTimeOptions extends CycleOptions {
/**
* Whether to use 12 or 24 hour time. If 12 hour time is chosen, the resulting value
* will remain in the same day period as the original value (e.g. if the value is AM,
* the resulting value also be AM).
* @default 24
*/
hourCycle?: 12 | 24
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 Mutable<T> = {
-readonly[P in keyof T]: T[P]
};
export function mod(amount: number, numerator: number): number {
return amount - numerator * Math.floor(amount / numerator);
}
+111
View File
@@ -0,0 +1,111 @@
/*
* 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.
*/
// Data from https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/weekData.json
// Locales starting on Sunday have been removed for compression.
export const weekStartData = {
'001': 1,
AD: 1,
AE: 6,
AF: 6,
AI: 1,
AL: 1,
AM: 1,
AN: 1,
AR: 1,
AT: 1,
AU: 1,
AX: 1,
AZ: 1,
BA: 1,
BE: 1,
BG: 1,
BH: 6,
BM: 1,
BN: 1,
BY: 1,
CH: 1,
CL: 1,
CM: 1,
CN: 1,
CR: 1,
CY: 1,
CZ: 1,
DE: 1,
DJ: 6,
DK: 1,
DZ: 6,
EC: 1,
EE: 1,
EG: 6,
ES: 1,
FI: 1,
FJ: 1,
FO: 1,
FR: 1,
GB: 1,
GE: 1,
GF: 1,
GP: 1,
GR: 1,
HR: 1,
HU: 1,
IE: 1,
IQ: 6,
IR: 6,
IS: 1,
IT: 1,
JO: 6,
KG: 1,
KW: 6,
KZ: 1,
LB: 1,
LI: 1,
LK: 1,
LT: 1,
LU: 1,
LV: 1,
LY: 6,
MC: 1,
MD: 1,
ME: 1,
MK: 1,
MN: 1,
MQ: 1,
MV: 5,
MY: 1,
NL: 1,
NO: 1,
NZ: 1,
OM: 6,
PL: 1,
QA: 6,
RE: 1,
RO: 1,
RS: 1,
RU: 1,
SD: 6,
SE: 1,
SI: 1,
SK: 1,
SM: 1,
SY: 6,
TJ: 1,
TM: 1,
TR: 1,
UA: 1,
UY: 1,
UZ: 1,
VA: 1,
VN: 1,
XK: 1
};