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 types1 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.
dateyExposure periods specified by the dates to typically mean that the
whole of the day and the whole of the day are included. In the
datey system this corresponds to using
start_day() for and end_day() for .
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
#> [1] 2024-03-07.0
mid_day(2024, 3, 7) # Middle of the day 7 March 2024
#> [1] 2024-03-07.5
end_day(2024, 3, 7) # End of the day 7 March 2024
#> [1] 2024-03-08.0The 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:
For an arbitrary position within a day, datey() accepts
a day fraction between 0 and 1:
It is often the case that data already contains dates defined using
the standard base R types Date2, POSIXct
or POSIXlt.
To convert these to a datey, use
start_day(), mid_day() or
end_day():
datey() also accepts a fractional calendar year or a
character string in YYYY-MM-DD[.f] format:
datey(2024) # Start of calendar year 2024
#> [1] 2024-01-01.0
datey(2024.5) # Midway through calendar year 2024
#> [1] 2024-07-02.0
datey("2024-03-07") # Start of the day 7 March 2024 (day fraction defaults to 0)
#> [1] 2024-03-07.0
datey("2024-03-07.5") # Middle of the day 7 March 2024
#> [1] 2024-03-07.5dateyThe $ operator extracts components of a
datey:
t <- mid_day(2024, 3, 7)
t$year
#> [1] 2024
t$month
#> [1] 3
t$day
#> [1] 7
t$day_fraction
#> [1] 0.5If you need several components at once, it is more efficient to use
to_ymdf() instead:
to_ymdf(t)
#> $year
#> [1] 2024
#>
#> $month
#> [1] 3
#>
#> $day
#> [1] 7
#>
#> $day_fraction
#> [1] 0.5as.double() converts to a fractional calendar year;
as.integer() gives the calendar year:
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():
durationydurationys typically arise as datey
differences:
dob <- start_day(as.Date("1965-09-12"))
dod <- mid_day(2024, 3, 7)
age <- dod - dob
age
#> [1] 58.485804 yrYou can create them explicitly using durationy(), which
accepts a number of years:
durationy(1) # One year
#> [1] 1 yr
durationy(0.5) # Half a year
#> [1] 0.5 yr
durationy(-2.5) # Two and a half years in the past
#> [1] −2.5 yrAnd you can convert them back to numerics using
as.double(), which gives the duration as years, and
as.integer(), which truncates toward zero:
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
#> [1] 2001-01-01.0
start - qtr_yr # Quarter of a year earlier
#> [1] 1999-10-01.75
one_yr - qtr_yr # Three quarters of a year
#> [1] 0.75 yr
one_yr + qtr_yr
#> [1] 1.25 yr
datey(2024) < datey(2025) # TRUE
#> [1] TRUE
durationy(1) > durationy(0.5) # TRUE
#> [1] TRUEYou can also do mixed arithmetic with datey and
durationy and numbers, in which case dateys
and durationys are first converted to
doubles:
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
#> [1] [2024-01-01.0, 2025-01-01.0)The $start, $end and $duration
properties extract the interval’s components:
durationy() accepts a datey_interval
directly:
%includes% tests whether a datey falls
inside the interval. The interval includes its start and excludes its
end:
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):
The & operator returns the intersection of two
datey_intervals. This is the most direct way to compute the
overlap of two time periods:
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:
By default, out-of-range inputs stop execution. With
strict = FALSE they become NA instead:
datey(999.9, strict = FALSE) # Outside [1000,3000]: NA
#> [1] <NA>
start_day(2000, 0, 12, strict = FALSE) # Invalid month: NA
#> [1] <NA>
mid_day(2001, 2, 29, strict = FALSE) # Invalid day (given year and month): NA
#> [1] <NA>
durationy(2000.1, strict = FALSE) # exceeds 2000-year limit: NA
#> [1] <NA>NA values propagate through arithmetic:
seq(), min(), max(),
range() and mean() all work on
datey and durationy vectors:
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.↩︎
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.↩︎