diff --git a/R/class-workbook-wrappers.R b/R/class-workbook-wrappers.R index 9f3bbc7ec..3c09497b6 100644 --- a/R/class-workbook-wrappers.R +++ b/R/class-workbook-wrappers.R @@ -282,6 +282,7 @@ wb_add_data <- function( #' @param banded_rows logical. If `TRUE`, rows are color banded. #' @param banded_cols logical. If `TRUE`, the columns are color banded. #' @param total_row logical. With the default `FALSE` no total row is added. +#' @param params list. Optional arguments passed to the data table creation. #' @param ... additional arguments #' #' @details # Modify total row argument @@ -329,7 +330,8 @@ wb_add_data_table <- function( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, - total_row = FALSE, + total_row = FALSE, + params = NULL, ... ) { assert_workbook(wb) @@ -354,6 +356,7 @@ wb_add_data_table <- function( na.strings = na.strings, inline_strings = inline_strings, total_row = total_row, + params = params, ... = ... ) } diff --git a/R/class-workbook.R b/R/class-workbook.R index 23f931ed4..61887e53f 100644 --- a/R/class-workbook.R +++ b/R/class-workbook.R @@ -1505,6 +1505,7 @@ wbWorkbook <- R6::R6Class( #' `na_strings()` uses the special `#N/A` value within the workbook. #' @param inline_strings write characters as inline strings #' @param total_row write total rows to table + #' @param params optional parameters passed to the data table creation #' @param ... additional arguments #' @return The `wbWorkbook` object add_data_table = function( @@ -1528,6 +1529,7 @@ wbWorkbook <- R6::R6Class( na.strings = na_strings(), inline_strings = TRUE, total_row = FALSE, + params = NULL, ... ) { @@ -1562,7 +1564,8 @@ wbWorkbook <- R6::R6Class( removeCellStyle = remove_cell_style, na.strings = na.strings, inline_strings = inline_strings, - total_row = total_row + total_row = total_row, + params = params ) invisible(self) }, @@ -3791,9 +3794,8 @@ wbWorkbook <- R6::R6Class( tActive <- self$tables$tab_act } - ### autofilter - autofilter <- if (withFilter) { + autofilter <- if (!isFALSE(withFilter)) { autofilter_ref <- ref xml_node_create(xml_name = "autoFilter", xml_attributes = c(ref = autofilter_ref)) } @@ -3867,6 +3869,48 @@ wbWorkbook <- R6::R6Class( #headerRowDxfId="1" ) + + # run this if withFilter is something (TRUE or a character) + autofilter <- NULL + if (!isFALSE(withFilter)) { + if (!isFALSE(totalsRowCount)) { + # exclude total row from filter + rowcol <- dims_to_rowcol(ref) + autofilter_ref <- rowcol_to_dims(as.integer(rowcol[[2]])[-length(rowcol[[2]])], rowcol[[1]]) + } else { + autofilter_ref <- ref + } + + ### autofilter + autofilter <- xml_node_create(xml_name = "autoFilter", xml_attributes = c(ref = autofilter_ref)) + } + + if (is.character(withFilter)) { + fltr_nms <- names(withFilter) + fltr_nms <- paste0("x$", escape_varname(fltr_nms)) + + filter <- vapply( + seq_along(fltr_nms), + function(i) { + gsub("^x", replacement = fltr_nms[i], x = withFilter[i]) + }, + NA_character_ + ) + names(filter) <- names(withFilter) + + assert_class(filter, "character") + + ## prepare condition list & autofilter xml + fltr <- create_conditions(filter) + autofilter <- prepare_autofilter(colNames, autofilter_ref, conditions = fltr) + + ## revese the condition to make sure that we have lower strings + ## select the rows to hide and hide them + filter <- reverse_conditions(fltr) + sel <- rows_to_hide(self, sheet, ref, filter) + self$set_row_heights(rows = sel, hidden = TRUE) + } + tab_xml_new <- xml_node_create( xml_name = "table", xml_children = c(autofilter, tableColumns, tableStyleXML), diff --git a/R/utils.R b/R/utils.R index 39c32493c..a278650ac 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1439,6 +1439,226 @@ file_ext2 <- function(filepath) { sub(".*\\.", "", basename2(filepath)) } +#' escape var name with upticks +#' @param var_name variable name +#' @keywords internal +#' @noRd +escape_varname <- function(var_name) { + vapply(var_name, function(x) { + ifelse(grepl("\\s", x), sprintf("`%s`", x), x) + }, NA_character_) +} + +#' helper to parse the string into something that can be converted into openxml +#' column filters +#' @param filter_expr an expression like "x$cyl != 4" +#' @keywords internal +#' @noRd +create_conditions <- function(filter_expr) { + + conditions <- vector("list", length(filter_expr)) + names(conditions) <- names(filter_expr) + + # Define a helper function to add conditions to the list + add_condition <- function(operator, value) { + list(val = value$vals, operator = operator) + } + + get_vals <- function(fltr_xpr, operator) { + args <- trimws(strsplit(fltr_xpr, operator)[[1]]) + + nams <- gsub("`", "", gsub("^x\\$", "", args[1])) + vals <- eval(parse(text = args[2])) + + # openxml does not care about case sensitivity + vals <- if (is.character(vals)) tolower(vals) else vals + + list(nams = nams, vals = vals) + } + + for (fltr_xpr in filter_expr) { + + # Extract conditions for "equal" + if (grepl("%in%", fltr_xpr)) { + values <- get_vals(fltr_xpr, "%in%") + conditions[[values$nams]] <- add_condition("equal", values) + } + + # Extract conditions for "equal" using == + if (grepl("==", fltr_xpr)) { + values <- get_vals(fltr_xpr, "==") + conditions[[values$nams]] <- add_condition("equal", values) + } + + # Extract conditions for "notEqual" + if (grepl("!=", fltr_xpr)) { + values <- get_vals(fltr_xpr, "!=") + conditions[[values$nams]] <- add_condition("notEqual", values) + } + + # Extract conditions for "lessThan" + if (grepl("<[^=]", fltr_xpr)) { # "<" but not "<=" + values <- get_vals(fltr_xpr, "<") + conditions[[values$nams]] <- add_condition("lessThan", values) + } + + # Extract conditions for "lessThanOrEqual" + if (grepl("<=", fltr_xpr)) { + values <- get_vals(fltr_xpr, "<=") + conditions[[values$nams]] <- add_condition("lessThanOrEqual", values) + } + + # Extract conditions for "greaterThan" + if (grepl(">[^=]", fltr_xpr)) { # ">" but not ">=" + values <- get_vals(fltr_xpr, ">") + conditions[[values$nams]] <- add_condition("greaterThan", values) + } + + # Extract conditions for "greaterThanOrEqual" + if (grepl(">=", fltr_xpr)) { + values <- get_vals(fltr_xpr, ">=") + conditions <- add_condition("greaterThanOrEqual", values) + } + } + + conditions +} + +#' helper to parse the conditions back into the expressions +#' so that we can have lower case strings +#' @param conditions a condition created by [create_conditions()] +#' @keywords internal +#' @noRd +reverse_conditions <- function(conditions) { + + filter_expr <- vector("character", length(conditions)) + i <- 1 + + for (name in names(conditions)) { + condition <- conditions[[name]] + + operator <- condition$operator + value <- condition$val + + value_str <- if (is.character(value)) { + shQuote(value, type = "cmd") + } else { + value + } + + # merge for potential "%in%" + if (operator == "equal") value_str <- paste0("c(", paste0(value_str, collapse = ", "), ")") + + name <- escape_varname(name) + + expr <- switch( + operator, + "equal" = paste0("x$", name, " %in% ", value_str), # not == + "notEqual" = paste0("x$", name, " != ", value_str), + "lessThan" = paste0("x$", name, " < ", value_str), + "lessThanOrEqual" = paste0("x$", name, " <= ", value_str), + "greaterThan" = paste0("x$", name, " > ", value_str), + "greaterThanOrEqual" = paste0("x$", name, " >= ", value_str), + stop("Unknown operator: ", operator) + ) + + filter_expr[i] <- expr + i <- i + 1 + } + + # Return the vector of filter expressions + filter_expr +} + +#' function to create the autofilter xml structure required for openxml +#' @param colNames character vector of column names +#' @param autofilter_ref the autofilter reference +#' @param conditions a named list with conditions to apply to the columns +#' @keywords internal +#' @noRd +prepare_autofilter <- function(colNames, autofilter_ref, conditions) { + + cond_vars <- names(conditions) + if (any(!cond_vars %in% colNames)) stop("Condtion variable not found in table.") + + autoFilter <- xml_node_create("autoFilter", xml_attributes = c(ref = autofilter_ref)) + + + aF <- NULL + for (cond_var in cond_vars) { + colId <- which(colNames == cond_var) - 1L + + condition <- conditions[[cond_var]] + + ## == default or %in% with multiple values + if (condition$operator == "equal") { + filter <- vapply( + condition$val, + function(x) { + xml_node_create("filter", xml_attributes = c(val = as_xml_attr(x))) + }, + FUN.VALUE = NA_character_ + ) + + filters <- xml_node_create("filters") + filterColumn <- xml_node_create("filterColumn", xml_attributes = c(colId = as_xml_attr(colId))) + + filters <- xml_add_child(filters, filter) + filterColumn <- xml_add_child(filterColumn, filters) + } + + ## != notEqual, < lessThan, <= lessThanOrEqual, > greaterThan, >= greaterThanOrEqual + ## TODO provide replacement function + ops <- c("notEqual", "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual") + if (condition$operator %in% ops) { + customFilter <- vapply( + condition$val, + function(x) { + xml_node_create( + "customFilter", + xml_attributes = c(operator = condition$operator, + val = as_xml_attr(x)) + ) + }, + FUN.VALUE = NA_character_ + ) + + customFilters <- xml_node_create("customFilters") + filterColumn <- xml_node_create("filterColumn", xml_attributes = c(colId = as_xml_attr(colId))) + + customFilters <- xml_add_child(customFilters, customFilter) + filterColumn <- xml_add_child(filterColumn, customFilters) + } + + aF <- c(aF, filterColumn) + } + + autoFilter <- xml_add_child(autoFilter, aF) + + autoFilter +} + +#' get rows to hide (in _all but not in _sel) +#' @param wb a wbWorkbook +#' @param sheet the sheet index +#' @param ref the dims +#' @param filter the filter as character string +#' @keywords internal +#' @noRd +rows_to_hide <- function(wb, sheet, ref, filter) { + x <- wb_to_df(wb, sheet = sheet, dims = ref) + rows_all <- rownames(x) + + chrs <- vapply(x, is.character, NA) + x[chrs] <- lapply(x[chrs], tolower) + + x <- x[eval(parse(text = paste0(filter, collapse = "&"))), , drop = FALSE] + rows_sel <- rownames(x) + + out <- rows_all[!rows_all %in% rows_sel] + as.integer(out) +} + if (getRversion() < "4.0.0") { deparse1 <- function(expr, collapse = " ") { paste(deparse(expr), collapse = collapse) diff --git a/R/write.R b/R/write.R index f46ab2b73..a04b2dc63 100644 --- a/R/write.R +++ b/R/write.R @@ -835,6 +835,7 @@ write_data2 <- function( #' @param inline_strings optional write strings as inline strings #' @param total_row optional write total rows #' @param shared shared formula +#' @param params optional list. contains data table filters in `choose` #' @noRd #' @keywords internal write_data_table <- function( @@ -863,7 +864,8 @@ write_data_table <- function( inline_strings = TRUE, total_row = FALSE, enforce = FALSE, - shared = FALSE + shared = FALSE, + params = NULL ) { ## Input validating @@ -1154,6 +1156,11 @@ write_data_table <- function( ref2 <- paste0(int2col(startCol + nCol - !rowNames), startRow + nRow) ref <- paste(ref1, ref2, sep = ":") + if (is.list(params)) { + if (!is.null(params$choose)) + withFilter <- params$choose + } + ## create table.xml and assign an id to worksheet tables wb$buildTable( sheet = sheet, @@ -1411,6 +1418,7 @@ do_write_datatable <- function( inline_strings = TRUE, total_row = FALSE, shared = FALSE, + params = params, ... ) { @@ -1441,6 +1449,7 @@ do_write_datatable <- function( na.strings = na.strings, inline_strings = inline_strings, total_row = total_row, - shared = shared + shared = shared, + params = params ) } diff --git a/R/write_xlsx.R b/R/write_xlsx.R index 3fac2ea44..c7d56fcbe 100644 --- a/R/write_xlsx.R +++ b/R/write_xlsx.R @@ -57,7 +57,7 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { "col.names", "row.names", "col_names", "row_names", "table_style", "table_name", "with_filter", "first_active_row", "first_active_col", "first_row", "first_col", "col_widths", "na.strings", - "overwrite", "title", "subject", "category", + "overwrite", "title", "subject", "category", "params", "font_size", "font_color", "font_name", "flush" ) @@ -388,6 +388,18 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { wb$add_worksheet(nms[[i]], grid_lines = gridLines[i], tab_color = tabColor[i], zoom = zoom[i]) if (as_table[i]) { + + # every sheet has uniqure choose parameters + # or all share the same + # or choose parameters were found + if (is.list(params$params$choose) && length(params$params$choose) <= nSheets) { + params_list <- list(choose = params$params$choose[[i]]) + } else if (is.character(params$params$choose)) { + params_list <- list(choose = params$params$choose) + } else { + params_list <- NULL + } + # add data table?? do_write_datatable( wb = wb, @@ -401,7 +413,8 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { table_name = NULL, with_filter = withFilter[[i]], na.strings = na.strings, - total_row = totalRow + total_row = totalRow, + params = params_list ) } else { # TODO add_data()? diff --git a/man/wbWorkbook.Rd b/man/wbWorkbook.Rd index d72d628de..1e63c37ae 100644 --- a/man/wbWorkbook.Rd +++ b/man/wbWorkbook.Rd @@ -610,6 +610,7 @@ add a data table na.strings = na_strings(), inline_strings = TRUE, total_row = FALSE, + params = NULL, ... )}\if{html}{\out{}} } @@ -658,6 +659,8 @@ add a data table \item{\code{total_row}}{write total rows to table} +\item{\code{params}}{optional parameters passed to the data table creation} + \item{\code{...}}{additional arguments} } \if{html}{\out{}} diff --git a/man/wb_add_data_table.Rd b/man/wb_add_data_table.Rd index ab47c4cd6..0a9af9943 100644 --- a/man/wb_add_data_table.Rd +++ b/man/wb_add_data_table.Rd @@ -26,6 +26,7 @@ wb_add_data_table( na.strings = na_strings(), inline_strings = TRUE, total_row = FALSE, + params = NULL, ... ) } @@ -81,6 +82,8 @@ uses the special \verb{#N/A} value within the workbook.} \item{total_row}{logical. With the default \code{FALSE} no total row is added.} +\item{params}{list. Optional arguments passed to the data table creation.} + \item{...}{additional arguments} } \description{ diff --git a/tests/testthat/test-write.R b/tests/testthat/test-write.R index aa60db992..20c43a524 100644 --- a/tests/testthat/test-write.R +++ b/tests/testthat/test-write.R @@ -1488,3 +1488,54 @@ test_that("writing without pugixml works", { expect_silent(wb <- wb_load(temp)) }) + +test_that("writing tables with filters works", { + + exp <- mtcars[mtcars$cyl %in% c(4, 8) & mtcars$am != 1 & mtcars$vs == 1, ] + + wb <- wb_workbook() %>% + wb_add_worksheet() %>% + wb_add_data_table( + x = mtcars, + params = list( + choose = c(cyl = "x %in% c(4, 8)", + am = c("x != 1"), + vs = c("x == 1") + ) + ) + ) + + got <- wb_to_df(wb, skip_hidden_rows = TRUE) + expect_equal(exp, got, ignore_attr = TRUE) + + wb <- write_xlsx( + x = mtcars, + as_table = TRUE, + params = list( + choose = c(cyl = "x %in% c(4, 8)", + am = c("x != 1"), + vs = c("x == 1") + ) + ) + ) + + got <- wb_to_df(wb, skip_hidden_rows = TRUE) + expect_equal(exp, got, ignore_attr = TRUE) + + choose <- c(city = "x %in% c(\"Berlin\", \"Paris\")") + + df <- data.frame( + city = c("berlin", "Berlin", "BERLIN", "Hamburg", "Paris", "Lyon"), + cnty = c(1, 1, 1, 1, 2, 2) + ) + + # create workbook + wb <- wb_workbook() %>% + wb_add_worksheet() %>% + wb_add_data_table(x = df, params = list(choose = choose)) + + exp <- c("berlin", "Berlin", "BERLIN", "Paris") + got <- wb_to_df(wb, skip_hidden_rows = TRUE)$city + expect_equal(exp, got) + +})