Skip to contents

This vignette documents the extension points made available in mixtime for creating new calendars and time units. As a running example, we will build the Symmetry454 calendar, a perpetual solar calendar where every year has 12 months arranged in a 4–5–4 week pattern (four weeks, five weeks, four weeks per month), with an occasional 53rd week added to December in leap years. This implementation closely matches the implementation of mixtime::cal_sym454.

The extension points covered are:

The mixtime package uses S7 for all extension methods. If you are new to S7, the vignette on generics and methods is a good place to start. It is best to develop new calendars and time units in a separate package that imports mixtime, and registering your methods with the following .onLoad hook:

.onLoad <- function(...) {
  S7::methods_register()
}

Time units

Time units are the building blocks of calendars. Time units are S7 classes that inherit mt_unit, a purely abstract time unit class.

In practice, most time units should inherit either:

  • mt_tz_unit for civil-time units whose boundaries depend on a timezone (e.g. months, days, hours, …), or
  • mt_loc_unit for astronomical-time units whose boundaries depend on geographic location (e.g. solar days).

All time units accept a numeric .data argument representing the size of the unit (e.g. 1L for “one month”). Additional arguments needed for your time unit can be defined with S7 by:

  • inheriting properties from mt_tz_unit (tz IANA names) or mt_loc_unit (for lon, lat, and alt),
  • defining custom S7 properties with S7::new_class(properties = ...).

The name of a time unit’s S7 class becomes the internal identifier for that unit, so it should be unique across all calendars. The class name is not important to users of the calendar, but it should be descriptive for developers.

# Timezone-aware Symmetry454 year and month units
S7::new_class("tu_symmetry454_year",  parent = mt_tz_unit)
#> <tu_symmetry454_year> class
#> @ parent     : <mixtime::mt_tz_unit>
#> @ constructor: function(.data, tz) {...}
#> @ validator  : <NULL>
#> @ properties :
#>  $ tz: <character>
S7::new_class("tu_symmetry454_month", parent = mt_tz_unit)
#> <tu_symmetry454_month> class
#> @ parent     : <mixtime::mt_tz_unit>
#> @ constructor: function(.data, tz) {...}
#> @ validator  : <NULL>
#> @ properties :
#>  $ tz: <character>

These time units are typically exported within a calendar object (shown below) and accessed via $ notation.

Calendars

A calendar is a named collection of time units, created with new_calendar(). An optional inherit argument pulls in all units from an existing calendar (units defined in ... take precedence). It is useful to inherit from a base calendar that defines common time units for civil and astronomical calendars. The base calendars provided by mixtime are:

  • cal_time_civil_midnight: civil time units anchored at midnight (e.g. days, hours, minutes, seconds, …)
  • cal_time_solar_***: solar time units anchored at sunrise, noon, sunset or midnight (e.g. solar days)
  • cal_time_lunar: lunar time units (e.g. lunar months, lunar phases)
cal_symmetry454 <- new_calendar(
  year  = S7::new_class("tu_symmetry454_year",  parent = mt_tz_unit),
  month = S7::new_class("tu_symmetry454_month", parent = mt_tz_unit),
  week  = cal_isoweek$week,
  # Inherit civil-time units (day, hour, minute, second, ...)
  inherit = cal_time_civil_midnight,
  class = "cal_symmetry454"
)

cal_symmetry454
#> <cal_symmetry454>
#> Time units:
#>   - year
#>   - month
#>   - week
#>   - day
#>   - ampm
#>   - hour
#>   - minute
#>   - second
#>   - millisecond

In this case, the time unit for weeks exactly matches the ISO week unit, so we can reuse the existing cal_isoweek$week unit rather than defining a new one. This is because our implementation below of the Symmetry454 calendar has the same week structure as the ISO calendar (same epoch, 7-day length, and Monday start). If your calendar unit shares the same name but has a different structure (e.g. a month comprised of 4 or 5 weeks), a new time unit with a unique S7 class must be defined (similar to the year and month units above).

Unit constructors are now accessible via $:

cal_symmetry454$year(1L)
#> <tu_symmetry454_year> int 1
#>  @ tz: chr ""
cal_symmetry454$month(1L, tz = "UTC")
#> <tu_symmetry454_month> int 1
#>  @ tz: chr "UTC"

These time units can be used as chronons/cycles within mixtime() (and also linear_time(), cyclical_time()). The temporal relationships between these units must be defined by the calendar arithmetic methods described below.

Calendar arithmetic

Two S7 generics drive all calendar calculations: chronon_cardinality() and chronon_divmod(). These functions are used to manipulate time, such as converting between units, generating sequences, and comparing time points. The details of how they work are not important for users of the calendar, but they are the main extension points for developers creating new calendars.

For a time unit to be useful in mixtime, at minimum a chronon_cardinality() method between itself and the next coarser unit must be defined. Time units with irregular relationships (e.g. days → months) also require chronon_divmod() to efficiently compute relationships between units.

Defining these methods for time units between different calendars is also possible, which allows for interoperability between calendars. In most cases, calendars will share a common base calendar (e.g. cal_time_civil_midnight) which provides a common time unit (e.g. civil days) or methods for converting between different calendars (e.g. civil midnight to solar midnight).

Cardinality

The cardinality is the number of finer units that fit inside a coarser unit. For example, there are 7 days in a week, so the cardinality of cal_isoweek$day(1L) in terms of cal_isoweek$week(1L) is 7.

chronon_cardinality(cal_isoweek$day(1L), cal_isoweek$week(1L))
#> [1] 7

For units with a fixed relationship (e.g. 7 days per week) the at argument can be ignored. The at variable disambiguates the point in time for variable cardinalities (e.g. days per month), at is the internal numeric representation of time in the coarser unit (e.g. months since epoch for days → months).

chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 0L) # Jan 1970
#> [1] 31
chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 1L) # Feb 1970
#> [1] 28
chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 25L) # Feb 1972 (leap year)
#> [1] 29

In the Symmetry454 calendar every year has exactly 12 months (a fixed relationship).

# Each Symmetry454 year has 12 months
S7::method(chronon_cardinality, list(cal_symmetry454$month, cal_symmetry454$year)) <-
  function(x, y, at = NULL) {
    vctrs::vec_data(y) * 12L / vctrs::vec_data(x)
  }
chronon_cardinality(cal_symmetry454$month(1L), cal_symmetry454$year(1L))
#> [1] 12

The number of weeks in each month follows the repeating 4–5–4 pattern across each quarter (months 1–3: 4, 5, 4 weeks; months 4–6: 4, 5, 4 weeks; and so on). The circsum() helper computes how many weeks fall in an n-month period by summing the appropriate slice of this cycle, which is especially useful when the month unit has a size greater than 1.

Symmetry454 uses a 293-year leap-week cycle: a year is a leap year (gaining a 53rd week in December) when (52 * year + 146) %% 293 < 52. This differs substantially from the familiar Gregorian leap-day rule and produces 52 leap years in every 293-year period.

S7::method(chronon_cardinality, list(cal_symmetry454$week, cal_symmetry454$month)) <-
  function(x, y, at = NULL) {
    # The number of weeks in each n-month period
    month_size <- vctrs::vec_data(y)
    nweeks_cyc <- circsum(c(4L, 5L, 4L), month_size)

    # Find which n-month period we're in based on the "at" position (months since epoch)
    period <- at %% length(nweeks_cyc) + 1L

    nweeks <- nweeks_cyc[period]

    # Add the extra week to December for Symmetry454 leap years.
    # A year is a leap year when (52*year + 146) %% 293 < 52.
    m1 <- at * month_size
    contains_dec <- which((m1 %% 12L) >= (12L - month_size))
    year <- 1970L + m1[contains_dec] %/% 12L
    is_leap_year <- ((52 * year + 146L) %% 293L) < 52L

    nweeks[contains_dec[is_leap_year]] <- nweeks[contains_dec[is_leap_year]] + 1L

    # Scale by the number of weeks in the week time unit
    nweeks / vctrs::vec_data(x)
  }

# The 4-5-4 cycle across a full Symmetry454 year
chronon_cardinality(cal_symmetry454$week(1L), cal_symmetry454$month(1L), at = 0:11)
#>  [1] 4 5 4 4 5 4 4 5 4 4 5 5

# 1970 is a leap year (53 weeks), so December has 5 weeks instead of 4
chronon_cardinality(cal_symmetry454$week(1L), cal_symmetry454$month(1L), at = 11L)
#> [1] 5

# Non-leap year: December has the usual 4 weeks
chronon_cardinality(cal_symmetry454$week(1L), cal_symmetry454$month(1L), at = 12:23) # Dec 1971
#>  [1] 4 5 4 4 5 4 4 5 4 4 5 4

# The number of weeks in a multi-month period is the sum of the weeks in each month
chronon_cardinality(cal_symmetry454$week(1L), cal_symmetry454$month(2L), at = 0:5)
#> [1]  9  8  9  9  8 10

Cardinality methods are only required for adjacent time units (e.g. month → year, week → month), but they can be defined for non-adjacent units as an optimization. If a direct cardinality method is not defined, mixtime will attempt to derive it by computing cardinality along a path of intermediate units.

# The number of weeks in a Symmetry454 year (derived from week → month → year)
# Note that the leap year in 1970 (at = 0) produces 53 weeks via week → month.
chronon_cardinality(cal_symmetry454$week(1L), cal_symmetry454$year(1L), at = 0:4)
#> [1] 53 52 52 52 52

# The number of days in a Symmetry454 year (derived from day → week → month → year)
chronon_cardinality(cal_symmetry454$day(1L), cal_symmetry454$year(1L), at = 0:4)
#> [1] 371 364 364 364 364

The cardinality methods also define which units are finer/coarser for operations requiring graph traversal, so it is essential that the first unit is the finer unit and the second unit is the coarser unit. The inverse cardinality method (with the units flipped) is automatically derived, so you should only define one direction (finer → coarser) of the relationship.

# The number of Symmetry454 years in a month is the inverse of months → years (1/12)
chronon_cardinality(cal_symmetry454$year(1L), cal_symmetry454$month(1L))
#> [1] 0.08333333

# The number of Symmetry454 months in 2 weeks (requires `at` since weeks → months is irregular)
chronon_cardinality(cal_symmetry454$month(1L), cal_symmetry454$week(2L), at = 0:4)
#> [1] 0.5 0.4 0.5 0.5 0.4

Divmod

The divmod operation is a combined division and modulus that converts between time units and is defined with chronon_divmod() methods. These methods are required to efficiently convert between units with irregular relationships, such as days → months. For regular relationships (e.g. days → weeks) the divmod is automatically derived from chronon_cardinality() methods alone.

chronon_divmod(from, to, x) converts times x in the from units into time points in to units. For example, the date “1970-02-15” is day 45 since epoch (x = 45 and from = cal_gregorian$day(1L)), converted to months (to = cal_gregorian$month(1L)) gives 1 month and 14 days (div = 1, mod = 14).

chronon_divmod(cal_gregorian$day(1L), cal_gregorian$month(1L), 45L)
#> $div
#> [1] 1
#> 
#> $mod
#> [1] 14

Methods for chronon_divmod() return a list with:

  • div - the quotient (chronons in to units)
  • mod - the remainder (leftover from chronons)

Note that all time values are zero-indexed, so $div = 0 is January 1970, and $mod = 0 is the first day of the month. So the above result can be interpreted as $div = 1 is February 1970 and $mod = 14 is 14 remaining days (i.e. the 15th).

The Symmetry454 calendar has an irregular cardinality for weeks → months (because of the 4–5–4 pattern and leap weeks), so a divmod method is required. The key challenge is correctly accounting for the 293-year leap year pattern. Symmetry454 leap weeks follow a structured sub-cycle pattern: 293 years decompose into five groups (45+79+45+79+45 years), each of which decomposes further into primary 17-year sub-cycles (with 3 leap years) and secondary 11-year sub-cycles (with 2 leap years). The majority of the following code uses this structure to efficiently count how many leap weeks have occurred before any given week number in a cycle.

S7::method(chronon_divmod, list(cal_symmetry454$week, cal_symmetry454$month)) <-
  function(from, to, x) {
    # Most of this code works on 1-week units
    week_size <- vctrs::vec_data(from)
    x <- x * week_size  # convert n-weeks to 1-weeks

    # 1. Account for leap weeks by regularising x to have a fixed 52 weeks per year

    # The symmetrical sub-cycles of the 293-year leap week cycle are:
    #   17+11+17 + 17+17+11+17+17 + 17+11+17 + 17+17+11+17+17 + 17+11+17
    #   = 45 + 79 + 45 + 79 + 45 = 293
    # Primary (length 17) sub-cycles have 3 leap years: 00100000100000100
    # Secondary (length 11) sub-cycles have 2 leap years: 00100000100
    leaps_cycle_17 <- function(w) (w >= 157L) + (w >= 470L) + (w >= 783L)
    leaps_cycle_11 <- function(w) (w >= 157L) + (w >= 470L)
    leaps_cycle_45 <- function(w) {
      ifelse(w < 887L,  leaps_cycle_17(w),
      ifelse(w < 1461L, 3L + leaps_cycle_11(w - 887L),
                        5L + leaps_cycle_17(w - 1461L)))
    }
    leaps_cycle_79 <- function(w) {
      ifelse(w < 887L,  leaps_cycle_17(w),
      ifelse(w < 1774L, 3L  + leaps_cycle_17(w - 887L),
      ifelse(w < 2348L, 6L  + leaps_cycle_11(w - 1774L),
      ifelse(w < 3235L, 8L  + leaps_cycle_17(w - 2348L),
                        11L + leaps_cycle_17(w - 3235L)))))
    }
    leaps_symmetry454 <- function(x) {
      # There are 15288 weeks in a full 293-year cycle (293*52 + 52 leap weeks)
      w <- x %% 15288L
      x %/% 15288L * 52L +
      ifelse(w < 2348L,  leaps_cycle_45(w),
      ifelse(w < 6470L,  8L  + leaps_cycle_79(w - 2348L),
      ifelse(w < 8818L,  22L + leaps_cycle_45(w - 6470L),
      ifelse(w < 12940L, 30L + leaps_cycle_79(w - 8818L),
                         44L + leaps_cycle_45(w - 12940L)))))
    }

    # Offset x to align with the nearest 293-year cycle boundary before the epoch.
    # There are 349 leap years between year 1-W1 and the 1970-W1 epoch, and the
    # nearest cycle start is (1969*52 + 349) %% (293*52 + 52) = 11009 weeks before epoch.
    x_cyc <- x + 11009L + week_size  # right align multi-week units
    n_leaps <- leaps_symmetry454(x_cyc)

    # Regularise x to have exactly 52 weeks per year by subtracting leap weeks.
    # (37 leap years occur in the cycle before the epoch, so we add 37 back.)
    x_reg <- x - n_leaps + 37L

    # 2. Use the 4-5-4 pattern to find the month (div) and week remainder (mod)
    ## The number of weeks in each n-month period
    month_size <- vctrs::vec_data(to)
    weeks_len <- circsum(c(4L, 5L, 4L), month_size)

    ## The total weeks in a full n-month cycle
    weeks_tot <- sum(weeks_len)

    ## Find which n-month cycle we're in based on the regularised week count
    period_full <- x_reg %/% weeks_tot

    ## Find which part within the n-month cycle we're in
    weeks_seq  <- cumsum(weeks_len[-length(weeks_len)])
    period_part <- rowSums(outer(x_reg %% weeks_tot, weeks_seq, ">="))

    # div: total complete n-month cycles + complete n-months within the current cycle
    div <- period_full * length(weeks_len) + period_part
    # mod: remaining (regularised) weeks within the current n-month period
    mod <- x_reg %% weeks_tot - c(0L, weeks_seq)[period_part + 1L]

    # 3. Adjust the remainder to account for leap weeks that were removed during regularisation.

    # Identify leap weeks re-using cumulative in-cycle leap week counts: leaps_symmetry454()
    # If the week added a leap week from the previous, then it itself must be a leap week.
    # Applied only to regularised 52/53rd weeks of the year (only these weeks can be leap weeks)
    last_weeks  <- which(x_reg %% 52L >= 51L)
    leap_weeks  <- last_weeks[n_leaps[last_weeks] - leaps_symmetry454(x_cyc[last_weeks] - week_size) > 0L]
    mod[leap_weeks] <- mod[leap_weeks] + 1L  # restore leap week to remainder

    # Scale mod back to the original n-unit week size
    mod <- mod %/% week_size

    # Return the divmod result
    list(div = div, mod = mod)
  }

The general strategy used in the above code is to first remove the complexity of the 293-year leap week cycle by regularising the week count to have a fixed 52 weeks per year (essentially subtracting the cumulative leap weeks from each cycle). This regularised week count is easier to work with the 4-5-4 pattern, which we use to find the converted month (div) as the number of complete n-month cycles plus the number of complete n-months within the current cycle, and the regularised week remainder (mod) as the remaining weeks within the current n-month period. The final step is to adjust the remainder to account for any leap weeks that were removed during regularisation.

# Week 19 (0-indexed) of 1970 is the 2nd week (div=1) of May 1970 (mod=4)
with(cal_symmetry454, chronon_divmod(week(1L), month(1L), 18L))
#> $div
#> [1] 4
#> 
#> $mod
#> [1] 1

# Week 52 (0-indexed) is the leap week of 1970; it is the 5th week (div=4) of Dec 1970 (mod=11)
with(cal_symmetry454, chronon_divmod(week(1L), month(1L), 52L))
#> $div
#> [1] 11
#> 
#> $mod
#> [1] 4

While most of this complication relates to the leap week pattern, chronon_divmod() methods are further complicated by the generality of converting between n-unit time granules (e.g. converting from 1 week to 2 month chronons). If this generality is not needed, your method can raise an error when the time unit is not size 1 (e.g. if (vctrs::vec_data(to) != 1L) stop("...")).

The inverse relationship must also be defined to convert from months → weeks. When converting from coarser → finer units, integer values for x introduce temporal indeterminacy. The convention for divmod methods is to left-align the conversion (use the first moment of the month). For brevity, the inverse divmod method is not shown here but can be found in the package’s cal_sym454 source code

# The 5th fortnight of 1970 is the 3rd fortnight (div=2) of Feb 1970 (mod=1)
with(cal_symmetry454, chronon_divmod(week(2L), month(1L), 4L))
#> $div
#> [1] 1
#> 
#> $mod
#> [1] 2

If a direct chronon_divmod() method is not defined for a pair of units, mixtime will attempt to derive it by chaining together divmod operations along the shortest path of intermediate units.

# Divmod for converting days → years is derived from days → weeks → months → years
# Gregorian day 839 since unix epoch (1972-04-19) is symmetry454 day 101 (mod=100) of year 1972 (div=2)
with(cal_symmetry454, chronon_divmod(day(1L), year(1L), 839L))
#> $div
#> [1] 2
#> 
#> $mod
#> [1] 100

Epoch anchors

The chronon_epoch() method defines the anchor point for a time unit, which is the reference point for all time calculations. The epoch is used as the zero point for time points, for example time point 0 in cal_gregorian$year(1L) corresponds to the year 1970. This anchor maps the internal numeric representation of time (e.g. years since epoch) to a real-world time point (e.g. “1970”), and is used as the origin for numeric inputs and labelling linear parts of time.

This implementation of the Symmetry454 calendar has the origin (t = 0) at 1970 W1, so cal_symmetry454$year(1L) has an epoch of 1970.

S7::method(chronon_epoch, cal_symmetry454$year) <- function(x) 1970L

Displaying time

The labelling methods control how time units and time points are displayed as text. They are used in messages, plot axes, and when formatting time vectors with format(). The formatting methods return format strings that are parsed by format_mixtime() to generate the final labels.

Time units

Each time unit has two format methods:

  • time_unit_full() returns the singular full name of the unit (e.g. “Symmetry454 year”)
  • time_unit_abbr() returns the short abbreviation of the unit (e.g. “Y”)

The full name is used in messages (e.g. “Can’t convert from Symmetry454 year to day”) and the abbreviation is used for displaying time intervals and durations.

S7::method(time_unit_full, cal_symmetry454$year)  <- function(x) "Symmetry454 year"
S7::method(time_unit_abbr, cal_symmetry454$year)  <- function(x) "Y"
S7::method(time_unit_full, cal_symmetry454$month) <- function(x) "Symmetry454 month"
S7::method(time_unit_abbr, cal_symmetry454$month) <- function(x) "M"

The time unit abbreviations are also used in the default time formatting string, so we can now create and view a Symmetry454 linear time vector:

linear_time(as.Date("1955-11-12"), chronon = cal_symmetry454$year(1L))
#> <mixtime[1]>
#> [1] Y1955

The linear time helper functions can also be used with the new calendar:

year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] Y1955

Cyclical time vectors can also be created with the new calendar, and the default formatting string will use the time unit abbreviations:

# Week of the month
cyclical_time(as.Date("1955-11-12"), chronon = week(1L), cycle = month(1L), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] W02

# Month of the year
month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] M10

Cyclical time labels default to 0-indexing, so the month of year is labelled as “10” for November. This can be improved by defining custom label methods as described in the next section.

Time labels

The time label functions describe how each granule of time (e.g. years, months, weeks) is displayed. There are two types of labels to consider: linear time labels are for continuous time units (usually the coarsest time unit, years), and cyclical time labels are for time units that nest (cycle over) another granule (e.g. months within a year, days within a week).

The linear_labels() and cyclical_labels() methods should return character vectors of the same length as the input time indices i.

While not enforced, we recommend using the following argument names for common label options:

  • label: if TRUE, return labelled values (e.g. “January”, “February”, …), otherwise return unlabelled values (e.g. “1”, “2”, …)
  • abbreviate: if TRUE (and label is TRUE), return abbreviated labels (e.g. “Jan”, “Feb”, …), otherwise return full labels (e.g. “January”, “February”, …)

Linear time labels: linear_labels()

In most cases, the default linear labels provided by linear_labels() are sufficient, which simply return the internal numeric representation of the time point (e.g. “0”, “1”, “2”, …). An example of when this default could be improved is by correctly labelling years about the era boundary (e.g. “2BC”, “1BC”, “1”, “2”, … instead of “-1”, “0”, “1”, “2”, …).

S7::method(linear_labels, cal_symmetry454$year) <- function(granule, i, ...) {
  ifelse(i <= 0L, paste0(-i + 1L, "BC"), i)
}

Then the transition from 1 BC to 1 AD is appropriately labelled:

year(-1:2, calendar = cal_symmetry454)
#> <mixtime[4]>
#> [1] Y2BC Y1BC Y1   Y2

We’ll remove the “Y” prefix from the year format strings with the time format methods below to make these labels more readable.

Cyclical time labels: cyclical_labels()

It is common to need custom labels for cyclical time units, since cyclical time points often have familiar labels (e.g. Jan, Feb, … for months in years) or are 1-indexed (e.g. 1, 2, … for days in a month). The default method for cyclical_labels() simply uses the internal 0-indexed numeric representation of the time point.

# Labels for months of the year, essentially the same as Gregorian months in years.
S7::method(cyclical_labels, list(cal_symmetry454$month, cal_symmetry454$year)) <-
  function(granule, cycle, i, label = FALSE, abbreviate = FALSE, ...) {
    if (label) {
      # Index into R's localised month name objects (month.name and month.abb)
      if (abbreviate) month.abb[i + 1L] else month.name[i + 1L]
    } else {
      # Use i + 1L for 1-indexing months (so January is 1, February is 2, ...)
      sprintf("%02d", i + 1L)
    }
  }

# Labels for weeks of the month are simply 1-indexed (e.g. "W1", "W2", ...)
S7::method(cyclical_labels, list(cal_symmetry454$week, cal_symmetry454$month)) <-
  function(granule, cycle, i, ...) {
    as.character(i + 1L)
  }

The other cyclical labels (e.g. day of week) are inherited from cal_isoweek since we reused the ISO week unit, so they are correctly labelled as days of the week (e.g. “Mon”, “Tue”, …).

# Month of year
month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] M11
# Week of month
cyclical_time(as.Date("1955-11-12"), chronon = cal_symmetry454$week(1L), cycle = cal_symmetry454$month(1L))
#> <mixtime[1]>
#> [1] W2

# Day of week (inherited from cal_isoweek)
day_of_week(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] Sat
# Day of year (inherited from cal_time_civil_midnight)
day_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] D363

With both linear and cyclical label methods in place, the formatting now produces correctly indexed and more informative labels for Symmetry454 time vectors:

date(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] 1955-11-13
yearweek(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] 1955 W52
yearmonth(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] M-170

These default formats can be improved to better suit the Symmetry454 calendar. For example, the week format of YYYY WW is inherited from cal_isoweek but YYYY-MM-WW is more informative for Symmetry454 since the weeks are nested within months. The default format for months is MM, but YYYY-MM is more informative. The time format methods below allow us to change the default formats for linear and cyclical time vectors.

Time formats

The default formatting for time vectors is defined by chronon_format_linear() for linear time and chronon_format_cyclical() for cyclical time. These methods return mixtime format strings which are used to format and print time vectors. A mixtime format string is a glue style string where time granules wrapped in {} are replaced with granule labels for the corresponding time points.

There are two types of granule labels useful for these time format strings:

  • lin(<time unit>) - Granule labels for linear time units, usually the largest granule (e.g. lin(year(1L))). These labels are produced by linear_labels().
  • cyc(<time unit>, <cycle unit>) - Granule labels for cyclical time units, where the labels are relative to the cycle (e.g. cyc(month(1L), year(1L)) is the month within the year). These labels are produced by cyclical_labels().

The <time unit> and <cycle unit> placeholders in the format string must evaluate to a time unit of a calendar. For convenience, unevaluated named time units (e.g. lin(year)) default to a size 1 time unit (e.g. lin(year(1L))), and the calendar is inferred from the time vector being formatted. It is also possible to explicitly specify the calendar (e.g. lin(cal_symmetry454$year(1L))), which allows time formats to use granules from different calendars.

Linear time formats: chronon_format_linear()

Linear time formats dispatch on the chronon and calendar. The calendar is needed to disambiguate identical time units that exist in multiple calendars. In this case, cal_symmetry454$day is common to most civil calendars, and cal_symmetry454$week shares the same implementation as cal_isoweek$week - in both cases a Symmetry 454 format method is needed to display dates appropriately.

# Simply display years as numbers (e.g. "1970", "1971", ...)
S7::method(
  chronon_format_linear,
  list(cal_symmetry454$year, S7::class_any)
) <- function(x, cal) "{lin(year(1L))}"

# Display labelled months as month within year (e.g. "1970 Jan", "1970 Feb", ...)
S7::method(
  chronon_format_linear,
  list(cal_symmetry454$month, S7::class_any)
) <- function(x, cal) "{lin(year(1L))}-{cyc(month(1L), year(1L), label=TRUE, abbreviate=TRUE)}"

# Display weeks as week in month in year (e.g. "1970-01-W1", "1970-01-W2", ...)
S7::method(
  chronon_format_linear,
  list(cal_symmetry454$week, S7::new_S3_class("cal_symmetry454"))
) <- function(x, cal) "{lin(year(1L))}-{cyc(month(1L), year(1L), label=TRUE, abbreviate=TRUE)}-W{cyc(week(1L), month(1L))}"

# Format days as day in week in month in year (e.g. "1970-Jan-W1-Mon", "1970-Jan-W1-Tue", ...)
S7::method(
  chronon_format_linear,
  list(cal_symmetry454$day, S7::new_S3_class("cal_symmetry454"))
) <- function(x, cal) "{lin(year(1L))}-{cyc(month(1L), year(1L), label=TRUE, abbreviate=TRUE)}-W{cyc(week(1L), month(1L))}-{cyc(day(1L), week(1L), label=TRUE, abbreviate=TRUE)}"

With these default time formatting strings in place, the linear time vectors now have more informative default labels:

# Years are formatted as YYYY
year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] 1955

# Months are formatted as YYYY Mon
yearmonth(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] 1955-Nov

# Weeks are formatted as YYYY-MM-WW
yearweek(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] 1955-Nov-W2

# Days are formatted as YYYY-MM-WW-DD
linear_time(as.Date(c("1985-10-26", "1955-11-05", "1955-11-12")), chronon = cal_symmetry454$day(1L))
#> <mixtime[3]>
#> [1] 1985-Oct-W4-Sat 1955-Nov-W1-Sat 1955-Nov-W2-Sat

Cyclical time formats: chronon_format_cyclical()

Cyclical time formats dispatch on the chronon and cycle. Defining methods for cyclical time formats is necessary when particular formatting with labels is desired (e.g. month of year or day of week labels). All other combinations of chronon and cycle will fall back to the default format, which combines the time unit abbreviation with the cycle label (e.g. “W1” for week 1 of the month or year).

# Format months in years as abbreviated month labels (e.g. "Jan", "Feb", ...)
S7::method(
  chronon_format_cyclical,
  list(cal_symmetry454$month, cal_symmetry454$year)
) <- function(x, y) "{cyc(month,year,label=TRUE,abbreviate=TRUE)}"

These methods are used when formatting cyclical time vectors, such as month of year:

month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
#> <mixtime[1]>
#> [1] Nov