Skip to content

Instantly share code, notes, and snippets.

@DavisVaughan
Created March 10, 2021 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DavisVaughan/88ec54be5354ea68c629f4be6ce68e1d to your computer and use it in GitHub Desktop.
Save DavisVaughan/88ec54be5354ea68c629f4be6ce68e1d to your computer and use it in GitHub Desktop.
@DavisVaughan
Copy link
Author

DavisVaughan commented Mar 10, 2021

library(tidyverse)
library(clock)

# Notes:
#
# This Gist uses clock to investigate some daylight saving time peculiarities
# that come up in 2021.
# https://clock.r-lib.org/
#
# If you don't care for the details, skip to the end for some charts displaying
# the difference in UTC offsets between New York and London over March, 2021.
#
# America/New_York has a daylight saving time gap (spring forward) on
# 2021-03-14, when the clocks jump from 01:59:59 directly to 03:00:00,
# skipping the two o'clock hour.
#
# Europe/London has a daylight saving time gap on
# 2021-03-28, when the clocks jump from 00:59:59 directly to 02:00:00,
# skipping the one o'clock hour.
#
# This results in an awkward two weeks where the US is 4 hours behind London,
# rather than the standard 5 hours. Let's investigate!
#

# 1 second before the America/New_York DST gap
us_east <- naive_time_parse("2021-03-14 01:59:59") %>%
  as_zoned_time("America/New_York")

london <- zoned_time_set_zone(us_east, "Europe/London")

times_14th <- tibble(us_east = us_east, london = london)

# Currently US is 5 hours behind London
times_14th
#> # A tibble: 1 x 2
#>   us_east                        london                     
#>   <zt<second><America/New_York>> <zt<second><Europe/London>>
#> 1 2021-03-14 01:59:59-05:00      2021-03-14 06:59:59+00:00

# Add 1 second (using sys-time to jump over the DST gap)
# to see that the US becomes 4 hours behind London
times_14th %>%
  mutate(
    us_east = us_east %>%
      as_sys_time() %>%
      add_seconds(1) %>%
      as_zoned_time(zoned_time_zone(us_east)),
    london = london %>%
      as_sys_time() %>%
      add_seconds(1) %>%
      as_zoned_time(zoned_time_zone(london))
  )
#> # A tibble: 1 x 2
#>   us_east                        london                     
#>   <zt<second><America/New_York>> <zt<second><Europe/London>>
#> 1 2021-03-14 03:00:00-04:00      2021-03-14 07:00:00+00:00

# Now let's investigate the 28th.
# We'll take the London time and shift it right before London's DST gap.
# First, we compute the naive-time that corresponds to the moment before the gap.
# Naive-times have a yet-to-be-specified time zone, and can be manipulated
# without fear of hitting any time zone related problems.
times_nt <- times_14th %>%
  mutate(
    london_nt = london %>%
      as_naive_time() %>%
      add_days(14) %>%
      add_hours(-6)
  )

times_nt
#> # A tibble: 1 x 3
#>   us_east                        london                      london_nt          
#>   <zt<second><America/New_York>> <zt<second><Europe/London>> <tp<naive><second>>
#> 1 2021-03-14 01:59:59-05:00      2021-03-14 06:59:59+00:00   2021-03-28 00:59:59

# Now that we have the right naive-time, we'll convert it back to London time,
# and then use that to figure out the equivalent US East time
times_28th <- times_nt %>%
  mutate(
    london = as_zoned_time(london_nt, zoned_time_zone(london)),
    us_east = zoned_time_set_zone(london, zoned_time_zone(us_east)),
  ) %>%
  select(-london_nt)

# At this point, US East is still 4 hours behind London,
# since London hasn't experience their DST gap yet
times_28th
#> # A tibble: 1 x 2
#>   us_east                        london                     
#>   <zt<second><America/New_York>> <zt<second><Europe/London>>
#> 1 2021-03-27 20:59:59-04:00      2021-03-28 00:59:59+00:00

# 1 second later, London has a DST gap,
# meaning that the US is again 5 hours behind London time
times_28th %>%
  mutate(
    us_east = us_east %>%
      as_sys_time() %>%
      add_seconds(1) %>%
      as_zoned_time(zoned_time_zone(us_east)),
    london = london %>%
      as_sys_time() %>%
      add_seconds(1) %>%
      as_zoned_time(zoned_time_zone(london))
  )
#> # A tibble: 1 x 2
#>   us_east                        london                     
#>   <zt<second><America/New_York>> <zt<second><Europe/London>>
#> 1 2021-03-27 21:00:00-04:00      2021-03-28 02:00:00+01:00



# To be a bit more illustrative, we'll create a plot that shows the changes

# First we'll generate bounds representing the first and last minute of March.
# When working with individual components, you generally use calendar types like
# this year-month-day type.
bounds <- tibble(
  first = year_month_day(2021, 03, 01, 00, 00),
  last = first %>%
    set_day("last") %>%
    set_hour(23) %>%
    set_minute(59)
)

bounds
#> # A tibble: 1 x 2
#>   first            last            
#>   <ymd<minute>>    <ymd<minute>>   
#> 1 2021-03-01 00:00 2021-03-31 23:59

# We are going to generate a minutely sequence, which requires us to convert
# our bounds to naive-time
bounds <- bounds %>%
  mutate(
    first = as_naive_time(first),
    last = as_naive_time(last)
  )

bounds
#> # A tibble: 1 x 2
#>   first               last               
#>   <tp<naive><minute>> <tp<naive><minute>>
#> 1 2021-03-01 00:00    2021-03-31 23:59

minutes <- seq(
  from = bounds$first,
  to = bounds$last,
  by = duration_minutes(1)
)

# All the minutes in March - without a specified time zone
head(minutes)
#> <time_point<naive><minute>[6]>
#> [1] "2021-03-01 00:00" "2021-03-01 00:01" "2021-03-01 00:02" "2021-03-01 00:03"
#> [5] "2021-03-01 00:04" "2021-03-01 00:05"

# For plots, we are going to need POSIXct's since clock doesn't provide
# any ggplot2 helpers yet.
# Converting this directly to a POSIXct (or zoned-time) with America/New_York
# as a time zone won't work
try(as.POSIXct(minutes, "America/New_York"))
#> Error : Nonexistent time due to daylight saving time at location 18841. Resolve nonexistent time issues by specifying the `nonexistent` argument.

# Ah, this is the exact time where the DST gap is happening in New York.
# The 2 o'clock hour doesn't exist there!
minutes[18841]
#> <time_point<naive><minute>[1]>
#> [1] "2021-03-14 02:00"

# To resolve this, we will just use a nonexistent time resolution strategy
# that "rolls forward" to the next valid time in that time zone
df_minutes <- tibble(
  minutes = minutes,
  us_east = as.POSIXct(minutes, "America/New_York", nonexistent = "roll-forward"),
  london = date_set_zone(us_east, "Europe/London")
)

# This results in some repeated times, but that won't matter for what we are doing
df_minutes[18840:18845,]
#> # A tibble: 6 x 3
#>   minutes             us_east             london             
#>   <tp<naive><minute>> <dttm>              <dttm>             
#> 1 2021-03-14 01:59    2021-03-14 01:59:00 2021-03-14 06:59:00
#> 2 2021-03-14 02:00    2021-03-14 03:00:00 2021-03-14 07:00:00
#> 3 2021-03-14 02:01    2021-03-14 03:00:00 2021-03-14 07:00:00
#> 4 2021-03-14 02:02    2021-03-14 03:00:00 2021-03-14 07:00:00
#> 5 2021-03-14 02:03    2021-03-14 03:00:00 2021-03-14 07:00:00
#> 6 2021-03-14 02:04    2021-03-14 03:00:00 2021-03-14 07:00:00

# We are going to end up plotting the number of hours that the US currently
# lags London on the y-axis, and the current time on the x-axis. We'll compute
# that as the difference in UTC offsets between London and US East. To get
# the UTC offset, we can use the low level `sys_time_info()` function. The
# details don't matter too much here, here is a helper to get the UTC offset
# in hours:
pull_offset <- function(x) {
  x %>%
    as_sys_time() %>%
    sys_time_info(date_zone(x)) %>%
    pull(offset) %>%
    duration_floor("hour") %>%
    as.integer()
}

df_minutes <- df_minutes %>%
  select(-minutes) %>%
  mutate(
    us_east_offset = pull_offset(us_east),
    london_offset = pull_offset(london),
    diff = factor(as.character(london_offset - us_east_offset), levels = c("4", "5"))
  )

df_minutes
#> # A tibble: 44,640 x 5
#>    us_east             london              us_east_offset london_offset diff 
#>    <dttm>              <dttm>                       <int>         <int> <fct>
#>  1 2021-03-01 00:00:00 2021-03-01 05:00:00             -5             0 5    
#>  2 2021-03-01 00:01:00 2021-03-01 05:01:00             -5             0 5    
#>  3 2021-03-01 00:02:00 2021-03-01 05:02:00             -5             0 5    
#>  4 2021-03-01 00:03:00 2021-03-01 05:03:00             -5             0 5    
#>  5 2021-03-01 00:04:00 2021-03-01 05:04:00             -5             0 5    
#>  6 2021-03-01 00:05:00 2021-03-01 05:05:00             -5             0 5    
#>  7 2021-03-01 00:06:00 2021-03-01 05:06:00             -5             0 5    
#>  8 2021-03-01 00:07:00 2021-03-01 05:07:00             -5             0 5    
#>  9 2021-03-01 00:08:00 2021-03-01 05:08:00             -5             0 5    
#> 10 2021-03-01 00:09:00 2021-03-01 05:09:00             -5             0 5    
#> # … with 44,630 more rows


# You can get a high level view of the impact of the two DST gaps with this
# okay ish chart. You can see the switch from 5->4 in mid-march, then back
# from 4->5 in late-march.
df_minutes %>%
  ggplot(aes(x = us_east, y = diff)) +
  geom_point() +
  theme_minimal() +
  labs(
    title = "# Hours that London is ahead of US East in March, 2021",
    y = "# Hours ahead",
    x = "Date (America/New_York)"
  )

# It is slightly more informative if we zoom in to each individual gap

# A nice helper to display the current hour in both time zones
label_dual_time <- function(x) {
  paste0(
    date_format(x, format = "US: %H"),
    "\n",
    date_format(date_set_zone(x, "Europe/London"), format = "EU: %H")
  )
}

# The America/New_York DST gap on the 14th
# US hour jumped from 1 -> 3
# EU hour had no jump from 6 -> 7
df_minutes %>%
  filter(
    get_day(us_east) == 14,
    get_hour(us_east) %in% c(1, 3)
  ) %>%
  ggplot(aes(x = us_east, y = diff)) +
  geom_point() +
  theme_minimal() +
  scale_x_datetime(breaks = "1 hour", labels = label_dual_time) +
  theme(panel.grid.minor.x = element_blank()) +
  labs(
    title = "Zooming in to March 14th (America/New_York DST gap)",
    x = "Current hour",
    y = "# Hours ahead"
  )

# The Europe/London DST gap on the 28th
# US hour had no jump from 20 -> 21
# EU hour jumped from 0 -> 2
df_minutes %>%
  filter(
    get_day(london) == 28,
    get_hour(london) %in% c(0, 2)
  ) %>%
  ggplot(aes(x = us_east, y = diff)) +
  geom_point() +
  theme_minimal() +
  scale_x_datetime(breaks = "1 hour", labels = label_dual_time) +
  theme(panel.grid.minor.x = element_blank()) +
  labs(
    title = "Zooming in to March 28th (Europe/London DST gap)",
    x = "Current hour",
    y = "# Hours ahead"
  )

Created on 2021-03-10 by the reprex package (v1.0.0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment