# Extending mixtime

    library(mixtime)
    library(S7)

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:

- Time units: S7 classes inheriting from `mt_unit`, `mt_tz_unit`, or `mt_loc_unit`
- Calendars: Creating calendars from time units with `new_calendar()`
- Calendar arithmetic for defining temporal relationships between time units:
  - Cardinality: Define the number of finer units in a coarser unit with `chronon_cardinality_fixed()` and `chronon_cardinality()`
  - Divmod: Define the division and remainder of time units with `chronon_divmod()`
  - Epoch anchors: Define the reference point for a time unit with `chronon_epoch()`
- Labels for displaying time, defined by:
  - Time units: temporal properties (e.g. interval) and user messages with `time_unit_abbr()`, `time_unit_full()`
  - Time labels: labelling granular components of time with `linear_labels()`, `cyclical_labels()`
  - Formatting time: default format strings with `chronon_format_linear()`, `chronon_format_cyclical()`, `chronon_format_duration()`
  - Parsing time: candidate formats for parsing text back into time with `chronon_parse_linear()`, `chronon_parse_cyclical()`

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(...) {
      methods_register()
    }

## Time units

Time units are the building blocks of calendars. Time units are S7 classes created with `new_time_unit()`, which inherit from `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 `n` 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 `new_time_unit(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
    new_time_unit("tu_symmetry454_year",  parent = mt_tz_unit)
    #> <tu_symmetry454_year> class
    #> @ parent     : <mixtime::mt_tz_unit>
    #> @ constructor: function(...) {...}
    #> @ validator  : <NULL>
    #> @ properties :
    #>  $ n : <integer> or <double>
    #>  $ tz: <character>
    new_time_unit("tu_symmetry454_month", parent = mt_tz_unit)
    #> <tu_symmetry454_month> class
    #> @ parent     : <mixtime::mt_tz_unit>
    #> @ constructor: function(...) {...}
    #> @ validator  : <NULL>
    #> @ properties :
    #>  $ n : <integer> or <double>
    #>  $ 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`: 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  = new_time_unit("tu_symmetry454_year",  parent = mt_tz_unit),
      month = new_time_unit("tu_symmetry454_month", parent = mt_tz_unit),
      week  = cal_isoweek$week,
      # Inherit civil-time units (day, hour, minute, second, ...)
      inherit = cal_time_civil,
      class = "cal_symmetry454"
    )

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

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>
    #>  @ n : int 1
    #>  @ tz: 'mt_naive' chr NA
    cal_symmetry454$month(1L, tz = "UTC")
    #> <tu_symmetry454_month>
    #>  @ n : 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

Three S7 generics drive all calendar calculations: `chronon_cardinality_fixed()`, `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_fixed()` or `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`) 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

Units with a fixed relationship (e.g. 7 days per week) don’t need a time context, so they should be defined with `chronon_cardinality_fixed()` instead, which takes just the two units (no `at`) and returns the number of unit (`n = 1L`) `x` granules that fit within one unit `y` granule. Defining `chronon_cardinality_fixed()` for a pair automatically provides the corresponding `chronon_cardinality()` method (scaled by the requested granule sizes), and is what allows the relationship to be used when deriving `chronon_divmod()` by graph traversal (see Divmod) — a plain `chronon_cardinality()` method cannot be used there since no `at` is available mid-traversal.

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

    # Each Symmetry454 year has 12 months
    method(chronon_cardinality_fixed, list(cal_symmetry454$month, cal_symmetry454$year)) <-
      function(x, y) {
        12L
      }
    chronon_cardinality(cal_symmetry454$month(1L), cal_symmetry454$year(1L))
    #> [1] 12

Variable cardinalities (e.g. days per month) instead need a `chronon_cardinality()` method, disambiguated by the `at` argument: 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

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.

    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 <- y@n
        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 n-week time granule
        nweeks / x@n
      }

    # 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 granules 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_fixed()` methods alone.

`chronon_divmod(from, to, x)` converts times `x` in the `from` granules into time points in `to` granules. 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` granules)
- `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.

    method(chronon_divmod, list(cal_symmetry454$week, cal_symmetry454$month)) <-
      function(from, to, x) {
        # Most of this code works on 1-week granules
        week_size <- from@n
        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 granules
        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 <- to@n
        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 granule is not size 1 (e.g. `if (to@n != 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.

    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 full name of the unit, optionally with cli plural marker (e.g. `"Symmetry454 year{?/s}"`)
- `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.

The plural marker uses `{?singular_suffix/plural_suffix}` syntax. Irregular plurals use the full form, e.g. `"centur{?y/ies}"`. The default is to suffix with “s” for plurals (i.e. `{?/s}`).

    method(time_unit_full, cal_symmetry454$year)  <- function(x) "Symmetry454 year{?/s}"
    method(time_unit_abbr, cal_symmetry454$year)  <- function(x) "Y"
    method(time_unit_full, cal_symmetry454$month) <- function(x) "Symmetry454 month{?/s}"
    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 granules (usually the coarsest time unit, years), and cyclical time labels are for time granules that nest (cycle over) another granule (e.g. months within a year, days within a week).

`linear_labels()` and `cyclical_labels()` are S7 generics that return a granule’s label scheme: a plain list describing how its position renders as text and parses back again. Declaring a scheme is usually just `method(cyclical_labels, list(granule, cycle)) <- label_scheme(...)`. Rendering and parsing are handled separately by `linear_labels_format()`/`linear_labels_parse()` (and their cyclical counterparts), which by default read the scheme and produce the label. The scheme’s main fields are:

- `start`: what raw index `0` displays as (e.g. `start = 1L` makes January, raw index `0`, display as “1”), for both numeric and named rendering
- `width`: zero-pads numeric rendering (e.g. `width = 2L` for “02”)
- `vocab`: supplies the names for labelled rendering (e.g. “January”, “Jan”), most easily built with `vocab_table()`

Labels that don’t fit this scheme (e.g. the year “2BC”) can define `linear_labels_format()`/`linear_labels_parse()` directly instead. See `?cyclical_labels` for the complete authoring reference, including `transform` for cycles whose names don’t follow a constant `start` shift.

We recommend using the following argument names for common label options (the default rendering methods already use these):

- `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 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”, …). This relabelling isn’t a constant shift (it flips direction and skips 0 at the era boundary), so a `linear_labels()` scheme can’t express it. Instead, define `linear_labels_format()` and `linear_labels_parse()` directly:

    method(linear_labels_format, cal_symmetry454$year) <- function(granule, i, ...) {
      ifelse(i <= 0L, paste0(-i + 1L, "BC"), i)
    }
    method(linear_labels_parse, cal_symmetry454$year) <- function(granule, ...) {
      list(
        pattern = "\\d+(?:BC)?",
        decode = function(text, at = NULL) {
          bc <- grepl("BC$", text)
          n <- as.integer(sub("BC$", "", text))
          ifelse(bc, 1L - n, n) - chronon_epoch(granule)
        }
      )
    }

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 granules, 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 scheme 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.
    method(cyclical_labels, list(cal_symmetry454$month, cal_symmetry454$year)) <-
      label_scheme(
        start = 1L,
        width = 2L,
        vocab = vocab_table(
          `en-GB` = list(wide = month.name, abbreviated = month.abb)
        )
      )

    # Labels for weeks of the month are simply 1-indexed (e.g. "1", "2", ...)
    method(cyclical_labels, list(cal_symmetry454$week, cal_symmetry454$month)) <-
      label_scheme(start = 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”, …).

For cycles where the raw-index-to-name mapping isn’t a constant shift (e.g. a leap month that splits one name into two in some years), `transform` overrides the indexing used for named (`label = TRUE`) rendering with hand-written `encode`/`decode` functions. These also receive `at`, the linear position of the cycle instance, so the mapping can vary between cycles. See `?cyclical_labels`’s “Irregular cycles” section for a worked example (the Hebrew calendar’s month-in-year cycle).

    # 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)
    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.

### Formatting time

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. These format strings are described in more detail in the `vignette("time-format-strings")` vignette.

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

- `lin(<time granule>)` - Granule labels for linear time granules, usually the largest granule (e.g. `lin(year(1L))`). These labels are produced by `linear_labels()`.
- `cyc(<time granule>, <cycle granule>)` - Granule labels for cyclical time granules, 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 granule>` and `<cycle granule>` placeholders in the format string must evaluate to a time granule from a calendar time unit. For convenience, using time units (e.g. `lin(year)`) default to a size 1 time granule (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", ...)
    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", ...)
    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", ...)
    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", ...)
    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", ...)
    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

#### Duration formats: `chronon_format_duration()`

Duration vectors (created with `duration()`) are formatted using `chronon_format_duration()`, which dispatches on the chronon. The default method is usually sufficient, which uses `time_unit_full()` to produce a label for the duration.

    duration(1L, cal_symmetry454$year(1L))
    #> <mixtime[1]>
    #> [1] 1 Symmetry454 year
    duration(3L, cal_symmetry454$month(1L))
    #> <mixtime[1]>
    #> [1] 3 Symmetry454 months

### Parsing time

Parsing text back into time is the inverse of formatting: `time_parse()` matches text against one or more candidate format strings and returns whichever parses. The candidates for a given chronon are supplied by `chronon_parse_linear()` and `chronon_parse_cyclical()`, which dispatch exactly like `chronon_format_linear()`/`chronon_format_cyclical()` above. Their default methods simply reuse the single format returned by `chronon_format_linear()`/`chronon_format_cyclical()`, but it’s often worth listing every format actually in use (e.g. both “1970-01” and “1970 Jan”), so `time_parse()` has more than one candidate to try.

`parse_format()` combines format strings into the candidate vector these methods should return; `time_parse()` tries each candidate in turn and keeps whichever parses the most values.

    method(chronon_parse_linear, list(cal_symmetry454$month, S7::class_any)) <- function(x, cal) {
      parse_format(
        "{lin(year(1L))}-{cyc(month(1L), year(1L), label=TRUE, abbreviate=TRUE)}",
        "{lin(year(1L))}-{cyc(month(1L), year(1L))}"
      )
    }

With this method in place, `time_parse()` can recover a Symmetry454 month from either the labelled or numeric form:

    time_parse(c("1970-Jan", "1970-03"), format = chronon_parse_linear(cal_symmetry454$month(1L)))
    #> <mixtime[2]>
    #> [1] 1970 Jan 1970 Mar

`parse_format()` also accepts a `regex` argument, which marks its candidates’ surrounding (non-token) text as regular expression syntax rather than literal text to match exactly - useful for irregular formats, e.g. `"[/-]"` to accept either `/` or `-` as a separator. This is a niche need; most candidates leave it at the default. See the `regex` argument of `time_parse()` for details.