-
Notifications
You must be signed in to change notification settings - Fork 171
Description
This issue is based on what remained in #789 after extracting rounding of non-Duration types into #827. The proposal below focuses only on changes to the Duration type itself. Please review #827 before this one.
Proposal Summary
- Extend the
Temporal.Durationtype with around()method to perform balancing and/or rounding. Most rounding behavior is already defined in Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827, so this proposal focuses on what's different when applied to the Duration type. - Add rounding options to
plusandminusto ensure that duration arithmetic can be done safely even with units that have variable lengths like months, days (if there's DST), and years (for some non-ISO calendars). - Add a
comparemethod which takes a subset of the options above.
Use Cases
Common Use Cases (should have ergonomic solutions for these)
- Normalization of existing durations, e.g. PT130M => PT2H10M
- Rounding to nearest, e.g. PT10M52S => PT11M
- Rounding up, e.g. PT10M52S => PT11M
- Truncation (rounding towards zero), e.g. PT10M52S => PT10M
- Format a duration in a normalized text format
- Calculating totals (e.g. how many total seconds in PT2H34M18S)
- Normalization that takes DST into account, e.g. convert PT1756H into days, assuming starting at midnight on a day that DST ends.
- Normalization that ignores DST and treats every day as 24 hours.
- Normalize days into months/years, e.g. P190D -> P6M12D (requires knowing the starting point and, optionally, the calendar)
- Rounding to multiples of a unit, e.g. round to nearest 15 minutes or truncate to integer number of 3-month quarters
- Non-ISO calendar-aware normalization of days into months, or months into years
Less Common Use Cases (mostly not supported)
- (not supported, but could add later via option?) Fractional remainders, e.g. "How many days are in 36 hours?" => 1.5 days.
- (not supported) "Get Remainder" / Top-end truncation, e.g. get time-only portion of a Duration, or fetch fractional seconds units.
- (partially supported) Get a date-only or time-only portion of a duration. The former is easy today via
{largestUnit: 'days'}. The latter is really just a special case of the "Get Remainder" case above. - (not supported) "Disjoint units" - some platforms store date/time values as a tuple of a "large" and "small" unit, where "large" is typically a day and "small" is usually the smallest unit of supported precision. For example, SQL Server's legacy date/time types:
(not supported) Values with the
datetimedata type are stored internally by the SQL Server 2005 Database Engine as two 4-byte integers. The first 4 bytes store the number of days before or after the base date: January 1, 1900. The base date is the system reference date. The other 4 bytes store the time of day represented as the number of 1/300-second units after midnight.The
smalldatetimedata type stores dates and times of day with less precision thandatetime. The Database Engine storessmalldatetimevalues as two 2-byte integers. The first 2 bytes store the number of days after January 1, 1900. The other 2 bytes store the number of minutes since midnight.
Open Issues
- How to handle the case where a duration is currently balanced, and then it's rounded away from zero? For example,
PT2H59M55Srounded to the nearest minute without balancing would bePT2H60M. This seems unexpected. Should the default be to balance, and users can opt out of balancing instead of opting in? Or should it always keep balanced durations balanced? EDIT 2020-10-08 Answer: Yes. Output durations will always be balanced except for the largest unit and exceptweekswill usually be zero. See below for more details. - Should Duration's
plusandminusmethods accept alargestUnitof'months'or'years'ifrelativeTois omitted? If yes, then there won't be any way to know at development time whether it will fail at runtime. If no, then failures will be found at development time, with the downside of disallowing cases which require no balancing likeP1Y2M + P2Y3M? My inclination is to disallow these units in this case, to avoid unpredictable runtime behavior. See (4.4) below. EDIT 2020-10-08 Answer: we will require a relativeTo for these cases.
Proposal
1. The Duration type will add a round() method that generally matches the behavior of round() on other types.
- 1.1 The
roundmethod will have only one optional parameter: anoptionsbag. - 1.2 Options are a superset of options used in the
differencemethods of Absolute, DateTime, etc. See Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827 for more info on options behavior and naming.- 1.2.1
largestUnitandsmallestUnitwill match the behavior defined in Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827 for thedifferencemethod of DateTime, including behavior affectingweeksas defined in Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827 section (7.7) EXCEPT...- 1.2.1.1 EDIT 2020-10-06
...the default forsmallestUnit/largestUnitoptions depends on therelativeTooption. - 1.2.1.2 EDIT 2020-10-06 If
largestUnitisundefinedor'auto'then the defaultlargestUnitwill be the largest nonzero unit in the duration forroundor the largest unit in either input forplusandminus. In other words, the default behavior is to limit durations to the largest unit that they started out with, with balancing happening underneath that unit. If the caller wants the equivalent of the now-removedoverflow: 'balance'option to expand the duration beyond its current largest unit, then the caller should explicitly setlargestUnit(andrelativeToif needed).
- 1.2.1.1 EDIT 2020-10-06
- 1.2.1.3 EDIT 2020-10-08 If
weeksis nonzero in the input ofround(or either input ofplus/minus) then weeks will be filled in the output if needed. Ifweeksis zero in the input(s) then behavior matches behavior of non-Duration types which is thatweekswill only be filled in the output if the user opts in via settinglargestUnit: 'weeks'orsmallestUnit: 'weeks'. - 1.2.2
roundingModeandroundingIncrementwill match the behavior defined in Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827, EXCEPT...- 1.2.2.1 The
'floor'and'truncrounding modes will behave differently in Duration, but they behave the same in non-Duration types where zero and negative infinity are considered the same.
- 1.2.2.1 The
- 1.2.3 As discussed in Proposal: Rounding method (and rounding for
differenceandtoString) for non-Duration types #827, the behavior of these options will align with Intl.NumberFormatV3 for decisions around option naming, which rounding modes are supported, etc.
- 1.2.1
- 1.3 Duration's
roundwill add EDIT 2020-10-08 a new option:relativeTo.two additional options:relativeToandoverflow.
2. No overflow option EDIT 2020-10-06: removed this option after discussion with @ptomato
2.1 Theoverflow: 'balance' | 'constrain'option behaves like the same option onfrom.- 2.2
If'balance'then the resulting duration will be balanced. - 2.3
If'constrain'(the default) will do no balancing. - 2.4 Duration's
overflow: 'constrain'option inplusandminusis currently used to control whether the output is fully balanced or minimally balanced. This behavior has a few problems, especially after rounding is in the mix:- 2.4.1 Rounding forces at least some balancing anyways (e.g. 23.6 hours=>1 day) and it will be hard for users to understand why balancing due to rounding is performed but balancing down from larger units is not performed.
- 2.4.2 It's not clear that
constrainmatches the most common use case for unbalanced durations which is where only the largest unit is unbalanced, e.g. 45 days, 3 hours, and 10 minutes, or @jasonwilliams's case where the BBC measures all program lengths in seconds. It's not clear that "unbalanced in the middle" durations like "45 days and 180 minutes" are a real use case that we need to support in the results ofplus,minus, andround. - 2.4.3 Even before adding rounding, the "balance constrain" algorithm was one of the hardest concepts in all of Temporal to understand and explain.
- 2.5 For these reasons, there will be no
overflowoptions onplus,minus, orround. Instead, we'll optimize for the "unbalanced largest unit" use case by automatically setting thelargestUnitdefault to prevent durations from growing into larger units by default. See (3.5.1) below for more details.- 2.5.1 Note that the
overflowoption was already inplusandminusbefore this proposal. It will be removed from those methods.
- 2.5.1 Note that the
- 2.6 This change implies simplification in the docs for the duration balancing page. A PR is coming soon for that page.
3. relativeTo option
- 3.1 This option defines the reference point used when balancing across a boundary where the number of smaller units per next-larger unit can vary. These boundaries include hours/days (because DST), days/months (month lengths vary), and months/years (non-ISO-calendar years may have 12 or 13 months).
- 3.1.1 Example 2:
P40Dbalances toP1M9DifrelativeTois2020-01-01. - 3.1.2 Example 1:
P40Dbalances toP1M11DifrelativeTois2020-02-01
- 3.1.1 Example 2:
- 3.2 In non-Duration types, the starting point is
thissorelativeToisn't needed. That's why it was omitted from Proposal: Rounding method (and rounding fordifferenceandtoString) for non-Duration types #827. - 3.3 This option can be:
- 3.3.1 A Temporal instance of DateTime or LocalDateTime type.
- 3.3.1.1 If it's a DateTime all days will be assumed to be 24 hours long.
- 3.3.1.2 However, if it's a LocalDateTime, then the time zone will be used to adjust for DST or other offset changes.
- 3.3.2 A property bag object or string that can be that can be parsed into a DateTime or LocalDateTime
- 3.3.3
undefined, which allows rounding durations withdaysor smaller units without worrying about DST or starting points. This is the default behavior if no options bag is provided.- 3.3.3.1 If
undefinedis used, then all days will be assumed to be 24 hours long. - 3.3.3.2 If
undefinedis used, then a RangeError will be thrown if the duration has a non-zeromonthsoryears. TS typings should enforce this restriction. - 3.3.3.3 EDIT 2020-10-06 If
undefinedis used, then a RangeError will be thrown if largestUnit in the input is'weeks'or larger.
- 3.3.3.1 If
- 3.3.4 Any other types will result in a RangeError
- 3.3.1 A Temporal instance of DateTime or LocalDateTime type.
- 3.4 If the value is an object property bag or string, then the value will be handled as follows:
- 3.4.1 If the value is valid for use with
LocalDateTime.from, then construct a new LocalDateTime instance from this value. - 3.4.2 Otherwise, if the value is valid for use with
DateTime.from, then construct a new DateTime instance from this value. - 3.4.4 Otherwise throw a RangeError
- 3.4.1 If the value is valid for use with
3.5 IfrelativeToIS NOTundefined, then the defaults for forlargestUnitandsmallestUnitwill be the full range of units:{largestUnit: 'years', smallestUnit: 'nanoseconds'}.3.5.1 However, ifEDIT 2020-10-06: defaults are no longer controlled by relativeTo. See (1.2.1.2).relativeToISundefinedthen defaults will be{largestUnit: 'hours', smallestUnit: 'nanoseconds'}. These defaults avoid variable-length units like months or days in a DST time zone.
4. Usage in plus and minus
- 4.1 Currently, duration arithmetic is limited to weeks or smaller units, and days are always assumed to be 24 hours which won't be accurate in a DST context. To resolve these limitations, one option would be to remove Duration arithmetic methods. Developers could use other types instead. For example,
dur1.minus(dur2)could be replaced byldt.plus(dur1).minus(dur2).difference(ldt). - 4.2 Another option, which is proposed below, is to add the same options available on
round(includingrelativeTo) toplusandminustoo. This would solve the DST issue and would also allow duration arithmetic to include all units including months and years. - 4.3 The
relativeTooption is applied tothis, not toother. This allows chaining multiple operations with the samerelativeTo, e.g.dur1.plus(dur2, {relativeTo: ldt}).plus(dur3, {relativeTo: ldt}). - 4.4 If
relativeTois omitted, thenlargestUnitmust be'weeks'or smaller, or a RangeError will be thrown - 4.5 EDIT 2020-10-06 If
relativeToisundefined, then:- 4.5.1 The default
largestUnitwill be the largest non-zero unit in either input. (see 3.5.1 for more details) - 4.5.2 Throw a RangeError if either input has a nonzero value in
'weeks'or larger units OR iflargestUnitisweeksor larger. This matches behavior ofround(see 3.3.3.2 and 3.3.3.3) andcompare(see 5.2).
- 4.5.1 The default
5. Usage with compare
- 5.1 The
comparealgorithm is simple: round down nanoseconds and compare the results. In order to compare across unbalanced durations and/or to adjust for DST,comparewill accept arelativeTooption which will behave identically torelativeToin other methods above. - 5.2 If
relativeTois undefined, thencomparewill throw if either duration has a non-zero unit inmonthsoryears
6. No equals
- 6.1 We decided not to offer
equalsbecause equality is ambiguous for unbalanced durations (e.g.PT3600SvsPT60M) so we opted to simply supportcomparewhich will force users to think a little bit about what the comparison will mean.
7. We'll add an 'auto' value for largestUnit in Duration and non-Duration difference methods where it's used. 'auto', like undefined, will select the default unit. EDIT 2020-10-06: added this section
- 7.1 On Duration methods,
'auto'means "the largest nonzero unit in the input(s)" - 7.2 On
differenceof non-Duration types'auto'means to pick the default unit of the type:- 7.2.1 Instant:
'seconds' - 7.2.2 Date / DateTime -
'days' - 7.2.3 ZonedDateTime -
'hours' - 7.2.4 Time -
'hours' - 7.2.5 YearMonth -
'years'
- 7.2.1 Instant:
- 7.3 If the default chosen above is smaller than the user-provided
smallestUnitthen setlargestUnit = smallestUnit.
8. EDIT 2020-10-06: added this section round will require either smallestUnit or largestUnit to be explicitly provided and not undefined. This is because without one of these two options, the rounding operation would be a no-op. This is analogous to how we require smallestUnit on the round methods of non-duration types. A RangeError should be thrown if neither is provided.