-
Notifications
You must be signed in to change notification settings - Fork 174
Description
This proposal is half of the rounding problem focused on non-Duration types. The other half (rounding methods on the Duration type itself) is in #856. Originally both halves were contained in #789 but we split into two proposals because the Duration parts took longer to reach consensus.
Summary
This proposal:
- Adds a
round()method to every non-Duration Temporal type that supports math operations. - Adds rounding options to the
differencemethod of the same types, in order to support rounding of the results ofdifference. - Defines rounding options that will be used by the Duration type too (see Proposal: rounding and balancing for Duration type (replaces #789) #856 that depends on this proposal).
Open Issues to Resolve
- Should we support rounding of months and years units? (see discussion in comments) Note that weeks and era are already not supported (see -- only months/years are in question. NO
- What should be the default rounding mode? Half up? Half even? HALF UP
- Should
with()have rounding options? (see discussion in comments) NO - Should
roundingIncrementrequire an exact divisor of the next-largest unit? YES, EXCEPT: ABSOLUTE TYPES HAVE SPECIAL RULES, and LOCALDATETIME FOR HOURS USES CLOCK HOURS, NOT ABSOLUTE HOURS. - Related: how should
roundingIncrementhandle cases where the next-largest unit has a variable size? (e.g. month length in days, year length in months for non-ISO calendars) N/A - Is it OK that
toStringincludes rounding options? MAYBE - MOVED to Why do toString methods truncate? #329 - Is it OK that
toStringcontrols decimal precision of sub-second units (and whether to show seconds at all) based onsmallestUnit? MAYBE - MOVED - Should
toLocaleStringaccept the same rounding options astoString()? N/A - Is
differenceready? Or does it need to wait for the rounding in Duration proposal (Proposal: rounding method & total method for Duration type #789) to be fully resolved? FWIW, I'd prefer not to block it if we don't have to. YES - Because the default (see below) for
smallestUnitis the smallest unit that the type supports, callinground()with no parameters will return a clone of the same instance but won't change the value. Is this OK? NO.. IT'S REQUIRED. -
differencecould theoretically choose eitherthisorotheras the reference point for rounding. I'm assuming that it should usethis. YES. HAS CONSENSUS.
Sample Use Cases
round(options)
- Round the current time to the nearest 5 minutes
Temporal.now.time().round({ smallestUnit: 'minute', roundingIncrement: 5 })What's the closest Saturday to this Date?- We removed week-related rounding from the scope of this proposal. The workaround is to calculate usingdayOfWeekdirectly.- Remove the seconds and smaller units from this Time
time.round({ smallestUnit: 'minute', roundingMode: 'trunc' })- Round this time to the nearest second
time.round({ smallestUnit: 'second' })What was the first day of this calendar quarter?- We chose to remove rounding for weeks and larger units due to their complexity (especially in non-ISO calendars) and their relatively easy workarounds. The workaround would be to do the 3-month rounding math directly using the property values andwithand/orfrom.- How many billable hours did a lawyer spend on a phone call, rounded up to the nearest 5 minutes?
endAbsolute.difference(startAbsolute, { smallestUnit: 'minutes', roundingIncrement: 5, roundingMode: 'ceil' })- How many days was a rental car rented for, rounded up to the nearest hour?
endLocalDateTime.difference(startLocalDateTime, { smallestUnit: 'hour', roundingMode: 'ceil' })Details
1. round() for non-Duration types
- 1.1 Rounding is focused on time. The following types that include time will get a
roundmethod:DateTime,Absolute,Time, andLocalDateTime.- 1.1.1 Because we'll only be rounding times, not dates, the Date, YearMonth, and MonthDay types won't get a
round()method.
- 1.1.1 Because we'll only be rounding times, not dates, the Date, YearMonth, and MonthDay types won't get a
- 1.2 Duration will also likely get a
roundmethod, but that's out of scope for this proposal. See Proposal: rounding method & total method for Duration type #789. - 1.3
round()will accept only one parameter,options, which is a property bag of options that control rounding. Each option is described below.- 1.3.1
optionsis a required parameter because one of the options,smallestUnit(see below) cannot be omitted.
- 1.3.1
- 1.4 We should use the same rounding modes and option names that will be used by Decimal and by Intl.NumberFormat (What rounding modes to include? proposal-intl-numberformat-v3#7). @sffc is driving Intl.NumberFormat so alignment should be do-able. Per @sffc, we don't need to wait for either proposal-- the first of the three out of the gate will define naming used by the others.
2. smallestUnit option
- 2.1 The
smallestUnitproperty defines where rounding happens. All smaller time units will be zero in the result. - 2.2 There is no default. A RangeError will be thrown if
smallestUnitis omitted. 2.3 IfsmallestUnitis larger thanlargestUnit, then a RangeError should be thrown.- 2.4
smallestUnitshould accept both plural and non-plural unit names because non-Duration types' fields aren't pluralized. This may be a reason to accept either plural or non-plural names in other places too. See Naming of Temporal.Duration fields #325. - 2.5 Even though both plural and non-plural units will be accepted, the docs should show singular units in samples because non-Duration field names are singular.
- 2.7
eraor any non-ISO calendar custom fields will throw aRangeError, because it's not possible to round those units without significant changes to Temporal.Calendar. Also that there's likely minimal demand for rounding these units. - 2.8 The Absolute type doesn't have a time zone so we'll limit its
smallestUnitto'minutes'or smaller to support the primary Absolute rounding use case which is to handle underlying storage that has limited precision at the low end. - 2.9 For Time,
smallestUnitcan be any valid unit for this type:'hours'or smaller. - 2.10 For DateTime and LocalDateTime,
smallestUnitmust be'days'or smaller. We decided to exclude larger units because the rounding behavior of weeks and months is confusingly calendar-dependent.
5. roundingMode option
- 5.1 This option defines how the remainder is handled: e.g. round up, round down, round to nearest, etc. There are a surprisingly large number of possible rounding modes (e.g. round down vs. round towards zero for negative numbers, how to handle 0.5 remainders, etc.).
- 5.1.1 There are two other potential users of rounding modes in JS: the Decimal proposal and Intl.NumberFormat V3. Temporal will align with these proposals for naming and modes available, so that all of JS would have consistent rounding syntax.
- 5.1.2 Per @sffc, NumberFormat V3 is the furthest ahead of all three proposals so we'll tentatively plan for the naming and rounding-mode decisions to be made there first, although if Temporal makes them first them @sffc will help to align Intl.
- 5.2 Possible option names include (pending alignment with those other proposals): "roundingMode", "rounding", "roundingType", "roundingMethod". This proposal uses "roundingMode" but the name may change.
- 5.3 The following rounding modes will be supported. Initial naming is aligned with the
Mathobject, although per above the names may change as we align with Intl.- 5.3.1
'ceil'- round up towards +∞ - 5.3.2
'floor'- round down towards -∞ - 5.4.3
'trunc'- round towards zero. Note that "zero" implies "the big bang", not UNIX epoch or 1 B.C. This means that'floor'and'trunc'act the same for non-Duration types because zero and -∞ are the same from Temporal's point of view. Note that they will not act differently when used in the Duration type (see Proposal: rounding and balancing for Duration type (replaces #789) #856) because durations can be negative. - 5.4.4
'nearest'- round to nearest integer, with 0.5 rounded away from zero. This is named differently fromround, to differentiate fromMath.roundthat rounds 0.5 remainders towards +∞ which is often unexpected for negative numbers per MDN:
- 5.3.1
If the fractional portion is exactly 0.5, the argument is rounded to the next integer in the direction of +∞. Note that this differs from many languages'
round()functions, which often round this case to the next integer away from zero, instead giving a different result in the case of negative numbers with a fractional part of exactly 0.5.
- 5.4 The default rounding mode is
'nearest'which means "half away from zero" because it's least surprising for a method calledround. Note that the name of this option may change depending on what is decided in What rounding modes to include? proposal-intl-numberformat-v3#7 (comment). @sffc owns coordinating with Intl and finalizing both rounding mode naming and which rounding modes are offered in both places. - 5.6 It's possible that Intl may choose to add additional rounding modes, and IMHO we should adopt those if they're added. However, it's a safe assumption that the 4 modes above will be included.
6. roundingIncrement option
- 6.1 The
roundingIncrementallows rounding with a coarser granularity than one unit, e.g. "nearest 15 minutes", "nearest second". - 6.2 The default rounding increment is 1
- 6.3 This option will be limited to exact divisors of the next-largest unit. For example, if
smallestUnitisminute, thenroundingIncrementcould be either 1 (default), 2, 3, 4, 5, 6, 10, 12, 15, 20, or 30. Others must throw a RangeError. - 6.4 There's been discussion of wanting to enable future non-ISO calendars that can introduce custom time measurement, which might add the same problem about variable number of minutes/seconds/etc. @sffc believes that this could be resolved, either by using increments relative to the calendar system, or we could reset the increment at each boundary of the next-largest unit. So we won't worry about non-ISO calendars relative to this feature.
- 6.5 Regardless of rounding mode or increment, the rounding increment will be measured in ascending order starting from zero of
smallestUnit. - 6.6 When applied to LocalDateTime,
roundingIncrementwill use clock time, not absolute time, to determine the result. If a DST transition is inside the increment, the absolute length of the increment can be longer or shorter (by the length of the DST transition) than the user-specifiedroundingIncrementvalue. Also, if the result's clock time is ambiguous, then it's disambiguated using the default ('compatible') disambiguation option. For example:
ldt = Temporal.LocalDateTime.from('2020-03-08T03:30-07:00[America/Los_Angeles]');
ldt.round({ smallestUnit: 'hour', roundingIncrement: 3, roundingMode: 'ceil' });
// => 2020-03-08T06:00-07:00[America/Los_Angeles]
// (result is 06:00 even though it's only 5 hours past midnight)
ldt.round({ smallestUnit: 'hour', roundingIncrement: 2, roundingMode: 'trunc' });
// => 2020-03-08T03:00-07:00[America/Los_Angeles]
// (result is 03:00 because 02:00-08:00 is disambiguated to 03:00-07:00)- 6.7 Absolute has a few differences from behavior of other types:
- 6.7.1 As noted above, rounding on Absolute is limited to
minutesor smaller units. If developers want to round an Absolute to an hour, they can just use{roundingIncrement: 60}. - 6.7.2 The reference point for rounding increments for Absolute is the UNIX epoch.
- 6.7.3 All Absolute rounding increments must divide evenly into one 24-hour day (86400 seconds). Unlike the increments in the other types'
round()methods, Absolute rounding increments can also be equal (not less than like other types) to 86400 seconds.
- 6.7.1 As noted above, rounding on Absolute is limited to
7. Rounding options in non-Duration difference methods
- 7.1 Currently,
differencemethods throughout Temporal accept alargestUnitoption. - 7.2 With this proposal, types that get a
roundmethod will also enhance thedifferencemethod to also accept the same options thatroundaccepts:smallestUnit,roundingMode, androundingIncrement. - 7.3 These options will behave the same as in
round, with the only exceptions noted below.- 7.3.1
smallestUnitdefaults tonanosecondsand is optional.
- 7.3.1
- 7.4 If
largestUnitis not specified butsmallestUnitis larger than its default value, thenlargestUnitshould be adjusted to be the same assmallestUnit.- 7.4.1 However, if
smallestUnitis larger than an explicitly specifiedlargestUnit, then aRangeErrorshould be thrown.
- 7.4.1 However, if
- 7.5 Unit names are plural e.g.
'days'in the docs, but both plural and singular units should be accepted. - 7.6 All units supported by the existing
largestUnitoption will be supported insmallestUnittoo. - 7.7 Weeks overlap with days and months in a duration. While there may be some cases where users might want weeks together with months and/or days (e.g. "3 months, 2 weeks, and 4 days") that's an uncommon case relative to the "3 months and 18 days" result that is likely expected. We'll handle these cases by setting weeks to zero when there's potential ambiguity between weeks vs. days/months. Specifically: if
largestUnitis'month'or'year'andsmallestUnitis'day'or smaller, then weeks will always be zero and only days and/or months will be included in the result. Edge cases that are NOT ambiguous are noted below.- 7.7.1 If
smallestUnitis'week', then there is no ambiguity and any remainder will go intoweek. - 7.7.2 If
largestUnitis'day', then there is no ambiguity because weeks are excluded andweekwill be zero. - 7.7.3 In the degenerate case of
{ largestUnit: 'week', smallestUnit: 'day' }then there is also no ambiguity so both fields will be populated.
- 7.7.1 If