diff --git a/NAMESPACE b/NAMESPACE index bd7bc18..8088964 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,7 +17,9 @@ S3method(extract_pmid,pmc_search) S3method(html_col_sort,data.frame) S3method(html_col_sort,default) S3method(identify_obsolete,doid_edit) -S3method(is_invariant,character) +S3method(is_invariant,data.frame) +S3method(is_invariant,default) +S3method(is_invariant,list) S3method(is_invariant,numeric) S3method(print,get_url_names) S3method(print,oieb) diff --git a/NEWS.md b/NEWS.md index b313196..7e2a67f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # DO.utils (development version) +## General + +## Updated +* `is_invariant()` no works for more than just character & numeric vectors, with new `list` and `data.frame` methods and a `default` method that should be able to handle more cases (and replaces the `character` method). + + ## DO Management & Analysis ### Updated diff --git a/R/predicates.R b/R/predicates.R index 9c9e316..13fd9ad 100644 --- a/R/predicates.R +++ b/R/predicates.R @@ -1,33 +1,61 @@ -#' Test if Vector is Invariant +#' Test if an Object is Invariant #' -#' Test if a vector is invariant (_i.e._ all values are equal, within a given +#' Test if an object is invariant (_i.e._ all values are equal, within a given #' tolerance for numeric vectors). #' -#' @param x vector to be tested +#' @param x object to be tested #' @param na.rm logical indicating whether to exclude NA values -#' @param tol double, tolerance to use (for numeric vectors) #' @param ... unused; for extensibility #' #' @family value predicates #' @family predicates #' @export -is_invariant <- function(x, na.rm = FALSE, ...) { +is_invariant <- function(x, ...) { UseMethod("is_invariant") } #' @export #' @rdname is_invariant -is_invariant.character <- function(x, na.rm = FALSE, ...) { - dplyr::n_distinct(x, na.rm = na.rm) == 1 +is_invariant.default <- function(x, na.rm = FALSE, ...) { + if (isTRUE(na.rm)) { + x <- stats::na.omit(x) + } + length(unique(x)) == 1 } #' @export #' @rdname is_invariant +#' @param tol double, tolerance to use (for numeric vectors) is_invariant.numeric <- function(x, na.rm = FALSE, tol = sqrt(.Machine$double.eps), ...) { + if (isFALSE(na.rm)) { + na_n <- sum(is.na(x)) + if (na_n == length(x)) return(TRUE) + if (na_n > 0) return(FALSE) + } + diff(range(x, na.rm = na.rm)) < tol } +#' @export +#' @rdname is_invariant +#' @param incl_nm Whether top-level names should be included in determining if a +#' list is invariant (default: `TRUE`). +is_invariant.list <- function(x, incl_nm = TRUE, ...) { + nm <- names(x) + if (isFALSE(incl_nm) || is.null(nm)) { + return(length(unique(x)) == 1) + } + + length(unique(x)) == 1 && length(unique(nm)) == 1 +} + +#' @export +#' @rdname is_invariant +is_invariant.data.frame <- function(x, ...) { + nrow(unique(x)) == 1 +} + #' Character value predicates #' diff --git a/man/is_invariant.Rd b/man/is_invariant.Rd index 049e5b2..e727273 100644 --- a/man/is_invariant.Rd +++ b/man/is_invariant.Rd @@ -2,27 +2,36 @@ % Please edit documentation in R/predicates.R \name{is_invariant} \alias{is_invariant} -\alias{is_invariant.character} +\alias{is_invariant.default} \alias{is_invariant.numeric} -\title{Test if Vector is Invariant} +\alias{is_invariant.list} +\alias{is_invariant.data.frame} +\title{Test if an Object is Invariant} \usage{ -is_invariant(x, na.rm = FALSE, ...) +is_invariant(x, ...) -\method{is_invariant}{character}(x, na.rm = FALSE, ...) +\method{is_invariant}{default}(x, na.rm = FALSE, ...) \method{is_invariant}{numeric}(x, na.rm = FALSE, tol = sqrt(.Machine$double.eps), ...) + +\method{is_invariant}{list}(x, incl_nm = TRUE, ...) + +\method{is_invariant}{data.frame}(x, ...) } \arguments{ -\item{x}{vector to be tested} - -\item{na.rm}{logical indicating whether to exclude NA values} +\item{x}{object to be tested} \item{...}{unused; for extensibility} +\item{na.rm}{logical indicating whether to exclude NA values} + \item{tol}{double, tolerance to use (for numeric vectors)} + +\item{incl_nm}{Whether top-level names should be included in determining if a +list is invariant (default: \code{TRUE}).} } \description{ -Test if a vector is invariant (\emph{i.e.} all values are equal, within a given +Test if an object is invariant (\emph{i.e.} all values are equal, within a given tolerance for numeric vectors). } \seealso{ diff --git a/tests/testthat/test-predicates.R b/tests/testthat/test-predicates.R index 5bfc99b..926d8a0 100644 --- a/tests/testthat/test-predicates.R +++ b/tests/testthat/test-predicates.R @@ -28,6 +28,112 @@ test_that("is_valid_doid works", { }) +# is_invariant() tests ---------------------------------------------------- + +test_that("is_invariant() default method works (chr, lgl)", { + .chr <- letters[1:2] + .chr_invar <- c("a", "a") + + expect_false(is_invariant(.chr)) + expect_true(is_invariant(.chr_invar)) + + .chr_na <- c("a", NA_character_) + + expect_false(is_invariant(.chr_na)) + expect_true(is_invariant(.chr_na, na.rm = TRUE)) + + .lgl <- c(T, F) + .lgl_invar <- rep(T, 2) + + expect_false(is_invariant(.lgl)) + expect_true(is_invariant(.lgl_invar)) + + .lgl_na <- c(T, NA_character_) + + expect_false(is_invariant(.lgl_na)) + expect_true(is_invariant(.lgl_na, na.rm = TRUE)) +}) + +test_that("is_invariant() list method works", { + ##### simple lists ##### + .l <- list(1, 2) + .l_invar <- list("a", "a") + expect_false(is_invariant(.l)) + expect_true(is_invariant(.l_invar)) + + # with names + .l_nm <- list(a = 1, b = 2) + .l_nm_only <- list(a = 1, b = 1) + .l_nm_invar <- list(a = 1, a = 1) + expect_false(is_invariant(.l_nm)) + expect_false(is_invariant(.l_nm_only)) + expect_true(is_invariant(.l_nm_only, incl_nm = FALSE)) + expect_true(is_invariant(.l_nm_invar)) + + ##### 2-level lists ##### fully unnamed + .ll <- list(list(1), list(2)) + .ll_invar <- list(list(1), list(1)) + expect_false(is_invariant(.ll)) + expect_true(is_invariant(.ll_invar)) + + # with names at lvl 2 + .ll_nm <- list(list(a = 1), list(b = 1)) + .ll_nm_invar <- list(list(a = 1), list(a = 1)) + expect_false(is_invariant(.ll_nm)) + expect_false(is_invariant(.ll_nm, incl_nm = FALSE)) + expect_true(is_invariant(.ll_nm_invar)) + + # with names at lvl 1 + .l_nm_l <- list(a = list(1), b = list(2)) + .l_nm_only_l <- list(a = list(1), b = list(1)) + .l_nm_l_invar <- list(a = list(1), a = list(1)) + expect_false(is_invariant(.l_nm_l)) + expect_false(is_invariant(.l_nm_only_l)) + expect_true(is_invariant(.l_nm_only_l, incl_nm = FALSE)) + expect_true(is_invariant(.l_nm_l_invar)) + + # with names at both lvls + .l_nm_l_nm <- list(a = list(x = 1), b = list(y = 2)) + .l_nm_l_nm_only <- list(a = list(x = 1), a = list(y = 1)) + .l_nm_only_l_nm <- list(a = list(x = 1), b = list(x = 1)) + .l_nm_l_nm_invar <- list(a = list(x = 1), a = list(x = 1)) + expect_false(is_invariant(.l_nm_l_nm)) + expect_false(is_invariant(.l_nm_l_nm_only)) + expect_false(is_invariant(.l_nm_only_l_nm)) + expect_true(is_invariant(.l_nm_only_l_nm, incl_nm = FALSE)) + expect_true(is_invariant(.l_nm_l_invar)) +}) + +test_that("is_invariant() data.frame method works", { + df <- datasets::mtcars[1:2, ] + df_invar <- df + df_invar[2, ] <- df_invar[1, ] + + expect_false(is_invariant(df)) + expect_true(is_invariant(df_invar)) +}) + +test_that("is_invariant() numeric method works", { + .int <- 1:2 + .int_invar <- rep(1, 2) + + expect_false(is_invariant(.int)) + expect_true(is_invariant(.int_invar)) + + .int_na <- c(1, NA) + + expect_false(is_invariant(.int_na)) + expect_true(is_invariant(.int_na, na.rm = TRUE)) + + .num <- c(1, 1 + sqrt(.Machine$double.eps)) + .num_invar <- c(1, 1 + sqrt(.Machine$double.eps) - 1e-14) + + expect_false(is_invariant(.num)) + expect_true(is_invariant(.num_invar)) + expect_true(is_invariant(.num, tol = sqrt(.Machine$double.eps) + 1e-14)) +}) + + # is_curie() tests -------------------------------------------------------- test_that("is_curie(def = 'obo') works", {