-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Rounding method (and rounding for difference
and toString
) for non-Duration types
#827
Comments
1.1: Within the context of time scale units (Date, DateTime, Time, LocalDateTime, YearMonth, MonthDay), I don't find a round method to be compelling with Using 2.6: Not relevant if we drop week/month/year rounding from time scale units, but on 6.2: A legitimate concern, but not relevant if we drop week/month/year rounding from time scale units. 6.3: If we do add custom time measurements, we can figure out how to handle this case. I don't think it will be that painful. I think the usefulness of rounding increments outweighs the possibility of custom time measurements. 6.4: I've convinced myself that people will want rounding increments, and that we should support them in V1. 7: I think rounding on the 8: I don't see this to be necessary, because you can just do |
UPDATE: I updated the response below to point to specific sections of the proposal that were updated based on @sffc's feedback. Also added a few more details to my response below.
BTW, my intent was that
I think that Proposal is now updated (2.8) to clarify that
I don't feel strongly about this one way or the other. I've already assumed that If we don't support those units fo rounding, then we should probably document a workaround, for example:
Do these algorithms work for all calendars? And if so, does that make you want to retain months and years as units that can be rounded? BTW, with cross-calendar (if one is ISO)
Because week-start-day varies so much across countries (even those that use the gregorian calendar), I think it'd be more confusing than valuable to do week rounding. IMHO, the potential bug rate seems too high relative to value. If we had the ability in Intl to fetch a locale's week-start date, and if we required callers of
AFAIK, the main use cases are to control precision on fractional seconds and to control whether seconds are emitted at all.
Yeah, I was thinking about toLocaleString but forgot to add that as an open issue in the proposal. Thanks for catching this. My initial take is this:
If that distinction makes sense, then controlling which units are shown in the output seems reasonable for
Could I added a new section (9) in the proposal as a placeholder to track
OK! I updated (6) to include this feature instead of postponing it. Out of curiosity, how do you think we could support variable-time calendars with increments? I'd like to include this explanation in the proposal.
AFAIK, the obstacle on the Duration side is I guess one thing that theoretically could be part of Are there any other rounding options that would be needed for |
This all seems pretty sensible, but I have a couple of queries.
As a layperson using the Temporal API, I would be highly confused by a default rounding method that rounds 0.5 values in different directions depending on the non-fractional value. I have a fairly limited understanding of the "half-to-even" and "half-to-odd" rounding methods, based primarily on the Wikipedia article on rounding 😉. But from I can see, the methods are designed to reduce total error when summing multiple rounded values. I don't think that scenario really applies to these Temporal methods, and it's definitely not consistent with any current JS I don't have a problem with "half-to-even" rounding being added as an extra option for those who want it, but I feel strongly that it shouldn't be the default.
How would this work with Likewise, how would it work with |
My initial thought would be that we throw an exception if the increment is not a divisor. Alternatively, we could define behavior that an increment "resets" at the unit mark; for example, a 25-minute increment in a 60-minute hour means that the valid minute values after rounding are 0, 25, and 50. So, a 10-minute increment in a hypothetical 45-minute hour could produce minute values of 0, 10, 20, 30, and 40.
The other challenge is that unlike time scale units, durations need not be balanced.
Half-up, not half-even, is already the default on Intl, and it should be the same on Temporal. |
YearMonth has arithmetic. MonthDay doesn't have it. Also, I realized that no Temporal time scale type has weeks as a concept; weeks are only a concept in Duration. Therefore, it doesn't make any sense to have week rounding on the time scale types. |
Oops. Fixed. Thanks for catching.
The current proposal already has |
As I've said before, I think there are two types of rounding options, which are similar but should not be seen as the same: time scale rounding, and duration rounding. |
This distinction makes a lot of sense, and I agree with your terms and definitions.
OK makes sense. The current proposal includes that |
Basically yes. I'm suggesting that Random idea: since we're adding rounding functionality to existing functions like .difference(), .plus(), .from(), etc., why not add it to .with() and make that the entrypoint instead of .round()? const now15 = Temporal.now.localDateTime("iso8601").with({ smallestUnit: "minutes", roundingIncrement: 15 }); |
Right, and I would go further to say that we explicitly don't support the rounding method or options on types on units without a time component (Date and YearMonth).
I don't know of use cases where programmers want "month rounding". More often, they want "1st of the month" or something like that. We can call that "month rounding", but I don't think that's the term people would look for in documentation. |
@sffc - I'm open to your overall point which is that non-time rounding is much less useful than time rounding. The main use-case I had in mind for month rounding is dealing with quarters (3-month periods), e.g. first day of this quarter / first day of the next quarter. Perhaps we should get feedback from others too? I don't have a strong opinion either way. |
Quarters are really complicated. They change not only on calendar system but also by region. See tc39/ecma402#345 (comment) |
IMHO it seems weird to have units and options cohabitating in the same property bag. What would happen if I did this? const now15 = Temporal.now.localDateTime("iso8601").with({
minute: 20,
second: 30,
smallestUnit: "minute",
roundingIncrement: 15
}); Another minor issue would be potential name conflicts between non-ISO-calendar custom fields. In practice this would probably never happen, so not sure how much of a concern this would be. Also, from an IDE discoverability standpoint, a method called "round" is probably much easier to discover than some options buried in |
Yep, agreed. Because of this complexity (not only by calendar and region, but also by use case within each region-- e.g. different companies' fiscal years start in different months) I don't think that a That said, 3-month calendar quarters in ISO-calendar-using cases is still quite common. I still don't have a strong opinion on month/year rounding. If other folks want to remove it, I'd be fine with that but would like to hear more feedback if possible. |
ISO-8601-2 defines a number of year divisions: quarters, seasons, trimesters, etc. If we start supporting rounding to the nearest quarter, we should consider extending that support to all of the year divisions in ISO-8601-2. I don't think we should do this, and therefore I don't think we should have quarter rounding. |
I think we're agreeing? I'm suggesting that if we support month rounding, then the most common quarter case (what 3-month-ISO-calendar-quarter am I in?) will be easier without Temporal having to offer explicitly support |
The proposal is now updated with a list of open issues remaining after discussions in the comments. Let me know if I missed any. Many thanks to @sffc and @gilmoreorless for the reviews. @ptomato, @pipobscure, @gibson042, @ryzokuken - if you guys have a minute, it'd be great to get your feedback on this proposal. Thanks! |
Overall this seems good. Reading the discussion I've been convinced by Shane's arguments against year and month rounding. I think it's too big of a can of worms, it's weird that I wasn't in favour of rounding options in Here are my suggestions for the open questions:
No (see above).
Whatever is consistent with
No, this makes
No opinion.
N/A if we don't have year and month rounding.
No (see above), at least not the full complement, but there does need to be some sort of solution for #329.
Yes (but I think this should be pushed to #329 and considered out of scope for this issue)
No, I don't think it should have any.
Sure.
It's slightly odd but I don't see anything wrong with it. I certainly can't think of a better default. |
It does as of tc39/ecma402#347, and that feature is shipped in Chrome 84.
At first I thought I was in favor of not cloning unnecessarily, but my mind has been changed from arguments by Daniel and others, so now I think these methods should always return new objects, even if the objects are the same. |
This proposal is updated to reflect decisions from 2020-08-28 meeting. |
@justingrant I was about to ask for clarification on one of the decisions. But then I realised that an example had been added further down — it just took me a while to see because there's no record of what changed in your edit. Can I suggest that future proposals of this size use PRs instead of multiple edits to an issue description? Even if it's adding a new document that's not intended to be actually merged, it would bring the following benefits:
|
@gilmoreorless You can view edit history; the "edited" link in the description header opens a menu. |
@gibson042 Thanks, I had no idea that was clickable! (That's a terrible piece of feature discovery in a UI.) That solves one of the problems, but it's a clunky implementation. It's still hard to match comments to the proposal text as it was at the time of the comment. |
I've started to work on an implementation of this and I think I've stumbled upon a few things that should be different in the proposal. I'll quote them here so they can be edited later but my comment here will still make sense.
Second sentence should be "All smaller units will be zero in the result." after dropping date rounding.
Change to "For Time, the
60 in this case is also an exact divisor of the next-largest unit. I think it's friendly to not throw on, e.g. 60 seconds, although I think it's fine to throw on 120 seconds.
Is this meant to apply to
What are the allowed values for |
This was really helpful, especially the sample code for use cases. Thanks for doing this! Proposal text is updated. |
Includes a new abstract operation ToSmallestTemporalDurationUnit. This is distinct from ToSmallestTemporalUnit because it prefers plural unit names and does have a fallback value. Also includes a new abstract operation ValidateTemporalDifferenceUnits. This makes sure that largestUnit is not smaller than smallestUnit. Both of these will be reused for Temporal.DateTime.difference and Temporal.Time.difference in subsequent commits. See: #827
Includes a new abstract operation RoundDuration, which will be used in the other types' difference() methods and Temporal.Duration.round as well. See: #827
Includes a new abstract operation RoundDuration, which will be used in the other types' difference() methods and Temporal.Duration.round as well. See: #827
Includes several new abstract operations: - ToSmallestTemporalDurationUnit, which is distinct from ToSmallestTemporalUnit because it prefers plural unit names and does have a fallback value; - ValidateTemporalDifferenceUnits, which makes sure that largestUnit is not smaller than smallestUnit; - and MaximumTemporalDurationRoundingIncrement, which computes the maximum (not inclusive) value for roundingIncrement given the value of smallestUnit, for rounding a Temporal.Duration. All of these will be reused for Temporal.DateTime.difference and Temporal.Time.difference in subsequent commits. See: #827
Includes a new abstract operation RoundDuration, which will be used in the other types' difference() methods and Temporal.Duration.round as well. See: #827
Includes several new abstract operations: - ToSmallestTemporalDurationUnit, which is distinct from ToSmallestTemporalUnit because it prefers plural unit names and does have a fallback value; - ValidateTemporalDifferenceUnits, which makes sure that largestUnit is not smaller than smallestUnit; - and MaximumTemporalDurationRoundingIncrement, which computes the maximum (not inclusive) value for roundingIncrement given the value of smallestUnit, for rounding a Temporal.Duration. All of these will be reused for Temporal.DateTime.difference and Temporal.Time.difference in subsequent commits. See: #827
Includes a new abstract operation RoundDuration, which will be used in the other types' difference() methods and Temporal.Duration.round as well. See: #827
Includes several new abstract operations: - ToSmallestTemporalDurationUnit, which is distinct from ToSmallestTemporalUnit because it prefers plural unit names and does have a fallback value; - ValidateTemporalDifferenceUnits, which makes sure that largestUnit is not smaller than smallestUnit; - and MaximumTemporalDurationRoundingIncrement, which computes the maximum (not inclusive) value for roundingIncrement given the value of smallestUnit, for rounding a Temporal.Duration. All of these will be reused for Temporal.DateTime.difference and Temporal.Time.difference in subsequent commits. See: #827
Includes a new abstract operation RoundDuration, which will be used in the other types' difference() methods and Temporal.Duration.round as well. See: #827
This landed yesterday but the issue didn't close. Follow ups in #908. |
To bring the default for largestUnit in PlainDate's since() and until() methods in line with other types' since() and until() methods, we have to add an algorithm step for LargerOfTwoTemporalUnits. Without this, code such as date1.until(date2, { smallestUnit: 'months' }) would throw because the default largestUnit is days. As per #827 (comment) this was not intended. PlainDate seems to be the only place where this was not working as intended. ZonedDateTime, Instant, PlainTime, PlainYearMonth, and PlainDateTime either already have this step or their default largestUnit is already the largest one so they wouldn't have this problem. The reference polyfill code is already correct in this regard. Closes: #1864
To bring the default for largestUnit in PlainDate's since() and until() methods in line with other types' since() and until() methods, we have to add an algorithm step for LargerOfTwoTemporalUnits. Without this, code such as date1.until(date2, { smallestUnit: 'months' }) would throw because the default largestUnit is days. As per #827 (comment) this was not intended. PlainDate seems to be the only place where this was not working as intended. ZonedDateTime, Instant, PlainTime, PlainYearMonth, and PlainDateTime either already have this step or their default largestUnit is already the largest one so they wouldn't have this problem. The reference polyfill code is already correct in this regard. Closes: #1864
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:
round()
method to every non-Duration Temporal type that supports math operations.difference
method of the same types, in order to support rounding of the results ofdifference
.Open Issues to Resolve
with()
have rounding options? (see discussion in comments) NOroundingIncrement
require 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.roundingIncrement
handle 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/AtoString
includes rounding options? MAYBE - MOVED to Why do toString methods truncate? #329toString
controls decimal precision of sub-second units (and whether to show seconds at all) based onsmallestUnit
? MAYBE - MOVEDtoLocaleString
accept the same rounding options astoString()
? N/Adifference
ready? 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. YESsmallestUnit
is 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.difference
could theoretically choose eitherthis
orother
as the reference point for rounding. I'm assuming that it should usethis
. YES. HAS CONSENSUS.Sample Use Cases
round(options)
What's the closest Saturday to this Date?- We removed week-related rounding from the scope of this proposal. The workaround is to calculate usingdayOfWeek
directly.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 andwith
and/orfrom
.Details
1.
round()
for non-Duration typesround
method:DateTime
,Absolute
,Time
, andLocalDateTime
.round()
method.round
method, but that's out of scope for this proposal. See Proposal: rounding method & total method for Duration type #789.round()
will accept only one parameter,options
, which is a property bag of options that control rounding. Each option is described below.options
is a required parameter because one of the options,smallestUnit
(see below) cannot be omitted.2.
smallestUnit
optionsmallestUnit
property defines where rounding happens. All smaller time units will be zero in the result.smallestUnit
is omitted.2.3 IfsmallestUnit
is larger thanlargestUnit
, then a RangeError should be thrown.smallestUnit
should 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.era
or 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.smallestUnit
to'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.smallestUnit
can be any valid unit for this type:'hours'
or smaller.smallestUnit
must be'days'
or smaller. We decided to exclude larger units because the rounding behavior of weeks and months is confusingly calendar-dependent.5.
roundingMode
optionMath
object, although per above the names may change as we align with Intl.'ceil'
- round up towards +∞'floor'
- round down towards -∞'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.'nearest'
- round to nearest integer, with 0.5 rounded away from zero. This is named differently fromround
, to differentiate fromMath.round
that rounds 0.5 remainders towards +∞ which is often unexpected for negative numbers per MDN:'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.6.
roundingIncrement
optionroundingIncrement
allows rounding with a coarser granularity than one unit, e.g. "nearest 15 minutes", "nearest second".smallestUnit
isminute
, thenroundingIncrement
could be either 1 (default), 2, 3, 4, 5, 6, 10, 12, 15, 20, or 30. Others must throw a RangeError.smallestUnit
.roundingIncrement
will 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-specifiedroundingIncrement
value. Also, if the result's clock time is ambiguous, then it's disambiguated using the default ('compatible'
) disambiguation option. For example:minutes
or smaller units. If developers want to round an Absolute to an hour, they can just use{roundingIncrement: 60}
.round()
methods, Absolute rounding increments can also be equal (not less than like other types) to 86400 seconds.7. Rounding options in non-Duration
difference
methodsdifference
methods throughout Temporal accept alargestUnit
option.round
method will also enhance thedifference
method to also accept the same options thatround
accepts:smallestUnit
,roundingMode
, androundingIncrement
.round
, with the only exceptions noted below.smallestUnit
defaults tonanoseconds
and is optional.largestUnit
is not specified butsmallestUnit
is larger than its default value, thenlargestUnit
should be adjusted to be the same assmallestUnit
.smallestUnit
is larger than an explicitly specifiedlargestUnit
, then aRangeError
should be thrown.'days'
in the docs, but both plural and singular units should be accepted.largestUnit
option will be supported insmallestUnit
too.largestUnit
is'month'
or'year'
andsmallestUnit
is'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.smallestUnit
is'week'
, then there is no ambiguity and any remainder will go intoweek
.largestUnit
is'day'
, then there is no ambiguity because weeks are excluded andweek
will be zero.{ largestUnit: 'week', smallestUnit: 'day' }
then there is also no ambiguity so both fields will be populated.The text was updated successfully, but these errors were encountered: