Some temporal operations produce time points that don’t exist. Adding one month to 31 January asks for 31 February, and adding one day to 02:30 on the day before daylight saving starts could produce a local time in the transition that the clocks skip. There is no single correct answer to these operations, so mixtime resolves them with one of several strategies you can choose. This vignette describes where invalid time points come from, the strategies available, and what each of them preserves.
Calendar arithmetic shifts a time point by decomposing it into
calendar components, shifting the component that matches the duration,
and recomposing the result. Adding one month to
2021-01-31 09:30:00 decomposes it into the month (January
2021), and finer components (day of the month 31 at time of day 09:30),
then increments the month to February 2021 producing the invalid time
point of 2021-02-31 09:30:00.
Recomposing only fails when a component’s valid range depends on the components around it. There are always 24 hours in a day and 60 minutes in an hour, so shifting by hours or minutes never produces an invalid time of day. These granularities have a regular cardinality. The number of days in a month varies from 28 to 31, so the day of the month has an irregular cardinality with respect to months, and day 31 is invalid in February.
# Valid: day 15 exists in every month
date("2021-01-15") + months(1L)
#> <mixtime[1]>
#> [1] 2021-02-15
# Invalid: February 2021 has no day 31
datetime("2021-01-31 09:30:00") + months(1L)
#> <mixtime[1]>
#> [1] 2021-02-28 09:30:00Only the invalid component needs resolving. In the example above, the month and the time of day are both still valid, and only the day of the month is out of range.
The same problem arises wherever one granularity doesn’t fit a whole number of times into another, such as leap days within years, or the 53rd week of a long ISO week year.
Shifting a time zone aware time point by days or longer keeps its local clock time: one day after 09:00 is 09:00 the next day, even if the day is only 23 hours long. When daylight saving starts, the clocks skip part of the day, and a local time in that gap doesn’t exist.
In Melbourne, the clocks went forward from 02:00 to 03:00 on 5 October 2025, so 02:30 that day never happened.
x <- datetime("2025-10-04 02:30:00", tz = "Australia/Melbourne")
x + days(1L)
#> <mixtime[1]>
#> [1] 2025-10-05 01:30:00 AESTShifting by hours, minutes or seconds adds elapsed time instead, so it never lands in a gap. Twenty-four hours after 02:30 is 03:30, since one hour was skipped.
When daylight saving ends, the clocks repeat part of the day, and a local time in the overlap happens twice. Such a time is ambiguous rather than invalid, and mixtime resolves it to its earliest occurrence (equivalent to 24 hours for day increments).
Text describing a time point that doesn’t exist fails to parse, and
becomes NA with a warning. This includes dates that don’t
exist in the calendar, and local times skipped in the time zone.
date("2026-02-30")
#> Warning: 1 value failed to parse and was set to `NA`.
#> ✖ "2026-02-30"
#> <mixtime[1]>
#> [1] NA
datetime("2025-10-05 02:30:00", tz = "Australia/Melbourne")
#> Warning: 1 value does not exist in time zone "Australia/Melbourne" and was set to `NA`.
#> ℹ It falls in local time skipped by a time zone change (e.g. when daylight
#> saving starts).
#> <mixtime[1]>
#> [1] NAThe rest of this vignette describes resolving invalid results from
operations, which is controlled with the on_invalid
argument.
time_shift() and seq() resolve invalid time
points with their on_invalid argument. The table below
shows each strategy applied to the invalid
2021-02-31 09:30:00, from adding one month to
2021-01-31 09:30:00. Each strategy keeps a different aspect
of the intended result, at the expense of others.
on_invalid |
Result | Preserves | Sacrifices |
|---|---|---|---|
"clamp" (default) |
2021-02-28 09:30:00 |
target month; finer components | target distance; order |
"previous" |
2021-02-28 23:59:59 |
target month; order | finer components |
"next" |
2021-03-01 00:00:00 |
order | target month; finer components |
"overflow" |
2021-03-03 09:30:00 |
offset from the start of the target month; finer components | target month; order |
"reject" |
NA with a warning |
every invariant, by producing no result | a result |
"error" |
an error | every invariant, by producing no result | a result |
"clamp" reduces the invalid component to the last value
in its valid range, leaving the other components unchanged. The day of
the month becomes 28, and the time of day stays at 09:30. This is the
default, and what most people expect from adding months.
"previous" and "next" resolve to the valid
time points either side of the invalid one. "previous"
gives the last time point in the target month, and "next"
gives the first time point in the month after it. Neither keeps the time
of day, since the result is at a boundary of the target month.
time_shift(x, months(1L), on_invalid = "previous")
#> <mixtime[1]>
#> [1] 2021-02-28 23:59:59
time_shift(x, months(1L), on_invalid = "next")
#> <mixtime[1]>
#> [1] 2021-03-01 00:00:00For dates, "previous" gives the same result as
"clamp", since there are no finer components to keep.
"next" gives the first day of the following month.
time_shift(date("2021-01-31"), months(1L), on_invalid = "previous")
#> <mixtime[1]>
#> [1] 2021-02-28
time_shift(date("2021-01-31"), months(1L), on_invalid = "next")
#> <mixtime[1]>
#> [1] 2021-03-01The last time point in February depends on the time point’s chronon.
For discrete time it is the last chronon in the month (the second
starting at 23:59:59 above). Continuous time has no last instant before
March, so "previous" gives the end of February, which is
the same instant as the start of March given by "next".
"overflow" lets the invalid component carry into the
following month. 31 February is three days past the end of February
2021, so it becomes 3 March, keeping the time of day.
"reject" and "error" refuse to resolve
invalid time points, since the operation doesn’t have a single correct
answer. "reject" replaces them with NA and
warns, while "error" stops with an error that gives their
positions.
The same strategies resolve local times skipped by daylight saving.
The gap is treated like the end of February: "clamp" moves
back by the size of the gap, "previous" and
"next" give the times either side of it, and
"overflow" moves forward by the size of the gap.
x <- datetime("2025-10-04 02:30:00", tz = "Australia/Melbourne")
time_shift(x, days(1L), on_invalid = "clamp")
#> <mixtime[1]>
#> [1] 2025-10-05 01:30:00 AEST
time_shift(x, days(1L), on_invalid = "previous")
#> <mixtime[1]>
#> [1] 2025-10-05 01:59:59 AEST
time_shift(x, days(1L), on_invalid = "next")
#> <mixtime[1]>
#> [1] 2025-10-05 03:00:00 AEDT
time_shift(x, days(1L), on_invalid = "overflow")
#> <mixtime[1]>
#> [1] 2025-10-05 03:30:00 AEDTseq() takes the same on_invalid argument.
Each time point in the sequence is shifted from the start, rather than
from the previous time point, so a time point clamped in February
doesn’t affect the rest of the sequence.
Since a sequence can resolve many time points at once,
seq() warns when it clamps without a strategy being
chosen.
seq(date("2021-01-31"), by = "1 month", length.out = 4)
#> Warning: Some time points in the sequence fall past the end of a shorter month.
#> ! Clamping them to the last valid time point in that month, `on_invalid =
#> "clamp"` (the default).
#> ℹ Specify `on_invalid`, or set the `mixtime.on_invalid` option, to suppress
#> this warning.
#> <mixtime[4]>
#> [1] 2021-01-31 2021-02-28 2021-03-31 2021-04-30Choosing a strategy explicitly silences the warning.
The + and - operators can’t take an
on_invalid argument, so they use the default strategy. The
default is set by the mixtime.on_invalid option, which is
"clamp" unless set. The option also sets the default for
time_shift() and seq(), and setting it
silences the warning from seq().
date("2021-01-31") + months(1L)
#> <mixtime[1]>
#> [1] 2021-02-28
options(mixtime.on_invalid = "next")
date("2021-01-31") + months(1L)
#> <mixtime[1]>
#> [1] 2021-03-01
seq(date("2021-01-31"), by = "1 month", length.out = 4)
#> <mixtime[4]>
#> [1] 2021-01-31 2021-03-01 2021-03-31 2021-05-01Setting the option to "error" makes any invalid time
point stop the analysis, which can be useful for checking that no time
points are being silently resolved.
Every strategy that produces a result changes the behaviour of the arithmetic, in ways worth knowing when choosing one.
Calendar arithmetic is not associative. Adding one month twice clamps in February, and the lost days aren’t recovered in March, whereas adding two months at once never passes through February.
date("2021-01-31") + months(1L) + months(1L)
#> <mixtime[1]>
#> [1] 2021-03-28
date("2021-01-31") + months(2L)
#> <mixtime[1]>
#> [1] 2021-03-31Nor is it reversible. Adding and subtracting one month doesn’t return to the starting date.
Only "previous" and "next" preserve the
order of the time points being shifted, although distinct time points
may become equal. Clamping keeps the time of day, so a later day with an
earlier time can end up before an earlier day with a later time.
Overflowing carries further past the end of the month for later days, so
31 January can overtake 1 February.
y <- datetime(c("2021-01-29 23:00:00", "2021-01-31 08:00:00"))
time_shift(y, months(1L), on_invalid = "clamp")
#> <mixtime[2]>
#> [1] 2021-02-28 23:00:00 2021-02-28 08:00:00
time_shift(y, months(1L), on_invalid = "previous")
#> <mixtime[2]>
#> [1] 2021-02-28 23:59:59 2021-02-28 23:59:59
time_shift(date(c("2021-01-31", "2021-02-01")), months(1L), on_invalid = "overflow")
#> <mixtime[2]>
#> [1] 2021-03-03 2021-03-01When the order of time points matters, such as when shifting the
index of a time series, "previous" or "next"
are safer choices than the default.