From 31a02a633a0230cd14b506f7e899535939d75853 Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Tue, 18 Apr 2023 12:57:00 -0400 Subject: [PATCH 1/3] Add paste_skip_na() --- NAMESPACE | 1 + NEWS.md | 4 ++ R/paste_skip_na.R | 64 +++++++++++++++++++++++++++++ man/paste_skip_na.Rd | 26 ++++++++++++ tests/testthat/test-paste_skip_na.R | 14 +++++++ 5 files changed, 109 insertions(+) create mode 100644 R/paste_skip_na.R create mode 100644 man/paste_skip_na.Rd create mode 100644 tests/testthat/test-paste_skip_na.R diff --git a/NAMESPACE b/NAMESPACE index 41ea3c89..0067ad1d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,6 +39,7 @@ export(fisher.test) export(get_dupes) export(get_one_to_one) export(make_clean_names) +export(paste_skip_na) export(remove_constant) export(remove_empty) export(remove_empty_cols) diff --git a/NEWS.md b/NEWS.md index 992ef09e..bde7d0a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # janitor 2.2.0.9000 - unreleased development version +## New features + +* A new function `paste_skip_na()` pastes without including NA values (#537). + ## Bug fixes * `adorn_totals("row")` now succeeds if the new `name` of the totals row is already a factor level of the input data.frame (#529, thanks @egozoglu for reporting). diff --git a/R/paste_skip_na.R b/R/paste_skip_na.R new file mode 100644 index 00000000..0f61bb88 --- /dev/null +++ b/R/paste_skip_na.R @@ -0,0 +1,64 @@ +#' Concatenate strings dropping missing values +#' +#' @details If all values are missing, the value from the first argument is +#' preserved. +#' +#' @param ...,sep,collapse See \code{?paste} +#' @return A character vector of pasted values. +#' @examples +#' paste_skip_na(NA) # NA_character_ +#' paste_skip_na("A", NA) # "A" +#' paste_skip_na("A", NA, c(NA, "B"), sep = ",") # c("A", "A,B") +#' @export +paste_skip_na <- function(..., sep=" ", collapse=NULL) { + args <- list(...) + if (length(args) <= 1) { + if (length(args) == 0) { + # match the behavior of paste + paste(sep=sep, collapse=collapse) + } else if (!is.null(collapse)) { + if (all(is.na(args[[1]]))) { + # Collapsing with all NA values results in NA + NA_character_ + } else { + # Collapsing without all NA values collapses the non-NA values + paste(na.omit(args[[1]]), sep=sep, collapse=collapse) + } + } else { + # as.character() to ensure that logical NA values are converted to + # NA_character_ + as.character(args[[1]]) + } + } else { + # There are at least 2 arguments; paste the first two and recurse + a1 <- args[[1]] + a2 <- args[[2]] + if (length(a1) != length(a2)) { + if (length(a1) == 1) { + a1 <- rep(a1, length(a2)) + } else if (length(a2) == 1) { + a2 <- rep(a2, length(a1)) + } else { + stop("Arguments must be the same length or one argument must be a scalar.") + } + } + # Which arguments are NA, if any? + mask1 <- !is.na(a1) + mask2 <- !is.na(a2) + mask_both <- mask1 & mask2 + mask_only2 <- (!mask1) & mask2 + firsttwo <- a1 + if (any(mask_only2)) { + firsttwo[mask_only2] <- a2[mask_only2] + } + if (any(mask_both)) { + # Collapse only occurs on the final pasting + firsttwo[mask_both] <- paste(a1[mask_both], a2[mask_both], sep=sep, collapse=NULL) + } + # prepare to recurse, and recurse + new_args <- append(list(firsttwo), args[-(1:2)]) + new_args$sep <- sep + new_args$collapse <- collapse + do.call(paste_skip_na, new_args) + } +} diff --git a/man/paste_skip_na.Rd b/man/paste_skip_na.Rd new file mode 100644 index 00000000..f2e41fdd --- /dev/null +++ b/man/paste_skip_na.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/paste_skip_na.R +\name{paste_skip_na} +\alias{paste_skip_na} +\title{Concatenate strings dropping missing values} +\usage{ +paste_skip_na(..., sep = " ", collapse = NULL) +} +\arguments{ +\item{..., sep, collapse}{See \code{?paste}} +} +\value{ +A character vector of pasted values. +} +\description{ +Concatenate strings dropping missing values +} +\details{ +If all values are missing, the value from the first argument is + preserved. +} +\examples{ +paste_skip_na(NA) # NA_character_ +paste_skip_na("A", NA) # "A" +paste_skip_na("A", NA, c(NA, "B"), sep = ",") # c("A", "A,B") +} diff --git a/tests/testthat/test-paste_skip_na.R b/tests/testthat/test-paste_skip_na.R new file mode 100644 index 00000000..e34288d5 --- /dev/null +++ b/tests/testthat/test-paste_skip_na.R @@ -0,0 +1,14 @@ +test_that("paste_skip_na", { + # handle no arguments the same as paste() + expect_equal(paste_skip_na(), paste()) + expect_equal(paste_skip_na(NA), NA_character_) + expect_equal(paste_skip_na(NA, NA), NA_character_) + expect_equal(paste_skip_na(NA, NA, sep = ","), NA_character_) + # It does not behave like paste(NA, NA, collapse = ",") nor does it behave like paste(c(), collapse = ",") + expect_equal(paste_skip_na(NA, NA, collapse = ","), NA_character_) + + expect_equal(paste_skip_na("A", NA), "A") + expect_equal(paste_skip_na("A", NA, collapse = ","), "A") + expect_equal(paste_skip_na("A", NA, c(NA, "B"), collapse = ","), "A,A B") + expect_equal(paste_skip_na("A", NA, c(NA, "B"), sep = ","), c("A", "A,B")) +}) From 0c89591b6343e750e9da5152545e35f7d99b5dba Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Tue, 18 Apr 2023 13:08:24 -0400 Subject: [PATCH 2/3] Improve test coverage --- tests/testthat/test-paste_skip_na.R | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/testthat/test-paste_skip_na.R b/tests/testthat/test-paste_skip_na.R index e34288d5..46f9d31c 100644 --- a/tests/testthat/test-paste_skip_na.R +++ b/tests/testthat/test-paste_skip_na.R @@ -11,4 +11,15 @@ test_that("paste_skip_na", { expect_equal(paste_skip_na("A", NA, collapse = ","), "A") expect_equal(paste_skip_na("A", NA, c(NA, "B"), collapse = ","), "A,A B") expect_equal(paste_skip_na("A", NA, c(NA, "B"), sep = ","), c("A", "A,B")) + + expect_equal(paste_skip_na(c("A", "B"), NA), c("A", "B")) + expect_equal(paste_skip_na(NA, c("A", "B")), c("A", "B")) +}) + +test_that("paste_skip_na expected errors", { + expect_error( + paste_skip_na(c("A", "B"), c("A", "B", "C")), + regexp = "Arguments must be the same length or one argument must be a scalar.", + fixed = TRUE + ) }) From 40c172a85d414a246db9b751036612f8b6d915f9 Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Mon, 1 May 2023 11:35:56 -0400 Subject: [PATCH 3/3] Update for code review comments; standardize coding style and improve documentation --- R/paste_skip_na.R | 10 +++++----- man/paste_skip_na.Rd | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/R/paste_skip_na.R b/R/paste_skip_na.R index 0f61bb88..27784c3d 100644 --- a/R/paste_skip_na.R +++ b/R/paste_skip_na.R @@ -1,4 +1,4 @@ -#' Concatenate strings dropping missing values +#' Like \code{paste()}, but missing values are omitted #' #' @details If all values are missing, the value from the first argument is #' preserved. @@ -10,19 +10,19 @@ #' paste_skip_na("A", NA) # "A" #' paste_skip_na("A", NA, c(NA, "B"), sep = ",") # c("A", "A,B") #' @export -paste_skip_na <- function(..., sep=" ", collapse=NULL) { +paste_skip_na <- function(..., sep = " ", collapse = NULL) { args <- list(...) if (length(args) <= 1) { if (length(args) == 0) { # match the behavior of paste - paste(sep=sep, collapse=collapse) + paste(sep = sep, collapse = collapse) } else if (!is.null(collapse)) { if (all(is.na(args[[1]]))) { # Collapsing with all NA values results in NA NA_character_ } else { # Collapsing without all NA values collapses the non-NA values - paste(na.omit(args[[1]]), sep=sep, collapse=collapse) + paste(na.omit(args[[1]]), sep = sep, collapse = collapse) } } else { # as.character() to ensure that logical NA values are converted to @@ -53,7 +53,7 @@ paste_skip_na <- function(..., sep=" ", collapse=NULL) { } if (any(mask_both)) { # Collapse only occurs on the final pasting - firsttwo[mask_both] <- paste(a1[mask_both], a2[mask_both], sep=sep, collapse=NULL) + firsttwo[mask_both] <- paste(a1[mask_both], a2[mask_both], sep = sep, collapse = NULL) } # prepare to recurse, and recurse new_args <- append(list(firsttwo), args[-(1:2)]) diff --git a/man/paste_skip_na.Rd b/man/paste_skip_na.Rd index f2e41fdd..1f41563b 100644 --- a/man/paste_skip_na.Rd +++ b/man/paste_skip_na.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/paste_skip_na.R \name{paste_skip_na} \alias{paste_skip_na} -\title{Concatenate strings dropping missing values} +\title{Like \code{paste()}, but missing values are omitted} \usage{ paste_skip_na(..., sep = " ", collapse = NULL) } @@ -13,7 +13,7 @@ paste_skip_na(..., sep = " ", collapse = NULL) A character vector of pasted values. } \description{ -Concatenate strings dropping missing values +Like \code{paste()}, but missing values are omitted } \details{ If all values are missing, the value from the first argument is