diff --git a/NAMESPACE b/NAMESPACE index ebae2a51..fb8f0613 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,7 @@ export(buntley.westin.index) export(checkHzDepthLogic) export(checkSPC) export(col2Munsell) +export(collapseHz) export(colorChart) export(colorContrast) export(colorContrastPlot) diff --git a/NEWS.md b/NEWS.md index 62469389..e1f4c4f9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,9 +2,10 @@ * added Munsell values of 8.5 and 9.5 to Munsell look up table and (interpolated) reference spectra (#318) * `munsell2rgb()` now safely selects the closest Munsell value and chroma to those available in the package LUT * new function `soilTextureColorPal()` for suggesting a color palette suitable for soil texture class - * **Breaking Change**: `@sp` slot of the `SoilProfileCollection` object, and dependency on sp package, has been removed. - * any `SoilProfileCollection` objects previously written to file (.rda, .rds) with aqp <2.1.x will need to be rebuilt using `rebuildSPC()` due to changes to S4 object structure + * **Breaking Change**: `@sp` slot of the SoilProfileCollection object, and dependency on sp package, has been removed. + * Any SoilProfileCollection objects previously written to file (.rda, .rds) with aqp <2.1.x will need to be rebuilt using `rebuildSPC()` due to changes to S4 object structure * `estimatePSCS()` gains argument `"lieutex"` for in lieu textures which are used in the new routine for identification of the particle size control section of organic soils + * new function `collapseHz()` combines and aggregates data for adjacent horizons matching a pattern or sharing a common ID # aqp 2.0.4 (2024-07-30) * CRAN release diff --git a/R/SoilProfileCollection-setters.R b/R/SoilProfileCollection-setters.R index f05bc454..0322fc00 100644 --- a/R/SoilProfileCollection-setters.R +++ b/R/SoilProfileCollection-setters.R @@ -90,6 +90,18 @@ setReplaceMethod("depths", "data.frame", return(depth) } +.checkDepthOrder <- function(x, depthcols) { + if (any(x[[depthcols[2]]] < x[[depthcols[1]]], na.rm = TRUE)) { + warning("One or more horizon bottom depths are shallower than top depth. Check depth logic with aqp::checkHzDepthLogic()", call. = FALSE) + } +} + +.screenDepths <- function(x, depthcols = horizonDepths(x)) { + .checkNAdepths(x[[depthcols[1]]], "top") + .checkNAdepths(x[[depthcols[2]]], "bottom") + .checkDepthOrder(x, depthcols) +} + # create 0-length spc from id and horizon depth columns (`idn`, `hzd`) # - allows template horizon (`hz`) and site (`st`) data to be provided (for additional columns) .prototypeSPC <- function(idn, hzd, @@ -178,6 +190,9 @@ setReplaceMethod("depths", "data.frame", data[[depthcols[1]]] <- .checkNAdepths(data[[depthcols[1]]], "top") data[[depthcols[2]]] <- .checkNAdepths(data[[depthcols[2]]], "bottom") + # warn if bottom depth shallower than top (old style O horizons, data entry issues, etc.) + .checkDepthOrder(data, depthcols) + tdep <- data[[depthcols[1]]] # calculate ID-top depth order, re-order input data diff --git a/R/collapseHz.R b/R/collapseHz.R new file mode 100644 index 00000000..2f6cc52a --- /dev/null +++ b/R/collapseHz.R @@ -0,0 +1,307 @@ +#' Collapse Horizons within Profiles Based on Pattern Matching +#' +#' Combines layers and aggregates data by grouping adjacent horizons which match `pattern` in +#' `hzdesgn` or, alternately, share a common value in `by` argument. Numeric properties are combined +#' using the weighted average, and other properties are derived from the dominant condition based on +#' thickness of layers and values in each group. +#' +#' @param x A _SoilProfileCollection_ +#' @param pattern _character_. A regular expression pattern to match in `hzdesgn` column. Default: +#' `NULL`. +#' @param by _character_. A column name specifying horizons that should be combined. Aggregation +#' will be applied to adjacent groups of layers within profiles that have the same value in `by`. +#' Used in lieu of `pattern` and `hzdesgn`. Default: `NULL`. +#' @param hzdesgn _character_. Any character column containing horizon-level identifiers. Default: +#' `hzdesgnname(x, required = TRUE)`. +#' @param FUN _function_. A function that returns a _logical_ vector equal in length to the number +#' of horizons in `x`. Used only when `pattern` is specified. See details. +#' @param ... Additional arguments passed to the matching function `FUN`. +#' @param AGGFUN _list_. A _named_ list containing custom aggregation functions. List element names +#' should match the column name that they transform. The functions defined should take three +#' arguments: `x` (a vector of horizon property values), `top` (a vector of top depths), and +#' `bottom` (a vector of bottom depths). Default: `NULL` applies `weighted.mean()` to all numeric +#' columns not listed in `ignore_numerics` and takes the dominant condition (value with greatest +#' aggregate thickness sum) for all other columns. See details. +#' @param ignore_numerics _character_. Vector of column names that contain numeric values which +#' should _not_ be aggregated using `weighted.mean()`. For example, soil color "value" and +#' "chroma". +#' @param na.rm _logical_. If `TRUE` `NA` values are ignored when calculating min/max boundaries for +#' each group and in weighted averages. If `FALSE` `NA` values are propagated to the result. +#' Default: `FALSE`. +#' +#' @details +#' +#' If a custom matching function (`FUN`) is used, it should accept arbitrary additional arguments +#' via an ellipsis (`...`). It is not necessary to do anything with arguments, but the result should +#' match the number of horizons found in the input SoilProfileCollection `x`. +#' +#' Custom aggregation functions defined in the `AGGFUN` argument should either return a single +#' vector value for each group*column combination, or should return a _data.frame_ object with named +#' columns. If the input column name is used as a column name in the result _data.frame_, then the +#' values of that column name in the result _SoilProfileCollection_ will be replaced by the output +#' of the aggregation function. See examples. +#' +#' @return A _SoilProfileCollection_ +#' +#' @author Andrew G. Brown +#' +#' @seealso `hz_dissolve()` +#' +#' @export +#' +#' @examples +#' data(jacobs2000) +#' +#' # calculate a new SPC with genhz column based on patterns +#' new_labels <- c("A", "E", "Bt", "Bh", "C") +#' patterns <- c("A", "E", "B.*t", "B.*h", "C") +#' jacobs2000_gen <- generalizeHz(jacobs2000, new = new_labels, pattern = patterns) +#' +#' # use existing generalized horizon labels +#' i <- collapseHz(jacobs2000_gen, by = "genhz") +#' +#' profile_id(i) <- paste0(profile_id(i), "_collapse") +#' +#' plot( +#' c(i, jacobs2000), +#' color = "genhz", +#' name = "name", +#' name.style = "center-center", +#' cex.names = 1 +#' ) +#' +#' # custom pattern argument +#' j <- collapseHz(jacobs2000, +#' c( +#' `A` = "^A", +#' `E` = "E", +#' `Bt` = "[ABC]+t", +#' `C` = "^C", +#' `foo` = "bar" +#' )) +#' profile_id(j) <- paste0(profile_id(j), "_collapse") +#' plot(c(j, jacobs2000), color = "clay") +#' +#' # custom aggregation function for matrix_color_munsell +#' k <- collapseHz(jacobs2000, +#' pattern = c( +#' `A` = "^A", +#' `E` = "E", +#' `Bt` = "[ABC]+t", +#' `C` = "^C", +#' `foo` = "bar" +#' ), +#' AGGFUN = list( +#' matrix_color_munsell = function(x, top, bottom) { +#' thk <- bottom - top +#' if (length(x) > 1) { +#' xord <- order(thk, decreasing = TRUE) +#' paste0(paste0(x[xord], " (t=", thk[xord], ")"), collapse = ", ") +#' } else +#' x +#' } +#' ) +#' ) +#' profile_id(k) <- paste0(profile_id(k), "_collapse_custom") +#' +#' unique(k$matrix_color_munsell) +#' +#' # custom aggregation function for matrix_color_munsell (returns data.frame) +#' m <- collapseHz(jacobs2000, +#' pattern = c( +#' `A` = "^A", +#' `E` = "E", +#' `Bt` = "[ABC]+t", +#' `C` = "^C", +#' `foo` = "bar" +#' ), +#' AGGFUN = list( +#' matrix_color_munsell = function(x, top, bottom) { +#' thk <- bottom - top +#' if (length(x) > 1) { +#' xord <- order(thk, decreasing = TRUE) +#' data.frame(matrix_color_munsell = paste0(x, collapse = ";"), +#' n_matrix_color = length(x)) +#' } else { +#' data.frame(matrix_color_munsell = x, +#' n_matrix_color = length(x)) +#' } +#' } +#' ) +#' ) +#' profile_id(m) <- paste0(profile_id(m), "_collapse_custom") +#' +#' m$matrix_color_munsell.n_matrix_color +collapseHz <- function(x, + pattern = NULL, + by = NULL, + hzdesgn = hzdesgnname(x, required = TRUE), + FUN = function(x, pattern, hzdesgn, ...) grepl(pattern, x[[hzdesgn]], ignore.case = FALSE), + ..., + AGGFUN = NULL, + ignore_numerics = NULL, + na.rm = FALSE) { + idn <- idname(x) + hzd <- horizonDepths(x) + + .screenDepths(x, hzd) + + # use exact match of existing genhz labels as default in lieu of pattern + if (is.null(pattern) & missing(by)) { + by <- GHL(x, required = TRUE) + } + + if (length(pattern) == 0) { + pattern <- NA + } + + # if a named vector of patterns is given, use the names as new labels + if (!is.null(names(pattern))) { + labels <- names(pattern) + pattern <- as.character(pattern) + } else { + # otherwise, the patterns and labels are the same + pattern <- as.character(pattern) + labels <- pattern + } + + h <- data.table::data.table(horizons(x)) + + # iterate over patterns + for (p in seq(pattern)) { + + # calculate matches + if (!is.null(by) && length(pattern) == 1 && is.na(pattern)) { + + if (!by %in% horizonNames(x)) { + stop("Column name `by` (\"", by, ") is not a horizon-level variable.", call. = FALSE) + } + + labels <- h[[by]] + + if (any(is.na(labels))) { + stop("Missing values are not allowed in `by` column argument", call. = FALSE) + } + + r <- rle(paste0(h[[idn]], "-", as.character(labels))) + l <- rep(TRUE, nrow(h)) + } else { + l <- FUN(x, pattern = pattern[p], hzdesgn = hzdesgn, na.rm = na.rm, ...) + r <- rle(l) + } + + # only apply aggregation if there are adjacent horizons that match the target criteria + if (any(r$lengths > 1)) { + g <- unlist(lapply(seq_along(r$lengths), function(i) rep(i, r$lengths[i]))) + hidx <- unlist(lapply(seq_along(r$lengths), function(i) if (r$lengths[i] == 1) TRUE else rep(FALSE, r$lengths[i]))) & l + gidx <- g %in% unique(g[l]) & !hidx + naf <- names(AGGFUN) + + # iterate over sets of layers needing aggregation within each matching group + if (sum(gidx) > 0){ + res <- h[gidx, c(list(hzdeptnew = suppressWarnings(min(.SD[[hzd[1]]], na.rm = na.rm)), + hzdepbnew = suppressWarnings(max(.SD[[hzd[2]]], na.rm = na.rm))), + + # process numeric depth weighted averages w/ dominant condition otherwise + sapply(colnames(.SD)[!colnames(.SD) %in% c(hzd, naf)], + function(n, top, bottom) { + v <- .SD[[n]] + if (length(v) > 1) { + if (!n %in% ignore_numerics && is.numeric(v)) { + + # weighted average by thickness (numerics not in exclusion list) + v <- weighted.mean(v, bottom - top, na.rm = na.rm) + + } else { + # take thickest value + # v[which.max(bottom - top)[1]] + + # convert factors etc to character + # results may not conform with existing factor levels + v <- as.character(v) + + # replace NA values for use in aggregate() + if (!na.rm) { + v[is.na(v)] <- "" + } + + # take dominant condition (based on sum of thickness) + cond <- aggregate(bottom - top, by = list(v), sum, na.rm = na.rm) + v <- cond[[1]][which.max(cond[[2]])[1]] + + if (!na.rm) { + v[v == ""] <- NA + } + } + } + out <- data.frame(v) + colnames(out) <- n + out + }, + top = .SD[[hzd[1]]], + bottom = .SD[[hzd[2]]]), + + # process custom aggregation functions (may return data.frames) + do.call('c', lapply(colnames(.SD)[colnames(.SD) %in% naf], + function(n, top, bottom) { + out <- AGGFUN[[n]](.SD[[n]], top, bottom) + if (!is.data.frame(out)) { + out <- data.frame(out) + colnames(out) <- n + } else { + colnames(out) <- paste0(n, ".", colnames(out)) + } + out + }, + top = .SD[[hzd[1]]], + bottom = .SD[[hzd[2]]]))), + by = g[gidx]] + # remove grouping ID + res$g <- NULL + } else { + res <- h[0, ] + } + + # allow for replacing values as well as adding new values with data.frame AGGFUN + test1.idx <- na.omit(match(colnames(res), paste0(colnames(h), ".", colnames(h)))) + test2.idx <- na.omit(match(paste0(colnames(h), ".", colnames(h)), colnames(res))) + colnames(res)[test2.idx] <- colnames(h)[test1.idx] + + # determine matches that are only a single layer (no aggregation applied) + res2 <- h[hidx & l, ] + res2$hzdeptnew <- res2[[hzd[1]]] + res2$hzdepbnew <- res2[[hzd[2]]] + res2[[hzd[1]]] <- NULL + res2[[hzd[2]]] <- NULL + + # combine matches + res3 <- data.table::rbindlist(list(res, res2), fill = TRUE) + if (missing(by) && nrow(res3) > 0){ + res3[[hzdesgn]] <- labels[p] + } + + # combine matches with horizons that did not match + agg.idx <- which(g %in% unique(g[l]) | hidx) + if (length(agg.idx) > 0) { + h <- h[-agg.idx, ] + } + h <- data.table::rbindlist(list(h, res3), fill = TRUE) + + # replace depths + hn <- !is.na(h$hzdeptnew) & !is.na(h$hzdepbnew) + h[[hzd[1]]][hn] <- h$hzdeptnew[hn] + h[[hzd[2]]][hn] <- h$hzdepbnew[hn] + h$hzdeptnew <- NULL + h$hzdepbnew <- NULL + + # sort horizons by id name and top depth + h <- h[order(h[[idn]], h[[hzd[1]]]),] + + } + + # replace horizons in parent SPC + replaceHorizons(x) <- h + } + x +} + diff --git a/man/collapseHz.Rd b/man/collapseHz.Rd new file mode 100644 index 00000000..a13a863a --- /dev/null +++ b/man/collapseHz.Rd @@ -0,0 +1,162 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/collapseHz.R +\name{collapseHz} +\alias{collapseHz} +\title{Collapse Horizons within Profiles Based on Pattern Matching} +\usage{ +collapseHz( + x, + pattern = NULL, + by = NULL, + hzdesgn = hzdesgnname(x, required = TRUE), + FUN = function(x, pattern, hzdesgn, ...) grepl(pattern, x[[hzdesgn]], ignore.case = + FALSE), + ..., + AGGFUN = NULL, + ignore_numerics = NULL, + na.rm = FALSE +) +} +\arguments{ +\item{x}{A \emph{SoilProfileCollection}} + +\item{pattern}{\emph{character}. A regular expression pattern to match in \code{hzdesgn} column. Default: +\code{NULL}.} + +\item{by}{\emph{character}. A column name specifying horizons that should be combined. Aggregation +will be applied to adjacent groups of layers within profiles that have the same value in \code{by}. +Used in lieu of \code{pattern} and \code{hzdesgn}. Default: \code{NULL}.} + +\item{hzdesgn}{\emph{character}. Any character column containing horizon-level identifiers. Default: +\code{hzdesgnname(x, required = TRUE)}.} + +\item{FUN}{\emph{function}. A function that returns a \emph{logical} vector equal in length to the number +of horizons in \code{x}. Used only when \code{pattern} is specified. See details.} + +\item{...}{Additional arguments passed to the matching function \code{FUN}.} + +\item{AGGFUN}{\emph{list}. A \emph{named} list containing custom aggregation functions. List element names +should match the column name that they transform. The functions defined should take three +arguments: \code{x} (a vector of horizon property values), \code{top} (a vector of top depths), and +\code{bottom} (a vector of bottom depths). Default: \code{NULL} applies \code{weighted.mean()} to all numeric +columns not listed in \code{ignore_numerics} and takes the dominant condition (value with greatest +aggregate thickness sum) for all other columns. See details.} + +\item{ignore_numerics}{\emph{character}. Vector of column names that contain numeric values which +should \emph{not} be aggregated using \code{weighted.mean()}. For example, soil color "value" and +"chroma".} + +\item{na.rm}{\emph{logical}. If \code{TRUE} \code{NA} values are ignored when calculating min/max boundaries for +each group and in weighted averages. If \code{FALSE} \code{NA} values are propagated to the result. +Default: \code{FALSE}.} +} +\value{ +A \emph{SoilProfileCollection} +} +\description{ +Combines layers and aggregates data by grouping adjacent horizons which match \code{pattern} in +\code{hzdesgn} or, alternately, share a common value in \code{by} argument. Numeric properties are combined +using the weighted average, and other properties are derived from the dominant condition based on +thickness of layers and values in each group. +} +\details{ +If a custom matching function (\code{FUN}) is used, it should accept arbitrary additional arguments +via an ellipsis (\code{...}). It is not necessary to do anything with arguments, but the result should +match the number of horizons found in the input SoilProfileCollection \code{x}. + +Custom aggregation functions defined in the \code{AGGFUN} argument should either return a single +vector value for each group*column combination, or should return a \emph{data.frame} object with named +columns. If the input column name is used as a column name in the result \emph{data.frame}, then the +values of that column name in the result \emph{SoilProfileCollection} will be replaced by the output +of the aggregation function. See examples. +} +\examples{ +data(jacobs2000) + +# calculate a new SPC with genhz column based on patterns +new_labels <- c("A", "E", "Bt", "Bh", "C") +patterns <- c("A", "E", "B.*t", "B.*h", "C") +jacobs2000_gen <- generalizeHz(jacobs2000, new = new_labels, pattern = patterns) + +# use existing generalized horizon labels +i <- collapseHz(jacobs2000_gen, by = "genhz") + +profile_id(i) <- paste0(profile_id(i), "_collapse") + +plot( + c(i, jacobs2000), + color = "genhz", + name = "name", + name.style = "center-center", + cex.names = 1 +) + +# custom pattern argument +j <- collapseHz(jacobs2000, + c( + `A` = "^A", + `E` = "E", + `Bt` = "[ABC]+t", + `C` = "^C", + `foo` = "bar" + )) +profile_id(j) <- paste0(profile_id(j), "_collapse") +plot(c(j, jacobs2000), color = "clay") + +# custom aggregation function for matrix_color_munsell +k <- collapseHz(jacobs2000, + pattern = c( + `A` = "^A", + `E` = "E", + `Bt` = "[ABC]+t", + `C` = "^C", + `foo` = "bar" + ), + AGGFUN = list( + matrix_color_munsell = function(x, top, bottom) { + thk <- bottom - top + if (length(x) > 1) { + xord <- order(thk, decreasing = TRUE) + paste0(paste0(x[xord], " (t=", thk[xord], ")"), collapse = ", ") + } else + x + } + ) + ) +profile_id(k) <- paste0(profile_id(k), "_collapse_custom") + +unique(k$matrix_color_munsell) + +# custom aggregation function for matrix_color_munsell (returns data.frame) +m <- collapseHz(jacobs2000, + pattern = c( + `A` = "^A", + `E` = "E", + `Bt` = "[ABC]+t", + `C` = "^C", + `foo` = "bar" + ), + AGGFUN = list( + matrix_color_munsell = function(x, top, bottom) { + thk <- bottom - top + if (length(x) > 1) { + xord <- order(thk, decreasing = TRUE) + data.frame(matrix_color_munsell = paste0(x, collapse = ";"), + n_matrix_color = length(x)) + } else { + data.frame(matrix_color_munsell = x, + n_matrix_color = length(x)) + } + } + ) + ) +profile_id(m) <- paste0(profile_id(m), "_collapse_custom") + +m$matrix_color_munsell.n_matrix_color +} +\seealso{ +\code{hz_dissolve()} +} +\author{ +Andrew G. Brown +} diff --git a/misc/sandbox/collapseHz-mixMunsell-examples.R b/misc/sandbox/collapseHz-mixMunsell-examples.R new file mode 100644 index 00000000..c22448a3 --- /dev/null +++ b/misc/sandbox/collapseHz-mixMunsell-examples.R @@ -0,0 +1,94 @@ +library(aqp) + +# example data +data("jacobs2000") + +# local copy +g <- jacobs2000 + +# spike some horizon colors with green / blue hues +g$matrix_color_munsell[4] <- '5G 4/6' +g$matrix_color_munsell[29] <- '5B 4/6' +g$matrix_color_munsell[36] <- '5R 4/6' + +# horizon correlation patterns +# applied to horizon desingation +a_pattern <- c(`A` = "^A", + `E` = "E", + `Bt` = "[B]+t", + `Bh` = "[B]+h", + `C` = "^C", + `foo` = "bar") + + +# safe wrapper around mixMunsell() +mixFun <- function(x, top, bottom) { + # weights + w <- bottom - top + + # index to non-NA values + .idx <- which(! is.na(x)) + .n <- length(x[.idx]) + + # if all NA, return NA + if(.n < 1) { + return(NA) + + # if only a single color, return that + } else if (.n == 1){ + print('just 1!') + return(x[.idx]) + + } else { + # mix colors, retain only munsell notation + .res <- mixMunsell(x[.idx], w[.idx], mixingMethod = 'exact')$munsell + return(.res) + } +} + +# collapse according to patterns +m <- collapseHz(g, + pattern = a_pattern, + AGGFUN = list( + matrix_color_munsell = mixFun + ) +) + +# new profile IDs so we can safely combine with source data +profile_id(m) <- sprintf("%s-c", profile_id(m)) + +# combine +z <- c(g, m) + +# convert Munsell colors -> sRGB in hex notation +z$soilcolor <- parseMunsell(z$matrix_color_munsell) + +# plot combined collection +par(mar = c(0, 0, 0, 3)) +plotSPC(z, color = 'soilcolor', name = 'name', name.style = 'center-center', width = 0.35, cex.names = 0.75) + +## start fresh + +# combine all horizons by profile + +g <- jacobs2000 +horizons(g)$.all <- 'soil' +collapseHz(g, by = '.all') + + +m <- collapseHz(g, + by = '.all', + AGGFUN = list( + matrix_color_munsell = mixFun + ) +) + +profile_id(m) <- sprintf("%s-c", profile_id(m)) +z <- c(g, m) +z$soilcolor <- parseMunsell(z$matrix_color_munsell) + +# neat +par(mar = c(0, 0, 0, 3)) +plotSPC(z, color = 'soilcolor', name = 'name', name.style = 'center-center', width = 0.35, cex.names = 0.75) + + diff --git a/tests/testthat/test-collapseHz.R b/tests/testthat/test-collapseHz.R new file mode 100644 index 00000000..2fbfb3e1 --- /dev/null +++ b/tests/testthat/test-collapseHz.R @@ -0,0 +1,113 @@ +context("collapseHz()") + +test_that("collapseHz works", { + data("jacobs2000", package = "aqp") + .BOTTOM <- NULL + + # use existing generalized horizon labels + new_labels <- c("A", "E", "Bt", "Bh", "C") + patterns <- c("A", "E", "B.*t", "B.*h", "C") + + # calculate a new SPC with genhz column based on patterns + jacobs2000_gen <- generalizeHz(jacobs2000, new = new_labels, pattern = patterns) + + # create a missing value + jacobs2000_gen$clay[19] <- NA + + # collapse that SPC based on genhz + i <- collapseHz(jacobs2000_gen, hzdesgn = "genhz") + expect_equal(length(jacobs2000), length(i)) + expect_equal(nrow(i), 26) + expect_equal(i[7, , .BOTTOM], c(15, 41, 61, 132, 140, 152)) + + # collapses adjacent horizons with same label + i <- collapseHz(jacobs2000_gen, by = "genhz") + ii <- collapseHz(jacobs2000_gen, by = "genhz", na.rm = TRUE) + + # no effect, horizon designations are unique within profiles + j <- collapseHz(jacobs2000_gen, by = "name") + + expect_equal(nrow(j), 46) + expect_equal(j[7, , .BOTTOM], jacobs2000[7, , .BOTTOM]) + + # if using `by` argument, all values must not be NA + expect_error(collapseHz(jacobs2000_gen, by = "matrix_color_munsell"), + "Missing values are not allowed") + + # `by` column must also be a horizon-level variable + expect_error(collapseHz(jacobs2000, by = "genhz"), "not a horizon-level variable") + + # matches input number of profiles + expect_equal(length(jacobs2000), length(i)) + + # horizons have been collapsed + expect_equal(nrow(i), 26) + + # weighted mean (no NA values) works as expected (clay=47.15) + expect_equal(i$clay[4], + weighted.mean(jacobs2000_gen$clay[6:7], (jacobs2000_gen$bottom - jacobs2000_gen$top)[6:7])) + + # weighted mean (contains NA values, na.rm=FALSE) (clay is NA) + expect_true(is.na(i$clay[11])) + + # weighted mean (contains NA values, na.rm=TRUE, clay=18.72414) + expect_equal(ii$clay[11], + weighted.mean(jacobs2000_gen$clay[17:20], (jacobs2000_gen$bottom - jacobs2000_gen$top)[17:20], na.rm = TRUE)) + + # dominant condition (NA values retained) + expect_true(is.na(i$depletion_munsell[13])) + + # dominant condition (NA values removed) + expect_equal(ii$depletion_munsell[13], "10YR 8/2") + + plot(jacobs2000_gen, color = "concentration_pct") + + expect_equal(i[7, , .BOTTOM], c(15, 41, 61, 132, 140, 152)) + expect_true(is.numeric(i$clay)) + expect_true(is.numeric(j$clay)) + + # "works" on empty SPC () + expect_equal(nrow(collapseHz(jacobs2000_gen[0,], by = "genhz")), 0) + + # works on SPC with filled profile (1 horizon with NA depths) + all_na <- subsetHz(jacobs2000_gen[1,], TRUE) + all_na$top <- NA_real_ + all_na$bottom <- NA_real_ + expect_warning(na_nonna <- c(all_na, jacobs2000_gen[2:5,])) + expect_warning(f <- collapseHz(all_na, by = "genhz"), "contain NA") + na_nonna$top[2] <- 19 + expect_warning(n <- collapseHz(na_nonna, by = "genhz"), "bottom depths are shallower than top") + expect_equal(nrow(n), 14) + + + a_pattern <- c(`A` = "^A", + `E` = "E", + `Bt` = "[ABC]+t", + `C` = "^C", + `foo` = "bar") + x <- collapseHz(jacobs2000, a_pattern) + expect_equal(length(jacobs2000), length(x)) + expect_equal(nrow(x), 29) + expect_true(is.numeric(x$clay)) + + m <- collapseHz(jacobs2000, + pattern = a_pattern, + AGGFUN = list( + matrix_color_munsell = function(x, top, bottom) { + thk <- bottom - top + if (length(x) > 1) { + xord <- order(thk, decreasing = TRUE) + data.frame(matrix_color_munsell = paste0(x, collapse = ";"), + n_matrix_color = length(x)) + } else { + data.frame(matrix_color_munsell = x, + n_matrix_color = length(x)) + } + } + ) + ) + profile_id(m) <- paste0(profile_id(m), "_collapse_custom") + + expect_true(all(c("matrix_color_munsell", "matrix_color_munsell.n_matrix_color") %in% names(m))) + expect_equal(nrow(m), 29) +})