knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
library(datey)
This vignette is a hands-on guide to the datey package. For the motivation behind the annual-grid approach and the associativity guarantee, see Why datey?. For the complete formal specification, see the datey specification.
datey provides three S3 classes:
datey -- a point in time, stored at day-fraction precision.durationy -- a duration in years.datey_interval -- a half-open [start, end) time interval.These are atomic types[^datey-interval-is-atomic] that store dates and durations as integers with units of 1/534 360 of a year). As a result, arithmetic with these types is exact and associative.
[^datey-interval-is-atomic]: Even though datey_interval stores the start and the end of
a time interval, it too is atomic, which means that datey_intervals can be
stored in a single vector without any additional special handling.
dateyExposure periods specified by the dates \eqn{a} to \eqn{b} typically mean that
the whole of the day \eqn{a} and the whole of the day \eqn{b} are included.
In the datey system this corresponds to using
start_day() for \eqn{a} and
end_day() for \eqn{b}.
Deaths on the other hand typically happen during a day.
In the datey system this corresponds to using
mid_day().
These distinctions may be new to you and your first reaction may be that they are immaterial. But it costs very little to be precise and sometimes systematic errors can accumulate and end up being material.
start_day(), mid_day() and end_day() create a datey from scratch:
start_day(2024, 3, 7) # Start of the day 7 March 2024 mid_day(2024, 3, 7) # Middle of the day 7 March 2024 end_day(2024, 3, 7) # End of the day 7 March 2024
The end of a day is the same point as the start of the next, so
end_day() applied to a day is identical to start_day() applied to the
following day:
identical(end_day(2024, 3, 7), start_day(2024, 3, 8))
For an arbitrary position within a day, datey() accepts a day fraction
between 0 and 1:
datey(2024, 3, 7, 0.25) # Quarter of the way through the day 7 March 2024
It is often the case that data already contains dates defined using the
standard base R types Date[^fractional-dates], POSIXct or POSIXlt.
To convert these to a datey, use start_day(), mid_day() or end_day():
d <- as.Date("2024-03-07") start_day(d) mid_day(d) end_day(d)
[^fractional-dates]: Even though the Date type is not designed for
fractional dates, it typically uses floating point under the covers,
and can unintentionally end up with a fractional value
e.g. by taking a mean of Dates. For this reason,
a day_fraction argument is always required for a Date.
datey() also accepts a fractional calendar year or a character string in
YYYY-MM-DD[.f] format:
datey(2024) # Start of calendar year 2024 datey(2024.5) # Midway through calendar year 2024 datey("2024-03-07") # Start of the day 7 March 2024 (day fraction defaults to 0) datey("2024-03-07.5") # Middle of the day 7 March 2024
dateyThe $ operator extracts components of a datey:
t <- mid_day(2024, 3, 7) t$year t$month t$day t$day_fraction
If you need several components at once, it is more efficient to use
to_ymdf() instead:
to_ymdf(t)
as.double() converts to a fractional calendar year; as.integer() gives
the calendar year:
as.double(t) as.integer(t)
is_start_day() and is_mid_day() test the position within the day. Note
that end_day() produces a datey at the start of the following day, so it
tests as is_start_day():
is_start_day(start_day(2024, 3, 7)) # TRUE is_mid_day(mid_day(2024, 3, 7)) # TRUE is_start_day(end_day(2024, 3, 7)) # TRUE because end = start of next day is_mid_day(datey(2024, 3, 7, 0.25)) # FALSE
durationydurationys typically arise as datey differences:
dob <- start_day(as.Date("1965-09-12")) dod <- mid_day(2024, 3, 7) age <- dod - dob age
You can create them explicitly using durationy(), which accepts a number of
years:
durationy(1) # One year durationy(0.5) # Half a year durationy(-2.5) # Two and a half years in the past
And you can convert them back to numerics using
as.double(), which gives the duration as years, and
as.integer(), which truncates toward
zero:
as.double(age) as.integer(age) # Whole years only
A number of arithmetic operations are available for datey, durationy and
datey_interval.
Beware that not all combinations are valid because, for instance, it doesn't make sense to add two dates together.
The table below summarises the valid arithmetic and comparison operations. All arithmetic is carried out as exact integer arithmetic on the underlying click counts, so the results are exact and associative.
| Left | Op | Right | Result |
|:---|:---|:---|:---|
| datey | - | datey | durationy |
| datey | + - | durationy | datey |
| durationy | + | datey | datey |
| durationy | + - | durationy | durationy |
| datey | == != < <= > >= | datey | logical |
| durationy | == != < <= > >= | durationy | logical |
| datey | %to% | datey | datey_interval |
| datey_interval | == != | datey_interval | logical |
| datey_interval | %includes% | datey | logical |
| datey_interval | & | datey_interval | datey_interval |
start <- start_day(2000, 1, 1) one_yr <- durationy(1) qtr_yr <- durationy(0.25) start + one_yr # One year later start - qtr_yr # Quarter of a year earlier one_yr - qtr_yr # Three quarters of a year one_yr + qtr_yr datey(2024) < datey(2025) # TRUE durationy(1) > durationy(0.5) # TRUE
You can also do mixed arithmetic with datey and durationy and numbers,
in which case dateys and durationys are first converted to doubles:
identical(datey(2000) + 25, 2025) # TRUE identical(durationy(2) * 0.05, 0.10) # TRUE
datey_interval -- representing a time periodA datey_interval is a half-open [start, end) interval. Create one with
datey_interval() or the %to% operator:
a <- start_day(2024, 1, 1) b <- start_day(2025, 1, 1) interval <- a %to% b interval
The $start, $end and $duration properties extract the interval's
components:
interval$start interval$end interval$duration
durationy() accepts a datey_interval directly:
durationy(interval)
%includes% tests whether a datey falls inside the interval. The interval
includes its start and excludes its end:
interval %includes% a # TRUE -- start is included interval %includes% b # FALSE -- end is excluded interval %includes% mid_day(2024, 6, 15) # TRUE
is_proper() returns TRUE when start ≤ end; is_collapsed() returns
TRUE when start ≥ end. A point interval [a, a) is both proper and
collapsed (it contains no time):
is_proper(interval) # TRUE because start <= end is_collapsed(interval) # FALSE because start < end point <- a %to% a # Empty (point) interval is_proper(point) # TRUE because a <= a is_collapsed(point) # TRUE because a >= a
The & operator returns the intersection of two datey_intervals. This is
the most direct way to compute the overlap of two time periods:
period <- start_day(2023, 7, 1) %to% end_day(2024, 6, 30) year_2024 <- start_day(2024, 1, 1) %to% end_day(2024, 12, 31) overlap <- period & year_2024 overlap overlap$duration # exposure in calendar year 2024, in years
Throughout the datey package, NA will cause an error when used where
a datey_, durationy_ or datey_interval_ is expected.
This is because the type of NA is logical. which has no meaningful
date or duration interpretation therefore potentially indicates user
error.
If you want an NA value with a datey system type, use the explicit forms
NA_datey_, NA_durationy_ or NA_datey_interval_ as appropriate.
is.na() and anyNA() work as expected:
is.na(NA_datey_) anyNA(c(datey(2000), NA_datey_, datey(2024)))
By default, out-of-range inputs stop execution. With
strict = FALSE they become NA instead:
datey(999.9, strict = FALSE) # Outside [1000,3000]: NA start_day(2000, 0, 12, strict = FALSE) # Invalid month: NA mid_day(2001, 2, 29, strict = FALSE) # Invalid day (given year and month): NA durationy(2000.1, strict = FALSE) # exceeds 2000-year limit: NA
NA values propagate through arithmetic:
start_day(2024, 1, 1) + NA_durationy_
seq(), min(), max(), range() and mean() all work on datey and
durationy vectors:
dates <- c(datey(2021), datey(2022), datey(2023)) min(dates) max(dates) mean(dates) seq(from = datey(2020), to = datey(2024), by = durationy(2))
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.