# Get started with datey

    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.

## The core types

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.

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

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))
    #> [1] TRUE

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

### From base R dates

It is often the case that data already contains dates defined using the standard base R types `Date`2, `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)
    #> [1] 2024-03-07.0
    mid_day(d)
    #> [1] 2024-03-07.5
    end_day(d)
    #> [1] 2024-03-08.0

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

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

If 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.5

`as.double()` converts to a fractional calendar year; `as.integer()` gives the calendar year:

    as.double(t)
    #> [1] 2024.182
    as.integer(t)
    #> [1] 2024

`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
    #> [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] FALSE

## Creating a `durationy`

`durationy`s 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 yr

You 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 yr

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)
    #> [1] 58.4858
    as.integer(age)   # Whole years only
    #> [1] 58

## Comparisons 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_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] TRUE

You can also do mixed arithmetic with `datey` and `durationy` and numbers, in which case `datey`s and `durationy`s are first converted to `double`s:

    identical(datey(2000) + 25, 2025)     # TRUE
    #> [1] TRUE
    identical(durationy(2) * 0.05, 0.10)  # TRUE
    #> [1] TRUE

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

`durationy()` accepts a `datey_interval` directly:

    durationy(interval)
    #> [1] 1 yr

### Interval 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] TRUE

### Interval 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] TRUE

### Intersection

The `&` operator returns the intersection of two `datey_interval`s. 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
    #> [1] [2024-01-01.0, 2024-07-01.0)

    overlap$duration  # exposure in calendar year 2024, in years
    #> [1] 0.497268 yr

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

    is.na(NA_datey_)
    #> [1] TRUE
    anyNA(c(datey(2000), NA_datey_, datey(2024)))
    #> [1] TRUE

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>

## Sequences and statistics

`seq()`, `min()`, `max()`, `range()` and `mean()` all work on `datey` and `durationy` vectors:

    dates <- c(datey(2021), datey(2022), datey(2023))

    min(dates)
    #> [1] 2021-01-01.0
    max(dates)
    #> [1] 2023-01-01.0
    mean(dates)
    #> [1] 2022-01-01.0

    seq(from = datey(2020), to = datey(2024), by = durationy(2))
    #> [1] 2020-01-01.0 2022-01-01.0 2024-01-01.0

1. Even though `datey_interval` stores the start and the end of a time interval, it too is atomic, which means that `datey_interval`s can be stored in a single vector without any additional special handling.↩︎

2. 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 `Date`s. For this reason, a `day_fraction` argument is always required for a `Date`.↩︎