Creating a new calendar

Rob J Hyndman

20 July 2025

library(calcal)

Dates in the calcal package are stored as rdvec objects which are simply vectors of Rata Die (RD) integer values, denoting the number of days since the onset of Monday 1 January 1 CE on the Gregorian calendar. A calendar is attached to each rdvec vector as an attribute, which allows for the conversion of the RD values to the desired calendar format when required. Converting from one calendar to another is simply a matter of changing the calendar attribute of the vector.

A calendar object is an object of class calendar which is a list containing the following elements:

For example, on the Gregorian calendar, here is the relevant function to convert from a Gregorian date to RD. The date argument is a list containing the year, month, and day components of the date.

gregorian_to_rd <- function(date) {
  result <- 365 * (date$year - 1) + # Ordinary days since day 0 to start of year
    (date$year - 1) %/% 4 - # Adjust for leap years
    (date$year - 1) %/% 100 + # Remove century leap years
    (date$year - 1) %/% 400 + # Add back 400-year leap years
    (367 * date$month - 362) %/% 12 # Add days in prior months this year
  # Adjust if a leap year
  adjustment <- (date$month > 2) * (leap_year(date$year) - 2)
  # Add days in current month
  result + adjustment + date$day
}
leap_year <- function(year) {
  (year %% 4 == 0) & !(year %% 400 %in% c(100, 200, 300))
}

To go the other way, from RD to a Gregorian date, we need to calculate the year, month, and day from the RD value. This is done by calculating the year first, then determining how many days have passed since the start of that year, and finally calculating the month and day based on that.

rd_to_gregorian <- function(rd) {
  # Calculate the year
  d0 <- rd - 1
  n400 <- d0 %/% 146097 # Completed 400-year cycles
  d1 <- d0 %% 146097 # Prior days not in n400
  n100 <- d1 %/% 36524 # 100-year cycles not in n400
  d2 <- d1 %% 36524 # Prior days not in n400 or n100
  n4 <- d2 %/% 1461 # 4-year cycles not in n400 or n100
  d3 <- d2 %% 1461 # Prior days not in n400, n100, or n4
  n1 <- d3 %/% 365 # Years not in n400, n100, or n4
  year <- 400 * n400 + 100 * n100 + 4 * n4 + n1
  # leap year adjustment
  year <- year + !(n100 == 4 | n1 == 4)
  # Calculate the month
  jan1 <- gregorian_to_rd(list(year = year, month = 1, day = 1))
  mar1 <- gregorian_to_rd(list(year = year, month = 3, day = 1))
  correction <- (rd >= mar1) * (2 - leap_year(year))
  month <- (12 * (rd - jan1 + correction) + 373) %/% 367
  # Calculate the day by subtraction
  day1_of_month <- gregorian_to_rd(list(year = year, month = month, day = 1))
  day <- 1 + rd - day1_of_month
  # Return the dates as a list
  list(year = year, month = month, day = day)
}

The next function we need validates the granularities of the calendar. For the Gregorian calendar, the following function is used:

validate_gregorian <- function(date) {
  if (any(date$month < 1 | date$month > 12, na.rm = TRUE)) {
    stop("month must be between 1 and 12")
  } else if (any(date$day > 30 & date$month %in% c(4, 6, 9, 11), na.rm = TRUE)) {
    stop("day must be between 1 and 30")
  } else if (any(date$day > 29 & date$month == 2, na.rm = TRUE)) {
    stop("days in February must be between 1 and 29")
  } else if (any(date$day > 28 & date$month == 2 & leap_year(date$year), na.rm = TRUE)) {
    stop("days in February must be between 1 and 28 when not a leap year")
  } else if (any(date$day < 1 | date$day > 31, na.rm = TRUE)) {
    stop("day must be between 1 and 31")
  }
}

Finally, we need a function to format the date as a character vector. For the Gregorian calendar, this can be done as follows:

format_gregorian <- function(rd) {
  date <- rd_to_gregorian(rd)
  date[["year"]] <- sprintf("%02d", date[["year"]])
  date[["month"]] <- month.name[date[["month"]]]
  date[["day"]] <- sprintf("%02d", date[["day"]])
  paste(date[["year"]], date[["month"]], date[["day"]], sep = "-")
}

To create a new Gregorian calendar object, we use the new_calendar function:

Gcal <- new_calendar(
  name = "Gregorian",
  short_name = "G",
  granularities = c("year", "month", "day"),
  validate_granularities = validate_gregorian,
  format = format_gregorian,
  from_rd = rd_to_gregorian,
  to_rd = gregorian_to_rd
)

Then we can use the Gcal calendar to create rdvec vectors:

as_date("2026-01-01", calendar = Gcal) + 0:10
#> <Gregorian[11]>
#>  [1] 2026-January-01 2026-January-02 2026-January-03 2026-January-04
#>  [5] 2026-January-05 2026-January-06 2026-January-07 2026-January-08
#>  [9] 2026-January-09 2026-January-10 2026-January-11
nd <- new_date(year = 2025, month = 7, day = 18:24, calendar = Gcal)
nd
#> <Gregorian[7]>
#> [1] 2025-July-18 2025-July-19 2025-July-20 2025-July-21 2025-July-22
#> [6] 2025-July-23 2025-July-24
tibble::tibble(
  greg = nd,
  RD = as.integer(nd)
)
#> # A tibble: 7 × 2
#>           greg     RD
#>            <G>  <dbl>
#> 1 2025-July-18 739450
#> 2 2025-July-19 739451
#> 3 2025-July-20 739452
#> 4 2025-July-21 739453
#> 5 2025-July-22 739454
#> 6 2025-July-23 739455
#> 7 2025-July-24 739456

Existing helper functions will also work with the new calendar. For example:

granularity_names(Gcal)
#> [1] "year"  "month" "day"
granularity(nd, "day")
#> [1] 18 19 20 21 22 23 24
day_of_week(nd)
#> [1] "Friday"    "Saturday"  "Sunday"    "Monday"    "Tuesday"   "Wednesday"
#> [7] "Thursday"
week_of_year(nd)
#> [1] 29 29 29 30 30 30 30
month_of_year(nd)
#> [1] 7 7 7 7 7 7 7

The dates on this new calendar can be converted to any other defined calendar. For example:

as_date(nd, calendar = cal_hebrew)
#> <hebrew[7]>
#> [1] 5785-Tammuz-22 5785-Tammuz-23 5785-Tammuz-24 5785-Tammuz-25 5785-Tammuz-26
#> [6] 5785-Tammuz-27 5785-Tammuz-28
as_iso(nd)
#> <iso[7]>
#> [1] 2025-29-05 2025-29-06 2025-29-07 2025-30-01 2025-30-02 2025-30-03 2025-30-04

If you want to regularly convert dates on other calendars to your new calendar, you can create an as_<calendar_name> function. For example:

as_Gregorian <- function(date) {
  as_date(date, calendar = Gcal)
}
as_Gregorian("2026-01-01")
#> <Gregorian[1]>
#> [1] 2026-January-01