---
title: "Extending mixtime"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Extending mixtime}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup, message = FALSE}
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](https://en.wikipedia.org/wiki/Symmetry454), 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](#time-units): S7 classes inheriting from `mt_unit`, `mt_tz_unit`, or `mt_loc_unit`
* [Calendars](#calendars): Creating calendars from time units with `new_calendar()`
* [Calendar arithmetic](#calendar-arithmetic) for defining temporal relationships between time units:
  * [Cardinality](#cardinality): Define the number of finer units in a coarser unit with `chronon_cardinality_fixed()` and `chronon_cardinality()`
  * [Divmod](#divmod): Define the division and remainder of time units with `chronon_divmod()`
  * [Epoch anchors](#epoch): Define the reference point for a time unit with `chronon_epoch()`
* [Labels](#labels) for displaying time, defined by: 
  * [Time units](#display-time-units): temporal properties (e.g. interval) and user messages with `time_unit_abbr()`, `time_unit_full()`
  * [Time labels](#time-labels): labelling granular components of time with `linear_labels()`, `cyclical_labels()`
  * [Formatting time](#time-formats): default format strings with `chronon_format_linear()`, `chronon_format_cyclical()`, `chronon_format_duration()`
  * [Parsing time](#time-parsing-formats): 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](https://rconsortium.github.io/S7/articles/generics-methods.html) 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](https://rconsortium.github.io/S7/articles/packages.html):

```{r}
.onLoad <- function(...) {
  methods_register()
}
```

## Time units {#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.

```{r time-units}
# Timezone-aware Symmetry454 year and month units
new_time_unit("tu_symmetry454_year",  parent = mt_tz_unit)
new_time_unit("tu_symmetry454_month", parent = mt_tz_unit)
```

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

## Calendars {#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)

```{r new-calendar}
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
```

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 `$`:

```{r calendar-access}
cal_symmetry454$year(1L)
cal_symmetry454$month(1L, tz = "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 {#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 {#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.

```{r}
chronon_cardinality(cal_isoweek$day(1L), cal_isoweek$week(1L))
```

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

```{r cardinality-fixed}
# 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))
```

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

```{r}
chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 0L) # Jan 1970
chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 1L) # Feb 1970
chronon_cardinality(cal_gregorian$day(1L), cal_gregorian$month(1L), at = 25L) # Feb 1972 (leap year)
```

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.

```{r cardinality-variable}
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)

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

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

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

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.

```{r}
# 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)

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

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.

```{r}
# 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))

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

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

```{r}
chronon_divmod(cal_gregorian$day(1L), cal_gregorian$month(1L), 45L)
```

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.

```{r divmod}
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.

```{r}
# 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))

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

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](https://github.com/mitchelloharawild/mixtime/blob/main/R/cal_sym454.R)

```{r}
# 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))
```

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.

```{r}
# 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))
```

### Epoch anchors {#epoch}

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.

```{r}
method(chronon_epoch, cal_symmetry454$year) <- function(x) 1970L
```

## Displaying time {#labels}

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 {#display-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}`).

```{r}
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:

```{r}
linear_time(as.Date("1955-11-12"), chronon = cal_symmetry454$year(1L))
```

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

```{r}
year(as.Date("1955-11-12"), calendar = cal_symmetry454)
```

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

```{r}
# Week of the month
cyclical_time(as.Date("1955-11-12"), chronon = week(1L), cycle = month(1L), calendar = cal_symmetry454)

# Month of the year
month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
```

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 {#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:

```{r}
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:

```{r}
year(-1:2, calendar = cal_symmetry454)
```

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.

```{r}
# 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).

```{r}
# Month of year
month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
# Week of month
cyclical_time(as.Date("1955-11-12"), chronon = cal_symmetry454$week(1L), cycle = cal_symmetry454$month(1L))

# Day of week (inherited from cal_isoweek)
day_of_week(as.Date("1955-11-12"), calendar = cal_symmetry454)
# Day of year (inherited from cal_time_civil)
day_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
```

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

```{r}
date(as.Date("1955-11-12"), calendar = cal_symmetry454)
yearweek(as.Date("1955-11-12"), calendar = cal_symmetry454)
yearmonth(as.Date("1955-11-12"), calendar = cal_symmetry454)
```

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 {#time-formats}

<!-- TODO: Time formatting vignette -->
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](https://glue.tidyverse.org/) 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.

```{r}
# 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:

```{r}
# Years are formatted as YYYY
year(as.Date("1955-11-12"), calendar = cal_symmetry454)

# Months are formatted as YYYY Mon
yearmonth(as.Date("1955-11-12"), calendar = cal_symmetry454)

# Weeks are formatted as YYYY-MM-WW
yearweek(as.Date("1955-11-12"), calendar = cal_symmetry454)

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

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

```{r}
# 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:
```{r}
month_of_year(as.Date("1955-11-12"), calendar = cal_symmetry454)
```

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

```{r}
duration(1L, cal_symmetry454$year(1L))
duration(3L, cal_symmetry454$month(1L))
```

### Parsing time {#time-parsing-formats}

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.

```{r}
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:

```{r}
time_parse(c("1970-Jan", "1970-03"), format = chronon_parse_linear(cal_symmetry454$month(1L)))
```

`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.
