With most types in Temporal, each unit has a natural maximum.
For example, there is no such time as 11:87, so when creating a Temporal.PlainTime
from 11:87 the time is either clipped to 11:59 ("constrain" mode) or an exception is thrown ("reject" mode).
With Temporal.Duration
, however, maximums are less clear-cut.
Take, for example, a duration of 100 seconds: Temporal.Duration.from({ seconds: 100 })
.
100 seconds is equal to 1 minute and 40 seconds.
In some cases you may want to "balance" it, yielding 1 minute and 40 seconds.
In other cases you may want to keep it as an "unbalanced" duration of 100 seconds.
When a Temporal.Duration
object is constructed from a string or a property bag object, no balancing is performed.
d = Temporal.Duration.from({ seconds: 100 });
d.minutes; // => 0
d.seconds; // => 100
d = Temporal.Duration.from('PT100S');
d.minutes; // => 0
d.seconds; // => 100
The most common kind of unbalanced duration is a "top-heavy" duration where only the largest nonzero unit is unbalanced, e.g. { days: 45, hours: 10 }
.
Unbalanced durations that are not top-heavy, like { days: 4, hours: 60 }
, are rarely used.
round()
Temporal.Duration.prototype.round()
, in addition to rounding duration units at the low end, can also balance durations too.
By default, round()
will not enlarge a top-heavy unbalanced duration.
By default, the largest unit in the input will be largest unit in the output.
d = Temporal.Duration.from({ minutes: 80, seconds: 30 }); // => PT80M30S
d.round({ largestUnit: 'auto' }); // => PT80M30S (unchanged)
However, round()
will balance units smaller than the largest one.
This only matters in the rare case that an unbalanced duration isn't top-heavy.
d = Temporal.Duration.from({ minutes: 80, seconds: 90 }); // => PT80M90S
d.round({ largestUnit: 'auto' });
// => PT81M30S (seconds balance to minutes, but not minutes=>hours)
To fully balance a duration, use the largestUnit
option:
d = Temporal.Duration.from({ minutes: 80, seconds: 90 }); // => PT80M90S
d.round({ largestUnit: 'hour' }); // => PT1H21M30S (fully balanced)
Balancing that includes days, weeks, months, and years is more complicated because those units can be different lengths. In the default ISO 8601 calendar, a year can be 365 or 366 days, and a month can be 28, 29, 30, or 31 days. In other calendars, years aren't always 12 months long and weeks aren't always 7 days. Finally, in time zones that use Daylight Saving Time (DST) days are not always 24 hours long.
Therefore, any Duration
object with nonzero days, weeks, months, or years can refer to a different length of time depending on the specific date and time that it starts from.
To handle this potential ambiguity, the relativeTo
option is used to provide a starting point.
relativeTo
must be (or be parseable into) a Temporal.ZonedDateTime
for timezone-specific durations or Temporal.PlainDate
for timezone-neutral data.
relativeTo
is required when balancing to or from weeks, months, or years.
d = Temporal.Duration.from({ days: 370 }); // => P370D
/* WRONG */ d.round({ largestUnit: 'year' }); // => RangeError (`relativeTo` is required)
d.round({ largestUnit: 'year', relativeTo: '2019-01-01' }); // => P1Y5D
d.round({ largestUnit: 'year', relativeTo: '2020-01-01' }); // => P1Y4D (leap year)
relativeTo
is optional when balancing to or from days
, and if relativeTo
is omitted then days are assumed to be 24 hours long.
However, if the duration is timezone-specific, then it's recommended to use a Temporal.ZonedDateTime
reference point to ensure that DST transitions are accounted for.
d = Temporal.Duration.from({ hours: 48 }); // => PT48H
d.round({ largestUnit: 'day' });
// => P2D
d.round({ largestUnit: 'day', relativeTo: '2020-03-08T00:00-08:00[America/Los_Angeles]' });
// => P2DT1H
// (because one clock hour was skipped by DST starting)
In addition to round()
as described above, add()
and subtract()
also balance their output into either a fully-balanced or a top-heavy duration depending on the largestUnit
option.
By default, add()
and subtract()
on Temporal.Duration
instances will only balance up to the largest unit in either input duration.
d1 = Temporal.Duration.from({ hours: 26, minutes: 45 }); // => PT26H45M
d2 = Temporal.Duration.from({ minutes: 30 }); // => PT30M
d1.add(d2); // => PT27H15M
The largestUnit
option can be used to balance to larger units than the inputs.
d1 = Temporal.Duration.from({ minutes: 80, seconds: 90 }); // => PT80M90S
d2 = Temporal.Duration.from({ minutes: 100, seconds: 15 }); // => PT100M15S
d1.add(d2).round({ largestUnit: 'hour' }); // => PT3H1M45S (fully balanced)
The relativeTo
option can be used to balance to, or from, weeks, months or years (or days for timezone-aware durations).
relativeTo
is interpreted relative to this
, not to other
, which allows the same relativeTo
value to be used for a chain of arithmetic operations.
d1 = Temporal.Duration.from({ hours: 48 }); // => PT48H
d2 = Temporal.Duration.from({ hours: 24 }); // => PT24H
d1.add(d2).round({ largestUnit: 'day' });
// => P3D
d1.add(d2).round({ largestUnit: 'day', relativeTo: '2020-03-08T00:00-08:00[America/Los_Angeles]' });
// => P3DT1H
// (because one clock hour was skipped by DST starting)
Normally, any Temporal object can be serialized to a string with its toString()
method, and deserialized by calling from()
on the string.
This goes for Temporal.Duration
as well.
However, if any of the milliseconds
, microseconds
, or nanoseconds
properties are greater than 999, then Temporal.Duration.from(duration.toString())
will not yield an identical Temporal.Duration
object.
The deserialized object will represent an equally long duration, but the sub-second fields will be balanced with the seconds
field so that they become 999 or less.
For example, 1000 nanoseconds will become 1 microsecond.
This is because the ISO 8601 string format for durations, which is used for serialization for reasons of interoperability, does not allow for specifying sub-second units separately, only as a decimal fraction of seconds.
If you need to serialize a Temporal.Duration
into a string that preserves unbalanced sub-second fields, you will need to use a custom serialization format or serialize it into an object or JSON instead.