Skip to content

Commit 2c66d62

Browse files
authored
GH-45643: [R] Implement hms functions to create and manipulate time of day variables (#46206)
### Rationale for this change Add support for hms functions for creating time objects ### What changes are included in this PR? Implementing `hms::hms()` and `hms::as_hms()` ### Are these changes tested? Yes ### Are there any user-facing changes? Yes * GitHub Issue: #45643 Authored-by: Nic Crane <thisisnic@gmail.com> Signed-off-by: Nic Crane <thisisnic@gmail.com>
1 parent 171ffe8 commit 2c66d62

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

r/R/dplyr-funcs-datetime.R

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ register_bindings_datetime <- function() {
2727
register_bindings_duration_helpers()
2828
register_bindings_datetime_parsers()
2929
register_bindings_datetime_rounding()
30+
register_bindings_hms()
3031
}
3132

3233
register_bindings_datetime_utility <- function() {
@@ -826,3 +827,59 @@ register_bindings_datetime_rounding <- function() {
826827
}
827828
)
828829
}
830+
831+
register_bindings_hms <- function() {
832+
numeric_to_time32 <- function(x) {
833+
# The only numeric which can be cast to time32 is int32 so double cast to make sure
834+
cast(cast(x, int32()), time32(unit = "s"))
835+
}
836+
837+
datetime_to_time32 <- function(datetime) {
838+
hour <- call_binding("hour", datetime)
839+
min <- call_binding("minute", datetime)
840+
sec <- call_binding("second", datetime)
841+
842+
return(call_binding("hms::hms", seconds = sec, minutes = min, hours = hour))
843+
}
844+
845+
register_binding(
846+
"hms::hms",
847+
function(seconds = 0, minutes = 0, hours = 0, days = 0) {
848+
if (!call_binding("is.numeric", seconds) || !call_binding("is.numeric", minutes) ||
849+
!call_binding("is.numeric", hours) || !call_binding("is.numeric", days)) {
850+
abort("All arguments must be numeric or NA_real_")
851+
}
852+
853+
total_secs <- seconds +
854+
Expression$create("multiply_checked", minutes, 60) +
855+
Expression$create("multiply_checked", hours, 3600) +
856+
Expression$create("multiply_checked", days, 86400)
857+
858+
return(numeric_to_time32(total_secs))
859+
}
860+
)
861+
862+
register_binding(
863+
"hms::as_hms",
864+
function(x = numeric()) {
865+
if (call_binding("is.POSIXct", x)) {
866+
return(datetime_to_time32(x))
867+
}
868+
869+
if (call_binding("is.numeric", x)) {
870+
return(numeric_to_time32(x))
871+
}
872+
873+
if (call_binding("is.character", x)) {
874+
dash <- call_binding("gsub", ":", "-", x)
875+
as_date_time_string <- call_binding("str_c", "1970-01-01", dash, sep = "-")
876+
as_date_time <- Expression$create(
877+
"strptime",
878+
as_date_time_string,
879+
options = list(format = "%Y-%m-%d-%H-%M-%S", unit = 0L)
880+
)
881+
return(datetime_to_time32(as_date_time))
882+
}
883+
}
884+
)
885+
}

r/R/dplyr-funcs-doc.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@
297297
#' * [`ymd_hms()`][lubridate::ymd_hms()]: `locale` argument not supported
298298
#' * [`yq()`][lubridate::yq()]: `locale` argument not supported
299299
#'
300+
#' ## hms
301+
#'
302+
#' * [`hms()`][hms::hms()]: subsecond times not supported
303+
#' * [`hms()`][hms::as_hms()]: subsecond times not supported
304+
#'
300305
#' ## methods
301306
#'
302307
#' * [`is()`][methods::is()]

r/man/acero.Rd

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

r/tests/testthat/test-dplyr-funcs-datetime.R

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ skip_on_r_older_than("3.5")
2020

2121
library(lubridate, warn.conflicts = FALSE)
2222
library(dplyr, warn.conflicts = FALSE)
23+
library(hms)
2324

2425
skip_if_not_available("acero")
2526
# Skip these tests on CRAN due to build times > 10 mins
@@ -3726,3 +3727,69 @@ test_that("with_tz() and force_tz() can add timezone to timestamp without timezo
37263727
)
37273728
)
37283729
})
3730+
3731+
test_that("hms::hms", {
3732+
test_df <- tibble::tibble(
3733+
s = c(1, 2, 0, NA),
3734+
m = c(3, 4, 0, NA),
3735+
h = c(5, 6, 0, NA),
3736+
d = c(7, 8, 0, NA)
3737+
)
3738+
3739+
compare_dplyr_binding(
3740+
.input %>%
3741+
mutate(
3742+
time = hms::hms(s),
3743+
time2 = hms::hms(s, m),
3744+
time3 = hms::hms(s, m, h),
3745+
time4 = hms::hms(s, m, h, d),
3746+
time5 = hms::hms(days = d)
3747+
) %>%
3748+
collect(),
3749+
test_df
3750+
)
3751+
3752+
expect_error(
3753+
call_binding("hms::hms", "nonsense"),
3754+
regexp = "All arguments must be numeric or NA"
3755+
)
3756+
3757+
# Works for NA_real_
3758+
expect_silent(
3759+
call_binding("hms::hms", seconds = NA_real_)
3760+
)
3761+
3762+
# raw NA is logical so we error here
3763+
expect_error(
3764+
call_binding("hms::hms", seconds = NA),
3765+
regexp = "All arguments must be numeric or NA_real_"
3766+
)
3767+
3768+
})
3769+
3770+
test_that("hms::as_hms", {
3771+
test_df <- tibble(
3772+
hms_string = c("0:7:45", "12:34:56"),
3773+
int = c(30L, 75L),
3774+
integerish_dbl = c(31, 76),
3775+
dbl = c(31.2, 76.4),
3776+
datetime = as.POSIXct(c(1645243500, 1745243500), tz = "UTC")
3777+
)
3778+
3779+
compare_dplyr_binding(
3780+
.input %>%
3781+
mutate(
3782+
x = hms::as_hms(hms_string),
3783+
x2 = hms::as_hms(int),
3784+
x3 = hms::as_hms(integerish_dbl),
3785+
x4 = hms::as_hms(datetime)
3786+
) %>%
3787+
collect(),
3788+
test_df
3789+
)
3790+
3791+
expect_error(
3792+
arrow_table(test_df) %>% mutate(y = hms::as_hms(dbl)) %>% collect(),
3793+
"was truncated converting to int32"
3794+
)
3795+
})

0 commit comments

Comments
 (0)