diff --git a/DESCRIPTION b/DESCRIPTION index a1182786..31315d4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,6 +14,7 @@ Imports: LinkingTo: Rcpp (>= 0.12.10) Suggests: + dplyr (>= 1.0.0), udunits2, NISTunits, measurements, @@ -24,7 +25,8 @@ Suggests: testthat, ggforce, rmarkdown, - magrittr + magrittr, + vctrs (>= 0.3.1) VignetteBuilder: knitr Description: Support for measurement units in R vectors, matrices and arrays: automatic propagation, conversion, derivation diff --git a/NAMESPACE b/NAMESPACE index f3337f1a..4cbe08ba 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -87,19 +87,6 @@ export(unitless) export(units_options) export(valid_udunits) export(valid_udunits_prefixes) -if(getRversion() >= "3.6.0") { - S3method(pillar::type_sum, units) - S3method(pillar::type_sum, mixed_units) - S3method(pillar::pillar_shaft, units) - S3method(pillar::pillar_shaft, mixed_units) - S3method(pillar::format_type_sum, type_sum_units) -} else { - export(type_sum.units) - export(type_sum.mixed_units) - export(pillar_shaft.units) - export(pillar_shaft.mixed_units) - export(format_type_sum.type_sum_units) -} import(graphics) import(stats) import(utils) diff --git a/NEWS.md b/NEWS.md index 6a4a2a23..6819866e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,8 @@ * fix replacement operation for `units` objects; #233 addressing #232 +* fix compatibility with dplyr 1.0; #247 addressing #239 + # version 0.6-6 * prettier `str` print for units and mixed units; #228 addressing #227 diff --git a/R/init.R b/R/init.R index 3b225c90..3a8b85ad 100644 --- a/R/init.R +++ b/R/init.R @@ -15,6 +15,8 @@ NULL else if (l10n_info()[["Latin-1"]]) "latin1" else "ascii" ud_set_encoding(native) + + register_all_s3_methods() } .onAttach <- function(libname, pkgname) { diff --git a/R/misc.R b/R/misc.R index 3ccec7b2..2d1cff99 100644 --- a/R/misc.R +++ b/R/misc.R @@ -114,48 +114,6 @@ seq.units = function(from, to, by = ((to - from)/(length.out - 1)), set_units(NextMethod(), uuu, mode = "standard") } -#' type_sum function for units -#' @name tibble -#' @param x see \link[pillar]{type_sum} -#' @param ... see \link[pillar]{type_sum} -#' @rawNamespace if(getRversion() >= "3.6.0") { -#' S3method(pillar::type_sum, units) -#' S3method(pillar::type_sum, mixed_units) -#' S3method(pillar::pillar_shaft, units) -#' S3method(pillar::pillar_shaft, mixed_units) -#' S3method(pillar::format_type_sum, type_sum_units) -#' } else { -#' export(type_sum.units) -#' export(type_sum.mixed_units) -#' export(pillar_shaft.units) -#' export(pillar_shaft.mixed_units) -#' export(format_type_sum.type_sum_units) -#' } -type_sum.units <- function(x, ...) { - gr = units_options("group") - # see https://github.com/r-lib/pillar/issues/73 : currently the [ and ] mess up things. - structure(paste0(gr[1], as.character(units(x)), gr[2]), - class = "type_sum_units") -} -#' @name tibble -#' @param width ignored -format_type_sum.type_sum_units <- function(x, width, ...) { - if (! requireNamespace("pillar", quietly = TRUE)) - stop("package pillar not available: install first?") - pillar::style_subtle(x) -} - -#' pillar_shaft function for units -#' @name tibble -pillar_shaft.units <- function(x, ...) { - u_char <- as.character(units(x)) - if (! requireNamespace("pillar", quietly = TRUE)) - stop("package pillar not available: install first?") - #out <- paste(format(unclass(x), ...), pillar::style_subtle(u_char)) - out <- format(unclass(x), ...) - pillar::new_pillar_shaft_simple(out, align = "right", min_width = 8) -} - #' @export str.units = function(object, ...) { gr <- units_options("group") diff --git a/R/mixed.R b/R/mixed.R index f9d00769..feee7218 100644 --- a/R/mixed.R +++ b/R/mixed.R @@ -121,16 +121,3 @@ Ops.mixed_units = function(e1, e2) { ret = .as.mixed_units(ret) ret } - -#' @name tibble -type_sum.mixed_units <- function(x, ...) { - "mixed_units" -} - -#' @name tibble -pillar_shaft.mixed_units <- function(x, ...) { - if (! requireNamespace("pillar", quietly = TRUE)) - stop("package pillar not available: install first?") - out <- format(x, ...) - pillar::new_pillar_shaft_simple(out, align = "right", min_width = 6) -} diff --git a/R/tidyverse.R b/R/tidyverse.R new file mode 100644 index 00000000..7a517067 --- /dev/null +++ b/R/tidyverse.R @@ -0,0 +1,115 @@ +type_sum.units <- function(x, ...) { + gr = units_options("group") + # see https://github.com/r-lib/pillar/issues/73 : currently the [ and ] mess up things. + structure(paste0(gr[1], as.character(units(x)), gr[2]), + class = "type_sum_units") +} + +type_sum.mixed_units <- function(x, ...) { + "mixed_units" +} + +pillar_shaft.units <- function(x, ...) { + u_char <- as.character(units(x)) + if (! requireNamespace("pillar", quietly = TRUE)) + stop("package pillar not available: install first?") + #out <- paste(format(unclass(x), ...), pillar::style_subtle(u_char)) + out <- format(unclass(x), ...) + pillar::new_pillar_shaft_simple(out, align = "right", min_width = 8) +} + +pillar_shaft.mixed_units <- function(x, ...) { + if (! requireNamespace("pillar", quietly = TRUE)) + stop("package pillar not available: install first?") + out <- format(x, ...) + pillar::new_pillar_shaft_simple(out, align = "right", min_width = 6) +} + +format_type_sum.type_sum_units <- function(x, width, ...) { + if (! requireNamespace("pillar", quietly = TRUE)) + stop("package pillar not available: install first?") + pillar::style_subtle(x) +} + + +# vctrs proxying and restoration ------------------------------------- + +vec_proxy.units = function(x, ...) { + x +} +vec_restore.units = function(x, to, ...) { + set_units(x, units(to), mode = "standard") +} + + +# vctrs coercion ----------------------------------------------------- + +vec_ptype2.units.units = function(x, y, ..., x_arg = "", y_arg = "") { + x_units = units(x) + y_units = units(y) + + if (!ud_are_convertible(x_units, y_units)) + vctrs::stop_incompatible_type(x, y, x_arg = x_arg, y_arg = y_arg) + + x_bare = drop_units(x) + y_bare = drop_units(y) + common = vctrs::vec_ptype2(x_bare, y_bare, ..., x_arg = x_arg, y_arg = y_arg) + + # Use left-hand side units + set_units(common, x_units, mode = "standard") +} + +vec_cast.units.units = function(x, to, ..., x_arg = "", to_arg = "") { + x_units = units(x) + to_units = units(to) + + if (!ud_are_convertible(x_units, to_units)) + vctrs::stop_incompatible_cast(x, to, x_arg = x_arg, to_arg = to_arg) + + # Convert to target units before converting base type. Unit + # conversion might change the type and so must happen first. + out = set_units(x, to_units, mode = "standard") + + out_bare = drop_units(out) + to_bare = drop_units(to) + out = vctrs::vec_cast(out_bare, to_bare, ..., x_arg = x_arg, to_arg = to_arg) + + # Set target units again + set_units(out, to_units, mode = "standard") +} + + +#nocov start +register_all_s3_methods <- function() { + register_s3_method("pillar::type_sum", "units") + register_s3_method("pillar::type_sum", "mixed_units") + register_s3_method("pillar::pillar_shaft", "units") + register_s3_method("pillar::pillar_shaft", "mixed_units") + register_s3_method("pillar::format_type_sum", "type_sum_units") + register_s3_method("vctrs::vec_proxy", "units") + register_s3_method("vctrs::vec_restore", "units") + register_s3_method("vctrs::vec_ptype2", "units.units") + register_s3_method("vctrs::vec_cast", "units.units") +} + +register_s3_method <- function(generic, class, fun=NULL) { + stopifnot(is.character(generic), length(generic) == 1) + stopifnot(is.character(class), length(class) == 1) + + pieces <- strsplit(generic, "::")[[1]] + stopifnot(length(pieces) == 2) + package <- pieces[[1]] + generic <- pieces[[2]] + + if (is.null(fun)) + fun <- get(paste0(generic, ".", class), envir=parent.frame()) + stopifnot(is.function(fun)) + + if (package %in% loadedNamespaces()) + registerS3method(generic, class, fun, envir=asNamespace(package)) + + # Always register hook in case package is later unloaded & reloaded + setHook(packageEvent(package, "onLoad"), function(...) + registerS3method(generic, class, fun, envir=asNamespace(package))) +} +# nocov end diff --git a/man/tibble.Rd b/man/tibble.Rd deleted file mode 100644 index 8ca8b4f7..00000000 --- a/man/tibble.Rd +++ /dev/null @@ -1,33 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/misc.R, R/mixed.R -\name{tibble} -\alias{tibble} -\alias{type_sum.units} -\alias{format_type_sum.type_sum_units} -\alias{pillar_shaft.units} -\alias{type_sum.mixed_units} -\alias{pillar_shaft.mixed_units} -\title{type_sum function for units} -\usage{ -type_sum.units(x, ...) - -format_type_sum.type_sum_units(x, width, ...) - -pillar_shaft.units(x, ...) - -type_sum.mixed_units(x, ...) - -pillar_shaft.mixed_units(x, ...) -} -\arguments{ -\item{x}{see \link[pillar]{type_sum}} - -\item{...}{see \link[pillar]{type_sum}} - -\item{width}{ignored} -} -\description{ -type_sum function for units - -pillar_shaft function for units -} diff --git a/tests/testthat/test_tidyverse.R b/tests/testthat/test_tidyverse.R new file mode 100644 index 00000000..e736cf0d --- /dev/null +++ b/tests/testthat/test_tidyverse.R @@ -0,0 +1,82 @@ + +skip_if_not_installed("vctrs") + +test_that("units have coercion methods", { + x = set_units(1:3, "cm") + y = set_units(4.0, "m") + z = set_units(10, "celsius") + + expect_error(vctrs::vec_ptype_common(y, x, z), class = "vctrs_error_incompatible_type") + expect_error(vctrs::vec_cast_common(y, x, z), class = "vctrs_error_incompatible_type") + + expect_identical(vctrs::vec_ptype_common(x, y, x), set_units(double(), "cm")) + expect_identical(vctrs::vec_ptype_common(x, x), set_units(integer(), "cm")) + expect_identical(vctrs::vec_ptype_common(y, x, x), set_units(double(), "m")) + + expect_identical( + vctrs::vec_cast_common(x, y), + list(set_units(c(1, 2, 3), "cm"), set_units(400, "cm")) + ) + expect_identical( + vctrs::vec_cast_common(y, x), + list(set_units(4, "m"), set_units(c(0.01, 0.02, 0.03), "m")) + ) + + # Casting to integer with fractional cm is lossy + expect_error( + vctrs::vec_cast_common(y, x, .to = set_units(0L, "m")), + class = "vctrs_error_cast_lossy" + ) +}) + +test_that("can combine units vectors", { + x <- set_units(1:3, "cm") + y <- set_units(4, "m") + + exp = set_units(c(1, 2, 3, 400), "cm") + expect_identical(vctrs::vec_c(x, y), exp) + + # Recursive case + df1 = tibble::tibble(x = tibble::tibble(x = x)) + df2 = tibble::tibble(x = tibble::tibble(x = y)) + df_exp = tibble::tibble(x = tibble::tibble(x = exp)) + expect_identical(vctrs::vec_c(df1, df2), df_exp) +}) + +test_that("can slice units vectors", { + x = set_units(1:3, "cm") + exp = list(set_units(1L, "cm"), set_units(2L, "cm"), set_units(3L, "cm")) + expect_identical(vctrs::vec_chop(x), exp) + + # Recursive case + df = tibble::tibble(tibble::tibble(x = x)) + exp = list( + tibble::tibble(x = set_units(1L, "cm")), + tibble::tibble(x = set_units(2L, "cm")), + tibble::tibble(x = set_units(3L, "cm")) + ) + expect_identical(vctrs::vec_chop(df), exp) +}) + + +skip_if_not_installed("dplyr") + +`%>%` <- dplyr::`%>%` + +test_that("split-apply-combine with dplyr and base agree", { + iris2 <- iris + for (i in 1:4) + units(iris2[,i]) <- "cm" + + out <- iris2 %>% + dplyr::group_by(Species) %>% + dplyr::summarise(dplyr::across(where(is.numeric), mean)) + + # Transform to list of lists + out <- vctrs::vec_chop(out[2:5]) %>% + stats::setNames(out$Species) %>% + lapply(as.list) + + exp <- lapply(split(iris2[1:4], iris2$Species), lapply, mean) + expect_equal(out, exp) +})