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, ormt_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() -
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()
-
Cardinality: Define the number of finer
units in a coarser unit with
-
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() -
Time formats: default format strings
with
chronon_format_linear(),chronon_format_cyclical()
-
Time units: temporal properties
(e.g. interval) and user messages with
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_unitfor civil-time units whose boundaries depend on a timezone (e.g. months, days, hours, …), or -
mt_loc_unitfor 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(tzIANA names) ormt_loc_unit(forlon,lat, andalt), - 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
#> - millisecondIn 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] 7For 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] 29In 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] 12The 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 10Cardinality 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 364The 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.4Divmod
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] 14Methods for chronon_divmod() return a list with:
-
div- the quotient (chronons intounits) -
mod- the remainder (leftoverfromchronons)
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] 4While 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] 2If 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] 100Epoch 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) 1970LDisplaying 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] Y1955The linear time helper functions can also be used with the new calendar:
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] M10Cyclical 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: ifTRUE, return labelled values (e.g. “January”, “February”, …), otherwise return unlabelled values (e.g. “1”, “2”, …) -
abbreviate: ifTRUE(andlabelisTRUE), 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 Y2We’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] D363With 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-170These 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 bylinear_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 bycyclical_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-SatCyclical 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