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