diff --git a/polyfill/lib/duration.mjs b/polyfill/lib/duration.mjs index 5cc43191d..bd578d695 100644 --- a/polyfill/lib/duration.mjs +++ b/polyfill/lib/duration.mjs @@ -10,7 +10,6 @@ import { // class static functions and methods MathAbs, - NumberIsNaN, ObjectCreate, ObjectDefineProperty, @@ -236,7 +235,7 @@ export class Duration { const calendar = GetSlot(zonedRelativeTo, CALENDAR); const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS); const targetEpochNs = ES.AddZonedDateTime(relativeEpochNs, timeZone, calendar, duration); - ({ duration } = ES.DifferenceZonedDateTimeWithRounding( + duration = ES.DifferenceZonedDateTimeWithRounding( relativeEpochNs, targetEpochNs, timeZone, @@ -245,7 +244,7 @@ export class Duration { roundingIncrement, smallestUnit, roundingMode - )); + ); if (ES.TemporalUnitCategory(largestUnit) === 'date') largestUnit = 'hour'; return ES.UnnormalizeDuration(duration, largestUnit); } @@ -260,7 +259,7 @@ export class Duration { const dateDuration = ES.AdjustDateDurationRecord(duration.date, targetTime.deltaDays); const targetDate = ES.CalendarDateAdd(calendar, isoRelativeToDate, dateDuration, 'constrain'); - ({ duration } = ES.DifferencePlainDateTimeWithRounding( + duration = ES.DifferencePlainDateTimeWithRounding( isoRelativeToDate.year, isoRelativeToDate.month, isoRelativeToDate.day, @@ -284,7 +283,7 @@ export class Duration { roundingIncrement, smallestUnit, roundingMode - )); + ); return ES.UnnormalizeDuration(duration, largestUnit); } @@ -297,7 +296,7 @@ export class Duration { } assert(!ES.IsCalendarUnit(smallestUnit), 'smallestUnit was larger than largestUnit'); let duration = ES.NormalizeDurationWith24HourDays(this); - ({ duration } = ES.RoundTimeDuration(duration, roundingIncrement, smallestUnit, roundingMode)); + duration = ES.RoundTimeDuration(duration, roundingIncrement, smallestUnit, roundingMode); return ES.UnnormalizeDuration(duration, largestUnit); } total(totalOf) { @@ -320,18 +319,7 @@ export class Duration { const calendar = GetSlot(zonedRelativeTo, CALENDAR); const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS); const targetEpochNs = ES.AddZonedDateTime(relativeEpochNs, timeZone, calendar, duration); - const { total } = ES.DifferenceZonedDateTimeWithRounding( - relativeEpochNs, - targetEpochNs, - timeZone, - calendar, - unit, - 1, - unit, - 'trunc' - ); - assert(!NumberIsNaN(total), 'total went through NudgeToZonedTime code path'); - return total; + return ES.DifferenceZonedDateTimeWithTotal(relativeEpochNs, targetEpochNs, timeZone, calendar, unit); } if (plainRelativeTo) { @@ -344,7 +332,7 @@ export class Duration { const dateDuration = ES.AdjustDateDurationRecord(duration.date, targetTime.deltaDays); const targetDate = ES.CalendarDateAdd(calendar, isoRelativeToDate, dateDuration, 'constrain'); - const { total } = ES.DifferencePlainDateTimeWithRounding( + return ES.DifferencePlainDateTimeWithTotal( isoRelativeToDate.year, isoRelativeToDate.month, isoRelativeToDate.day, @@ -364,13 +352,8 @@ export class Duration { targetTime.microsecond, targetTime.nanosecond, calendar, - unit, - 1, - unit, - 'trunc' + unit ); - assert(!NumberIsNaN(total), 'total went through NudgeToZonedTime code path'); - return total; } // No reference date to calculate difference relative to @@ -382,8 +365,7 @@ export class Duration { throw new RangeErrorCtor(`a starting point is required for ${unit}s total`); } const duration = ES.NormalizeDurationWith24HourDays(this); - const { total } = ES.RoundTimeDuration(duration, 1, unit, 'trunc'); - return total; + return ES.TotalTimeDuration(duration.norm, unit); } toString(options = undefined) { if (!ES.IsTemporalDuration(this)) throw new TypeErrorCtor('invalid receiver'); @@ -400,7 +382,7 @@ export class Duration { const largestUnit = ES.DefaultTemporalLargestUnit(this); let duration = ES.NormalizeDuration(this); - ({ duration } = ES.RoundTimeDuration(duration, increment, unit, roundingMode)); + duration = ES.RoundTimeDuration(duration, increment, unit, roundingMode); const roundedDuration = ES.UnnormalizeDuration(duration, ES.LargerOfTwoTemporalUnits(largestUnit, 'second')); return ES.TemporalDurationToString(roundedDuration, precision); } diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 9f86935c3..4a040694e 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -3457,12 +3457,12 @@ function NudgeToCalendarUnit(sign, duration, destEpochNs, dateTime, timeZone, ca const didExpandCalendarUnit = roundedUnit === MathAbs(r2); duration = { date: didExpandCalendarUnit ? endDuration : startDuration, norm: TimeDuration.ZERO }; - return { + const nudgeResult = { duration, - total, nudgedEpochNs: didExpandCalendarUnit ? endEpochNs : startEpochNs, didExpandCalendarUnit }; + return { nudgeResult, total }; } // Attempts rounding of time units within a time zone's day, but if the rounding @@ -3513,7 +3513,6 @@ function NudgeToZonedTime(sign, duration, dateTime, timeZone, calendar, incremen const resultDuration = CombineDateAndNormalizedTimeDuration(dateDuration, roundedNorm); return { duration: resultDuration, - total: NaN, // Not computed in this path, so we assert that it is not NaN later on nudgedEpochNs, didExpandCalendarUnit: didRoundBeyondDay }; @@ -3526,7 +3525,6 @@ function NudgeToDayOrTime(duration, destEpochNs, largestUnit, increment, smalles const norm = duration.norm.add24HourDays(duration.date.days); // Convert to nanoseconds and round const unitLength = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [smallestUnit]); - const total = norm.fdiv(unitLength); const roundedNorm = norm.round(increment * unitLength, roundingMode); const diffNorm = roundedNorm.subtract(norm); @@ -3547,7 +3545,6 @@ function NudgeToDayOrTime(duration, destEpochNs, largestUnit, increment, smalles const dateDuration = AdjustDateDurationRecord(duration.date, days); return { duration: { date: dateDuration, norm: remainder }, - total, nudgedEpochNs, didExpandCalendarUnit: didExpandDays }; @@ -3667,7 +3664,7 @@ function RoundRelativeDuration( let nudgeResult; if (irregularLengthUnit) { // Rounding an irregular-length unit? Use epoch-nanosecond-bounding technique - nudgeResult = NudgeToCalendarUnit( + ({ nudgeResult } = NudgeToCalendarUnit( sign, duration, destEpochNs, @@ -3677,11 +3674,9 @@ function RoundRelativeDuration( increment, smallestUnit, roundingMode - ); + )); } else if (timeZone) { - // Special-case for rounding time units within a zoned day. total() never - // takes this path because largestUnit is then also a time unit, so - // DifferenceZonedDateTimeWithRounding uses Instant math + // Special-case for rounding time units within a zoned day nudgeResult = NudgeToZonedTime(sign, duration, dateTime, timeZone, calendar, increment, smallestUnit, roundingMode); } else { // Rounding uniform-length days/hours/minutes/etc units. Simple nanosecond @@ -3706,7 +3701,25 @@ function RoundRelativeDuration( ); } - return { duration, total: nudgeResult.total }; + return duration; +} + +function TotalRelativeDuration(duration, destEpochNs, dateTime, timeZone, calendar, unit) { + // The duration must already be balanced. This should be achieved by calling + // one of the non-rounding since/until internal methods prior. It's okay to + // have a bottom-heavy weeks because weeks don't bubble-up into months. It's + // okay to have >24 hour day assuming the final day of relativeTo+duration has + // >24 hours in its timezone. (should automatically end up like this if using + // non-rounding since/until internal methods prior) + if (IsCalendarUnit(unit) || (timeZone && unit === 'day')) { + // Rounding an irregular-length unit? Use epoch-nanosecond-bounding technique + const sign = NormalizedDurationSign(duration) < 0 ? -1 : 1; + return NudgeToCalendarUnit(sign, duration, destEpochNs, dateTime, timeZone, calendar, 1, unit, 'trunc').total; + } + // Rounding uniform-length days/hours/minutes/etc units. Simple nanosecond + // math. years/months/weeks unchanged + const norm = duration.norm.add24HourDays(duration.date.days); + return TotalTimeDuration(norm, unit); } export function DifferencePlainDateTimeWithRounding( @@ -3735,10 +3748,7 @@ export function DifferencePlainDateTimeWithRounding( roundingMode ) { if (CompareISODateTime(y1, mon1, d1, h1, min1, s1, ms1, µs1, ns1, y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2) == 0) { - return { - duration: { date: ZeroDateDuration(), norm: TimeDuration.ZERO }, - total: 0 - }; + return { date: ZeroDateDuration(), norm: TimeDuration.ZERO }; } const duration = DifferenceISODateTime( @@ -3764,9 +3774,7 @@ export function DifferencePlainDateTimeWithRounding( largestUnit ); - if (smallestUnit === 'nanosecond' && roundingIncrement === 1) { - return { duration, total: duration.norm.totalNs.toJSNumber() }; - } + if (smallestUnit === 'nanosecond' && roundingIncrement === 1) return duration; const dateTime = { year: y1, @@ -3793,6 +3801,72 @@ export function DifferencePlainDateTimeWithRounding( ); } +export function DifferencePlainDateTimeWithTotal( + y1, + mon1, + d1, + h1, + min1, + s1, + ms1, + µs1, + ns1, + y2, + mon2, + d2, + h2, + min2, + s2, + ms2, + µs2, + ns2, + calendar, + unit +) { + if (CompareISODateTime(y1, mon1, d1, h1, min1, s1, ms1, µs1, ns1, y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2) == 0) { + return 0; + } + + const duration = DifferenceISODateTime( + y1, + mon1, + d1, + h1, + min1, + s1, + ms1, + µs1, + ns1, + y2, + mon2, + d2, + h2, + min2, + s2, + ms2, + µs2, + ns2, + calendar, + unit + ); + + if (unit === 'nanosecond') return duration.norm.totalNs.toJSNumber(); + + const dateTime = { + year: y1, + month: mon1, + day: d1, + hour: h1, + minute: min1, + second: s1, + millisecond: ms1, + microsecond: µs1, + nanosecond: ns1 + }; + const destEpochNs = GetUTCEpochNanoseconds(y2, mon2, d2, h2, min2, s2, ms2, µs2, ns2); + return TotalRelativeDuration(duration, destEpochNs, dateTime, null, calendar, unit); +} + export function DifferenceZonedDateTimeWithRounding( ns1, ns2, @@ -3810,9 +3884,7 @@ export function DifferenceZonedDateTimeWithRounding( const duration = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, largestUnit); - if (smallestUnit === 'nanosecond' && roundingIncrement === 1) { - return { duration, total: duration.norm.totalNs.toJSNumber() }; - } + if (smallestUnit === 'nanosecond' && roundingIncrement === 1) return duration; const dateTime = GetISODateTimeFor(timeZone, ns1); return RoundRelativeDuration( @@ -3828,6 +3900,17 @@ export function DifferenceZonedDateTimeWithRounding( ); } +export function DifferenceZonedDateTimeWithTotal(ns1, ns2, timeZone, calendar, unit) { + if (TemporalUnitCategory(unit) === 'time') { + // The user is only asking for a time difference, so return difference of instants. + return TotalTimeDuration(TimeDuration.fromEpochNsDiff(ns2, ns1), unit); + } + + const duration = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, unit); + const dateTime = GetISODateTimeFor(timeZone, ns1); + return TotalRelativeDuration(duration, ns2, dateTime, timeZone, calendar, unit); +} + export function GetDifferenceSettings(op, options, group, disallowed, fallbackSmallest, smallestLargestDefaultUnit) { const ALLOWED_UNITS = Call(ArrayPrototypeReduce, TEMPORAL_UNITS, [ (allowed, unitInfo) => { @@ -3888,7 +3971,7 @@ export function DifferenceTemporalInstant(operation, instant, other, options) { const onens = GetSlot(instant, EPOCHNANOSECONDS); const twons = GetSlot(other, EPOCHNANOSECONDS); - const { duration } = DifferenceInstant( + const duration = DifferenceInstant( onens, twons, settings.roundingIncrement, @@ -3947,7 +4030,7 @@ export function DifferenceTemporalPlainDate(operation, plainDate, other, options settings.roundingIncrement, settings.smallestUnit, settings.roundingMode - ).duration; + ); } let result = UnnormalizeDuration(duration, 'day'); @@ -3981,7 +4064,7 @@ export function DifferenceTemporalPlainDateTime(operation, plainDateTime, other, return new Duration(); } - const { duration } = DifferencePlainDateTimeWithRounding( + const duration = DifferencePlainDateTimeWithRounding( GetSlot(plainDateTime, ISO_YEAR), GetSlot(plainDateTime, ISO_MONTH), GetSlot(plainDateTime, ISO_DAY), @@ -4034,12 +4117,7 @@ export function DifferenceTemporalPlainTime(operation, plainTime, other, options ); let duration = { date: ZeroDateDuration(), norm }; if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) { - ({ duration } = RoundTimeDuration( - duration, - settings.roundingIncrement, - settings.smallestUnit, - settings.roundingMode - )); + duration = RoundTimeDuration(duration, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode); } let result = UnnormalizeDuration(duration, settings.largestUnit); @@ -4099,7 +4177,7 @@ export function DifferenceTemporalPlainYearMonth(operation, yearMonth, other, op settings.roundingIncrement, settings.smallestUnit, settings.roundingMode - ).duration; + ); } let result = UnnormalizeDuration(duration, 'day'); @@ -4126,7 +4204,7 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, let result; if (TemporalUnitCategory(settings.largestUnit) !== 'date') { // The user is only asking for a time difference, so return difference of instants. - const { duration } = DifferenceInstant( + const duration = DifferenceInstant( ns1, ns2, settings.roundingIncrement, @@ -4145,7 +4223,7 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other, if (ns1.equals(ns2)) return new Duration(); - const { duration } = DifferenceZonedDateTimeWithRounding( + const duration = DifferenceZonedDateTimeWithRounding( ns1, ns2, timeZone, @@ -4463,24 +4541,23 @@ export function RoundTime(hour, minute, second, millisecond, microsecond, nanose export function RoundTimeDuration(duration, increment, unit, roundingMode) { // unit must not be a calendar unit - - let days = duration.date.days; - let norm = duration.norm; - let total; if (unit === 'day') { // First convert time units up to days - const { quotient, remainder } = norm.divmod(DAY_NANOS); - days += quotient; - total = days + remainder.fdiv(DAY_NANOS); - days = RoundNumberToIncrement(total, increment, roundingMode); - norm = TimeDuration.ZERO; - } else { - const divisor = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit]); - total = norm.fdiv(divisor); - norm = norm.round(divisor * increment, roundingMode); + const { quotient, remainder } = duration.norm.divmod(DAY_NANOS); + let days = duration.date.days + quotient + remainder.fdiv(DAY_NANOS); + days = RoundNumberToIncrement(days, increment, roundingMode); + const dateDuration = AdjustDateDurationRecord(duration.date, days); + return CombineDateAndNormalizedTimeDuration(dateDuration, TimeDuration.ZERO); } - const dateDuration = AdjustDateDurationRecord(duration.date, days); - return { duration: CombineDateAndNormalizedTimeDuration(dateDuration, norm), total }; + + const divisor = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit]); + const norm = duration.norm.round(divisor * increment, roundingMode); + return CombineDateAndNormalizedTimeDuration(duration.date, norm); +} + +export function TotalTimeDuration(norm, unit) { + const divisor = Call(MapPrototypeGet, NS_PER_TIME_UNIT, [unit]); + return norm.fdiv(divisor); } export function CompareISODate(y1, m1, d1, y2, m2, d2) { diff --git a/spec/duration.html b/spec/duration.html index 9acec68c8..c277c42b1 100644 --- a/spec/duration.html +++ b/spec/duration.html @@ -432,9 +432,9 @@