Holidays and calendars

library(almanac)

# A rule for weekends
on_weekends <- weekly() %>%
  recur_on_weekends()

almanac provides low-level tools for building all manner of recurrence rules, but it also provides a fairly high-level set of tools for working with holidays and calendars. These are arguably the most useful part of almanac, because they do a lot of the heavy lifting of defining your own holidays and observance rules for you.

Holidays

There are a number of pre-generated holidays in almanac, which all start with hol_*(), such as hol_christmas(). If the holiday is specific to a country, then it is also prefixed as such, like hol_us_veterans_day() (currently only global and US holidays are provided).

A holiday object is just another type of recurrence schedule:

on_christmas <- hol_christmas()
on_christmas
#> <Christmas>
#>  <rrule>
#>  • frequency: yearly
#>  • range: [1900-01-01, 2100-01-01]
#>  • month of year: Dec
#>  • day of month: 25

This means that you can use it with any of the alma_*() functions you might have learned about in the other vignettes.

alma_events(on_christmas, year = 2020:2025)
#> [1] "2020-12-25" "2021-12-25" "2022-12-25" "2023-12-25" "2024-12-25"
#> [6] "2025-12-25"

You’ll use these holiday objects to build up a calendar, which is a bundle of individual holidays that represent the rules of your specific business.

There are 3 helpers that are specific to holidays:

hol_observe()

hol_observe() adjusts a holiday to respect observance rules that might be set by your company. For example, if Christmas falls on Saturday, then your company may actually observe it on the preceding Friday or following Monday.

on_christmas <- hol_christmas() %>%
  hol_observe(adjust_on = on_weekends, adjustment = adj_nearest)

on_christmas
#> <Christmas>
#>  <radjusted>
#>   adjust:
#>   <rrule>
#>   • frequency: yearly
#>   • range: [1900-01-01, 2100-01-01]
#>   • month of year: Dec
#>   • day of month: 25
#>   adjust on:
#>   <rrule>
#>   • frequency: weekly
#>   • range: [1900-01-01, 2100-01-01]
#>   • day of week: Sat, and Sun

If you’ve read vignette("adjust-and-shift"), then you’ll recognize that observances look similar to radjusted(), and in fact that is the tooling that is used under the hood.

If we run our updated holiday through alma_events() again, you’ll see that it now rolls to the nearest weekday if Christmas falls on a weekend.

alma_events(on_christmas, year = 2020:2025)
#> [1] "2020-12-25" "2021-12-24" "2022-12-26" "2023-12-25" "2024-12-25"
#> [6] "2025-12-25"

hol_offset()

hol_offset() allows you to create holidays that are relative to some other holiday. For example, Good Friday is always the Friday before Easter.

Let’s take a look at building a holiday for Boxing Day, the day after Christmas. hol_offset() retains the name of the original holiday, so we’ll also need hol_rename() to rename it to Boxing Day:

on_boxing_day <- hol_christmas() %>%
  hol_offset(by = 1) %>%
  hol_rename("Boxing Day")

on_boxing_day
#> <Boxing Day>
#>  <roffset[by = 1]>
#>   <rrule>
#>   • frequency: yearly
#>   • range: [1900-01-01, 2100-01-01]
#>   • month of year: Dec
#>   • day of month: 25
alma_events(on_boxing_day, year = 2020:2025)
#> [1] "2020-12-26" "2021-12-26" "2022-12-26" "2023-12-26" "2024-12-26"
#> [6] "2025-12-26"

Great! Now what if we wanted to add an observance rule to Boxing Day? This is actually pretty complicated, because it is dependent on the observance rules for Christmas. For example, if Christmas falls on a Saturday, but is observed on Friday, then we probably want Boxing Day to be observed on the following Monday. If Christmas falls on a Sunday and is observed on the following Monday, then Boxing Day would be observed on the following Tuesday. Luckily you can layer hol_observe() with hol_offset() to build up a recurrence schedule that matches the rules we are looking for:

on_christmas <- hol_christmas() %>%
  hol_observe(adjust_on = on_weekends, adjustment = adj_nearest)

on_boxing_day <- hol_christmas() %>%
  hol_observe(adjust_on = on_weekends, adjustment = adj_nearest) %>%
  hol_offset(by = 1) %>%
  hol_observe(adjust_on = on_weekends, adjustment = adj_following) %>%
  hol_rename("Boxing Day")
df <- data.frame(
  christmas = alma_events(on_christmas, year = 2020:2025),
  boxing_day = alma_events(on_boxing_day, year = 2020:2025)
)
df$christmas_weekday <- lubridate::wday(df$christmas, label = TRUE)
df$boxing_day_weekday <- lubridate::wday(df$boxing_day, label = TRUE)

df
#>    christmas boxing_day christmas_weekday boxing_day_weekday
#> 1 2020-12-25 2020-12-28               Fri                Mon
#> 2 2021-12-24 2021-12-27               Fri                Mon
#> 3 2022-12-26 2022-12-27               Mon                Tue
#> 4 2023-12-25 2023-12-26               Mon                Tue
#> 5 2024-12-25 2024-12-26               Wed                Thu
#> 6 2025-12-25 2025-12-26               Thu                Fri

Custom holidays

almanac doesn’t attempt to provide a comprehensive set of holidays. Instead, it makes it easy for you to create your own using rholiday(). All you need is a recurrence rule that aligns with the holiday date and a name. Let’s create one for Canada Day, which occurs on July 1st each year.

hol_canada_day <- function(since = NULL, until = NULL) {
  out <- yearly(since = since, until = until)
  out <- recur_on_month_of_year(out, "July")
  out <- recur_on_day_of_month(out, 1L)
  
  rholiday(rschedule = out, name = "Canada Day")
}
hol_canada_day()
#> <Canada Day>
#>  <rrule>
#>  • frequency: yearly
#>  • range: [1900-01-01, 2100-01-01]
#>  • month of year: Jul
#>  • day of month: 1

alma_next(as.Date("2019-01-01"), hol_canada_day())
#> [1] "2019-07-01"

Calendars

Holidays are great for one off operations, but typically if you are building a business calendar then you’ll need to bundle multiple holidays together. To do that, you’ll need a calendar object. You can create one by providing holidays to rcalendar():

cal <- rcalendar(
  hol_christmas(),
  hol_new_years_day(),
  hol_canada_day()
)

cal
#> <rcalendar[3]>
#> • Christmas
#> • New Year's Day
#> • Canada Day

Like holidays, calendars are recurrence schedules that work with any of the alma_*() functions, but they also have their own special cal_*() API.

cal_events()

If you’d like to generate the holidays for a particular year, then you should use cal_events():

cal_events(cal, year = 2023)
#>             name       date
#> 1 New Year's Day 2023-01-01
#> 2     Canada Day 2023-07-01
#> 3      Christmas 2023-12-25

This is similar to alma_events(), but there are two big differences:

Note that we didn’t add any observance rules into our original calendar, let’s go back and adjust our holidays to roll off weekends:

cal <- rcalendar(
  hol_christmas() %>%
    hol_observe(adjust_on = on_weekends, adjustment = adj_nearest),
  hol_new_years_day() %>%
    hol_observe(adjust_on = on_weekends, adjustment = adj_nearest),
  # Canada normally rolls their holidays forward to the following Monday
  hol_canada_day() %>%
    hol_observe(adjust_on = on_weekends, adjustment = adj_following)
)

Note that the holidays generated by cal_events() are different from the previous ones because they now respect the observance rules when a holiday falls on a weekend.

cal_events(cal, year = 2023)
#>             name       date
#> 1 New Year's Day 2023-01-02
#> 2     Canada Day 2023-07-03
#> 3      Christmas 2023-12-25

There is one more observance related feature worth pointing out. Let’s take a look at holidays for 2011:

cal_events(cal, year = 2011)
#>             name       date
#> 1 New Year's Day 2010-12-31
#> 2     Canada Day 2011-07-01
#> 3      Christmas 2011-12-26

New Year’s Day fell on a Saturday that year, so it was adjusted backwards to the preceding Friday, which actually fell in 2010. cal_events() is smart enough to know that if you are requesting “2011’s holidays,” then New Year’s Day should probably be included even if it was observed in a different year.

If you don’t want this behavior, you can set observed = TRUE to use the observed year when filtering for the year.

# New Year's Day is gone
cal_events(cal, year = 2011, observed = TRUE)
#>         name       date
#> 1 Canada Day 2011-07-01
#> 2  Christmas 2011-12-26

# And is now listed twice here
cal_events(cal, year = 2010, observed = TRUE)
#>             name       date
#> 1 New Year's Day 2010-01-01
#> 2     Canada Day 2010-07-01
#> 3      Christmas 2010-12-24
#> 4 New Year's Day 2010-12-31

cal_match()

If you’ve ever needed to determine if a date is a holiday, then you’ve likely reached for alma_in(), which works like %in% and returns a logical vector. But if you also need to determine which holiday that date corresponded to, then you’ll need to use cal_match() instead:

x <- as.Date(c(
  "2019-12-25",
  "2019-12-26",
  "2010-12-31",
  "2011-01-01"
))

data.frame(
  x = x,
  name = cal_match(x, cal)
)
#>            x           name
#> 1 2019-12-25      Christmas
#> 2 2019-12-26           <NA>
#> 3 2010-12-31 New Year's Day
#> 4 2011-01-01           <NA>

Note that because our calendar has observance rules baked in, it shows 2010-12-31 (a Friday) as New Year’s Day rather than the 1st of the year (a Saturday).

US federal calendar

almanac comes with one pre-built calendar, cal_us_federal(), which contains the federally recognized holidays in the United States. It is meant to serve as an example of what you can build with almanac, but it is also pretty useful on its own:

cal_us_federal()
#> <rcalendar[11]>
#> • New Year's Day
#> • US Martin Luther King Jr. Day
#> • US Presidents' Day
#> • US Memorial Day
#> • US Juneteenth
#> • US Independence Day
#> • US Labor Day
#> • US Indigenous Peoples' Day
#> • US Veterans Day
#> • US Thanksgiving
#> • Christmas
cal_events(cal_us_federal(), year = 2023)
#>                             name       date
#> 1                 New Year's Day 2023-01-02
#> 2  US Martin Luther King Jr. Day 2023-01-16
#> 3             US Presidents' Day 2023-02-20
#> 4                US Memorial Day 2023-05-29
#> 5                  US Juneteenth 2023-06-19
#> 6            US Independence Day 2023-07-04
#> 7                   US Labor Day 2023-09-04
#> 8     US Indigenous Peoples' Day 2023-10-09
#> 9                US Veterans Day 2023-11-10
#> 10               US Thanksgiving 2023-11-23
#> 11                     Christmas 2023-12-25

Note that this calendar doesn’t claim to be historically accurate, it is only intended to be a representation of the current federally recognized holidays. You can build historically accurate calendars (i.e. you can represent that Juneteenth wasn’t celebrated before 2021) it just takes a bit more effort.