Skip to content

Commit 6237036

Browse files
authored
Merge pull request #535 from cmu-delphi/lcb/slide-window-validation-edit
Fix & improve .window_size validation
2 parents 2c7b77c + 493a24a commit 6237036

12 files changed

+145
-97
lines changed

DESCRIPTION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Type: Package
22
Package: epiprocess
33
Title: Tools for basic signal processing in epidemiology
4-
Version: 0.9.1
4+
Version: 0.9.2
55
Authors@R: c(
66
person("Jacob", "Bien", role = "ctb"),
77
person("Logan", "Brooks", , "[email protected]", role = c("aut", "cre")),

NEWS.md

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Pre-1.0.0 numbering scheme: 0.x will indicate releases, while 0.x.y will indicate PR's.
44

5+
# epiprocess 0.10
6+
7+
## Breaking changes
8+
9+
- Removed `.window_size = 1` default from `epi_slide_{mean,sum,opt}`; this
10+
argument is now mandatory, and should nearly always be greater than 1 except
11+
for testing purposes.
12+
13+
## Improvements
14+
15+
- Improved validation of `.window_size` arguments.
16+
517
# epiprocess 0.9
618

719
## Breaking changes

R/slide.R

+15-45
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ get_before_after_from_window <- function(window_size, align, time_type) {
576576
#' ungroup()
577577
epi_slide_opt <- function(
578578
.x, .col_names, .f, ...,
579-
.window_size = 1, .align = c("right", "center", "left"),
579+
.window_size = NULL, .align = c("right", "center", "left"),
580580
.ref_time_values = NULL, .all_rows = FALSE) {
581581
assert_class(.x, "epi_df")
582582

@@ -678,46 +678,16 @@ epi_slide_opt <- function(
678678
ref_time_values <- sort(.ref_time_values)
679679

680680
# Handle window arguments
681-
align <- rlang::arg_match(.align)
681+
.align <- rlang::arg_match(.align)
682682
time_type <- attr(.x, "metadata")$time_type
683-
validate_slide_window_arg(.window_size, time_type)
684-
if (identical(.window_size, Inf)) {
685-
if (align == "right") {
686-
before <- Inf
687-
if (time_type %in% c("day", "week")) {
688-
after <- as.difftime(0, units = glue::glue("{time_type}s"))
689-
} else {
690-
after <- 0
691-
}
692-
} else {
693-
cli_abort(
694-
"`epi_slide`: center and left alignment are not supported with an infinite window size."
695-
)
696-
}
697-
} else {
698-
if (align == "right") {
699-
before <- .window_size - 1
700-
if (time_type %in% c("day", "week")) {
701-
after <- as.difftime(0, units = glue::glue("{time_type}s"))
702-
} else {
703-
after <- 0
704-
}
705-
} else if (align == "center") {
706-
# For .window_size = 5, before = 2, after = 2. For .window_size = 4, before = 2, after = 1.
707-
before <- floor(.window_size / 2)
708-
after <- .window_size - before - 1
709-
} else if (align == "left") {
710-
if (time_type %in% c("day", "week")) {
711-
before <- as.difftime(0, units = glue::glue("{time_type}s"))
712-
} else {
713-
before <- 0
714-
}
715-
after <- .window_size - 1
716-
}
683+
if (is.null(.window_size)) {
684+
cli_abort("epi_slide_opt: `.window_size` must be specified.")
717685
}
686+
validate_slide_window_arg(.window_size, time_type)
687+
window_args <- get_before_after_from_window(.window_size, .align, time_type)
718688

719689
# Make a complete date sequence between min(.x$time_value) and max(.x$time_value).
720-
date_seq_list <- full_date_seq(.x, before, after, time_type)
690+
date_seq_list <- full_date_seq(.x, window_args$before, window_args$after, time_type)
721691
all_dates <- date_seq_list$all_dates
722692
pad_early_dates <- date_seq_list$pad_early_dates
723693
pad_late_dates <- date_seq_list$pad_late_dates
@@ -786,16 +756,16 @@ epi_slide_opt <- function(
786756
# `before` and `after` params. Right-aligned `frollmean` results'
787757
# `ref_time_value`s will be `after` timesteps ahead of where they should
788758
# be; shift results to the left by `after` timesteps.
789-
if (before != Inf) {
790-
window_size <- before + after + 1L
759+
if (window_args$before != Inf) {
760+
window_size <- window_args$before + window_args$after + 1L
791761
roll_output <- .f(x = .data_group[, col_names_chr], n = window_size, ...)
792762
} else {
793763
window_size <- list(seq_along(.data_group$time_value))
794764
roll_output <- .f(x = .data_group[, col_names_chr], n = window_size, adaptive = TRUE, ...)
795765
}
796-
if (after >= 1) {
766+
if (window_args$after >= 1) {
797767
.data_group[, result_col_names] <- purrr::map(roll_output, function(.x) {
798-
c(.x[(after + 1L):length(.x)], rep(NA, after))
768+
c(.x[(window_args$after + 1L):length(.x)], rep(NA, window_args$after))
799769
})
800770
} else {
801771
.data_group[, result_col_names] <- roll_output
@@ -805,8 +775,8 @@ epi_slide_opt <- function(
805775
for (i in seq_along(col_names_chr)) {
806776
.data_group[, result_col_names[i]] <- .f(
807777
x = .data_group[[col_names_chr[i]]],
808-
before = as.numeric(before),
809-
after = as.numeric(after),
778+
before = as.numeric(window_args$before),
779+
after = as.numeric(window_args$after),
810780
...
811781
)
812782
}
@@ -902,7 +872,7 @@ epi_slide_opt <- function(
902872
#' ungroup()
903873
epi_slide_mean <- function(
904874
.x, .col_names, ...,
905-
.window_size = 1, .align = c("right", "center", "left"),
875+
.window_size = NULL, .align = c("right", "center", "left"),
906876
.ref_time_values = NULL, .all_rows = FALSE) {
907877
# Deprecated argument handling
908878
provided_args <- rlang::call_args_names(rlang::call_match())
@@ -979,7 +949,7 @@ epi_slide_mean <- function(
979949
#' ungroup()
980950
epi_slide_sum <- function(
981951
.x, .col_names, ...,
982-
.window_size = 1, .align = c("right", "center", "left"),
952+
.window_size = NULL, .align = c("right", "center", "left"),
983953
.ref_time_values = NULL, .all_rows = FALSE) {
984954
# Deprecated argument handling
985955
provided_args <- rlang::call_args_names(rlang::call_match())

R/utils.R

+56-28
Original file line numberDiff line numberDiff line change
@@ -982,14 +982,28 @@ guess_period.POSIXt <- function(time_values, time_values_arg = rlang::caller_arg
982982
as.numeric(NextMethod(), units = "secs")
983983
}
984984

985-
validate_slide_window_arg <- function(arg, time_type, lower = 1, allow_inf = TRUE, arg_name = rlang::caller_arg(arg)) {
986-
if (!checkmate::test_scalar(arg) || arg < lower) {
987-
cli_abort(
988-
"Slide function expected `{arg_name}` to be a non-null, scalar integer >= {lower}.",
989-
class = "epiprocess__validate_slide_window_arg"
990-
)
985+
#' Is `x` an "int" with a sensible class? TRUE/FALSE
986+
#'
987+
#' Like [`checkmate::test_int`] but disallowing some non-sensible classes that
988+
#' `test_int` accepts, such as `difftime`s. We rely on [`is.numeric`] to
989+
#' determine class appropriateness; note that `is.numeric` is NOT simply
990+
#' checking for the class to be "numeric" (or else we'd fail on integer class).
991+
#'
992+
#' @param x object
993+
#' @return Boolean
994+
#'
995+
#' @importFrom checkmate test_int
996+
#' @keywords internal
997+
test_sensible_int <- function(x, na.ok = FALSE, lower = -Inf, upper = Inf, # nolint: object_name_linter
998+
tol = sqrt(.Machine$double.eps), null.ok = FALSE) { # nolint: object_name_linter
999+
if (null.ok && is.null(x)) {
1000+
TRUE
1001+
} else {
1002+
is.numeric(x) && test_int(x, na.ok = na.ok, lower = lower, upper = upper, tol = tol)
9911003
}
1004+
}
9921005

1006+
validate_slide_window_arg <- function(arg, time_type, lower = 1, allow_inf = TRUE, arg_name = rlang::caller_arg(arg)) {
9931007
if (time_type == "custom") {
9941008
cli_abort(
9951009
"Unsure how to interpret slide units with a custom time type. Consider converting your time
@@ -999,31 +1013,45 @@ validate_slide_window_arg <- function(arg, time_type, lower = 1, allow_inf = TRU
9991013
}
10001014

10011015
msg <- ""
1002-
if (!identical(arg, Inf)) {
1003-
if (time_type == "day") {
1004-
if (!test_int(arg, lower = 0L) && !(inherits(arg, "difftime") && units(arg) == "days")) {
1005-
msg <- glue::glue_collapse(c("difftime with units in days", "non-negative integer", "Inf"), " or ")
1006-
}
1007-
} else if (time_type == "week") {
1008-
if (!(inherits(arg, "difftime") && units(arg) == "weeks")) {
1009-
msg <- glue::glue_collapse(c("difftime with units in weeks", "Inf"), " or ")
1010-
}
1011-
} else if (time_type == "yearmonth") {
1012-
if (!test_int(arg, lower = 0L) || inherits(arg, "difftime")) {
1013-
msg <- glue::glue_collapse(c("non-negative integer", "Inf"), " or ")
1014-
}
1015-
} else if (time_type == "integer") {
1016-
if (!test_int(arg, lower = 0L) || inherits(arg, "difftime")) {
1017-
msg <- glue::glue_collapse(c("non-negative integer", "Inf"), " or ")
1018-
}
1019-
} else {
1020-
msg <- glue::glue_collapse(c("difftime", "non-negative integer", "Inf"), " or ")
1021-
}
1016+
inf_if_okay <- if (allow_inf) {
1017+
"Inf"
10221018
} else {
1023-
if (!allow_inf) {
1024-
msg <- glue::glue_collapse(c("a difftime", "a non-negative integer"), " or ")
1019+
character(0L)
1020+
}
1021+
1022+
# nolint start: indentation_linter.
1023+
if (time_type == "day") {
1024+
if (!(test_sensible_int(arg, lower = lower) ||
1025+
inherits(arg, "difftime") && length(arg) == 1L && units(arg) == "days" ||
1026+
allow_inf && identical(arg, Inf)
1027+
)) {
1028+
msg <- glue::glue_collapse(c("length-1 difftime with units in days", "non-negative integer", inf_if_okay), " or ")
10251029
}
1030+
} else if (time_type == "week") {
1031+
if (!(inherits(arg, "difftime") && length(arg) == 1L && units(arg) == "weeks" ||
1032+
allow_inf && identical(arg, Inf)
1033+
)) {
1034+
msg <- glue::glue_collapse(c("length-1 difftime with units in weeks", inf_if_okay), " or ")
1035+
}
1036+
} else if (time_type == "yearmonth") {
1037+
if (!(test_sensible_int(arg, lower = lower) ||
1038+
allow_inf && identical(arg, Inf)
1039+
)) {
1040+
msg <- glue::glue_collapse(c("non-negative integer", inf_if_okay), " or ")
1041+
}
1042+
} else if (time_type == "integer") {
1043+
if (!(test_sensible_int(arg, lower = lower) ||
1044+
allow_inf && identical(arg, Inf)
1045+
)) {
1046+
msg <- glue::glue_collapse(c("non-negative integer", inf_if_okay), " or ")
1047+
}
1048+
} else {
1049+
cli_abort('`epiprocess` internal error: unrecognized time_type: "{time_type}"',
1050+
class = "epiprocess__unrecognized_time_type"
1051+
)
10261052
}
1053+
# nolint end
1054+
10271055
if (msg != "") {
10281056
cli_abort(
10291057
"Slide function expected `{arg_name}` to be a {msg}.",

man-roxygen/basic-slide-params.R

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
#' with units "days"
1111
#' - if time_type is Date and the cadence is weekly, then `.window_size` must
1212
#' be a difftime with units "weeks"
13-
#' - if time_type is an integer, then `.window_size` must be an integer
13+
#' - if time_type is an yearmonth or integer, then `.window_size` must be an
14+
#' integer
1415
#'
1516
#' @param .align The alignment of the sliding window. If `right` (default), then
1617
#' the window has its end at the reference time; if `center`, then the window is

man/epi_slide.Rd

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/epi_slide_mean.Rd

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/epi_slide_opt.Rd

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/epi_slide_sum.Rd

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/test_sensible_int.Rd

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-epi_slide.R

+2-10
Original file line numberDiff line numberDiff line change
@@ -711,17 +711,9 @@ test_that("epi_slide_opt helper `full_date_seq` returns expected date values", {
711711

712712
test_that("`epi_slide_opt` errors when passed non-`data.table`, non-`slider` functions", {
713713
reexport_frollmean <- data.table::frollmean
714-
expect_no_error(
715-
epi_slide_opt(
716-
test_data,
717-
.col_names = value, .f = reexport_frollmean
718-
)
719-
)
714+
expect_no_error(epi_slide_opt(test_data, .col_names = value, .f = reexport_frollmean, .window_size = 7))
720715
expect_error(
721-
epi_slide_opt(
722-
test_data,
723-
.col_names = value, .f = mean
724-
),
716+
epi_slide_opt(test_data, .col_names = value, .f = mean),
725717
class = "epiprocess__epi_slide_opt__unsupported_slide_function"
726718
)
727719
})

0 commit comments

Comments
 (0)