INIT
This commit is contained in:
+406
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user