diff --git a/polyfill/lib/calendar.mjs b/polyfill/lib/calendar.mjs index 8fcddb9164..ee6bc9ea06 100644 --- a/polyfill/lib/calendar.mjs +++ b/polyfill/lib/calendar.mjs @@ -22,6 +22,7 @@ import { HasSlot, SetSlot } from './slots.mjs'; +import { TimeDuration } from './timeduration.mjs'; const ArrayFrom = Array.from; const ArrayIncludes = Array.prototype.includes; @@ -157,16 +158,15 @@ export class Calendar { duration = ES.ToTemporalDuration(duration); options = ES.GetOptionsObject(options); const overflow = ES.ToTemporalOverflow(options); - const { days } = ES.BalanceTimeDuration( - GetSlot(duration, DAYS), + const norm = TimeDuration.normalize( GetSlot(duration, HOURS), GetSlot(duration, MINUTES), GetSlot(duration, SECONDS), GetSlot(duration, MILLISECONDS), GetSlot(duration, MICROSECONDS), - GetSlot(duration, NANOSECONDS), - 'day' + GetSlot(duration, NANOSECONDS) ); + const days = GetSlot(duration, DAYS) + ES.BalanceTimeDuration(norm, 'day').days; const id = GetSlot(this, CALENDAR_ID); return impl[id].dateAdd( date, diff --git a/polyfill/lib/duration.mjs b/polyfill/lib/duration.mjs index c49c41384a..ec9a0b5168 100644 --- a/polyfill/lib/duration.mjs +++ b/polyfill/lib/duration.mjs @@ -1,7 +1,5 @@ /* global __debug__ */ -import bigInt from 'big-integer'; - import * as ES from './ecmascript.mjs'; import { MakeIntrinsicClass } from './intrinsicclass.mjs'; import { CalendarMethodRecord } from './methodrecord.mjs'; @@ -18,10 +16,12 @@ import { NANOSECONDS, CALENDAR, INSTANT, + EPOCHNANOSECONDS, CreateSlots, GetSlot, SetSlot } from './slots.mjs'; +import { TimeDuration } from './timeduration.mjs'; const MathAbs = Math.abs; const ObjectCreate = Object.create; @@ -65,6 +65,7 @@ export class Duration { SetSlot(this, NANOSECONDS, nanoseconds); if (typeof __debug__ !== 'undefined' && __debug__) { + const normSeconds = TimeDuration.normalize(0, 0, seconds, milliseconds, microseconds, nanoseconds); Object.defineProperty(this, '_repr_', { value: `Temporal.Duration <${ES.TemporalDurationToString( years, @@ -73,10 +74,7 @@ export class Duration { days, hours, minutes, - seconds, - milliseconds, - microseconds, - nanoseconds + normSeconds )}>`, writable: false, enumerable: false, @@ -340,56 +338,40 @@ export class Duration { plainRelativeTo, calendarRec )); - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - ES.RoundDuration( + let norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + ({ years, months, weeks, days, norm } = ES.RoundDuration( + years, + months, + weeks, + days, + norm, + roundingIncrement, + smallestUnit, + roundingMode, + plainRelativeTo, + calendarRec, + zonedRelativeTo, + timeZoneRec, + precalculatedPlainDateTime + )); + if (zonedRelativeTo) { + ({ years, months, weeks, days, norm } = ES.AdjustRoundedDurationDays( years, months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, roundingIncrement, smallestUnit, roundingMode, - plainRelativeTo, - calendarRec, zonedRelativeTo, + calendarRec, timeZoneRec, precalculatedPlainDateTime )); - if (zonedRelativeTo) { - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - ES.AdjustRoundedDurationDays( - years, - months, - weeks, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - roundingIncrement, - smallestUnit, - roundingMode, - zonedRelativeTo, - calendarRec, - timeZoneRec, - precalculatedPlainDateTime - )); ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDurationRelative( days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, largestUnit, zonedRelativeTo, timeZoneRec, @@ -397,13 +379,7 @@ export class Duration { )); } else { ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDuration( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm.add24HourDays(days), largestUnit )); } @@ -480,6 +456,7 @@ export class Duration { plainRelativeTo, calendarRec )); + let norm; // If the unit we're totalling is smaller than `days`, convert days down to that unit. if (zonedRelativeTo) { const intermediate = ES.MoveRelativeZonedDateTime( @@ -492,29 +469,29 @@ export class Duration { 0, precalculatedPlainDateTime ); - ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDurationRelative( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - unit, - intermediate, - timeZoneRec - )); + norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + + // Inline BalanceTimeDurationRelative, without the final balance step + const start = GetSlot(intermediate, INSTANT); + const startNs = GetSlot(intermediate, EPOCHNANOSECONDS); + let intermediateNs = startNs; + let startDt; + if (days !== 0) { + startDt = ES.GetPlainDateTimeFor(timeZoneRec, start, 'iso8601'); + intermediateNs = ES.AddDaysToZonedDateTime(start, startDt, timeZoneRec, 'iso8601', days).epochNs; + } + const endNs = ES.AddInstant(intermediateNs, norm); + norm = TimeDuration.fromEpochNsDiff(endNs, startNs); + if (unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day') { + if (!norm.isZero()) startDt ??= ES.GetPlainDateTimeFor(timeZoneRec, start, 'iso8601'); + ({ days, norm } = ES.NormalizedTimeDurationToDays(norm, intermediate, timeZoneRec, startDt)); + } else { + days = 0; + } } else { - ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDuration( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - unit - )); + norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + norm = norm.add24HourDays(days); + days = 0; } // Finally, truncate to the correct unit and calculate remainder const { total } = ES.RoundDuration( @@ -522,12 +499,7 @@ export class Duration { months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, 1, unit, 'trunc', @@ -562,6 +534,7 @@ export class Duration { let nanoseconds = GetSlot(this, NANOSECONDS); if (unit !== 'nanosecond' || increment !== 1) { + let norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); const largestUnit = ES.DefaultTemporalLargestUnit( years, months, @@ -574,49 +547,33 @@ export class Duration { microseconds, nanoseconds ); - ({ seconds, milliseconds, microseconds, nanoseconds } = ES.RoundDuration( - 0, - 0, - 0, - 0, - 0, - 0, - seconds, - milliseconds, - microseconds, - nanoseconds, - increment, - unit, - roundingMode - )); - ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDuration( - days, + ({ norm } = ES.RoundDuration(0, 0, 0, 0, norm, increment, unit, roundingMode)); + let deltaDays; + ({ + days: deltaDays, hours, minutes, seconds, milliseconds, microseconds, - nanoseconds, - largestUnit - )); + nanoseconds + } = ES.BalanceTimeDuration(norm, largestUnit)); + days += deltaDays; } - return ES.TemporalDurationToString( - years, - months, - weeks, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - precision - ); + const normSeconds = TimeDuration.normalize(0, 0, seconds, milliseconds, microseconds, nanoseconds); + return ES.TemporalDurationToString(years, months, weeks, days, hours, minutes, normSeconds, precision); } toJSON() { if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); + const normSeconds = TimeDuration.normalize( + 0, + 0, + GetSlot(this, SECONDS), + GetSlot(this, MILLISECONDS), + GetSlot(this, MICROSECONDS), + GetSlot(this, NANOSECONDS) + ); return ES.TemporalDurationToString( GetSlot(this, YEARS), GetSlot(this, MONTHS), @@ -624,10 +581,7 @@ export class Duration { GetSlot(this, DAYS), GetSlot(this, HOURS), GetSlot(this, MINUTES), - GetSlot(this, SECONDS), - GetSlot(this, MILLISECONDS), - GetSlot(this, MICROSECONDS), - GetSlot(this, NANOSECONDS) + normSeconds ); } toLocaleString(locales = undefined, options = undefined) { @@ -636,6 +590,14 @@ export class Duration { return new Intl.DurationFormat(locales, options).format(this); } console.warn('Temporal.Duration.prototype.toLocaleString() requires Intl.DurationFormat.'); + const normSeconds = TimeDuration.normalize( + 0, + 0, + GetSlot(this, SECONDS), + GetSlot(this, MILLISECONDS), + GetSlot(this, MICROSECONDS), + GetSlot(this, NANOSECONDS) + ); return ES.TemporalDurationToString( GetSlot(this, YEARS), GetSlot(this, MONTHS), @@ -643,10 +605,7 @@ export class Duration { GetSlot(this, DAYS), GetSlot(this, HOURS), GetSlot(this, MINUTES), - GetSlot(this, SECONDS), - GetSlot(this, MILLISECONDS), - GetSlot(this, MICROSECONDS), - GetSlot(this, NANOSECONDS) + normSeconds ); } valueOf() { @@ -718,6 +677,8 @@ export class Duration { const instant = GetSlot(zonedRelativeTo, INSTANT); const precalculatedPlainDateTime = ES.GetPlainDateTimeFor(timeZoneRec, instant, calendarRec.receiver); + const norm1 = TimeDuration.normalize(h1, min1, s1, ms1, µs1, ns1); + const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2); const after1 = ES.AddZonedDateTime( instant, timeZoneRec, @@ -726,12 +687,7 @@ export class Duration { mon1, w1, d1, - h1, - min1, - s1, - ms1, - µs1, - ns1, + norm1, precalculatedPlainDateTime ); const after2 = ES.AddZonedDateTime( @@ -742,12 +698,7 @@ export class Duration { mon2, w2, d2, - h2, - min2, - s2, - ms2, - µs2, - ns2, + norm2, precalculatedPlainDateTime ); return ES.ComparisonResult(after1.minus(after2).toJSNumber()); @@ -758,11 +709,9 @@ export class Duration { ({ days: d1 } = ES.UnbalanceDateDurationRelative(y1, mon1, w1, d1, 'day', plainRelativeTo, calendarRec)); ({ days: d2 } = ES.UnbalanceDateDurationRelative(y2, mon2, w2, d2, 'day', plainRelativeTo, calendarRec)); } - h1 = bigInt(h1).add(bigInt(d1).multiply(24)); - h2 = bigInt(h2).add(bigInt(d2).multiply(24)); - ns1 = ES.TotalDurationNanoseconds(h1, min1, s1, ms1, µs1, ns1); - ns2 = ES.TotalDurationNanoseconds(h2, min2, s2, ms2, µs2, ns2); - return ES.ComparisonResult(ns1.minus(ns2).toJSNumber()); + const norm1 = TimeDuration.normalize(h1, min1, s1, ms1, µs1, ns1).add24HourDays(d1); + const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2).add24HourDays(d2); + return norm1.cmp(norm2); } } diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index be437b5d87..5415b059b9 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -59,8 +59,9 @@ import OwnPropertyKeys from 'es-abstract/helpers/OwnPropertyKeys.js'; import some from 'es-abstract/helpers/some.js'; import { GetIntrinsic } from './intrinsicclass.mjs'; -import { TruncatingDivModByPowerOf10 } from './math.mjs'; +import { FMAPowerOf10, TruncatingDivModByPowerOf10 } from './math.mjs'; import { CalendarMethodRecord, TimeZoneMethodRecord } from './methodrecord.mjs'; +import { TimeDuration } from './timeduration.mjs'; import { CreateSlots, GetSlot, @@ -683,6 +684,7 @@ export function ParseTemporalDurationString(isoString) { seconds += MathTrunc(excessNanoseconds / 1e9) % 60; minutes += MathTrunc(excessNanoseconds / 60e9); + RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } @@ -2418,20 +2420,8 @@ export function DisambiguatePossibleInstants(possibleInstants, timeZoneRec, date const nanoseconds = offsetAfter - offsetBefore; switch (disambiguation) { case 'earlier': { - const earlierTime = AddTime( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond, - 0, - 0, - 0, - 0, - 0, - -nanoseconds - ); + const norm = TimeDuration.normalize(0, 0, 0, 0, 0, -nanoseconds); + const earlierTime = AddTime(hour, minute, second, millisecond, microsecond, nanosecond, norm); const earlierDate = AddISODate(year, month, day, 0, 0, 0, earlierTime.deltaDays, 'constrain'); const earlierPlainDateTime = CreateTemporalDateTime( earlierDate.year, @@ -2449,7 +2439,8 @@ export function DisambiguatePossibleInstants(possibleInstants, timeZoneRec, date case 'compatible': // fall through because 'compatible' means 'later' for "spring forward" transitions case 'later': { - const laterTime = AddTime(hour, minute, second, millisecond, microsecond, nanosecond, 0, 0, 0, 0, 0, nanoseconds); + const norm = TimeDuration.normalize(0, 0, 0, 0, 0, nanoseconds); + const laterTime = AddTime(hour, minute, second, millisecond, microsecond, nanosecond, norm); const laterDate = AddISODate(year, month, day, 0, 0, 0, laterTime.deltaDays, 'constrain'); const laterPlainDateTime = CreateTemporalDateTime( laterDate.year, @@ -2546,25 +2537,8 @@ function formatAsDecimalNumber(num) { return bigInt(num).toString(); } -export function TemporalDurationToString( - years, - months, - weeks, - days, - hours, - minutes, - seconds, - ms, - µs, - ns, - precision = 'auto' -) { - const sign = DurationSign(years, months, weeks, days, hours, minutes, seconds, ms, µs, ns); - - let total = TotalDurationNanoseconds(0, 0, seconds, ms, µs, ns); - ({ quotient: total, remainder: ns } = total.divmod(1000)); - ({ quotient: total, remainder: µs } = total.divmod(1000)); - ({ quotient: seconds, remainder: ms } = total.divmod(1000)); +export function TemporalDurationToString(years, months, weeks, days, hours, minutes, normSeconds, precision = 'auto') { + const sign = DurationSign(years, months, weeks, days, hours, minutes, normSeconds.sec, 0, 0, normSeconds.subsec); let datePart = ''; if (years !== 0) datePart += `${formatAsDecimalNumber(MathAbs(years))}Y`; @@ -2577,17 +2551,12 @@ export function TemporalDurationToString( if (minutes !== 0) timePart += `${formatAsDecimalNumber(MathAbs(minutes))}M`; if ( - !seconds.isZero() || - !ms.isZero() || - !µs.isZero() || - !ns.isZero() || + !normSeconds.isZero() || (years === 0 && months === 0 && weeks === 0 && days === 0 && hours === 0 && minutes === 0) || precision !== 'auto' ) { - const secondsPart = formatAsDecimalNumber(seconds.abs()); - const subSecondNanoseconds = - MathAbs(ms.toJSNumber()) * 1e6 + MathAbs(µs.toJSNumber()) * 1e3 + MathAbs(ns.toJSNumber()); - const subSecondsPart = FormatFractionalSeconds(subSecondNanoseconds, precision); + const secondsPart = formatAsDecimalNumber(MathAbs(normSeconds.sec)); + const subSecondsPart = FormatFractionalSeconds(MathAbs(normSeconds.subsec), precision); timePart += `${secondsPart}${subSecondsPart}S`; } let result = `${sign < 0 ? '-' : ''}P${datePart}`; @@ -3235,24 +3204,15 @@ export function BalanceTime(hour, minute, second, millisecond, microsecond, nano }; } -export function TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds) { - minutes = bigInt(minutes).add(bigInt(hours).multiply(60)); - seconds = bigInt(seconds).add(minutes.multiply(60)); - milliseconds = bigInt(milliseconds).add(seconds.multiply(1000)); - microseconds = bigInt(microseconds).add(milliseconds.multiply(1000)); - return bigInt(nanoseconds).add(microseconds.multiply(1000)); -} - -export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime) { +export function NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime) { // getOffsetNanosecondsFor and getPossibleInstantsFor must be looked up const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); - const sign = MathSign(nanoseconds); - nanoseconds = bigInt(nanoseconds); - if (sign === 0) return { days: 0, nanoseconds: bigInt.zero, dayLengthNs: DAY_NANOS.toJSNumber() }; + const sign = norm.sign(); + if (sign === 0) return { days: 0, norm, dayLengthNs: DAY_NANOS }; const startNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS); const start = GetSlot(zonedRelativeTo, INSTANT); - const endNs = startNs.add(nanoseconds); + const endNs = norm.addToEpochNs(startNs); const end = new TemporalInstant(endNs); const calendar = GetSlot(zonedRelativeTo, CALENDAR); @@ -3303,7 +3263,7 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre // may do disambiguation } } - nanoseconds = endNs.subtract(relativeResult.epochNs); + norm = TimeDuration.fromEpochNsDiff(endNs, relativeResult.epochNs); let isOverflow = false; let dayLengthNs; @@ -3317,10 +3277,11 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre sign ); - dayLengthNs = oneDayFarther.epochNs.subtract(relativeResult.epochNs).toJSNumber(); - isOverflow = nanoseconds.subtract(dayLengthNs).multiply(sign).geq(0); + dayLengthNs = TimeDuration.fromEpochNsDiff(oneDayFarther.epochNs, relativeResult.epochNs); + const oneDayLess = norm.subtract(dayLengthNs); + isOverflow = oneDayLess.sign() * sign >= 0; if (isOverflow) { - nanoseconds = nanoseconds.subtract(dayLengthNs); + norm = oneDayLess; relativeResult = oneDayFarther; days += sign; } @@ -3328,85 +3289,103 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre if (days !== 0 && MathSign(days) != sign) { throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign'); } - if (!nanoseconds.isZero() && MathSign(nanoseconds.toJSNumber()) != sign) { - if (nanoseconds.lt(0) && sign === 1) { + if (!norm.isZero() && norm.sign() !== sign) { + if (norm.sign() === -1 && sign === 1) { throw new Error('assert not reached'); } throw new RangeError('Time zone or calendar ended up with a remainder of nanoseconds with the opposite sign'); } - if (nanoseconds.abs().geq(MathAbs(dayLengthNs))) { + if (norm.abs().cmp(dayLengthNs.abs()) >= 0) { throw new Error('assert not reached'); } - return { days, nanoseconds, dayLengthNs: MathAbs(dayLengthNs) }; + return { days, norm, dayLengthNs: dayLengthNs.abs().totalNs }; } -export function BalanceTimeDuration( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - largestUnit -) { - hours = bigInt(hours).add(bigInt(days).multiply(24)); - nanoseconds = TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - - const sign = nanoseconds.lesser(0) ? -1 : 1; - nanoseconds = nanoseconds.abs(); - microseconds = milliseconds = seconds = minutes = hours = days = bigInt.zero; +export function BalanceTimeDuration(norm, largestUnit) { + const sign = norm.sign(); + let nanoseconds = norm.abs().subsec; + let microseconds = 0; + let milliseconds = 0; + let seconds = norm.abs().sec; + let minutes = 0; + let hours = 0; + let days = 0; switch (largestUnit) { case 'year': case 'month': case 'week': case 'day': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); - ({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000)); - ({ quotient: seconds, remainder: milliseconds } = milliseconds.divmod(1000)); - ({ quotient: minutes, remainder: seconds } = seconds.divmod(60)); - ({ quotient: hours, remainder: minutes } = minutes.divmod(60)); - ({ quotient: days, remainder: hours } = hours.divmod(24)); + microseconds = MathTrunc(nanoseconds / 1000); + nanoseconds %= 1000; + milliseconds = MathTrunc(microseconds / 1000); + microseconds %= 1000; + seconds += MathTrunc(milliseconds / 1000); + milliseconds %= 1000; + minutes = MathTrunc(seconds / 60); + seconds %= 60; + hours = MathTrunc(minutes / 60); + minutes %= 60; + days = MathTrunc(hours / 24); + hours %= 24; break; case 'hour': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); - ({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000)); - ({ quotient: seconds, remainder: milliseconds } = milliseconds.divmod(1000)); - ({ quotient: minutes, remainder: seconds } = seconds.divmod(60)); - ({ quotient: hours, remainder: minutes } = minutes.divmod(60)); + microseconds = MathTrunc(nanoseconds / 1000); + nanoseconds %= 1000; + milliseconds = MathTrunc(microseconds / 1000); + microseconds %= 1000; + seconds += MathTrunc(milliseconds / 1000); + milliseconds %= 1000; + minutes = MathTrunc(seconds / 60); + seconds %= 60; + hours = MathTrunc(minutes / 60); + minutes %= 60; break; case 'minute': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); - ({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000)); - ({ quotient: seconds, remainder: milliseconds } = milliseconds.divmod(1000)); - ({ quotient: minutes, remainder: seconds } = seconds.divmod(60)); + microseconds = MathTrunc(nanoseconds / 1000); + nanoseconds %= 1000; + milliseconds = MathTrunc(microseconds / 1000); + microseconds %= 1000; + seconds += MathTrunc(milliseconds / 1000); + milliseconds %= 1000; + minutes = MathTrunc(seconds / 60); + seconds %= 60; break; case 'second': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); - ({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000)); - ({ quotient: seconds, remainder: milliseconds } = milliseconds.divmod(1000)); + microseconds = MathTrunc(nanoseconds / 1000); + nanoseconds %= 1000; + milliseconds = MathTrunc(microseconds / 1000); + microseconds %= 1000; + seconds += MathTrunc(milliseconds / 1000); + milliseconds %= 1000; break; case 'millisecond': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); - ({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000)); + microseconds = MathTrunc(nanoseconds / 1000); + nanoseconds %= 1000; + milliseconds = FMAPowerOf10(seconds, 3, MathTrunc(microseconds / 1000)); + microseconds %= 1000; + seconds = 0; break; case 'microsecond': - ({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000)); + microseconds = FMAPowerOf10(seconds, 6, MathTrunc(nanoseconds / 1000)); + nanoseconds %= 1000; + seconds = 0; break; case 'nanosecond': + nanoseconds = FMAPowerOf10(seconds, 9, nanoseconds); + seconds = 0; break; default: throw new Error('assert not reached'); } - days = days.toJSNumber() * sign; - hours = hours.toJSNumber() * sign; - minutes = minutes.toJSNumber() * sign; - seconds = seconds.toJSNumber() * sign; - milliseconds = milliseconds.toJSNumber() * sign; - microseconds = microseconds.toJSNumber() * sign; - nanoseconds = nanoseconds.toJSNumber() * sign; + days *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; RejectDuration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; @@ -3414,12 +3393,7 @@ export function BalanceTimeDuration( export function BalanceTimeDurationRelative( days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, largestUnit, zonedRelativeTo, timeZoneRec, @@ -3440,31 +3414,21 @@ export function BalanceTimeDurationRelative( ).epochNs; } - const endNs = AddInstant(intermediateNs, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - nanoseconds = endNs.subtract(startNs); - if (nanoseconds.isZero()) { + const endNs = AddInstant(intermediateNs, norm); + norm = TimeDuration.fromEpochNsDiff(endNs, startNs); + if (norm.isZero()) { return { days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0, microseconds: 0, nanoseconds: 0 }; } if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week' || largestUnit === 'day') { precalculatedPlainDateTime ??= GetPlainDateTimeFor(timeZoneRec, startInstant, 'iso8601'); - ({ days, nanoseconds } = NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime)); + ({ days, norm } = NormalizedTimeDurationToDays(norm, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime)); largestUnit = 'hour'; } else { days = 0; } - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - 0, - 0, - 0, - 0, - 0, - 0, - nanoseconds, - largestUnit - )); - + const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, largestUnit); return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } @@ -3705,6 +3669,14 @@ export function RejectDuration(y, mon, w, d, h, min, s, ms, µs, ns) { } } +function CombineDateAndNormalizedTimeDuration(y, m, w, d, norm) { + const dateSign = DurationSign(y, m, w, d, 0, 0, 0, 0, 0, 0); + const timeSign = norm.sign(); + if (dateSign !== 0 && timeSign !== 0 && dateSign !== timeSign) { + throw new RangeError('mixed-sign values not allowed as duration fields'); + } +} + export function DifferenceISODate(y1, m1, d1, y2, m2, d2, largestUnit = 'days') { switch (largestUnit) { case 'year': @@ -3799,71 +3771,24 @@ export function DifferenceISODate(y1, m1, d1, y2, m2, d2, largestUnit = 'days') } export function DifferenceTime(h1, min1, s1, ms1, µs1, ns1, h2, min2, s2, ms2, µs2, ns2) { - let hours = h2 - h1; - let minutes = min2 - min1; - let seconds = s2 - s1; - let milliseconds = ms2 - ms1; - let microseconds = µs2 - µs1; - let nanoseconds = ns2 - ns1; - - const sign = DurationSign(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - hours *= sign; - minutes *= sign; - seconds *= sign; - milliseconds *= sign; - microseconds *= sign; - nanoseconds *= sign; + const hours = h2 - h1; + const minutes = min2 - min1; + const seconds = s2 - s1; + const milliseconds = ms2 - ms1; + const microseconds = µs2 - µs1; + const nanoseconds = ns2 - ns1; + const norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - let deltaDays = 0; - ({ - deltaDays, - hour: hours, - minute: minutes, - second: seconds, - millisecond: milliseconds, - microsecond: microseconds, - nanosecond: nanoseconds - } = BalanceTime(hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); - - if (deltaDays != 0) throw new Error('assertion failure in DifferenceTime: _bt_.[[Days]] should be 0'); - hours *= sign; - minutes *= sign; - seconds *= sign; - milliseconds *= sign; - microseconds *= sign; - nanoseconds *= sign; + if (norm.abs().sec >= 86400) throw new Error('assertion failure in DifferenceTime: _bt_.[[Days]] should be 0'); - return { hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + return norm; } -export function DifferenceInstant(ns1, ns2, increment, smallestUnit, largestUnit, roundingMode) { - const diff = ns2.minus(ns1); - - let hours = 0; - let minutes = 0; - let nanoseconds = diff.mod(1e3).toJSNumber(); - let microseconds = diff.divide(1e3).mod(1e3).toJSNumber(); - let milliseconds = diff.divide(1e6).mod(1e3).toJSNumber(); - let seconds = diff.divide(1e9).toJSNumber(); +export function DifferenceInstant(ns1, ns2, increment, smallestUnit, roundingMode) { + const diff = TimeDuration.fromEpochNsDiff(ns2, ns1); + if (smallestUnit === 'nanosecond' && increment === 1) return diff; - if (smallestUnit !== 'nanosecond' || increment !== 1) { - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( - 0, - 0, - 0, - 0, - 0, - 0, - seconds, - milliseconds, - microseconds, - nanoseconds, - increment, - smallestUnit, - roundingMode - )); - } - return BalanceTimeDuration(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit); + return RoundDuration(0, 0, 0, 0, diff, increment, smallestUnit, roundingMode).norm; } export function DifferenceDate(calendarRec, plainDate1, plainDate2, options) { @@ -3906,35 +3831,13 @@ export function DifferenceISODateTime( ) { // dateUntil must be looked up if date parts are not identical and largestUnit // is greater than 'day' - let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceTime( - h1, - min1, - s1, - ms1, - µs1, - ns1, - h2, - min2, - s2, - ms2, - µs2, - ns2 - ); + let timeDuration = DifferenceTime(h1, min1, s1, ms1, µs1, ns1, h2, min2, s2, ms2, µs2, ns2); - const timeSign = DurationSign(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + const timeSign = timeDuration.sign(); const dateSign = CompareISODate(y2, mon2, d2, y1, mon1, d1); if (dateSign === -timeSign) { ({ year: y1, month: mon1, day: d1 } = BalanceISODate(y1, mon1, d1 - timeSign)); - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - -timeSign, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - largestUnit - )); + timeDuration = timeDuration.add24HourDays(-timeSign); } const date1 = CreateTemporalDate(y1, mon1, d1, calendarRec.receiver); @@ -3946,19 +3849,8 @@ export function DifferenceISODateTime( const years = GetSlot(untilResult, YEARS); const months = GetSlot(untilResult, MONTHS); const weeks = GetSlot(untilResult, WEEKS); - let days = GetSlot(untilResult, DAYS); - // Signs of date part and time part may not agree; balance them together - ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - largestUnit - )); - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + const days = GetSlot(untilResult, DAYS); + return { years, months, weeks, days, norm: timeDuration }; } export function DifferenceZonedDateTime( @@ -3982,12 +3874,7 @@ export function DifferenceZonedDateTime( months: 0, weeks: 0, days: 0, - hours: 0, - minutes: 0, - seconds: 0, - milliseconds: 0, - microseconds: 0, - nanoseconds: 0 + norm: TimeDuration.ZERO }; } @@ -3998,7 +3885,7 @@ export function DifferenceZonedDateTime( const dtStart = precalculatedDtStart ?? GetPlainDateTimeFor(timeZoneRec, start, calendarRec.receiver); const dtEnd = GetPlainDateTimeFor(timeZoneRec, end, calendarRec.receiver); - let { years, months, weeks, days } = DifferenceISODateTime( + let { years, months, weeks } = DifferenceISODateTime( GetSlot(dtStart, ISO_YEAR), GetSlot(dtStart, ISO_MONTH), GetSlot(dtStart, ISO_DAY), @@ -4029,31 +3916,18 @@ export function DifferenceZonedDateTime( months, weeks, 0, - 0, - 0, - 0, - 0, - 0, - 0, + TimeDuration.ZERO, dtStart ); // may disambiguate - let timeRemainderNs = ns2.subtract(intermediateNs); + + let norm = TimeDuration.fromEpochNsDiff(ns2, intermediateNs); const intermediate = CreateTemporalZonedDateTime(intermediateNs, timeZoneRec.receiver, calendarRec.receiver); - ({ nanoseconds: timeRemainderNs, days } = NanosecondsToDays(timeRemainderNs, intermediate, timeZoneRec)); + let days; + ({ norm, days } = NormalizedTimeDurationToDays(norm, intermediate, timeZoneRec)); - // Finally, merge the date and time durations and return the merged result. - let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - 0, - 0, - 0, - 0, - 0, - 0, - timeRemainderNs, - 'hour' - ); - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm); + return { years, months, weeks, days, norm }; } export function GetDifferenceSettings(op, options, group, disallowed, fallbackSmallest, smallestLargestDefaultUnit) { @@ -4110,14 +3984,17 @@ export function DifferenceTemporalInstant(operation, instant, other, options) { const onens = GetSlot(instant, EPOCHNANOSECONDS); const twons = GetSlot(other, EPOCHNANOSECONDS); - let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( + const norm = DifferenceInstant( onens, twons, settings.roundingIncrement, settings.smallestUnit, - settings.largestUnit, settings.roundingMode ); + const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( + norm, + settings.largestUnit + ); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( 0, @@ -4168,12 +4045,7 @@ export function DifferenceTemporalPlainDate(operation, plainDate, other, options months, weeks, days, - 0, - 0, - 0, - 0, - 0, - 0, + TimeDuration.ZERO, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, @@ -4224,45 +4096,40 @@ export function DifferenceTemporalPlainDateTime(operation, plainDateTime, other, const calendarRec = new CalendarMethodRecord(calendar, ['dateAdd', 'dateUntil']); - let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - DifferenceISODateTime( - GetSlot(plainDateTime, ISO_YEAR), - GetSlot(plainDateTime, ISO_MONTH), - GetSlot(plainDateTime, ISO_DAY), - GetSlot(plainDateTime, ISO_HOUR), - GetSlot(plainDateTime, ISO_MINUTE), - GetSlot(plainDateTime, ISO_SECOND), - GetSlot(plainDateTime, ISO_MILLISECOND), - GetSlot(plainDateTime, ISO_MICROSECOND), - GetSlot(plainDateTime, ISO_NANOSECOND), - GetSlot(other, ISO_YEAR), - GetSlot(other, ISO_MONTH), - GetSlot(other, ISO_DAY), - GetSlot(other, ISO_HOUR), - GetSlot(other, ISO_MINUTE), - GetSlot(other, ISO_SECOND), - GetSlot(other, ISO_MILLISECOND), - GetSlot(other, ISO_MICROSECOND), - GetSlot(other, ISO_NANOSECOND), - calendarRec, - settings.largestUnit, - resolvedOptions - ); + let { years, months, weeks, days, norm } = DifferenceISODateTime( + GetSlot(plainDateTime, ISO_YEAR), + GetSlot(plainDateTime, ISO_MONTH), + GetSlot(plainDateTime, ISO_DAY), + GetSlot(plainDateTime, ISO_HOUR), + GetSlot(plainDateTime, ISO_MINUTE), + GetSlot(plainDateTime, ISO_SECOND), + GetSlot(plainDateTime, ISO_MILLISECOND), + GetSlot(plainDateTime, ISO_MICROSECOND), + GetSlot(plainDateTime, ISO_NANOSECOND), + GetSlot(other, ISO_YEAR), + GetSlot(other, ISO_MONTH), + GetSlot(other, ISO_DAY), + GetSlot(other, ISO_HOUR), + GetSlot(other, ISO_MINUTE), + GetSlot(other, ISO_SECOND), + GetSlot(other, ISO_MILLISECOND), + GetSlot(other, ISO_MICROSECOND), + GetSlot(other, ISO_NANOSECOND), + calendarRec, + settings.largestUnit, + resolvedOptions + ); + let hours, minutes, seconds, milliseconds, microseconds, nanoseconds; const roundingIsNoop = settings.smallestUnit === 'nanosecond' && settings.roundingIncrement === 1; if (!roundingIsNoop) { const relativeTo = TemporalDateTimeToDate(plainDateTime); - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( + ({ years, months, weeks, days, norm } = RoundDuration( years, months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, @@ -4270,13 +4137,7 @@ export function DifferenceTemporalPlainDateTime(operation, plainDateTime, other, calendarRec )); ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm.add24HourDays(days), settings.largestUnit )); ({ years, months, weeks, days } = BalanceDateDurationRelative( @@ -4289,6 +4150,11 @@ export function DifferenceTemporalPlainDateTime(operation, plainDateTime, other, relativeTo, calendarRec )); + } else { + ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( + norm.add24HourDays(days), + settings.largestUnit + )); } return new Duration( @@ -4312,7 +4178,7 @@ export function DifferenceTemporalPlainTime(operation, plainTime, other, options const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null); const settings = GetDifferenceSettings(operation, resolvedOptions, 'time', [], 'nanosecond', 'hour'); - let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceTime( + let norm = DifferenceTime( GetSlot(plainTime, ISO_HOUR), GetSlot(plainTime, ISO_MINUTE), GetSlot(plainTime, ISO_SECOND), @@ -4327,32 +4193,21 @@ export function DifferenceTemporalPlainTime(operation, plainTime, other, options GetSlot(other, ISO_NANOSECOND) ); if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) { - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( + ({ norm } = RoundDuration( 0, 0, 0, 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode )); } - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( + norm, settings.largestUnit - )); + ); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( 0, @@ -4407,12 +4262,7 @@ export function DifferenceTemporalPlainYearMonth(operation, yearMonth, other, op months, 0, 0, - 0, - 0, - 0, - 0, - 0, - 0, + TimeDuration.ZERO, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, @@ -4461,13 +4311,10 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, months = 0; weeks = 0; days = 0; - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( - ns1, - ns2, - settings.roundingIncrement, - settings.smallestUnit, - settings.largestUnit, - settings.roundingMode + const norm = DifferenceInstant(ns1, ns2, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode); + ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( + norm, + settings.largestUnit )); } else { const timeZone = GetSlot(zonedDateTime, TIME_ZONE); @@ -4494,30 +4341,25 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, const plainRelativeTo = TemporalDateTimeToDate(precalculatedPlainDateTime); resolvedOptions.largestUnit = settings.largestUnit; - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - DifferenceZonedDateTime( - ns1, - ns2, - timeZoneRec, - calendarRec, - settings.largestUnit, - resolvedOptions, - precalculatedPlainDateTime - )); + let norm; + ({ years, months, weeks, days, norm } = DifferenceZonedDateTime( + ns1, + ns2, + timeZoneRec, + calendarRec, + settings.largestUnit, + resolvedOptions, + precalculatedPlainDateTime + )); const roundingIsNoop = settings.smallestUnit === 'nanosecond' && settings.roundingIncrement === 1; if (!roundingIsNoop) { - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( + ({ years, months, weeks, days, norm } = RoundDuration( years, months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, @@ -4527,26 +4369,23 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, timeZoneRec, precalculatedPlainDateTime )); - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - AdjustRoundedDurationDays( - years, - months, - weeks, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - settings.roundingIncrement, - settings.smallestUnit, - settings.roundingMode, - zonedDateTime, - calendarRec, - timeZoneRec, - precalculatedPlainDateTime - )); + let deltaDays; + ({ days: deltaDays, norm } = NormalizedTimeDurationToDays(norm, zonedDateTime, timeZoneRec)); + days += deltaDays; + ({ years, months, weeks, days, norm } = AdjustRoundedDurationDays( + years, + months, + weeks, + days, + norm, + settings.roundingIncrement, + settings.smallestUnit, + settings.roundingMode, + zonedDateTime, + calendarRec, + timeZoneRec, + precalculatedPlainDateTime + )); // BalanceTimeDuration already performed in AdjustRoundedDurationDays ({ years, months, weeks, days } = BalanceDateDurationRelative( years, @@ -4558,7 +4397,9 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, plainRelativeTo, calendarRec )); + CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm); } + ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, 'hour')); } return new Duration( @@ -4600,50 +4441,23 @@ export function AddDate(calendarRec, plainDate, duration, options = undefined) { let month = GetSlot(plainDate, ISO_MONTH); let day = GetSlot(plainDate, ISO_DAY); const overflow = ToTemporalOverflow(options); - const { days } = BalanceTimeDuration( - GetSlot(duration, DAYS), + const norm = TimeDuration.normalize( GetSlot(duration, HOURS), GetSlot(duration, MINUTES), GetSlot(duration, SECONDS), GetSlot(duration, MILLISECONDS), GetSlot(duration, MICROSECONDS), - GetSlot(duration, NANOSECONDS), - 'day' + GetSlot(duration, NANOSECONDS) ); + const days = GetSlot(duration, DAYS) + BalanceTimeDuration(norm, 'day').days; ({ year, month, day } = AddISODate(year, month, day, 0, 0, 0, days, overflow)); return CreateTemporalDate(year, month, day, calendarRec.receiver); } -export function AddTime( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds -) { - hour += hours; - minute += minutes; - second += seconds; - millisecond += milliseconds; - microsecond += microseconds; - nanosecond += nanoseconds; - let deltaDays = 0; - ({ deltaDays, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceTime( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond - )); - return { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond }; +export function AddTime(hour, minute, second, millisecond, microsecond, nanosecond, norm) { + second += norm.sec; + nanosecond += norm.subsec; + return BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond); } export function AddDuration( @@ -4686,14 +4500,10 @@ export function AddDuration( throw new RangeError('relativeTo is required for years, months, or weeks arithmetic'); } years = months = weeks = 0; + const norm1 = TimeDuration.normalize(h1, min1, s1, ms1, µs1, ns1); + const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2); ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - d1 + d2, - bigInt(h1).add(h2), - bigInt(min1).add(min2), - bigInt(s1).add(s2), - bigInt(ms1).add(ms2), - bigInt(µs1).add(µs2), - bigInt(ns1).add(ns2), + norm1.add(norm2).add24HourDays(d1 + d2), largestUnit )); } else if (plainRelativeTo) { @@ -4713,14 +4523,10 @@ export function AddDuration( weeks = GetSlot(untilResult, WEEKS); days = GetSlot(untilResult, DAYS); // Signs of date part and time part may not agree; balance them together + const norm1 = TimeDuration.normalize(h1, min1, s1, ms1, µs1, ns1); + const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2); ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - days, - bigInt(h1).add(h2), - bigInt(min1).add(min2), - bigInt(s1).add(s2), - bigInt(ms1).add(ms2), - bigInt(µs1).add(µs2), - bigInt(ns1).add(ns2), + norm1.add(norm2).add24HourDays(days), largestUnit )); } else { @@ -4732,6 +4538,8 @@ export function AddDuration( if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week' || largestUnit === 'day') { startDateTime ??= GetPlainDateTimeFor(timeZoneRec, startInstant, calendar); } + const norm1 = TimeDuration.normalize(h1, min1, s1, ms1, µs1, ns1); + const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2); const intermediateNs = AddZonedDateTime( startInstant, timeZoneRec, @@ -4740,12 +4548,7 @@ export function AddDuration( mon1, w1, d1, - h1, - min1, - s1, - ms1, - µs1, - ns1, + norm1, startDateTime ); const endNs = AddZonedDateTime( @@ -4756,12 +4559,7 @@ export function AddDuration( mon2, w2, d2, - h2, - min2, - s2, - ms2, - µs2, - ns2 + norm2 ); if (largestUnit !== 'year' && largestUnit !== 'month' && largestUnit !== 'week' && largestUnit !== 'day') { // The user is only asking for a time difference, so return difference of instants. @@ -4769,42 +4567,28 @@ export function AddDuration( months = 0; weeks = 0; days = 0; - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( + const norm = TimeDuration.fromEpochNsDiff(endNs, GetSlot(zonedRelativeTo, EPOCHNANOSECONDS)); + ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, largestUnit)); + } else { + let norm; + ({ years, months, weeks, days, norm } = DifferenceZonedDateTime( GetSlot(zonedRelativeTo, EPOCHNANOSECONDS), endNs, - 1, - 'nanosecond', + timeZoneRec, + calendarRec, largestUnit, - 'halfExpand' + ObjectCreate(null), + startDateTime )); - } else { - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = - DifferenceZonedDateTime( - GetSlot(zonedRelativeTo, EPOCHNANOSECONDS), - endNs, - timeZoneRec, - calendarRec, - largestUnit, - ObjectCreate(null), - startDateTime - )); + ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, 'hour')); } } - RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } -export function AddInstant(epochNanoseconds, h, min, s, ms, µs, ns) { - let sum = bigInt.zero; - sum = sum.plus(bigInt(ns)); - sum = sum.plus(bigInt(µs).multiply(1e3)); - sum = sum.plus(bigInt(ms).multiply(1e6)); - sum = sum.plus(bigInt(s).multiply(1e9)); - sum = sum.plus(bigInt(min).multiply(60 * 1e9)); - sum = sum.plus(bigInt(h).multiply(60 * 60 * 1e9)); - - const result = bigInt(epochNanoseconds).plus(sum); +export function AddInstant(epochNanoseconds, norm) { + const result = norm.addToEpochNs(epochNanoseconds); ValidateEpochNanoseconds(result); return result; } @@ -4824,12 +4608,7 @@ export function AddDateTime( months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, options ) { // dateAdd must be looked up if years, months, weeks != 0 @@ -4842,12 +4621,7 @@ export function AddDateTime( millisecond, microsecond, nanosecond, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds + norm )); days += deltaDays; @@ -4878,12 +4652,7 @@ export function AddZonedDateTime( months, weeks, days, - h, - min, - s, - ms, - µs, - ns, + norm, precalculatedPlainDateTime = undefined, options = undefined ) { @@ -4905,14 +4674,14 @@ export function AddZonedDateTime( // BTW, this behavior is similar in spirit to offset: 'prefer' in `with`. const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); if (DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0) === 0) { - return AddInstant(GetSlot(instant, EPOCHNANOSECONDS), h, min, s, ms, µs, ns); + return AddInstant(GetSlot(instant, EPOCHNANOSECONDS), norm); } const dt = precalculatedPlainDateTime ?? GetPlainDateTimeFor(timeZoneRec, instant, calendarRec.receiver); if (DurationSign(years, months, weeks, 0, 0, 0, 0, 0, 0, 0) === 0) { const overflow = ToTemporalOverflow(options); const intermediate = AddDaysToZonedDateTime(instant, dt, timeZoneRec, calendarRec.receiver, days, overflow).epochNs; - return AddInstant(intermediate, h, min, s, ms, µs, ns); + return AddInstant(intermediate, norm); } // RFC 5545 requires the date portion to be added in calendar days and the @@ -4941,7 +4710,7 @@ export function AddZonedDateTime( // Note that 'compatible' is used below because this disambiguation behavior // is required by RFC 5545. const instantIntermediate = GetInstantFor(timeZoneRec, dtIntermediate, 'compatible'); - return AddInstant(GetSlot(instantIntermediate, EPOCHNANOSECONDS), h, min, s, ms, µs, ns); + return AddInstant(GetSlot(instantIntermediate, EPOCHNANOSECONDS), norm); } export function AddDaysToZonedDateTime(instant, dateTime, timeZoneRec, calendar, days, overflow = 'constrain') { @@ -5037,8 +4806,7 @@ export function AddDurationToOrSubtractDurationFromInstant(operation, instant, d 'weeks', 'days' ]); - const ns = AddInstant( - GetSlot(instant, EPOCHNANOSECONDS), + const norm = TimeDuration.normalize( sign * hours, sign * minutes, sign * seconds, @@ -5046,6 +4814,7 @@ export function AddDurationToOrSubtractDurationFromInstant(operation, instant, d sign * microseconds, sign * nanoseconds ); + const ns = AddInstant(GetSlot(instant, EPOCHNANOSECONDS), norm); const Instant = GetIntrinsic('%Temporal.Instant%'); return new Instant(ns); } @@ -5058,6 +4827,14 @@ export function AddDurationToOrSubtractDurationFromPlainDateTime(operation, date const calendarRec = new CalendarMethodRecord(GetSlot(dateTime, CALENDAR), ['dateAdd']); + const norm = TimeDuration.normalize( + sign * hours, + sign * minutes, + sign * seconds, + sign * milliseconds, + sign * microseconds, + sign * nanoseconds + ); const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = AddDateTime( GetSlot(dateTime, ISO_YEAR), GetSlot(dateTime, ISO_MONTH), @@ -5073,12 +4850,7 @@ export function AddDurationToOrSubtractDurationFromPlainDateTime(operation, date sign * months, sign * weeks, sign * days, - sign * hours, - sign * minutes, - sign * seconds, - sign * milliseconds, - sign * microseconds, - sign * nanoseconds, + norm, options ); return CreateTemporalDateTime( @@ -5098,6 +4870,14 @@ export function AddDurationToOrSubtractDurationFromPlainDateTime(operation, date export function AddDurationToOrSubtractDurationFromPlainTime(operation, temporalTime, durationLike) { const sign = operation === 'subtract' ? -1 : 1; const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(durationLike); + const norm = TimeDuration.normalize( + sign * hours, + sign * minutes, + sign * seconds, + sign * milliseconds, + sign * microseconds, + sign * nanoseconds + ); let { hour, minute, second, millisecond, microsecond, nanosecond } = AddTime( GetSlot(temporalTime, ISO_HOUR), GetSlot(temporalTime, ISO_MINUTE), @@ -5105,12 +4885,7 @@ export function AddDurationToOrSubtractDurationFromPlainTime(operation, temporal GetSlot(temporalTime, ISO_MILLISECOND), GetSlot(temporalTime, ISO_MICROSECOND), GetSlot(temporalTime, ISO_NANOSECOND), - sign * hours, - sign * minutes, - sign * seconds, - sign * milliseconds, - sign * microseconds, - sign * nanoseconds + norm ); ({ hour, minute, second, millisecond, microsecond, nanosecond } = RegulateTime( hour, @@ -5142,8 +4917,9 @@ export function AddDurationToOrSubtractDurationFromPlainYearMonth(operation, yea }; } let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - ({ days } = BalanceTimeDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'day')); options = GetOptionsObject(options); + const norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + days += BalanceTimeDuration(norm, 'day').days; const sign = DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); const calendarRec = new CalendarMethodRecord(GetSlot(yearMonth, CALENDAR), [ @@ -5200,6 +4976,14 @@ export function AddDurationToOrSubtractDurationFromZonedDateTime(operation, zone 'getPossibleInstantsFor' ]); const calendarRec = new CalendarMethodRecord(GetSlot(zonedDateTime, CALENDAR), ['dateAdd']); + const norm = TimeDuration.normalize( + sign * hours, + sign * minutes, + sign * seconds, + sign * milliseconds, + sign * microseconds, + sign * nanoseconds + ); const epochNanoseconds = AddZonedDateTime( GetSlot(zonedDateTime, INSTANT), timeZoneRec, @@ -5208,12 +4992,7 @@ export function AddDurationToOrSubtractDurationFromZonedDateTime(operation, zone sign * months, sign * weeks, sign * days, - sign * hours, - sign * minutes, - sign * seconds, - sign * milliseconds, - sign * microseconds, - sign * nanoseconds, + norm, undefined, options ); @@ -5395,12 +5174,7 @@ export function MoveRelativeZonedDateTime( months, weeks, days, - 0, - 0, - 0, - 0, - 0, - 0, + TimeDuration.ZERO, precalculatedPlainDateTime ); return CreateTemporalZonedDateTime(intermediateNs, timeZoneRec.receiver, calendarRec.receiver); @@ -5411,12 +5185,7 @@ export function AdjustRoundedDurationDays( months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, increment, unit, roundingMode, @@ -5434,7 +5203,7 @@ export function AdjustRoundedDurationDays( unit === 'day' || (unit === 'nanosecond' && increment === 1) ) { - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + return { years, months, weeks, days, norm }; } // There's one more round of rounding possible: if relativeTo is a @@ -5445,8 +5214,7 @@ export function AdjustRoundedDurationDays( // duration, there's no way for another full day to come from the next // round of rounding. And if it were possible (e.g. contrived calendar // with 30-minute-long "days") then it'd risk an infinite loop. - let timeRemainderNs = TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - const direction = MathSign(timeRemainderNs.toJSNumber()); + const direction = norm.sign(); const calendar = GetSlot(zonedRelativeTo, CALENDAR); // requires dateAdd if years...weeks != 0 @@ -5458,22 +5226,17 @@ export function AdjustRoundedDurationDays( months, weeks, days, - 0, - 0, - 0, - 0, - 0, - 0, + TimeDuration.ZERO, precalculatedPlainDateTime ); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const dayStartInstant = new TemporalInstant(dayStart); const dayStartDateTime = GetPlainDateTimeFor(timeZoneRec, dayStartInstant, calendar); const dayEnd = AddDaysToZonedDateTime(dayStartInstant, dayStartDateTime, timeZoneRec, calendar, direction).epochNs; - const dayLengthNs = dayEnd.subtract(dayStart); + const dayLength = TimeDuration.fromEpochNsDiff(dayEnd, dayStart); - const oneDayLess = timeRemainderNs.subtract(dayLengthNs); - if (oneDayLess.multiply(direction).geq(0)) { + const oneDayLess = norm.subtract(dayLength); + if (oneDayLess.sign() * direction >= 0) { // requires dateAdd and dateUntil if years...weeks != 0 ({ years, months, weeks, days } = AddDuration( years, @@ -5502,34 +5265,10 @@ export function AdjustRoundedDurationDays( timeZoneRec, precalculatedPlainDateTime )); - // no calendar calls - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - oneDayLess.toJSNumber(), - increment, - unit, - roundingMode - )); - ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration( - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'hour' - )); + ({ norm } = RoundDuration(0, 0, 0, 0, oneDayLess, increment, unit, roundingMode)); } - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm); + return { years, months, weeks, days, norm }; } export function RoundDuration( @@ -5537,12 +5276,7 @@ export function RoundDuration( months, weeks, days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + norm, increment, unit, roundingMode, @@ -5563,7 +5297,6 @@ export function RoundDuration( // If rounding relative to a ZonedDateTime, then some days may not be 24h. let dayLengthNs; if (unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day') { - nanoseconds = TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds); let deltaDays; if (zonedRelativeTo) { const intermediate = MoveRelativeZonedDateTime( @@ -5576,14 +5309,12 @@ export function RoundDuration( days, precalculatedPlainDateTime ); - ({ days: deltaDays, nanoseconds, dayLengthNs } = NanosecondsToDays(nanoseconds, intermediate, timeZoneRec)); + ({ days: deltaDays, norm, dayLengthNs } = NormalizedTimeDurationToDays(norm, intermediate, timeZoneRec)); } else { - ({ quotient: deltaDays, remainder: nanoseconds } = nanoseconds.divmod(DAY_NANOS)); - deltaDays = deltaDays.toJSNumber(); - dayLengthNs = DAY_NANOS.toJSNumber(); + ({ quotient: deltaDays, remainder: norm } = norm.divmod(DAY_NANOS)); + dayLengthNs = DAY_NANOS; } days += deltaDays; - hours = minutes = seconds = milliseconds = microseconds = 0; } let total; @@ -5634,12 +5365,13 @@ export function RoundDuration( oneYearDays = MathAbs(oneYearDays); if (oneYearDays === 0) throw new RangeError('custom calendar reported that a year is 0 days long'); const divisor = bigInt(oneYearDays).multiply(dayLengthNs); - nanoseconds = divisor.multiply(years).plus(bigInt(days).multiply(dayLengthNs)).plus(nanoseconds); + const nanoseconds = divisor.multiply(years).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment).toJSNumber(), roundingMode); const { quotient, remainder } = nanoseconds.divmod(divisor); total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; years = rounded.divide(divisor).toJSNumber(); - nanoseconds = months = weeks = days = 0; + months = weeks = days = 0; + norm = TimeDuration.ZERO; break; } case 'month': { @@ -5682,12 +5414,13 @@ export function RoundDuration( oneMonthDays = MathAbs(oneMonthDays); if (oneMonthDays === 0) throw new RangeError('custom calendar reported that a month is 0 days long'); const divisor = bigInt(oneMonthDays).multiply(dayLengthNs); - nanoseconds = divisor.multiply(months).plus(bigInt(days).multiply(dayLengthNs)).plus(nanoseconds); + const nanoseconds = divisor.multiply(months).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); const { quotient, remainder } = nanoseconds.divmod(divisor); total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; months = rounded.divide(divisor).toJSNumber(); - nanoseconds = weeks = days = 0; + weeks = days = 0; + norm = TimeDuration.ZERO; break; } case 'week': { @@ -5720,97 +5453,63 @@ export function RoundDuration( oneWeekDays = MathAbs(oneWeekDays); if (oneWeekDays === 0) throw new RangeError('custom calendar reported that a week is 0 days long'); const divisor = bigInt(oneWeekDays).multiply(dayLengthNs); - nanoseconds = divisor.multiply(weeks).plus(bigInt(days).multiply(dayLengthNs)).plus(nanoseconds); + const nanoseconds = divisor.multiply(weeks).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); const { quotient, remainder } = nanoseconds.divmod(divisor); total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; weeks = rounded.divide(divisor).toJSNumber(); - nanoseconds = days = 0; + days = 0; + norm = TimeDuration.ZERO; break; } case 'day': { const divisor = bigInt(dayLengthNs); - nanoseconds = divisor.multiply(days).plus(nanoseconds); + const nanoseconds = divisor.multiply(days).plus(norm.totalNs); const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); const { quotient, remainder } = nanoseconds.divmod(divisor); total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; days = rounded.divide(divisor).toJSNumber(); - nanoseconds = 0; + norm = TimeDuration.ZERO; break; } case 'hour': { const divisor = 3600e9; - nanoseconds = bigInt(hours) - .multiply(3600e9) - .plus(bigInt(minutes).multiply(60e9)) - .plus(bigInt(seconds).multiply(1e9)) - .plus(bigInt(milliseconds).multiply(1e6)) - .plus(bigInt(microseconds).multiply(1e3)) - .plus(nanoseconds); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - const rounded = RoundNumberToIncrement(nanoseconds, divisor * increment, roundingMode); - hours = rounded.divide(divisor).toJSNumber(); - minutes = seconds = milliseconds = microseconds = nanoseconds = 0; + total = norm.fdiv(divisor); + norm = norm.round(divisor * increment, roundingMode); break; } case 'minute': { const divisor = 60e9; - nanoseconds = bigInt(minutes) - .multiply(60e9) - .plus(bigInt(seconds).multiply(1e9)) - .plus(bigInt(milliseconds).multiply(1e6)) - .plus(bigInt(microseconds).multiply(1e3)) - .plus(nanoseconds); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - const rounded = RoundNumberToIncrement(nanoseconds, divisor * increment, roundingMode); - minutes = rounded.divide(divisor).toJSNumber(); - seconds = milliseconds = microseconds = nanoseconds = 0; + total = norm.fdiv(divisor); + norm = norm.round(divisor * increment, roundingMode); break; } case 'second': { const divisor = 1e9; - nanoseconds = bigInt(seconds) - .multiply(1e9) - .plus(bigInt(milliseconds).multiply(1e6)) - .plus(bigInt(microseconds).multiply(1e3)) - .plus(nanoseconds); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - const rounded = RoundNumberToIncrement(nanoseconds, divisor * increment, roundingMode); - seconds = rounded.divide(divisor).toJSNumber(); - milliseconds = microseconds = nanoseconds = 0; + total = norm.fdiv(divisor); + norm = norm.round(divisor * increment, roundingMode); break; } case 'millisecond': { const divisor = 1e6; - nanoseconds = bigInt(milliseconds).multiply(1e6).plus(bigInt(microseconds).multiply(1e3)).plus(nanoseconds); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - const rounded = RoundNumberToIncrement(nanoseconds, divisor * increment, roundingMode); - milliseconds = rounded.divide(divisor).toJSNumber(); - microseconds = nanoseconds = 0; + total = norm.fdiv(divisor); + norm = norm.round(divisor * increment, roundingMode); break; } case 'microsecond': { const divisor = 1e3; - nanoseconds = bigInt(microseconds).multiply(1e3).plus(nanoseconds); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - const rounded = RoundNumberToIncrement(nanoseconds, divisor * increment, roundingMode); - microseconds = rounded.divide(divisor).toJSNumber(); - nanoseconds = 0; + total = norm.fdiv(divisor); + norm = norm.round(divisor * increment, roundingMode); break; } case 'nanosecond': { - total = nanoseconds; - nanoseconds = RoundNumberToIncrement(bigInt(nanoseconds), increment, roundingMode).toJSNumber(); + total = norm.totalNs.toJSNumber(); + norm = norm.round(increment, roundingMode); break; } } - RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, total }; + CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm); + return { years, months, weeks, days, norm, total }; } export function CompareISODate(y1, m1, d1, y2, m2, d2) { diff --git a/polyfill/lib/math.mjs b/polyfill/lib/math.mjs index 751e3eac2d..fc8fcb6cb6 100644 --- a/polyfill/lib/math.mjs +++ b/polyfill/lib/math.mjs @@ -4,6 +4,8 @@ const MathSign = Math.sign; const MathTrunc = Math.trunc; const NumberParseInt = Number.parseInt; const NumberPrototypeToPrecision = Number.prototype.toPrecision; +const StringPrototypePadStart = String.prototype.padStart; +const StringPrototypeRepeat = String.prototype.repeat; const StringPrototypeSlice = String.prototype.slice; import Call from 'es-abstract/2022/Call.js'; @@ -30,3 +32,24 @@ export function TruncatingDivModByPowerOf10(x, p) { return { div, mod }; } + +// Computes x * 10**p + z with precision loss only at the end, by string +// manipulation. If the result is a safe integer, then it is exact. x must be +// an integer. p must be a non-negative integer. z must have the same sign as +// x and be less than 10**p. +export function FMAPowerOf10(x, p, z) { + if (x === 0) return z; + + const sign = MathSign(x) || MathSign(z); + x = MathAbs(x); + z = MathAbs(z); + + const xStr = Call(NumberPrototypeToPrecision, x, [MathTrunc(1 + MathLog10(x))]); + + if (z === 0) return sign * NumberParseInt(xStr + Call(StringPrototypeRepeat, '0', [p]), 10); + + const zStr = Call(NumberPrototypeToPrecision, z, [MathTrunc(1 + MathLog10(z))]); + + const resStr = xStr + Call(StringPrototypePadStart, zStr, [p, '0']); + return sign * NumberParseInt(resStr, 10); +} diff --git a/polyfill/lib/timeduration.mjs b/polyfill/lib/timeduration.mjs new file mode 100644 index 0000000000..90f0e7a5b6 --- /dev/null +++ b/polyfill/lib/timeduration.mjs @@ -0,0 +1,134 @@ +import bigInt from 'big-integer'; + +const MathAbs = Math.abs; +const NumberIsInteger = Number.isInteger; +const NumberIsSafeInteger = Number.isSafeInteger; + +export class TimeDuration { + static MAX = bigInt('9007199254740991999999999'); + static ZERO = new TimeDuration(bigInt.zero); + + constructor(totalNs) { + if (typeof totalNs === 'number') throw new Error('assertion failed: big integer required'); + this.totalNs = bigInt(totalNs); + if (this.totalNs.abs().greater(TimeDuration.MAX)) throw new Error('assertion failed: integer too big'); + + const { quotient, remainder } = this.totalNs.divmod(1e9); + this.sec = quotient.toJSNumber(); + this.subsec = remainder.toJSNumber(); + if (!NumberIsSafeInteger(this.sec)) throw new Error('assertion failed: seconds too big'); + if (MathAbs(this.subsec) > 999_999_999) throw new Error('assertion failed: subseconds too big'); + } + + static #validateNew(totalNs, operation) { + if (totalNs.abs().greater(TimeDuration.MAX)) { + throw new RangeError(`${operation} of duration time units cannot exceed ${TimeDuration.MAX} s`); + } + return new TimeDuration(totalNs); + } + + static fromEpochNsDiff(epochNs1, epochNs2) { + const diff = bigInt(epochNs1).subtract(epochNs2); + // No extra validate step. Should instead fail assertion if too big + return new TimeDuration(diff); + } + + static normalize(h, min, s, ms, µs, ns) { + const totalNs = bigInt(ns) + .add(bigInt(µs).multiply(1e3)) + .add(bigInt(ms).multiply(1e6)) + .add(bigInt(s).multiply(1e9)) + .add(bigInt(min).multiply(60e9)) + .add(bigInt(h).multiply(3600e9)); + return TimeDuration.#validateNew(totalNs, 'total'); + } + + abs() { + return new TimeDuration(this.totalNs.abs()); + } + + add(other) { + return TimeDuration.#validateNew(this.totalNs.add(other.totalNs), 'sum'); + } + + add24HourDays(days) { + if (!NumberIsInteger(days)) throw new Error('assertion failed: days is an integer'); + return TimeDuration.#validateNew(this.totalNs.add(bigInt(days).multiply(86400e9)), 'sum'); + } + + addToEpochNs(epochNs) { + return bigInt(epochNs).add(this.totalNs); + } + + cmp(other) { + return this.totalNs.compare(other.totalNs); + } + + divmod(n) { + if (n === 0) throw new Error('division by zero'); + const { quotient, remainder } = this.totalNs.divmod(n); + const q = quotient.toJSNumber(); + const r = new TimeDuration(remainder); + return { quotient: q, remainder: r }; + } + + fdiv(n) { + const { quotient, remainder } = this.divmod(n); + return quotient + remainder.totalNs.toJSNumber() / n; + } + + isZero() { + return this.totalNs.isZero(); + } + + round(increment, mode) { + if (increment === 1) return this; + let { quotient, remainder } = this.totalNs.divmod(increment); + if (remainder.equals(bigInt.zero)) return this; + const sign = remainder.lt(bigInt.zero) ? -1 : 1; + const tiebreaker = remainder.multiply(2).abs(); + const tie = tiebreaker.equals(increment); + const expandIsNearer = tiebreaker.gt(increment); + switch (mode) { + case 'ceil': + if (sign > 0) quotient = quotient.add(sign); + break; + case 'floor': + if (sign < 0) quotient = quotient.add(sign); + break; + case 'expand': + // always expand if there is a remainder + quotient = quotient.add(sign); + break; + case 'trunc': + // no change needed, because divmod is a truncation + break; + case 'halfCeil': + if (expandIsNearer || (tie && sign > 0)) quotient = quotient.add(sign); + break; + case 'halfFloor': + if (expandIsNearer || (tie && sign < 0)) quotient = quotient.add(sign); + break; + case 'halfExpand': + // "half up away from zero" + if (expandIsNearer || tie) quotient = quotient.add(sign); + break; + case 'halfTrunc': + if (expandIsNearer) quotient = quotient.add(sign); + break; + case 'halfEven': { + if (expandIsNearer || (tie && quotient.isOdd())) quotient = quotient.add(sign); + break; + } + } + return TimeDuration.#validateNew(quotient.multiply(increment), 'rounding'); + } + + sign() { + return this.cmp(new TimeDuration(0n)); + } + + subtract(other) { + return TimeDuration.#validateNew(this.totalNs.subtract(other.totalNs), 'difference'); + } +} diff --git a/polyfill/test/all.mjs b/polyfill/test/all.mjs index fa738de130..d25fd9ae04 100644 --- a/polyfill/test/all.mjs +++ b/polyfill/test/all.mjs @@ -17,6 +17,9 @@ import './ecmascript.mjs'; // Power-of-10 math import './math.mjs'; +// Internal 96-bit integer implementation, not suitable for test262 +import './timeduration.mjs'; + Promise.resolve() .then(() => { return Demitasse.report(Pretty.reporter); diff --git a/polyfill/test/math.mjs b/polyfill/test/math.mjs index bd46381267..2e86b9c555 100644 --- a/polyfill/test/math.mjs +++ b/polyfill/test/math.mjs @@ -5,9 +5,9 @@ import Pretty from '@pipobscure/demitasse-pretty'; const { reporter } = Pretty; import { strict as assert } from 'assert'; -const { deepEqual } = assert; +const { deepEqual, equal } = assert; -import { TruncatingDivModByPowerOf10 as div } from '../lib/math.mjs'; +import { TruncatingDivModByPowerOf10 as div, FMAPowerOf10 as fma } from '../lib/math.mjs'; describe('Math', () => { describe('TruncatingDivModByPowerOf10', () => { @@ -61,6 +61,47 @@ describe('Math', () => { it('-9007199254740990926258176/10**9 = -MAX_SAFE_INTEGER+1, -926258176', () => deepEqual(div(-9007199254740990926258176, 9), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -926258176 })); }); + + describe('FMAPowerOf10', () => { + it('0*10**0+0 = 0', () => equal(fma(0, 0, 0), 0)); + it('-0*10**0-0 = -0', () => equal(fma(-0, 0, -0), -0)); + it('1*10**0+0 = 1', () => equal(fma(1, 0, 0), 1)); + it('-1*10**0+0 = -1', () => equal(fma(-1, 0, 0), -1)); + it('0*10**50+1234 = 1234', () => equal(fma(0, 50, 1234), 1234)); + it('-0*10**50-1234 = -1234', () => equal(fma(-0, 50, -1234), -1234)); + it('1234*10**12+0', () => equal(fma(1234, 12, 0), 1234000000000000)); + it('-1234*10**12-0', () => equal(fma(-1234, 12, -0), -1234000000000000)); + + it('2*10**2+45 = 245', () => equal(fma(2, 2, 45), 245)); + it('2*10**3+45 = 2045', () => equal(fma(2, 3, 45), 2045)); + it('2*10**4+45 = 20045', () => equal(fma(2, 4, 45), 20045)); + it('2*10**5+45 = 200045', () => equal(fma(2, 5, 45), 200045)); + it('2*10**6+45 = 2000045', () => equal(fma(2, 6, 45), 2000045)); + + it('-2*10**2-45 = -245', () => equal(fma(-2, 2, -45), -245)); + it('-2*10**3-45 = -2045', () => equal(fma(-2, 3, -45), -2045)); + it('-2*10**4-45 = -20045', () => equal(fma(-2, 4, -45), -20045)); + it('-2*10**5-45 = -200045', () => equal(fma(-2, 5, -45), -200045)); + it('-2*10**6-45 = -2000045', () => equal(fma(-2, 6, -45), -2000045)); + + it('8692288669465520*10**9+321414345 = 8692288669465520321414345, rounded to 8692288669465520839327744', () => + equal(fma(8692288669465520, 9, 321414345), 8692288669465520839327744)); + it('-8692288669465520*10**9-321414345 = -8692288669465520321414345, rounded to -8692288669465520839327744', () => + equal(fma(-8692288669465520, 9, -321414345), -8692288669465520839327744)); + + it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () => + equal(fma(Number.MAX_SAFE_INTEGER, 3, 999), 9007199254740992000)); + it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () => + equal(fma(-Number.MAX_SAFE_INTEGER, 3, -999), -9007199254740992000)); + it('MAX_SAFE_INTEGER*10**6+999999 rounded to 9007199254740992000000', () => + equal(fma(Number.MAX_SAFE_INTEGER, 6, 999999), 9007199254740992000000)); + it('-MAX_SAFE_INTEGER*10**6-999999 rounded to -9007199254740992000000', () => + equal(fma(-Number.MAX_SAFE_INTEGER, 6, -999999), -9007199254740992000000)); + it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () => + equal(fma(Number.MAX_SAFE_INTEGER, 9, 999999999), 9007199254740992000000000)); + it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () => + equal(fma(-Number.MAX_SAFE_INTEGER, 9, -999999999), -9007199254740992000000000)); + }); }); import { normalize } from 'path'; diff --git a/polyfill/test/timeduration.mjs b/polyfill/test/timeduration.mjs new file mode 100644 index 0000000000..babcd8db82 --- /dev/null +++ b/polyfill/test/timeduration.mjs @@ -0,0 +1,507 @@ +import Demitasse from '@pipobscure/demitasse'; +const { describe, it, report } = Demitasse; + +import Pretty from '@pipobscure/demitasse-pretty'; +const { reporter } = Pretty; + +import { strict as assert, AssertionError } from 'assert'; +const { equal, throws } = assert; + +import { TimeDuration } from '../lib/timeduration.mjs'; + +function check(timeDuration, sec, subsec) { + equal(timeDuration.sec, sec); + equal(timeDuration.subsec, subsec); +} + +function checkBigInt(value, bigint) { + if (value && typeof value === 'object') { + equal(value.value, bigint); // bigInteger wrapper + } else { + equal(value, bigint); // real bigint + } +} + +function checkFloat(value, float) { + if (Math.abs(value - float) > Number.EPSILON) { + throw new AssertionError({ + message: `Expected ${value} to be within ɛ of ${float}`, + expected: float, + actual: value, + operator: 'checkFloat' + }); + } +} + +describe('Normalized time duration', () => { + describe('construction', () => { + it('basic', () => { + check(new TimeDuration(123456789_987654321n), 123456789, 987654321); + check(new TimeDuration(-987654321_123456789n), -987654321, -123456789); + }); + + it('either sign with zero in the other component', () => { + check(new TimeDuration(123n), 0, 123); + check(new TimeDuration(-123n), 0, -123); + check(new TimeDuration(123_000_000_000n), 123, 0); + check(new TimeDuration(-123_000_000_000n), -123, 0); + }); + }); + + describe('construction impossible', () => { + it('out of range', () => { + throws(() => new TimeDuration(2n ** 53n * 1_000_000_000n)); + throws(() => new TimeDuration(-(2n ** 53n * 1_000_000_000n))); + }); + + it('not an integer', () => { + throws(() => new TimeDuration(Math.PI)); + }); + }); + + describe('fromEpochNsDiff()', () => { + it('basic', () => { + check(TimeDuration.fromEpochNsDiff(1695930183_043174412n, 1695930174_412168313n), 8, 631006099); + check(TimeDuration.fromEpochNsDiff(1695930174_412168313n, 1695930183_043174412n), -8, -631006099); + }); + + it('pre-epoch', () => { + check(TimeDuration.fromEpochNsDiff(-80000_987_654_321n, -86400_123_456_789n), 6399, 135802468); + check(TimeDuration.fromEpochNsDiff(-86400_123_456_789n, -80000_987_654_321n), -6399, -135802468); + }); + + it('cross-epoch', () => { + check(TimeDuration.fromEpochNsDiff(1_000_001_000n, -2_000_002_000n), 3, 3000); + check(TimeDuration.fromEpochNsDiff(-2_000_002_000n, 1_000_001_000n), -3, -3000); + }); + + it('maximum epoch difference', () => { + const max = 86400_0000_0000_000_000_000n; + check(TimeDuration.fromEpochNsDiff(max, -max), 172800_0000_0000, 0); + check(TimeDuration.fromEpochNsDiff(-max, max), -172800_0000_0000, 0); + }); + }); + + describe('normalize()', () => { + it('basic', () => { + check(TimeDuration.normalize(1, 1, 1, 1, 1, 1), 3661, 1001001); + check(TimeDuration.normalize(-1, -1, -1, -1, -1, -1), -3661, -1001001); + }); + + it('overflow from one unit to another', () => { + check(TimeDuration.normalize(1, 61, 61, 998, 1000, 1000), 7321, 999001000); + check(TimeDuration.normalize(-1, -61, -61, -998, -1000, -1000), -7321, -999001000); + }); + + it('overflow from subseconds to seconds', () => { + check(TimeDuration.normalize(0, 0, 1, 1000, 0, 0), 2, 0); + check(TimeDuration.normalize(0, 0, -1, -1000, 0, 0), -2, 0); + }); + + it('multiple overflows from subseconds to seconds', () => { + check(TimeDuration.normalize(0, 0, 0, 1234567890, 1234567890, 1234567890), 1235803, 692457890); + check(TimeDuration.normalize(0, 0, 0, -1234567890, -1234567890, -1234567890), -1235803, -692457890); + }); + + it('fails on overflow', () => { + throws(() => TimeDuration.normalize(2501999792984, 0, 0, 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(-2501999792984, 0, 0, 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 150119987579017, 0, 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, -150119987579017, 0, 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, 2 ** 53, 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, -(2 ** 53), 0, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, Number.MAX_SAFE_INTEGER, 1000, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, -Number.MAX_SAFE_INTEGER, -1000, 0, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, Number.MAX_SAFE_INTEGER, 0, 1000000, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, -Number.MAX_SAFE_INTEGER, 0, -1000000, 0), RangeError); + throws(() => TimeDuration.normalize(0, 0, Number.MAX_SAFE_INTEGER, 0, 0, 1000000000), RangeError); + throws(() => TimeDuration.normalize(0, 0, -Number.MAX_SAFE_INTEGER, 0, 0, -1000000000), RangeError); + }); + }); + + describe('abs()', () => { + it('positive', () => { + const d = new TimeDuration(123_456_654_321n); + check(d.abs(), 123, 456_654_321); + }); + + it('negative', () => { + const d = new TimeDuration(-123_456_654_321n); + check(d.abs(), 123, 456_654_321); + }); + + it('zero', () => { + const d = new TimeDuration(0n); + check(d.abs(), 0, 0); + }); + }); + + describe('add()', () => { + it('basic', () => { + const d1 = new TimeDuration(123_456_654_321_123_456n); + const d2 = new TimeDuration(654_321_123_456_654_321n); + check(d1.add(d2), 777_777_777, 777_777_777); + }); + + it('negative', () => { + const d1 = new TimeDuration(-123_456_654_321_123_456n); + const d2 = new TimeDuration(-654_321_123_456_654_321n); + check(d1.add(d2), -777_777_777, -777_777_777); + }); + + it('signs differ', () => { + const d1 = new TimeDuration(333_333_333_333_333_333n); + const d2 = new TimeDuration(-222_222_222_222_222_222n); + check(d1.add(d2), 111_111_111, 111_111_111); + + const d3 = new TimeDuration(-333_333_333_333_333_333n); + const d4 = new TimeDuration(222_222_222_222_222_222n); + check(d3.add(d4), -111_111_111, -111_111_111); + }); + + it('cross zero', () => { + const d1 = new TimeDuration(222_222_222_222_222_222n); + const d2 = new TimeDuration(-333_333_333_333_333_333n); + check(d1.add(d2), -111_111_111, -111_111_111); + }); + + it('overflow from subseconds to seconds', () => { + const d1 = new TimeDuration(999_999_999n); + const d2 = new TimeDuration(2n); + check(d1.add(d2), 1, 1); + }); + + it('fails on overflow', () => { + const d1 = new TimeDuration(2n ** 52n * 1_000_000_000n); + throws(() => d1.add(d1), RangeError); + }); + }); + + describe('add24HourDays()', () => { + it('basic', () => { + const d = new TimeDuration(111_111_111_111_111_111n); + check(d.add24HourDays(10), 111_975_111, 111_111_111); + }); + + it('negative', () => { + const d = new TimeDuration(-111_111_111_111_111_111n); + check(d.add24HourDays(-10), -111_975_111, -111_111_111); + }); + + it('signs differ', () => { + const d1 = new TimeDuration(864000_000_000_000n); + check(d1.add24HourDays(-5), 432000, 0); + + const d2 = new TimeDuration(-864000_000_000_000n); + check(d2.add24HourDays(5), -432000, 0); + }); + + it('cross zero', () => { + const d1 = new TimeDuration(86400_000_000_000n); + check(d1.add24HourDays(-2), -86400, 0); + + const d2 = new TimeDuration(-86400_000_000_000n); + check(d2.add24HourDays(3), 172800, 0); + }); + + it('overflow from subseconds to seconds', () => { + const d1 = new TimeDuration(-86400_333_333_333n); + check(d1.add24HourDays(2), 86399, 666_666_667); + + const d2 = new TimeDuration(86400_333_333_333n); + check(d2.add24HourDays(-2), -86399, -666_666_667); + }); + + it('does not accept non-integers', () => { + const d = new TimeDuration(0n); + throws(() => d.add24HourDays(1.5), Error); + }); + + it('fails on overflow', () => { + const d = new TimeDuration(0n); + throws(() => d.add24HourDays(104249991375), RangeError); + throws(() => d.add24HourDays(-104249991375), RangeError); + }); + }); + + describe('addToEpochNs()', () => { + it('basic', () => { + const d = new TimeDuration(123_456_654_321_123_456n); + checkBigInt(d.addToEpochNs(654_321_123_456_654_321n), 777_777_777_777_777_777n); + }); + + it('negative', () => { + const d = new TimeDuration(-123_456_654_321_123_456n); + checkBigInt(d.addToEpochNs(-654_321_123_456_654_321n), -777_777_777_777_777_777n); + }); + + it('signs differ', () => { + const d1 = new TimeDuration(333_333_333_333_333_333n); + checkBigInt(d1.addToEpochNs(-222_222_222_222_222_222n), 111_111_111_111_111_111n); + + const d2 = new TimeDuration(-333_333_333_333_333_333n); + checkBigInt(d2.addToEpochNs(222_222_222_222_222_222n), -111_111_111_111_111_111n); + }); + + it('cross zero', () => { + const d = new TimeDuration(222_222_222_222_222_222n); + checkBigInt(d.addToEpochNs(-333_333_333_333_333_333n), -111_111_111_111_111_111n); + }); + + it('does not fail on overflow, epochNs overflow is checked elsewhere', () => { + const d = new TimeDuration(86400_0000_0000_000_000_000n); + checkBigInt(d.addToEpochNs(86400_0000_0000_000_000_000n), 172800_0000_0000_000_000_000n); + }); + }); + + describe('cmp()', () => { + it('equal', () => { + const d1 = new TimeDuration(123_000_000_456n); + const d2 = new TimeDuration(123_000_000_456n); + equal(d1.cmp(d2), 0); + equal(d2.cmp(d1), 0); + }); + + it('unequal', () => { + const smaller = new TimeDuration(123_000_000_456n); + const larger = new TimeDuration(654_000_000_321n); + equal(smaller.cmp(larger), -1); + equal(larger.cmp(smaller), 1); + }); + + it('cross sign', () => { + const neg = new TimeDuration(-654_000_000_321n); + const pos = new TimeDuration(123_000_000_456n); + equal(neg.cmp(pos), -1); + equal(pos.cmp(neg), 1); + }); + }); + + describe('divmod()', () => { + it('divide by 1', () => { + const d = new TimeDuration(1_234_567_890_987n); + const { quotient, remainder } = d.divmod(1); + equal(quotient, 1234567890987); + check(remainder, 0, 0); + }); + + it('divide by self', () => { + const d = new TimeDuration(1_234_567_890n); + const { quotient, remainder } = d.divmod(1_234_567_890); + equal(quotient, 1); + check(remainder, 0, 0); + }); + + it('no remainder', () => { + const d = new TimeDuration(1_234_000_000n); + const { quotient, remainder } = d.divmod(1e6); + equal(quotient, 1234); + check(remainder, 0, 0); + }); + + it('divide by -1', () => { + const d = new TimeDuration(1_234_567_890_987n); + const { quotient, remainder } = d.divmod(-1); + equal(quotient, -1_234_567_890_987); + check(remainder, 0, 0); + }); + + it('zero seconds remainder has sign of dividend', () => { + const d1 = new TimeDuration(1_234_567_890n); + let { quotient, remainder } = d1.divmod(-1e6); + equal(quotient, -1234); + check(remainder, 0, 567890); + const d2 = new TimeDuration(-1_234_567_890n); + ({ quotient, remainder } = d2.divmod(1e6)); + equal(quotient, -1234); + check(remainder, 0, -567890); + }); + + it('nonzero seconds remainder has sign of dividend', () => { + const d1 = new TimeDuration(10_234_567_890n); + let { quotient, remainder } = d1.divmod(-9e9); + equal(quotient, -1); + check(remainder, 1, 234567890); + const d2 = new TimeDuration(-10_234_567_890n); + ({ quotient, remainder } = d2.divmod(9e9)); + equal(quotient, -1); + check(remainder, -1, -234567890); + }); + + it('negative with zero seconds remainder', () => { + const d = new TimeDuration(-1_234_567_890n); + const { quotient, remainder } = d.divmod(-1e6); + equal(quotient, 1234); + check(remainder, 0, -567890); + }); + + it('negative with nonzero seconds remainder', () => { + const d = new TimeDuration(-10_234_567_890n); + const { quotient, remainder } = d.divmod(-9e9); + equal(quotient, 1); + check(remainder, -1, -234567890); + }); + + it('quotient larger than seconds', () => { + const d = TimeDuration.normalize(25 + 5 * 24, 0, 86401, 333, 666, 999); + const { quotient, remainder } = d.divmod(86400e9); + equal(quotient, 7); + check(remainder, 3601, 333666999); + }); + + it('quotient smaller than seconds', () => { + const d = new TimeDuration(90061_333666999n); + const result1 = d.divmod(1000); + equal(result1.quotient, 90061333666); + check(result1.remainder, 0, 999); + + const result2 = d.divmod(10); + equal(result2.quotient, 9006133366699); + check(result2.remainder, 0, 9); + + const result3 = d.divmod(3); + equal(result3.quotient, 30020444555666); + check(result3.remainder, 0, 1); + }); + + it('divide by 0', () => { + const d = new TimeDuration(90061_333666999n); + throws(() => d.divmod(0), Error); + }); + }); + + describe('fdiv()', () => { + it('divide by 1', () => { + const d = new TimeDuration(1_234_567_890_987n); + equal(d.fdiv(1), 1_234_567_890_987); + }); + + it('no remainder', () => { + const d = new TimeDuration(1_234_000_000n); + equal(d.fdiv(1e6), 1234); + }); + + it('divide by -1', () => { + const d = new TimeDuration(1_234_567_890_987n); + equal(d.fdiv(-1), -1_234_567_890_987); + }); + + it('opposite sign', () => { + const d1 = new TimeDuration(1_234_567_890n); + checkFloat(d1.fdiv(-1e6), -1234.56789); + const d2 = new TimeDuration(-1_234_567_890n); + checkFloat(d2.fdiv(1e6), -1234.56789); + }); + + it('negative', () => { + const d = new TimeDuration(-1_234_567_890n); + checkFloat(d.fdiv(-1e6), 1234.56789); + }); + + it('quotient larger than seconds', () => { + const d = TimeDuration.normalize(25 + 5 * 24, 0, 86401, 333, 666, 999); + checkFloat(d.fdiv(86400e9), 7.041682102627303); + }); + + it('quotient smaller than seconds', () => { + const d = new TimeDuration(90061_333666999n); + checkFloat(d.fdiv(1000), 90061333666.999); + checkFloat(d.fdiv(10), 9006133366699.9); + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + checkFloat(d.fdiv(3), 30020444555666.333); + }); + + it('divide by 0', () => { + const d = new TimeDuration(90061_333666999n); + throws(() => d.fdiv(0), Error); + }); + }); + + it('isZero()', () => { + assert(new TimeDuration(0n).isZero()); + assert(!new TimeDuration(1_000_000_000n).isZero()); + assert(!new TimeDuration(-1n).isZero()); + assert(!new TimeDuration(1_000_000_001n).isZero()); + }); + + describe('round()', () => { + it('basic', () => { + const d = new TimeDuration(1_234_567_890n); + check(d.round(1000, 'halfExpand'), 1, 234568000); + }); + + it('increment 1', () => { + const d = new TimeDuration(1_234_567_890n); + check(d.round(1, 'ceil'), 1, 234567890); + }); + + it('rounds up from subseconds to seconds', () => { + const d = new TimeDuration(1_999_999_999n); + check(d.round(1e9, 'halfExpand'), 2, 0); + }); + + describe('Rounding modes', () => { + const increment = 100; + const testValues = [-150, -100, -80, -50, -30, 0, 30, 50, 80, 100, 150]; + const expectations = { + ceil: [-100, -100, 0, 0, 0, 0, 100, 100, 100, 100, 200], + floor: [-200, -100, -100, -100, -100, 0, 0, 0, 0, 100, 100], + trunc: [-100, -100, 0, 0, 0, 0, 0, 0, 0, 100, 100], + expand: [-200, -100, -100, -100, -100, 0, 100, 100, 100, 100, 200], + halfCeil: [-100, -100, -100, 0, 0, 0, 0, 100, 100, 100, 200], + halfFloor: [-200, -100, -100, -100, 0, 0, 0, 0, 100, 100, 100], + halfTrunc: [-100, -100, -100, 0, 0, 0, 0, 0, 100, 100, 100], + halfExpand: [-200, -100, -100, -100, 0, 0, 0, 100, 100, 100, 200], + halfEven: [-200, -100, -100, 0, 0, 0, 0, 0, 100, 100, 200] + }; + for (const roundingMode of Object.keys(expectations)) { + describe(roundingMode, () => { + testValues.forEach((value, ix) => { + const expected = expectations[roundingMode][ix]; + + it(`rounds ${value} ns to ${expected} ns`, () => { + const d = new TimeDuration(BigInt(value)); + const result = d.round(increment, roundingMode); + check(result, 0, expected); + }); + + it(`rounds ${value} s to ${expected} s`, () => { + const d = new TimeDuration(BigInt(value * 1e9)); + const result = d.round(increment * 1e9, roundingMode); + check(result, expected, 0); + }); + }); + }); + } + }); + }); + + it('sign()', () => { + equal(new TimeDuration(0n).sign(), 0); + equal(new TimeDuration(-1n).sign(), -1); + equal(new TimeDuration(-1_000_000_000n).sign(), -1); + equal(new TimeDuration(1n).sign(), 1); + equal(new TimeDuration(1_000_000_000n).sign(), 1); + }); + + describe('subtract', () => { + it('basic', () => { + const d1 = new TimeDuration(321_987654321n); + const d2 = new TimeDuration(123_123456789n); + check(d1.subtract(d2), 198, 864197532); + check(d2.subtract(d1), -198, -864197532); + }); + + it('signs differ in result', () => { + const d1 = new TimeDuration(3661_001001001n); + const d2 = new TimeDuration(86400_000_000_000n); + check(d1.subtract(d2), -82738, -998998999); + check(d2.subtract(d1), 82738, 998998999); + }); + }); +}); + +import { normalize } from 'path'; +if (normalize(import.meta.url.slice(8)) === normalize(process.argv[1])) { + report(reporter).then((failed) => process.exit(failed ? 1 : 0)); +} diff --git a/spec/abstractops.html b/spec/abstractops.html index 3e917c86a4..139b7d17e3 100644 --- a/spec/abstractops.html +++ b/spec/abstractops.html @@ -1624,7 +1624,18 @@
+ A Normalized Time Duration Record is a Record value used to represent the portion of a Temporal.Duration object that deals with time units, but as a combined value. + Normalized Time Duration Records are produced by the abstract operation NormalizeTimeDuration, among others. +
+
+ Normalized Time Duration Records have the fields listed in
Field Name | +Value | +Meaning | +
---|---|---|
[[TotalNanoseconds]] | +
+ an integer in the inclusive interval from -maxTimeDuration to maxTimeDuration, where
+ |
+ + The number of nanoseconds in the duration. + | +
+ A Normalized Duration Record is a Record value used to represent the combination of a Date Duration Record with a Normalized Time Duration Record. + Such Records are used by operations that deal with both date and time portions of durations, such as RoundDuration. +
+
+ Normalized Duration Records have the fields listed in
Field Name | +Value | +Meaning | +
---|---|---|
[[Years]] | +a float64-representable integer | ++ The number of years in the duration. + | +
[[Months]] | +a float64-representable integer | ++ The number of months in the duration. + | +
[[Weeks]] | +a float64-representable integer | ++ The number of weeks in the duration. + | +
[[Days]] | +a float64-representable integer | ++ The number of days in the duration. + | +
[[NormalizedTime]] | +a Normalized Time Duration Record | ++ The time portion of the duration. + | +
Unless _ns1_ and _ns2_ are equal, _timeZoneRec_ must have looked up both `getOffsetNanosecondsFor` and `getPossibleInstantsFor`.
Unless _nanoseconds_ = 0, _timeZoneRec_ must have looked up both `getOffsetNanosecondsFor` and `getPossibleInstantsFor`.