Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paste_skip_na() #538

Merged
merged 3 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
64 changes: 64 additions & 0 deletions R/paste_skip_na.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#' Like \code{paste()}, but missing values are omitted
#'
#' @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
sfirke marked this conversation as resolved.
Show resolved Hide resolved
# NA_character_
as.character(args[[1]])
}
} else {
# There are at least 2 arguments; paste the first two and recurse
billdenney marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
26 changes: 26 additions & 0 deletions man/paste_skip_na.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions tests/testthat/test-paste_skip_na.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 = ",")
billdenney marked this conversation as resolved.
Show resolved Hide resolved
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"))

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
)
})