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.
Core types
datey provides three atomic1 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 all store dates and durations as integers with units of 1/534 360 of a year (clicks). As a result, arithmetic with these types is exact and associative.
Creating a datey
When in the day?
Exposure 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.
From year, month and day
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:
datey(2024, 3, 7, 0.25) # Quarter of the way through the day 7 March 2024
#> [1] 2024-03-07.25From base R dates
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():
From fractional years or text
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.5Properties of a datey
The $ 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:
as.double(t)
#> [1] 2024.182
as.integer(t)
#> [1] 2024is_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
#> [1] TRUE
is_mid_day(mid_day(2024, 3, 7)) # TRUE
#> [1] TRUE
is_start_day(end_day(2024, 3, 7)) # TRUE because end = start of next day
#> [1] TRUE
is_mid_day(datey(2024, 3, 7, 0.25)) # FALSE
#> [1] FALSECreating a durationy
durationys typically arise as datey
differences:
birth_date <- start_day(as.Date("1965-09-12"))
death_date <- mid_day(2024, 3, 7)
age <- death_date - birth_date
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:
as.double(age)
#> [1] 58.4858
as.integer(age) # Whole years only
#> [1] 58Comparisons and arithmetic
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_year <- durationy(1)
quarter_year <- durationy(0.25)
start + one_year # One year later
#> [1] 2001-01-01.0
start - quarter_year # Quarter of a year earlier
#> [1] 1999-10-01.75
one_year - quarter_year # Three quarters of a year
#> [1] 0.75 yr
one_year + quarter_year
#> [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 period
A 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:
interval$start
#> [1] 2024-01-01.0
interval$end
#> [1] 2025-01-01.0
interval$duration
#> [1] 1 yrdurationy() accepts a datey_interval
directly:
durationy(interval)
#> [1] 1 yrInterval membership testing
%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
#> [1] TRUE
interval %includes% b # FALSE -- end is excluded
#> [1] FALSE
interval %includes% mid_day(2024, 6, 15) # TRUE
#> [1] TRUEInterval properties
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
#> [1] TRUE
is_collapsed(interval) # FALSE because start < end
#> [1] FALSE
point <- a %to% a # Empty (point) interval
is_proper(point) # TRUE because a <= a
#> [1] TRUE
is_collapsed(point) # TRUE because a >= a
#> [1] TRUENA values
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:
start_day(2024, 1, 1) + NA_durationy_
#> [1] <NA>