Frequently Asked Questions

library(clock)
library(magrittr)

Why can’t I do day arithmetic on a year-month-day?

It might seem intuitive that since you can do:

x <- year_month_day(2019, 1, 5)

add_months(x, 1)
#> <year_month_day<day>[1]>
#> [1] "2019-02-05"

That you should also be able to do:

add_days(x, 1)
#> Error in `add_days()`:
#> ! Can't perform this operation on a <clock_year_month_day>.
#> ℹ Do you need to convert to a time point first?
#> ℹ Use `as_naive_time()` or `as_sys_time()` to convert to a time point.

Generally, calendars don’t support day based arithmetic, nor do they support arithmetic at more precise precisions than day. Instead, you have to convert to a time point, do the arithmetic there, and then convert back (if you still need a year-month-day after that).

x %>%
  as_naive_time() %>%
  add_days(1) %>%
  as_year_month_day()
#> <year_month_day<day>[1]>
#> [1] "2019-01-06"

The first reason for this is performance. A year-month-day is a field type, implemented as multiple parallel vectors holding the year, month, day, and all other components separately. There are two ways that day based arithmetic could be implemented for this:

Both approaches are relatively expensive. One of the goals of the low-level API of clock is to make these expensive operations explicit. This helps make it apparent that when you need to chain together multiple operations, you should try and do all of your calendrical arithmetic steps first, then convert to a time point (i.e. the second bullet point from above) to do all of your chronological arithmetic.

The second reason for this has to do with invalid dates, such as the three in this vector:

odd_dates <- year_month_day(2019, 2, 28:31)
odd_dates
#> <year_month_day<day>[4]>
#> [1] "2019-02-28" "2019-02-29" "2019-02-30" "2019-02-31"

What does it mean to “add 1 day” to these? There is no obvious answer to this question. Since clock requires that you first convert to a time point to do day based arithmetic, you’ll be forced to call invalid_resolve() to handle these invalid dates first. After resolving them manually, then day based arithmetic again makes sense.

odd_dates %>%
  invalid_resolve(invalid = "next")
#> <year_month_day<day>[4]>
#> [1] "2019-02-28" "2019-03-01" "2019-03-01" "2019-03-01"

odd_dates %>%
  invalid_resolve(invalid = "next") %>%
  as_naive_time() %>%
  add_days(2)
#> <naive_time<day>[4]>
#> [1] "2019-03-02" "2019-03-03" "2019-03-03" "2019-03-03"

odd_dates %>%
  invalid_resolve(invalid = "overflow")
#> <year_month_day<day>[4]>
#> [1] "2019-02-28" "2019-03-01" "2019-03-02" "2019-03-03"

odd_dates %>%
  invalid_resolve(invalid = "overflow") %>%
  as_naive_time() %>%
  add_days(2)
#> <naive_time<day>[4]>
#> [1] "2019-03-02" "2019-03-03" "2019-03-04" "2019-03-05"

Why can’t I add time to a zoned-time?

If you have a zoned-time, such as:

x <- zoned_time_parse_complete("1970-04-26T01:30:00-05:00[America/New_York]")
x
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T01:30:00-05:00"

You might wonder why you can’t add any units of time to it:

add_days(x, 1)
#> Error in `add_days()`:
#> ! Can't perform this operation on a <clock_zoned_time>.
#> ℹ Do you need to convert to a time point first?
#> ℹ Use `as_naive_time()` or `as_sys_time()` to convert to a time point.

add_seconds(x, 1)
#> Error in `add_seconds()`:
#> ! Can't perform this operation on a <clock_zoned_time>.
#> ℹ Do you need to convert to a time point first?
#> ℹ Use `as_naive_time()` or `as_sys_time()` to convert to a time point.

In clock, you can’t do much with zoned-times directly. The best way to understand this is to think of a zoned-time as containing 3 things: a sys-time, a naive-time, and a time zone name. You can access those things with:

x
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T01:30:00-05:00"

# The printed time with no time zone info
as_naive_time(x)
#> <naive_time<second>[1]>
#> [1] "1970-04-26T01:30:00"

# The equivalent time in UTC
as_sys_time(x)
#> <sys_time<second>[1]>
#> [1] "1970-04-26T06:30:00"

zoned_time_zone(x)
#> [1] "America/New_York"

Calling add_days() on a zoned-time is then an ambiguous operation. Should we add to the sys-time or the naive-time that is contained in the zoned-time? The answer changes depending on the scenario.

Because of this, you have to extract out the relevant time point that you care about, operate on that, and then convert back to zoned-time. This often produces the same result:

x %>%
  as_naive_time() %>%
  add_seconds(1) %>%
  as_zoned_time(zoned_time_zone(x))
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T01:30:01-05:00"

x %>%
  as_sys_time() %>%
  add_seconds(1) %>%
  as_zoned_time(zoned_time_zone(x))
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T01:30:01-05:00"

But not always! When daylight saving time is involved, the choice of sys-time or naive-time matters. Let’s try adding 30 minutes:

# There is a DST gap 1 second after 01:59:59,
# which jumps us straight to 03:00:00,
# skipping the 2 o'clock hour entirely

x %>%
  as_naive_time() %>%
  add_minutes(30) %>%
  as_zoned_time(zoned_time_zone(x))
#> Error in `as_zoned_time()`:
#> ! Nonexistent time due to daylight saving time at location 1.
#> ℹ Resolve nonexistent time issues by specifying the `nonexistent` argument.

x %>%
  as_sys_time() %>%
  add_minutes(30) %>%
  as_zoned_time(zoned_time_zone(x))
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T03:00:00-04:00"

When adding to the naive-time, we got an error. With the sys-time, everything seems okay. What happened?

The sys-time scenario is easy to explain. Technically this converts to UTC, adds the time there, then converts back to your time zone. An easier way to think about this is that you sat in front of your computer for exactly 30 minutes (1800 seconds), then looked at the clock. Assuming that that clock automatically changes itself correctly for daylight saving time, it should read 3 o’clock.

The naive-time scenario makes more sense if you break down the steps. First, we convert to naive-time, dropping all time zone information but keeping the printed time:

x
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T01:30:00-05:00"

x %>%
  as_naive_time()
#> <naive_time<second>[1]>
#> [1] "1970-04-26T01:30:00"

We add 30 minutes to this. Because we don’t have any time zone information, this lands us at 2 o’clock, which isn’t an issue when working with naive-time:

x %>%
  as_naive_time() %>%
  add_minutes(30)
#> <naive_time<second>[1]>
#> [1] "1970-04-26T02:00:00"

Finally, we convert back to zoned-time. If possible, this tries to keep the printed time, and just attaches the relevant time zone onto it. However, in this case that isn’t possible, since 2 o’clock didn’t exist in this time zone! This nonexistent time must be handled explicitly by setting the nonexistent argument of as_zoned_time(). We can choose from a variety of strategies to handle nonexistent times, but here we just roll forward to the next valid moment in time.

x %>%
  as_naive_time() %>%
  add_minutes(30) %>%
  as_zoned_time(zoned_time_zone(x), nonexistent = "roll-forward")
#> <zoned_time<second><America/New_York>[1]>
#> [1] "1970-04-26T03:00:00-04:00"

As a general rule, it often makes the most sense to add:

This is what the high-level API for POSIXct does. However, this isn’t always what you want, so the low-level API requires you to be more explicit.

Where did my POSIXct subseconds go?

old <- options(digits.secs = 6, digits = 22)

Consider the following POSIXct:

x <- as.POSIXct("2019-01-01 01:00:00.2", "America/New_York")
x
#> [1] "2019-01-01 01:00:00.2 EST"

It looks like there is some fractional second information here, but converting it to naive-time drops it:

as_naive_time(x)
#> <naive_time<second>[1]>
#> [1] "2019-01-01T01:00:00"

This is purposeful. clock treats POSIXct as a second precision data type. The reason for this has to do with the fact that POSIXct is implemented as a vector of doubles, which have a limit to how precisely they can store information. For example, try parsing a slightly smaller or larger fractional second:

y <- as.POSIXct(
  c("2019-01-01 01:00:00.1", "2019-01-01 01:00:00.3"),