Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom diff() method for time points and calendars #381

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,13 @@ S3method(date_start,Date)
S3method(date_start,POSIXt)
S3method(date_zone,Date)
S3method(date_zone,POSIXt)
S3method(diff,clock_iso_year_week_day)
S3method(diff,clock_time_point)
S3method(diff,clock_year_day)
S3method(diff,clock_year_month_day)
S3method(diff,clock_year_month_weekday)
S3method(diff,clock_year_quarter_day)
S3method(diff,clock_year_week_day)
S3method(format,clock_duration)
S3method(format,clock_iso_year_week_day)
S3method(format,clock_time_point)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# clock (development version)

* Added a `diff()` method for time points and calendars to ensure that durations
are always returned, even in the empty result case (#364).

# clock 0.7.1

* Removed usage of non-API `STRING_PTR()` in favor of `STRING_PTR_RO()`.
Expand Down
37 changes: 37 additions & 0 deletions R/calendar.R
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,28 @@ calendar_check_minimum_precision <- function(
cli::cli_abort(message, call = call)
}

calendar_check_maximum_precision <- function(
x,
precision,
...,
arg = caller_arg(x),
call = caller_env()
) {
x_precision <- calendar_precision_attribute(x)

if (x_precision <= precision) {
return(invisible(NULL))
}

message <- c(
"Can't perform this operation because of the precision of {.arg {arg}}.",
i = "The precision of {.arg {arg}} must be at most {.str {precision_to_string(precision)}}.",
i = "{.arg {arg}} has a precision of {.str {precision_to_string(x_precision)}}."
)

cli::cli_abort(message, call = call)
}

calendar_check_exact_precision <- function(
x,
precision,
Expand Down Expand Up @@ -1207,6 +1229,21 @@ arith_numeric_and_calendar <- function(op, x, y, ...) {

# ------------------------------------------------------------------------------

# Special support for when `lag * differences >= n`
#
# In vctrs, this forces a return value of `vec_slice(x, 0L)`, but this is
# not correct for calendar types. See also, `diff.Date()` which exists for a
# similar reason.
calendar_diff_is_empty <- function(x, lag, differences) {
# Same errors as vctrs
stopifnot(length(lag) == 1L, lag >= 1L)
stopifnot(length(differences) == 1L, differences >= 1L)
n <- vec_size(x)
lag * differences >= n
}

# ------------------------------------------------------------------------------

#' @export
as_year_month_day.clock_calendar <- function(x, ...) {
check_dots_empty0(...)
Expand Down
12 changes: 12 additions & 0 deletions R/gregorian-year-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,18 @@ year_day_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_year_day <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_YEAR)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to year-day
#'
#' `as_year_day()` converts a vector to the year-day calendar.
Expand Down
12 changes: 12 additions & 0 deletions R/gregorian-year-month-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,18 @@ year_month_day_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_year_month_day <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_MONTH)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to year-month-day
#'
#' `as_year_month_day()` converts a vector to the year-month-day calendar.
Expand Down
12 changes: 12 additions & 0 deletions R/gregorian-year-month-weekday.R
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,18 @@ year_month_weekday_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_year_month_weekday <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_MONTH)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to year-month-weekday
#'
#' `as_year_month_weekday()` converts a vector to the year-month-weekday
Expand Down
12 changes: 12 additions & 0 deletions R/iso-year-week-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,18 @@ iso_year_week_day_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_iso_year_week_day <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_YEAR)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to iso-year-week-day
#'
#' `as_iso_year_week_day()` converts a vector to the iso-year-week-day
Expand Down
12 changes: 12 additions & 0 deletions R/quarterly-year-quarter-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,18 @@ year_quarter_day_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_year_quarter_day <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_QUARTER)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to year-quarter-day
#'
#' @description
Expand Down
11 changes: 11 additions & 0 deletions R/time-point.R
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,17 @@ add_months.clock_time_point <- function(x, n, ...) {

# ------------------------------------------------------------------------------

#' @export
diff.clock_time_point <- function(x, lag = 1L, differences = 1L, ...) {
# Special care to ensure that when `lag * differences >= n`, we still
# return a duration type rather than `vec_slice(x, 0L)` which vctrs does by
# default. It is always valid to diff the duration in place of the time point.
x <- as_duration(x)
diff(x, lag = lag, differences = differences, ...)
}

# ------------------------------------------------------------------------------

#' @export
as_duration.clock_time_point <- function(x, ...) {
check_dots_empty0(...)
Expand Down
12 changes: 12 additions & 0 deletions R/week-year-week-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,18 @@ year_week_day_plus_duration <- function(

# ------------------------------------------------------------------------------

#' @export
diff.clock_year_week_day <- function(x, lag = 1L, differences = 1L, ...) {
calendar_check_maximum_precision(x, PRECISION_YEAR)
if (calendar_diff_is_empty(x, lag, differences)) {
duration_helper(integer(), calendar_precision_attribute(x))
} else {
NextMethod()
}
}

# ------------------------------------------------------------------------------

#' Convert to year-week-day
#'
#' `as_year_week_day()` converts a vector to the year-week-day
Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/_snaps/gregorian-year-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@
! Invalid date found at location 1.
i Resolve invalid date issues by specifying the `invalid` argument.

# errors on invalid precisions

Code
diff(year_day(2019, 1))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "year".
i `x` has a precision of "day".

# minimums are right

Code
Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/_snaps/gregorian-year-month-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,16 @@
! Invalid date found at location 1.
i Resolve invalid date issues by specifying the `invalid` argument.

# errors on invalid precisions

Code
diff(year_month_day(2019, 1, 2))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "month".
i `x` has a precision of "day".

# minimums are right

Code
Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/_snaps/gregorian-year-month-weekday.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,16 @@
! Invalid date found at location 1.
i Resolve invalid date issues by specifying the `invalid` argument.

# errors on invalid precisions

Code
diff(year_month_weekday(2019, 1, 1, index = 2))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "month".
i `x` has a precision of "day".

# minimums are right

Code
Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/_snaps/iso-year-week-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,16 @@
! Invalid date found at location 1.
i Resolve invalid date issues by specifying the `invalid` argument.

# errors on invalid precisions

Code
diff(iso_year_week_day(2019, 1))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "year".
i `x` has a precision of "week".

# minimums are right

Code
Expand Down
10 changes: 10 additions & 0 deletions tests/testthat/_snaps/quarterly-year-quarter-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,3 +613,13 @@
Error in `add_years()`:
! Can't recycle `x` (size 2) to match `n` (size 3).

# errors on invalid precisions

Code
diff(year_quarter_day(2019, 1, 1))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "quarter".
i `x` has a precision of "day".

10 changes: 10 additions & 0 deletions tests/testthat/_snaps/week-year-week-day.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,13 @@
Error in `add_years()`:
! Can't recycle `x` (size 2) to match `n` (size 3).

# errors on invalid precisions

Code
diff(year_week_day(2019, 1))
Condition
Error in `diff()`:
! Can't perform this operation because of the precision of `x`.
i The precision of `x` must be at most "year".
i `x` has a precision of "week".

35 changes: 35 additions & 0 deletions tests/testthat/test-gregorian-year-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,41 @@ test_that("is.infinite() works", {
expect_identical(is.infinite(x), c(FALSE, FALSE))
})

# ------------------------------------------------------------------------------
# diff()

test_that("works with `lag` and `differences`", {
x <- year_day(2019:2026)
expect_identical(diff(x), rep(duration_years(1), 7))
expect_identical(diff(x, lag = 2), rep(duration_years(2), 6))
expect_identical(diff(x, differences = 2), rep(duration_years(0), 6))
expect_identical(
diff(x, lag = 3, differences = 2),
rep(duration_years(0), 2)
)
})

test_that("works with `lag` and `differences` that force an empty result (#364)", {
expect_identical(diff(year_day(integer())), duration_years())
expect_identical(diff(year_day(1)), duration_years())
expect_identical(
diff(year_day(1:8), lag = 4, differences = 3),
duration_years()
)
})

test_that("errors on invalid precisions", {
expect_snapshot(error = TRUE, {
diff(year_day(2019, 1))
})
})

test_that("errors on invalid lag/differences", {
# These are base R errors we don't control
expect_error(diff(year_day(2019), lag = 1:2))
expect_error(diff(year_day(2019), differences = 1:2))
})

# ------------------------------------------------------------------------------
# clock_minimum() / clock_maximum()

Expand Down
43 changes: 43 additions & 0 deletions tests/testthat/test-gregorian-year-month-day.R
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,49 @@ test_that("is.infinite() works", {
expect_identical(is.infinite(x), c(FALSE, FALSE))
})

# ------------------------------------------------------------------------------
# diff()

test_that("works with `lag` and `differences`", {
x <- year_month_day(2019, 1:8)
expect_identical(diff(x), rep(duration_months(1), 7))
expect_identical(diff(x, lag = 2), rep(duration_months(2), 6))
expect_identical(diff(x, differences = 2), rep(duration_months(0), 6))
expect_identical(
diff(x, lag = 3, differences = 2),
rep(duration_months(0), 2)
)
})

test_that("works with `lag` and `differences` that force an empty result (#364)", {
expect_identical(diff(year_month_day(integer())), duration_years())
expect_identical(diff(year_month_day(integer(), integer())), duration_months())

expect_identical(diff(year_month_day(1)), duration_years())
expect_identical(diff(year_month_day(1, 1)), duration_months())

expect_identical(
diff(year_month_day(1:8), lag = 4, differences = 3),
duration_years()
)
expect_identical(
diff(year_month_day(1:8, 1), lag = 4, differences = 3),
duration_months()
)
})

test_that("errors on invalid precisions", {
expect_snapshot(error = TRUE, {
diff(year_month_day(2019, 1, 2))
})
})

test_that("errors on invalid lag/differences", {
# These are base R errors we don't control
expect_error(diff(year_month_day(2019, 1), lag = 1:2))
expect_error(diff(year_month_day(2019, 1), differences = 1:2))
})

# ------------------------------------------------------------------------------
# clock_minimum() / clock_maximum()

Expand Down
Loading
Loading