|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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>) { |
|
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) { |
|
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) { |
|
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>) { |
|
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) { |
|
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(mutable); |
|
break; |
|
} |
|
case 'year': { |
|
if (mutable.calendar.isInverseEra?.(mutable)) { |
|
amount = -amount; |
|
} |
|
|
|
|
|
|
|
|
|
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) { |
|
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 |
|
}); |
|
|
|
|
|
|
|
ms = toAbsolute(res, dateTime.timeZone); |
|
} else { |
|
|
|
ms = epochFromDate(dateTime) - dateTime.offset; |
|
} |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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()!; |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar); |
|
} |
|
case 'minute': |
|
case 'second': |
|
case 'millisecond': |
|
|
|
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 { |
|
|
|
|
|
let plainDateTime = toCalendarDateTime(dateTime); |
|
let res = setTime(set(plainDateTime, fields), fields); |
|
|
|
|
|
|
|
if (res.compare(plainDateTime) === 0) { |
|
return dateTime; |
|
} |
|
|
|
let ms = toAbsolute(res, dateTime.timeZone, disambiguation); |
|
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar); |
|
} |
|
|