Proposal-temporal: How to round/ceil/floor?

Created on 13 May 2020  路  13Comments  路  Source: tc39/proposal-temporal

Many common date/time-related operation is coercing an imprecise Date to a nearby unit boundary, e.g. nearest half-hour, next day, end of next week, etc. How are these operations expected to work with Temporal? These aren't very hard problems but I've seen them be a persistent source of bugs in the past.

"Round down" is usually a trivial problem because you can just clone the object while zeroing out (or 1-out in the case of day-of-month) units you don't want.

But "round up" use-cases are harder because you need to think about overflow, and "round nearest" can be hard because knowing the midpoint of a unit (e.g. month or week) isn't always easy.

DST is also potentially a problem. For example, if it's during the hour before DST ends, then depending on how you calculate the "next" hour boundary you might end up with a time that's actually earlier!

Should Temporal have round/ciel/floor methods, or a coerce method that accepts a shape like this?

{
  unit, // required: 'year' | 'month' ..., 
  count, // optional, default=1. e.g. 5 for nearest 5-minute time
  direction // optional. 'nearest' (default) | 'next' | 'previous' | 'last' | 'first',
}

BTW, what got me thinking about this was when I saw the largestUnit parameter of difference and immediately thought "where's smallestUnit?"

documentation ergonomics

Most helpful comment

Should this be merged with #337? The comments I just left there about keeping rounding separate from balancing apply here as well, and in fact the only relevant distinction I can think of would be if someone wants to advocate for including rounding on Temporal.Duration but not on other types. Absent that, I'd rather keep the discussion in one place.

All 13 comments

rounding is usually done in presentation-layer (e.g. committed 3 hours ago), where lossiness doesn't matter.

floors and ceilings however are legitimate business-logic concerns (e.g. beginning-of-month, end-of-quarter).

rounding is usually done in presentation-layer (e.g. committed 3 hours ago), where lossiness doesn't matter.

Exactly. This is something that is (or should be) done inside Intl.DateTimeFormat.

Rounding is helpful for formatting, but I was actually thinking of cases where business logic needs to align dates and times on a boundary, e.g.

  • Doctor appointments are only available in even 15-minute timeslots, so when the user clicks on an open slot we need to find the right 15-minute timeslot to select.
  • Doctor clicks on a day in in a month calendar. We want to pull data from the back end for all days in that week, so we need to find the first day of the week the doctor clicked on. Can easily subtract .dayOfWeek-1 days, but that's more complicated and introduces a possible off-by-one error.
  • To save space in a custom serialization format, we want to round all times to the nearest minute before serializing them.
  • A lawyer's billing will be rounded up (ceil, rhymes with "steal") to the closest 5-minute block. (this is ceil-ing a Duration)
  • An analytics app wants to display time-series data using a chart library whose render time is proportional to points being drawn. When chart size is small, the app wants to aggregate all data within a 2-minute period into a single point, speeding up chart rendering by 2x w/o impacting usability.

Interesting. Thanks. We would brainstorm this and try to come up with something.

Thanks for filing this issue @justingrant ; I am not sure how "ceil" should be accomplished with Temporal's current logic. Offhand, could you check if you're on the boundary already, if not, add 1 and then do floor? Would that always work, or be broken sometimes? (EDIT: No, don't do that, use with( {disambiguation: 'balance'}) as described below)

In general, I prefer to have processing (such as rounding) decoupled from formatting; it's true that Intl does include some support for rounding, but I'd like to avoid having new capabilities coming in that coupled way. It's hard to verify strong statements like @kaizhu256 did about things always coinciding, and good to have @justingrant 's concrete use cases where you really don't want them mixed.

How do other datetime libraries meet this use case?

I'm not sure off the top of my head how other libraries do this, but I can elaborate on how you would do it with the _current_ Temporal API. We do have a few examples of how to do this in the cookbook and API docs:

As you say, rounding down is easily done by cloning the object and zeroing out the lower units, using the with() method. Rounding up can be done by adding one with with(..., { disambiguation: 'balance' }). To be fair, this is quite a lot less obvious, so I'd consider it worth exploring other options.

I suggested something similar for Temporal.Duration in #337. We could put the same method in other types like Temporal.DateTime:

dateTime = dateTime.balance({ smallestUnit: "hours", roundingMode: "halfUp" });

I like @sffc's smallestUnit/roundingMode idea. For brevity, I'd consider using rounding instead of the longer roundingMode, and up, down, round as options with round as default.

If we add rounding options, it would be useful to add one more: last, which is essentially up minus one smallestUnit, e.g. last day of month, last second in day, etc.

Given that opt-in options are required, I'm not sure a separate balance method would be better than adding those options to existing methods. Also, I think largestUnit and smallestUnit on the same options object has a nice symmetry that I think would be easier for users to understand than splitting the operation across two method calls.

@pdunkel suggested that you may want to round to the nearest 15 minutes, for example.

Possible API:

dateTime.roundToNearest(Temporal.Duration.from("PT15M"))

@pdunkel suggested that this may be out-of-scope and something that could be done in user land. @littledan thinks that we should consider this sooner. @ryzokuken says we should wait to get more feedback to get a larger sample set. @jasonwilliams agrees.

To clarify, I think we should consider adding APIs like this before Stage 3, but I think it's OK to ship the initial polyfill without this API and consider the overall feedback before adding it.

I'm not sure this needs to be on the meeting agenda. Someone should just write a comment or PR showing what the API would look like.

Should this be merged with #337? The comments I just left there about keeping rounding separate from balancing apply here as well, and in fact the only relevant distinction I can think of would be if someone wants to advocate for including rounding on Temporal.Duration but not on other types. Absent that, I'd rather keep the discussion in one place.

Agreed. I'll close this issue & migrate any content over to #337.

Was this page helpful?
0 / 5 - 0 ratings