diff --git a/NAMESPACE b/NAMESPACE index 1e357627..68121ead 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -21,6 +21,8 @@ export(assert_valid_list_format) export(attr_label) export(attr_label_df) export(co_relevels) +export(combine_list_rules) +export(combine_rules) export(cut_by_group) export(get_arg) export(get_log) diff --git a/NEWS.md b/NEWS.md index c0597066..929f691d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Rules specified under the `all_datasets` keyword in a format list will apply to every data set of the reformatted object unless specified otherwise. * New `verbose` argument in the `reformat` method. When applied to `list` the value of this augment can be controlled with the `dunlin.reformat.verbose` option or the `R_DUNLIN_REFORMAT_VERBOSE` environment variable. * Improve the output when printing `rule` objects. +* New `combine_rules` and `combine_list_rules` functions to combine rules or list of rules into a single rule or a single list of rules. # dunlin 0.1.6 diff --git a/R/rules.R b/R/rules.R index 97e77c52..61428d92 100644 --- a/R/rules.R +++ b/R/rules.R @@ -125,3 +125,91 @@ as.list.rule <- function(x, ...) { r_list } + +#' Combine Two Rules +#' +#' @param x (`rule`) to modify. +#' @param y (`rule`) rule whose mapping will take precedence over the ones described in `x`. +#' @param ... not used. +#' +#' @note The order of the mappings in the resulting rule corresponds to the order of the mappings in `x` followed by the +#' mappings that are only present in `y`. +#' +#' @returns a `rule`. +#' @export +#' @examples +#' r1 <- rule( +#' "first" = c("from ori rule", "FROM ORI RULE"), +#' "last" = c(NA, "last"), +#' .to_NA = "X", +#' .drop = TRUE +#' ) +#' r2 <- rule( +#' "first" = c("F", "f"), +#' "second" = c("S", "s"), +#' "third" = c("T", "t"), +#' .to_NA = "something" +#' ) +#' combine_rules(r1, r2) +combine_rules <- function(x, y, ...) { + checkmate::assert_class(x, "rule", null.ok = TRUE) + checkmate::assert_class(y, "rule", null.ok = TRUE) + + if (is.null(x) && is.null(y)) { + rlang::abort("Both rules are NULL.") + } + + # If one of the rules is NULL, return the other (via empty list). + x <- as.list(x) + y <- as.list(y) + + x[names(y)] <- y + + r <- do.call(rule, x) + r +} + +#' Combine Rules Found in Lists of Rules. +#' +#' @param x (`list`) of `rule` objects. +#' @param val (`list`) of `rule` objects. +#' @param ... passed to [`dunlin::combine_rules`]. +#' +#' @returns a `list` of `rule` objects. +#' @export +#' @examples +#' l1 <- list( +#' r1 = rule( +#' "first" = c("overwritten", "OVERWRITTEN"), +#' "almost first" = c(NA, "almost") +#' ), +#' r2 = rule( +#' ANYTHING = "anything" +#' ) +#' ) +#' +#' l2 <- list( +#' r1 = rule( +#' "first" = c("F", "f"), +#' "second" = c("S", "s"), +#' "third" = c("T", "t"), +#' .to_NA = "something" +#' ), +#' r3 = rule( +#' SOMETHING = "something" +#' ) +#' ) +#' +#' combine_list_rules(l1, l2) +combine_list_rules <- function(x, val, ...) { + # Unique names prevents zero-character names. + checkmate::assert_list(x, types = "rule", null.ok = FALSE, names = "unique") + checkmate::assert_list(val, types = "rule", null.ok = FALSE, names = "unique") + + vnames <- names(val) + + for (v in vnames) { + x[[v]] <- combine_rules(x[[v]], val[[v]], ...) + } + x +} diff --git a/_pkgdown.yaml b/_pkgdown.yaml index 7aa00249..c537db15 100644 --- a/_pkgdown.yaml +++ b/_pkgdown.yaml @@ -47,6 +47,8 @@ reference: - rule - as.list.rule - list2rules + - combine_rules + - combine_list_rules - title: Filtering contents: - log_filter diff --git a/man/combine_list_rules.Rd b/man/combine_list_rules.Rd new file mode 100644 index 00000000..afe247e0 --- /dev/null +++ b/man/combine_list_rules.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rules.R +\name{combine_list_rules} +\alias{combine_list_rules} +\title{Combine Rules Found in Lists of Rules.} +\usage{ +combine_list_rules(x, val, ...) +} +\arguments{ +\item{x}{(\code{list}) of \code{rule} objects.} + +\item{val}{(\code{list}) of \code{rule} objects.} + +\item{...}{passed to \code{\link{combine_rules}}.} +} +\value{ +a \code{list} of \code{rule} objects. +} +\description{ +Combine Rules Found in Lists of Rules. +} +\examples{ +l1 <- list( + r1 = rule( + "first" = c("overwritten", "OVERWRITTEN"), + "almost first" = c(NA, "almost") + ), + r2 = rule( + ANYTHING = "anything" + ) +) + +l2 <- list( + r1 = rule( + "first" = c("F", "f"), + "second" = c("S", "s"), + "third" = c("T", "t"), + .to_NA = "something" + ), + r3 = rule( + SOMETHING = "something" + ) +) + +combine_list_rules(l1, l2) +} diff --git a/man/combine_rules.Rd b/man/combine_rules.Rd new file mode 100644 index 00000000..a6d6e5a0 --- /dev/null +++ b/man/combine_rules.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rules.R +\name{combine_rules} +\alias{combine_rules} +\title{Combine Two Rules} +\usage{ +combine_rules(x, y, ...) +} +\arguments{ +\item{x}{(\code{rule}) to modify.} + +\item{y}{(\code{rule}) rule whose mapping will take precedence over the ones described in \code{x}.} + +\item{...}{not used.} +} +\value{ +a \code{rule}. +} +\description{ +Combine Two Rules +} +\note{ +The order of the mappings in the resulting rule corresponds to the order of the mappings in \code{x} followed by the +mappings that are only present in \code{y}. +} +\examples{ +r1 <- rule( + "first" = c("from ori rule", "FROM ORI RULE"), + "last" = c(NA, "last"), + .to_NA = "X", + .drop = TRUE +) +r2 <- rule( + "first" = c("F", "f"), + "second" = c("S", "s"), + "third" = c("T", "t"), + .to_NA = "something" +) +combine_rules(r1, r2) +} diff --git a/tests/testthat/test-rules.R b/tests/testthat/test-rules.R index 5f06c0b4..b7c46641 100644 --- a/tests/testthat/test-rules.R +++ b/tests/testthat/test-rules.R @@ -122,3 +122,111 @@ test_that("as.list and rule are reversible when .to_NA is NULL", { test_rule <- rule(a = c("a", "b"), b = c("c", "d"), .drop = FALSE, .na_last = TRUE, .to_NA = NULL) expect_identical(do.call(rule, as.list(test_rule)), test_rule) }) + +# combine_rules ---- + +test_that("combine_rules works as expected", { + r1 <- rule(a = "1", b = "2", .to_NA = "x", .drop = TRUE, .na_last = FALSE) + r2 <- rule(a = "3", c = "4", .to_NA = "y", .drop = FALSE) + + res <- combine_rules(r1, r2) + expect_s3_class(res, "rule") + expect_identical(res, rule(a = "3", b = "2", c = "4", .to_NA = "y", .drop = FALSE, .na_last = TRUE)) +}) + +test_that("combine_rules works as expected with `NULL` values", { + r1 <- NULL + r2 <- rule(a = "3", c = "4", .to_NA = "y") + + res <- combine_rules(r1, r2) + expect_s3_class(res, "rule") + expect_identical(res, r2) +}) + +test_that("combine_rules works as expected with `NULL` values", { + r1 <- rule(a = "1", b = "2", .to_NA = "x", .drop = TRUE, .na_last = FALSE) + r2 <- NULL + + res <- combine_rules(r1, r2) + expect_s3_class(res, "rule") + expect_identical(res, r1) +}) + +test_that("combine_rules fails as expected when both rules are `NULL` values", { + r1 <- NULL + r2 <- NULL + expect_error(combine_rules(r1, r2), "Both rules are NULL.") +}) + +# combine_list_rules ---- + +test_that("combine_list_rules works as expected", { + l1 <- list( + r1 = rule( + "first" = c("will be overwritten", "WILL BE OVERWRITTEN"), + "almost first" = c(NA, "ALMOST FIRST") + ), + r2 = rule( + ANYTHING = "anything" + ) + ) + + l2 <- list( + r1 = rule( + "first" = c("F", "f"), + "second" = c("S", "s"), + "third" = c("T", "t"), + .to_NA = "something" + ), + r3 = rule( + SOMETHING = "something" + ) + ) + + res <- combine_list_rules(l1, l2) + checkmate::expect_list(res, types = "rule", len = 3, names = "named") + expect_identical(names(res), c("r1", "r2", "r3")) + + expect_identical( + res$r1, + rule( + "first" = c("F", "f"), + "almost first" = c(NA, "ALMOST FIRST"), + "second" = c("S", "s"), + "third" = c("T", "t"), + .to_NA = "something" + ) + ) + + expect_identical( + res$r2, + rule( + ANYTHING = "anything" + ) + ) + + expect_identical( + res$r3, + rule( + SOMETHING = "something" + ) + ) +}) + + +test_that("combine_list_rules fails as expected when elements are not rules", { + l1 <- list( + r1 = NULL + ) + + l2 <- list( + r1 = rule( + "first" = c("F", "f"), + "second" = c("S", "s"), + "third" = c("T", "t"), + .to_NA = "something" + ) + ) + + expect_error(res <- combine_list_rules(l1, l2)) +})