import {CalendarUnit, DateTimeUnit, DateUnit, Plural, RoundingMode, TimeUnit, UnsignedRoundingMode} from './types.js'; import Duration from './duration.js'; import {ZonedDateTime, ZonedDateTimeLike, toZonedDateTime} from './zoned-date-time.js'; import Calendar, {daysInMonth, inLeapYear} from './calendar.js'; import {PlainDate, PlainDateLike} from './plain-date.js'; import {PlainTime} from './plain-time.js'; import {PlainDateTime} from './plain-date-time.js'; import {Instant} from './instant.js'; import {TimeZone} from './time-zone.js'; import {DAY, HOUR, MINUTE, PER_UNIT, SECOND} from './units.js'; enum TemporalUnits { year = 0, month = 1, week = 2, day = 3, hour = 4, minute = 5, second = 6, millisecond = 7, } interface NudgeRecord { timeDuration: DurationRecord; total: number; nudgedEpoch: number; didExpandDays: boolean; } export interface DurationRecord { years: number; months: number; weeks: number; days: number; timeDuration: number; } export interface DurationDiff { duration: Duration; total: number; } export interface TimeDuration { days: number; hours: number; minutes: number; seconds: number; milliseconds: number; } export interface TimeComponents { hour: number; minute: number; second: number; millisecond: number; } export function maxTemporalUnit(one: A, two: B): A | B { let oneValue = TemporalUnits[one]; let twoValue = TemporalUnits[two]; if (oneValue > twoValue) { return two; } return one; } export function defaultLargestUnit(duration: Duration): DateTimeUnit { if (duration.years) { return 'year'; } else if (duration.months) { return 'month'; } else if (duration.weeks) { return 'week'; } else if (duration.days) { return 'day'; } else if (duration.hours) { return 'hour'; } else if (duration.minutes) { return 'minute'; } else if (duration.seconds) { return 'second'; } return 'millisecond'; } function mod(n: number, d: number) { return ((n % d) + d) % d; } export function balanceTime(hour: number, minute: number, second: number, millisecond: number) { second = second + Math.floor(millisecond / 1000); millisecond = mod(millisecond, 1000); minute = minute + Math.floor(second / 60); second = mod(second, 60); hour = hour + Math.floor(minute / 60); minute = mod(minute, 60); let deltaDays = Math.floor(hour / 24); hour = mod(hour, 24); return { hour, minute, second, millisecond, deltaDays, }; } export function balanceTimeDuration(value: number, largestUnit?: DateTimeUnit): TimeDuration { let sign = Math.sign(value); value = Math.abs(value); if (largestUnit === 'millisecond') { return { days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: value * sign, }; } let seconds = Math.floor(value / 1000); let milliseconds = value % 1000; let minutes = Math.floor(seconds / 60); if (largestUnit === 'second') { return { days: 0, hours: 0, minutes: 0, seconds: seconds * sign, milliseconds: milliseconds * sign, }; } seconds = seconds % 60; let hours = Math.floor(minutes / 60); if (largestUnit === 'minute') { return { days: 0, hours: 0, minutes: minutes * sign, seconds: seconds * sign, milliseconds: milliseconds * sign, }; } minutes = minutes % 60; let days = Math.floor(hours / 24); if (largestUnit === 'hour') { return { days: 0, hours: hours * sign, minutes: minutes * sign, seconds: seconds * sign, milliseconds: milliseconds * sign, }; } hours = hours % 24; return { days: days * sign, hours: hours * sign, minutes: minutes * sign, seconds: seconds * sign, milliseconds: milliseconds * sign, }; } export function deltaTimeComponents( one: ZonedDateTime | PlainTime | PlainDateTime, two: ZonedDateTime | PlainTime | PlainDateTime, ) { let hours = two.hour - one.hour; let minutes = two.minute - one.minute; let seconds = two.second - one.second; let milliseconds = two.millisecond - one.millisecond; return milliseconds + seconds * SECOND + minutes * MINUTE + hours * HOUR; } function zonedDateUntil( one: ZonedDateTime, two: ZonedDateTime, largestUnit: DateUnit, calendar: Calendar, ): DurationRecord { let sign = Math.sign(two.epochMilliseconds - one.epochMilliseconds); if (sign === 0) { return {years: 0, months: 0, weeks: 0, days: 0, timeDuration: 0}; } let delta = two.epochMilliseconds - one.epochMilliseconds; let offset = two.offsetMilliseconds - one.offsetMilliseconds; if (Math.abs(delta + offset) < DAY) { return {years: 0, months: 0, weeks: 0, days: 0, timeDuration: delta}; } let onePlainDate = one.toPlainDate(); let twoPlainDate = two.toPlainDate(); let timeComponents = deltaTimeComponents(one, two); let timeComponentsSign = Math.sign(timeComponents); if (sign === -timeComponentsSign) { onePlainDate = onePlainDate.add({days: -timeComponentsSign}); timeComponents = timeComponents - timeComponentsSign * 86400e3; } let dateDelta = calendar.dateUntil(onePlainDate, twoPlainDate, {largestUnit}); return { years: dateDelta.years, months: dateDelta.months, weeks: dateDelta.weeks, days: dateDelta.days, timeDuration: timeComponents, }; } function applyUnsignedRoundingMode(value: number, r1: number, r2: number, unsignedRoundingMode: UnsignedRoundingMode) { if (value === r1) { return r1; } if (unsignedRoundingMode === 'zero') { return r1; } else if (unsignedRoundingMode === 'infinity') { return r2; } let d1 = value - r1; let d2 = r2 - value; if (d1 < d2) { return r1; } else if (d2 < d1) { return r2; } if (unsignedRoundingMode === 'half-zero') { return r1; } else if (unsignedRoundingMode === 'half-infinity') { return r2; } let cardinality = r1 / (r2 - r1); // % 2; cardinality = ((cardinality % 2) + 2) % 2; return cardinality === 0 ? r1 : r2; } function getUnsignedRoundingMode(roundingMode: RoundingMode, isNegative: boolean): UnsignedRoundingMode { switch (roundingMode) { case 'ceil': return isNegative ? 'zero' : 'infinity'; case 'floor': return isNegative ? 'infinity' : 'zero'; case 'expand': return isNegative ? 'infinity' : 'infinity'; case 'trunc': return isNegative ? 'zero' : 'zero'; case 'halfCeil': return isNegative ? 'half-zero' : 'half-infinity'; case 'halfFloor': return isNegative ? 'half-infinity' : 'half-zero'; case 'halfExpand': return isNegative ? 'half-infinity' : 'half-infinity'; case 'halfTrunc': return isNegative ? 'half-zero' : 'half-zero'; case 'halfEven': return isNegative ? 'half-even' : 'half-even'; } } export function roundToIncrement(quantity: number, increment: number, mode: RoundingMode) { let quotient = Math.abs(quantity / increment); let sign = Math.sign(quantity); let unsignedRoundingMode = getUnsignedRoundingMode(mode, sign === -1); let r1 = Math.trunc(quotient); let r2 = r1 + 1; let rounded = applyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode); return rounded * increment * sign; } const wednesday = 3; const friday = 5; const saturday = 6; export function toISOWeekOfYear(zonedDate: ZonedDateTime | PlainDate | PlainDateTime) { let dayOfYear = zonedDate.dayOfYear; let dayOfWeek = zonedDate.dayOfWeek; let week = Math.floor((dayOfYear + 7 - dayOfWeek + wednesday) / 7); if (week < 1) { let date = new Date(Date.UTC(zonedDate.year, 0, 1)); let legacyDayOfWeek = date.getUTCDay(); let dayOfJan1st = legacyDayOfWeek === 0 ? 7 : legacyDayOfWeek; if (dayOfJan1st === friday) { return {week: 53, year: zonedDate.year - 1}; } else if (dayOfJan1st === saturday && inLeapYear(zonedDate.year - 1)) { return {week: 53, year: zonedDate.year - 1}; } return {week: 52, year: zonedDate.year - 1}; } else if (week === 53) { let daysInYear = zonedDate.daysInYear; let daysLaterInYear = daysInYear - dayOfYear; let daysAfterThursday = 4 - dayOfWeek; if (daysLaterInYear < daysAfterThursday) { return {week: 53, year: zonedDate.year + 1}; } } return {week, year: zonedDate.year}; } export function balanceISOYearMonth(year: number, month: number) { month -= 1; year += Math.floor(month / 12); month %= 12; if (month < 0) { month += 12; } month += 1; return {year, month}; } export function normalizeDateAdd( year: number, month: number, day: number, duration: Duration, overflow?: 'reject' | 'constrain', ) { overflow ??= 'constrain'; year += duration.years; month += duration.months; ({year, month} = balanceISOYearMonth(year, month)); return verifyDateComponents(year, month, day, overflow); } export function verifyDateComponents(year: number, month: number, day: number, overflow: 'reject' | 'constrain') { if (overflow === 'reject') { if (outsideOfRange(1, 13, month) || outsideOfRange(1, daysInMonth(year, month) + 1, day)) { throw new RangeError(); } } else if (overflow === 'constrain') { month = Math.min(Math.max(1, month), 12); day = Math.min(Math.max(1, day), daysInMonth(year, month)); } return {year, month, day}; } function outsideOfRange(min: number, max: number, value: number) { return value < min || value >= max; } export function verifyTimeComponents( hour: number, minute: number, second: number, millisecond: number, overflow: 'reject' | 'constrain', ): TimeComponents { if ( overflow === 'reject' && (outsideOfRange(0, 24, hour) || outsideOfRange(0, 60, minute) || outsideOfRange(0, 60, second) || outsideOfRange(0, 1000, millisecond)) ) { throw new RangeError(); } else if (overflow === 'constrain') { hour = Math.min(Math.max(0, hour), 23); minute = Math.min(Math.max(0, minute), 59); second = Math.min(Math.max(0, second), 59); millisecond = Math.min(Math.max(0, millisecond), 999); } return {hour, minute, second, millisecond}; } export function differenceZonedDateTimeWithRounding( from: ZonedDateTime, to: ZonedDateTime, largestUnit: DateTimeUnit, smallestUnit: DateTimeUnit, roundingIncrement?: number, roundingMode?: RoundingMode, ) { if (!isDateUnit(largestUnit)) { let differenceRecord = differenceInstantWithRounding( from.toInstant(), to.toInstant(), smallestUnit as 'day' | TimeUnit, roundingIncrement ?? 1, roundingMode ?? 'trunc', ); let timeDuration = balanceTimeDuration(differenceRecord.timeDuration, largestUnit); return { duration: Duration.from(timeDuration), total: differenceRecord.total, }; } let difference = zonedDateUntil(from, to, largestUnit, from.getCalendar()); let roundingGranularityIsNoop = smallestUnit === 'millisecond' && roundingIncrement === 1; if (roundingGranularityIsNoop) { let timeRecord = balanceTimeDuration(difference.timeDuration, 'hour'); // Missing step 4.b, need to understand it.. return { duration: new Duration( difference.years, difference.months, difference.weeks, difference.days, timeRecord.hours, timeRecord.minutes, timeRecord.seconds, timeRecord.milliseconds, ), total: difference.timeDuration, }; } return roundRelativeDuration( difference, to.epochMilliseconds, from, smallestUnit, largestUnit, roundingIncrement ?? 1, roundingMode ?? 'trunc', ); } // DifferencePlainDateTimeWithRounding export function differencePlainDateTimeWithRounding( from: PlainDateTime, to: PlainDateTime, smallestUnit: DateTimeUnit, largestUnit: DateTimeUnit, roundingIncrement: number, roundingMode: RoundingMode, ) { if (from.equals(to)) { return {duration: new Duration(), total: 0}; } let record = differenceDateTime(from, to, largestUnit); let roundingGranularityIsNoop = smallestUnit === 'millisecond' && roundingIncrement === 1; if (roundingGranularityIsNoop) { let timeDurationWithDays = record.timeDuration + record.days * DAY; let timeResult = balanceTimeDuration(timeDurationWithDays, largestUnit); let duration = new Duration( record.years, record.months, record.weeks, timeResult.days, timeResult.hours, timeResult.minutes, timeResult.seconds, timeResult.milliseconds, ); return {duration, total: timeDurationWithDays}; } let destination = toUTC(to); return roundRelativeDuration(record, destination, from, smallestUnit, largestUnit, roundingIncrement, roundingMode); } function nudgeToCalendarUnit( sign: -1 | 1, dateTime: PlainDateTime, duration: DurationRecord, destination: number, unit: DateUnit, timeZone: TimeZone | null, roundingIncrement: number, roundingMode: RoundingMode, ): NudgeRecord { let from: DurationRecord; let to: DurationRecord; let r1: number; let r2: number; switch (unit) { case 'year': let years = roundToIncrement(duration.years, roundingIncrement, 'trunc'); r1 = years; r2 = years + roundingIncrement * sign; from = {years: r1, months: 0, weeks: 0, days: 0, timeDuration: 0}; to = {years: r2, months: 0, weeks: 0, days: 0, timeDuration: 0}; break; case 'month': let months = roundToIncrement(duration.months, roundingIncrement, 'trunc'); r1 = months; r2 = months + roundingIncrement * sign; from = {years: duration.years, months: r1, weeks: 0, days: 0, timeDuration: 0}; to = {years: duration.years, months: r2, weeks: 0, days: 0, timeDuration: 0}; break; case 'week': let weeksStart = dateTime.toPlainDate(); let weeksEnd = weeksStart.add({days: duration.days}); let delta = weeksEnd.until(weeksStart, {largestUnit: 'week'}); let weeks = roundToIncrement(delta.weeks, roundingIncrement, 'trunc'); r1 = weeks; r2 = weeks + roundingIncrement * sign; from = {years: duration.years, months: duration.months, weeks: r1, days: 0, timeDuration: 0}; to = {years: duration.years, months: duration.months, weeks: r2, days: 0, timeDuration: 0}; break; case 'day': let days = roundToIncrement(duration.days, roundingIncrement, 'trunc'); r1 = days; r2 = days + roundingIncrement * sign; from = {years: duration.years, months: duration.months, weeks: duration.weeks, days: r1, timeDuration: 0}; to = {years: duration.years, months: duration.months, weeks: duration.weeks, days: r2, timeDuration: 0}; break; } let start = dateTime.add(from); let end = dateTime.add(to); let startEpoch: number; let endEpoch: number; if (timeZone) { startEpoch = start.toZonedDateTime(timeZone).epochMilliseconds; endEpoch = end.toZonedDateTime(timeZone).epochMilliseconds; } else { startEpoch = toUTC(start); endEpoch = toUTC(end); } let unsignedRoundingMode = getUnsignedRoundingMode(roundingMode, sign < 0); let progress = (destination - startEpoch) / (endEpoch - startEpoch); let total = r1 + progress * roundingIncrement * sign; let roundedUnit = applyUnsignedRoundingMode(total, r1, r2, unsignedRoundingMode); let roundedSign = Math.sign(roundedUnit - total); if (roundedSign === sign) { return { timeDuration: to, total, nudgedEpoch: endEpoch, didExpandDays: true, }; } return { timeDuration: from, total, nudgedEpoch: startEpoch, didExpandDays: false, }; } // NudgeToDayOrTime function nudgeToDayOrTime( timeDuration: DurationRecord, destination: number, smallestUnit: TimeUnit, largestUnit: DateTimeUnit, roundingIncrement: number, roundingMode: RoundingMode, ): NudgeRecord { let time = timeDuration.days * DAY + timeDuration.timeDuration; let roundedTimeDuration = internalRoundTimeDuration(0, time, roundingIncrement, roundingMode, smallestUnit); let diff = roundedTimeDuration.timeDuration - time; let wholeDays = Math.trunc(time / DAY); let roundedFractionalDays = roundedTimeDuration.timeDuration / DAY; let roundedWholeDays = Math.trunc(roundedFractionalDays); let dayDelta = roundedWholeDays - wholeDays; let dayDeltaSign = Math.sign(dayDelta); let didExpandDays = dayDeltaSign === Math.sign(time); let nudgedEpoch = destination + diff; let days = 0; let remainder = roundedTimeDuration.timeDuration; if (maxTemporalUnit(largestUnit, 'day') === largestUnit) { days = roundedWholeDays; remainder = roundedTimeDuration.timeDuration % DAY; } let result: DurationRecord = { years: timeDuration.years, months: timeDuration.months, weeks: timeDuration.weeks, days, timeDuration: remainder, }; return { timeDuration: result, total: roundedTimeDuration.total, nudgedEpoch, didExpandDays, }; } function nudgeToZonedTime( sign: 1 | -1, dateTime: PlainDateTime, timeZone: TimeZone, timeDuration: DurationRecord, unit: TimeUnit, roundingIncrement: number, roundingMode: RoundingMode, ): NudgeRecord { let start = dateTime .add({ years: timeDuration.years, months: timeDuration.months, weeks: timeDuration.weeks, days: timeDuration.days, }) .toZonedDateTime(timeZone); let end = new PlainDateTime(start.year, start.month, start.day).add({days: sign}).toZonedDateTime(timeZone); let daySpan = end.epochMilliseconds - start.epochMilliseconds; let unitLength = PER_UNIT[unit]; let roundedTime = roundToIncrement(timeDuration.timeDuration, roundingIncrement * unitLength, roundingMode); let beyondDaySpan = roundedTime - daySpan; // Step: 15 let didExpandDays: boolean; let dayDelta: number; let nudgedEpoch: number; if (Math.sign(beyondDaySpan) !== -sign) { didExpandDays = true; dayDelta = sign; roundedTime = roundToIncrement(beyondDaySpan, roundingIncrement * unitLength, roundingMode); nudgedEpoch = roundedTime + end.epochMilliseconds; } else { didExpandDays = false; dayDelta = 0; nudgedEpoch = roundedTime + end.epochMilliseconds; } let resultDuration = { years: timeDuration.years, months: timeDuration.months, weeks: timeDuration.weeks, days: timeDuration.days + dayDelta, timeDuration: roundedTime, }; return { didExpandDays, nudgedEpoch, total: 0, timeDuration: resultDuration, }; } function roundRelativeDuration( timeDuration: DurationRecord, destination: number, relativeTo: ZonedDateTime | PlainDateTime, smallestUnit: DateTimeUnit, largestUnit: DateTimeUnit, roundingIncrement: number, roundingMode: RoundingMode, ): DurationDiff { let irregularLengthUnit = isCalendarUnit(smallestUnit); if (relativeTo instanceof PlainDateTime && smallestUnit === 'day') { irregularLengthUnit = true; } let duration = new Duration( timeDuration.years, timeDuration.months, timeDuration.weeks, timeDuration.days, 0, 0, 0, timeDuration.timeDuration, ); let sign = duration.sign; // assert sign is -1 or 1; let nudge: NudgeRecord; let dateTime = PlainDateTime.from(relativeTo); if (irregularLengthUnit) { let timeZone = relativeTo instanceof ZonedDateTime ? relativeTo.getTimeZone() : null; nudge = nudgeToCalendarUnit( sign as -1 | 1, dateTime, timeDuration, destination, smallestUnit as DateUnit, timeZone, roundingIncrement, roundingMode, ); } else if (relativeTo instanceof ZonedDateTime) { nudge = nudgeToZonedTime( sign as -1 | 1, dateTime, relativeTo.getTimeZone(), timeDuration, smallestUnit as TimeUnit, roundingIncrement, roundingMode, ); } else if (isDateUnit(smallestUnit) === false) { nudge = nudgeToDayOrTime(timeDuration, destination, smallestUnit, largestUnit, roundingIncrement, roundingMode); } else { throw new RangeError(); } let durationRecord = nudge.timeDuration; // Step: 9 if (nudge.didExpandDays) { let timeZone = relativeTo instanceof ZonedDateTime ? relativeTo.getTimeZone() : null; let startUnit = maxTemporalUnit(smallestUnit, 'day'); durationRecord = bubbleRelativeDuration( sign as 1 | -1, durationRecord, nudge.nudgedEpoch, dateTime, timeZone, largestUnit as DateUnit, startUnit as DateUnit, ); } // Step: 10 if (isDateUnit(largestUnit)) { largestUnit = 'hour'; } let balanceResult = balanceTimeDuration(durationRecord.timeDuration, largestUnit); return { total: nudge.total, duration: new Duration( durationRecord.years, durationRecord.months, durationRecord.weeks, durationRecord.days, balanceResult.hours, balanceResult.minutes, balanceResult.seconds, balanceResult.milliseconds, ), }; } function bubbleRelativeDuration( sign: -1 | 1, duration: DurationRecord, nudgedEpoch: number, dateTime: PlainDateTime, timeZone: TimeZone | null, largestUnit: DateUnit, smallestUnit: DateUnit, ) { if (smallestUnit === 'year') { return duration; } let largestUnitIndex = TemporalUnits[largestUnit]; let smallestUnitIndex = TemporalUnits[largestUnit]; for (let unitIndex = smallestUnitIndex - 1; unitIndex >= largestUnitIndex; unitIndex--) { let endDuration: DurationRecord; if (unitIndex != TemporalUnits.week || largestUnitIndex === TemporalUnits.week) { switch (unitIndex) { case TemporalUnits.year: endDuration = {years: duration.years + sign, months: 0, weeks: 0, days: 0, timeDuration: 0}; break; case TemporalUnits.month: endDuration = {years: duration.years, months: duration.months + sign, weeks: 0, days: 0, timeDuration: 0}; break; case TemporalUnits.week: endDuration = { years: duration.years, months: duration.months, weeks: duration.weeks + sign, days: 0, timeDuration: 0, }; break; default: endDuration = { years: duration.years, months: duration.months, weeks: duration.weeks, days: duration.days + sign, timeDuration: 0, }; break; } let end = dateTime.add({ years: endDuration.years, months: endDuration.months, weeks: endDuration.weeks, days: endDuration.days, milliseconds: endDuration.timeDuration, }); let endEpoch: number; if (timeZone) { endEpoch = end.toZonedDateTime(timeZone).epochMilliseconds; } else { endEpoch = toUTC(end); } let beyondEnd = nudgedEpoch - endEpoch; let beyondEndSign = Math.sign(beyondEnd); if (beyondEndSign !== -sign) { duration = endDuration; } else { break; } } } return duration; } // DifferenceInstant export function differenceInstantWithRounding( from: Instant, to: Instant, smallestUnit: 'day' | TimeUnit, roundingIncrement: number, roundingMode: RoundingMode, ) { let difference = to.epochMilliseconds - from.epochMilliseconds; return internalRoundTimeDuration(0, difference, roundingIncrement, roundingMode, smallestUnit); } // NormalizeTimeDuration export function durationToTimeDuration(duration: Duration) { return duration.hours * HOUR + duration.minutes * MINUTE + duration.seconds * SECOND + duration.milliseconds; } export function toTimeDuration(timeComponents: TimeComponents) { return ( timeComponents.hour * HOUR + timeComponents.minute * MINUTE + timeComponents.second * SECOND + timeComponents.millisecond ); } // RoundTimeDuration export function internalRoundTimeDuration( days: number, timeDuration: number, increment: number, roundingMode: RoundingMode, unit: 'day' | 'hour' | 'minute' | 'second' | 'millisecond', ) { let incrementUnit = PER_UNIT[unit]; switch (unit) { case 'day': let fractionalDays = days + timeDuration / incrementUnit; return { days: roundToIncrement(fractionalDays, increment, roundingMode), timeDuration: 0, total: fractionalDays, }; default: return { days, timeDuration: roundToIncrement(timeDuration, increment * incrementUnit, roundingMode), total: timeDuration / incrementUnit, }; } } export function isCalendarUnit(unit: DateTimeUnit): unit is CalendarUnit { return unit === 'year' || unit === 'month' || unit === 'week'; } export function isDateUnit(unit: DateTimeUnit): unit is CalendarUnit | 'day' { return unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day'; } function differenceDateTime(one: PlainDateTime, two: PlainDateTime, largestUnit: DateTimeUnit): DurationRecord { let timeDuration = deltaTimeComponents(one, two); let adjustedDate = one.toPlainDate(); let twoDate = two.toPlainDate(); let timeSign = Math.sign(timeDuration); let dateSign = PlainDate.compare(twoDate, adjustedDate); if (timeSign === -dateSign) { timeDuration -= timeSign * DAY; adjustedDate = adjustedDate.add({days: -timeSign}); } let delta = one.getCalendar().dateUntil(adjustedDate, twoDate, { largestUnit: maxTemporalUnit('day', largestUnit) as 'day', }); return { years: delta.years, months: delta.months, weeks: delta.weeks, days: delta.days, timeDuration, }; } function toUTC(dateTime: PlainDateTime) { return Date.UTC( dateTime.year, dateTime.month - 1, dateTime.day, dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond, ); } export type RelativeToLike = PlainDate | ZonedDateTime | ZonedDateTimeLike | PlainDateLike; export type RelativeTo = PlainDate | ZonedDateTime; export function parseRelativeTo(relativeTo: RelativeToLike): PlainDate | ZonedDateTime { try { return toZonedDateTime(relativeTo as any); } catch (error) { try { return PlainDate.from(relativeTo as any); } catch (error) { throw new RangeError('Unsupported relativeTo, can’t convert to ZonedDateTime or PlainDate'); } } } export function parseUnit(input: Plural | undefined, defaultValue?: T): T; export function parseUnit(input: Plural | undefined, defaultValue: T): T { if (input) { switch (input) { case 'years': return 'year' as T; case 'months': return 'month' as T; case 'days': return 'day' as T; case 'hours': return 'hour' as T; case 'minutes': return 'minute' as T; case 'seconds': return 'second' as T; case 'milliseconds': return 'millisecond' as T; default: return input as T; } } return defaultValue; } export function getPossibleMilliseconds(current: number, timeZone: TimeZone) { let earlierOffset = timeZone.getOffsetMillisecondsFor(current - DAY); let laterOffset = timeZone.getOffsetMillisecondsFor(current + DAY); if (earlierOffset === laterOffset) { return [current]; } let currentOffset = timeZone.getOffsetMillisecondsFor(current); let candiateOffset = currentOffset === earlierOffset ? laterOffset : earlierOffset; let candiate = current + currentOffset - candiateOffset; let candiateRealOffset = timeZone.getOffsetMillisecondsFor(candiate); if (candiateRealOffset === candiateOffset) { if (candiate > current) { return [current, candiate]; } return [candiate, current]; } return [current]; }