diff --git a/NEWS.md b/NEWS.md index ed086a748..9b5133291 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,8 @@ * It's now possible to pass array formula vectors to `wb_add_formula()`. +* `wb_add_data_table()` gained a new `total_row` argument. This allows to add a total row to spreadsheets including text and spreadsheet formulas. + ## Fixes * Export `wb_add_ignore_error()`. [955](https://github.com/JanMarvin/openxlsx2/pull/955) diff --git a/R/class-workbook-wrappers.R b/R/class-workbook-wrappers.R index 319219a87..773768756 100644 --- a/R/class-workbook-wrappers.R +++ b/R/class-workbook-wrappers.R @@ -260,10 +260,31 @@ wb_add_data <- function( #' @param last_column logical. If `TRUE`, the last column is bold. #' @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 ... additional arguments #' +#' @details # Modify total row argument +#' It is possible to further tweak the total row. In addition to the default +#' `FALSE` possible values are `TRUE` (the xlsx file will create column sums +#' each variable). +#' +#' In addition it is possible to tweak this further using a character string +#' with one of the following functions for each variable: `"average"`, +#' `"count"`, `"countNums"`, `"max"`, `"min"`, `"stdDev"`, `"sum"`, `"var"`. +#' It is possible to leave the cell empty `"none"` or to create a text input +#' using a named character with name `text` like: `c(text = "Total")`. +#' It's also possible to pass other spreadsheet software functions if they +#' return a single value and hence `"SUM"` would work too. +#' #' @family worksheet content functions #' @family workbook wrappers +#' @examples +#' wb <- wb_workbook()$add_worksheet()$ +#' add_data_table( +#' x = as.data.frame(USPersonalExpenditure), +#' row_names = TRUE, +#' total_row = c(text = "Total", "none", "sum", "sum", "sum", "SUM") +#' ) #' @export wb_add_data_table <- function( wb, @@ -286,6 +307,7 @@ wb_add_data_table <- function( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... ) { assert_workbook(wb) @@ -309,6 +331,7 @@ wb_add_data_table <- function( remove_cell_style = remove_cell_style, na.strings = na.strings, inline_strings = inline_strings, + total_row = total_row, ... = ... ) } @@ -530,7 +553,7 @@ wb_add_slicer <- function( #' cells (see the `MMULT()` example below). For this type of formula, the #' output range must be known a priori and passed to `dims`, otherwise only the #' value of the first cell will be returned. This type of formula, whose result -#' extends over several cells, is only possible with scalar values. If a vector +#' extends over several cells, is only possible with single strings. If a vector #' is passed, it is only possible to return individual cells. #' #' @param wb A Workbook object containing a worksheet. diff --git a/R/class-workbook.R b/R/class-workbook.R index d90126826..bed945f2e 100644 --- a/R/class-workbook.R +++ b/R/class-workbook.R @@ -1339,6 +1339,7 @@ wbWorkbook <- R6::R6Class( #' @param na.strings Value used for replacing `NA` values from `x`. Default #' `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 ... additional arguments #' @return The `wbWorkbook` object add_data_table = function( @@ -1361,6 +1362,7 @@ wbWorkbook <- R6::R6Class( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... ) { @@ -1394,7 +1396,8 @@ wbWorkbook <- R6::R6Class( applyCellStyle = apply_cell_style, removeCellStyle = remove_cell_style, na.strings = na.strings, - inline_strings = inline_strings + inline_strings = inline_strings, + total_row = total_row ) invisible(self) }, @@ -2754,23 +2757,25 @@ wbWorkbook <- R6::R6Class( #' @param tableName tableName #' @param withFilter withFilter #' @param totalsRowCount totalsRowCount + #' @param totalLabel totalLabel #' @param showFirstColumn showFirstColumn #' @param showLastColumn showLastColumn #' @param showRowStripes showRowStripes #' @param showColumnStripes showColumnStripes #' @return The `wbWorksheet` object, invisibly buildTable = function( - sheet = current_sheet(), + sheet = current_sheet(), colNames, ref, showColNames, tableStyle, tableName, - withFilter, # TODO set default for withFilter? - totalsRowCount = 0, - showFirstColumn = 0, - showLastColumn = 0, - showRowStripes = 1, + withFilter = TRUE, + totalsRowCount = 0, + totalLabel = FALSE, + showFirstColumn = 0, + showLastColumn = 0, + showRowStripes = 1, showColumnStripes = 0 ) { @@ -2799,32 +2804,65 @@ wbWorkbook <- R6::R6Class( } if (is.null(self$tables)) { - nms <- NULL + nms <- NULL tSheets <- NULL - tNames <- NULL + tNames <- NULL tActive <- NULL } else { - nms <- self$tables$tab_ref + nms <- self$tables$tab_ref tSheets <- self$tables$tab_sheet - tNames <- self$tables$tab_name + tNames <- self$tables$tab_name tActive <- self$tables$tab_act } ### autofilter autofilter <- if (withFilter) { - xml_node_create(xml_name = "autoFilter", xml_attributes = c(ref = ref)) + 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 + } + xml_node_create(xml_name = "autoFilter", xml_attributes = c(ref = autofilter_ref)) + } + + trf <- NULL + has_total_row <- FALSE + has_total_lbl <- FALSE + if (!isFALSE(totalsRowCount)) { + trf <- totalsRowCount + has_total_row <- TRUE + + if (length(totalLabel) == length(colNames)) { + lbl <- totalLabel + has_total_lbl <- all(is.na(totalLabel)) + } else { + lbl <- rep(NA_character_, length(colNames)) + has_total_lbl <- FALSE + } } ### tableColumn tableColumn <- sapply(colNames, function(x) { id <- which(colNames %in% x) - xml_node_create("tableColumn", xml_attributes = c(id = id, name = x)) + trf_id <- if (has_total_row) trf[[id]] else NULL + lbl_id <- if (has_total_lbl && !is.na(lbl[[id]])) lbl[[id]] else NULL + xml_node_create( + "tableColumn", + xml_attributes = c( + id = id, + name = x, + totalsRowFunction = trf_id, + totalsRowLabel = lbl_id + ) + ) }) tableColumns <- xml_node_create( - xml_name = "tableColumns", - xml_children = tableColumn, + xml_name = "tableColumns", + xml_children = tableColumn, xml_attributes = c(count = as.character(length(colNames))) ) @@ -2849,8 +2887,8 @@ wbWorkbook <- R6::R6Class( name = tableName, displayName = tableName, ref = ref, - totalsRowCount = totalsRowCount, - totalsRowShown = "0" + totalsRowCount = as_xml_attr(has_total_row), + totalsRowShown = as_xml_attr(has_total_row) #headerRowDxfId="1" ) diff --git a/R/helper-functions.R b/R/helper-functions.R index 0ef42864a..deb6b93e2 100644 --- a/R/helper-functions.R +++ b/R/helper-functions.R @@ -976,3 +976,94 @@ clone_shared_strings <- function(wb_old, old, wb_new, new) { # print(sprintf("cloned: %s", length(new_ids))) } + +known_subtotal_funs <- function(x, total, table, row_names = FALSE) { + + # unfortunately x has no row names at this point + ncol_x <- ncol(x) + row_names + nms_x <- names(x) + if (row_names) nms_x <- c("_rowNames_", nms_x) + + fml <- vector("character", ncol_x) + atr <- vector("character", ncol_x) + lbl <- rep(NA_character_, ncol_x) + + if (isTRUE(total) || all(as.character(total) == "109") || all(total == "sum")) { + fml <- paste0("SUBTOTAL(109,", table, "[", names(x), "])") + atr <- rep("sum", ncol_x) + } else { + + # all get the same total_row value + if (length(total) == 1) { + total <- rep(total, ncol_x) + } + + if (length(total) != ncol_x) { + stop("length of total_row and table columns do not match", call. = FALSE) + } + + builtinIds <- c("101", "103", "102", "104", "105", "107", "109", "110") + builtins <- c("average", "count", "countNums", "max", "min", "stdDev", "sum", "var") + + ttl <- as.character(total) + + for (i in seq_len(ncol_x)) { + + if (any(names(total)[i] == "") && (ttl[i] %in% builtinIds || ttl[i] %in% builtins)) { + if (ttl[i] == "101" || ttl[i] == "average") { + fml[i] <- paste0("SUBTOTAL(", 101, ",", table, "[", nms_x[i], "])") + atr[i] <- "average" + } else if (ttl[i] == "102" || ttl[i] == "countNums") { + fml[i] <- paste0("SUBTOTAL(", 102, ",", table, "[", nms_x[i], "])") + atr[i] <- "countNums" + } else if (ttl[i] == "103" || ttl[i] == "count") { + fml[i] <- paste0("SUBTOTAL(", 103, ",", table, "[", nms_x[i], "])") + atr[i] <- "count" + } else if (ttl[i] == "104" || ttl[i] == "max") { + fml[i] <- paste0("SUBTOTAL(", 104, ",", table, "[", nms_x[i], "])") + atr[i] <- "max" + } else if (ttl[i] == "105" || ttl[i] == "min") { + fml[i] <- paste0("SUBTOTAL(", 105, ",", table, "[", nms_x[i], "])") + atr[i] <- "min" + } else if (ttl[i] == "107" || ttl[i] == "stdDev") { + fml[i] <- paste0("SUBTOTAL(", 107, ",", table, "[", nms_x[i], "])") + atr[i] <- "stdDev" + } else if (ttl[i] == "109" || ttl[i] == "sum") { + fml[i] <- paste0("SUBTOTAL(", 109, ",", table, "[", nms_x[i], "])") + atr[i] <- "sum" + } else if (ttl[i] == "110" || ttl[i] == "var") { + fml[i] <- paste0("SUBTOTAL(", 110, ",", table, "[", nms_x[i], "])") + atr[i] <- "var" + } + + } else if (ttl[i] == "0" || ttl[i] == "none") { + fml[i] <- "" + atr[i] <- "none" + } else if (any(names(total)[i] == "text")) { + fml[i] <- as_xml_attr(ttl[i]) + atr[i] <- "" + lbl[i] <- as_xml_attr(ttl[i]) + } else { + # works, but in excel the formula is added to tables.xml as a child to the column + fml[i] <- paste0(ttl[i], "(", table, "[", nms_x[i], "])") + atr[i] <- "custom" + } + + } + + } + + # prepare output + fml <- as.data.frame(t(fml)) + names(fml) <- nms_x + names(atr) <- nms_x + names(lbl) <- nms_x + + # prepare output to be written with formulas + for (i in seq_along(fml)) { + if (is.na(lbl[[i]])) class(fml[[i]]) <- c("formula", fml[[i]]) + } + + list(fml, atr, lbl) + +} diff --git a/R/write.R b/R/write.R index 85291a068..10521d60e 100644 --- a/R/write.R +++ b/R/write.R @@ -725,6 +725,7 @@ write_data2 <- function( #' @param na.strings Value used for replacing `NA` values from `x`. Default #' `na_strings()` uses the special `#N/A` value within the workbook. #' @param inline_strings optional write strings as inline strings +#' @param total_row optional write total rows #' @noRd #' @keywords internal write_data_table <- function( @@ -750,7 +751,8 @@ write_data_table <- function( removeCellStyle = FALSE, data_table = FALSE, na.strings = na_strings(), - inline_strings = TRUE + inline_strings = TRUE, + total_row = FALSE ) { ## Input validating @@ -873,10 +875,10 @@ write_data_table <- function( wb$worksheets[[sheet]]$autoFilter <- sprintf('', ref) - l <- int2col(unlist(coords[, 2])) + l <- int2col(unlist(coords[, 2])) dfn <- sprintf("'%s'!%s", wb$get_sheet_names(escape = TRUE)[sheet], stri_join("$", l, "$", coords[, 1], collapse = ":")) - dn <- sprintf('', sheet - 1L, dfn) + dn <- sprintf('', sheet - 1L, dfn) if (!is.null(wbdn <- wb$get_named_regions())) { @@ -897,40 +899,40 @@ write_data_table <- function( ### End: Only in data -------------------------------------------------------- if (data_table) { - overwrite_nrows <- 1L + overwrite_nrows <- 1L check_tab_head_only <- FALSE - error_msg <- "Cannot overwrite existing table with another table" + error_msg <- "Cannot overwrite existing table with another table" } else { - overwrite_nrows <- colNames + overwrite_nrows <- colNames check_tab_head_only <- TRUE - error_msg <- "Cannot overwrite table headers. Avoid writing over the header row or see wb_get_tables() & wb_remove_tabless() to remove the table object." + error_msg <- "Cannot overwrite table headers. Avoid writing over the header row or see wb_get_tables() & wb_remove_tabless() to remove the table object." } ## Check not overwriting existing table headers wb_check_overwrite_tables( - wb = wb, - sheet = sheet, - new_rows = c(startRow, startRow + nRow - 1L + overwrite_nrows), - new_cols = c(startCol, startCol + nCol - 1L), + wb = wb, + sheet = sheet, + new_rows = c(startRow, startRow + nRow - 1L + overwrite_nrows), + new_cols = c(startCol, startCol + nCol - 1L), check_table_header_only = check_tab_head_only, - error_msg = error_msg + error_msg = error_msg ) ## actual driver, the rest should not create data used for writing wb <- write_data2( - wb = wb, - sheet = sheet, - data = x, - name = name, - colNames = colNames, - rowNames = rowNames, - startRow = startRow, - startCol = startCol, - applyCellStyle = applyCellStyle, + wb = wb, + sheet = sheet, + data = x, + name = name, + colNames = colNames, + rowNames = rowNames, + startRow = startRow, + startCol = startCol, + applyCellStyle = applyCellStyle, removeCellStyle = removeCellStyle, - na.strings = na.strings, - data_table = data_table, - inline_strings = inline_strings + na.strings = na.strings, + data_table = data_table, + inline_strings = inline_strings ) ### Beg: Only in datatable --------------------------------------------------- @@ -950,6 +952,39 @@ write_data_table <- function( tableName <- wb_validate_table_name(wb, tableName) } + ## write total rows column. this is a formula and needs to be written separately + total_fml <- FALSE + total_lbl <- FALSE + if (!isFALSE(total_row)) { + + total <- known_subtotal_funs( + x = x, + total = total_row, + table = tableName, + row_names = rowNames + ) + + total_row <- total[[1]] + total_fml <- total[[2]] + total_lbl <- total[[3]] + + wb <- write_data2( + wb = wb, + sheet = sheet, + data = total_row, + name = name, + colNames = FALSE, + rowNames = FALSE, + startRow = startRow + nrow(x), + startCol = startCol, + applyCellStyle = applyCellStyle, + removeCellStyle = removeCellStyle, + na.strings = na.strings, + data_table = data_table, + inline_strings = inline_strings + ) + } + ## If 0 rows append a blank row cstm_tableStyles <- wb$styles_mgr$tableStyle$name validNames <- c("none", paste0("TableStyleLight", seq_len(21)), paste0("TableStyleMedium", seq_len(28)), paste0("TableStyleDark", seq_len(11)), cstm_tableStyles) @@ -972,21 +1007,22 @@ write_data_table <- function( ref1 <- paste0(int2col(startCol), startRow) ref2 <- paste0(int2col(startCol + nCol - !rowNames), startRow + nRow) - ref <- paste(ref1, ref2, sep = ":") + ref <- paste(ref1, ref2, sep = ":") ## create table.xml and assign an id to worksheet tables wb$buildTable( - sheet = sheet, - colNames = col_names, - ref = ref, - showColNames = colNames, - tableStyle = tableStyle, - tableName = tableName, - withFilter = withFilter, - totalsRowCount = 0L, - showFirstColumn = firstColumn, - showLastColumn = lastColumn, - showRowStripes = bandedRows, + sheet = sheet, + colNames = col_names, + ref = ref, + showColNames = colNames, + tableStyle = tableStyle, + tableName = tableName, + totalLabel = total_lbl, + withFilter = withFilter, + totalsRowCount = total_fml, + showFirstColumn = firstColumn, + showLastColumn = lastColumn, + showRowStripes = bandedRows, showColumnStripes = bandedCols ) } @@ -1210,6 +1246,7 @@ write_datatable <- function( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... ) { @@ -1238,6 +1275,7 @@ write_datatable <- function( applyCellStyle = apply_cell_style, removeCellStyle = remove_cell_style, na.strings = na.strings, - inline_strings = inline_strings + inline_strings = inline_strings, + total_row = total_row ) } diff --git a/R/write_xlsx.R b/R/write_xlsx.R index 08f2b52d2..ad2258314 100644 --- a/R/write_xlsx.R +++ b/R/write_xlsx.R @@ -6,12 +6,12 @@ #' columns of `x` with class `Date` or `POSIXt` are automatically #' styled as dates and datetimes respectively. #' -#' @param x An object or a list of objects that can be handled by [wb_add_data()] to write to file -#' @param file An xlsx file name +#' @param x An object or a list of objects that can be handled by [wb_add_data()] to write to file. +#' @param file An optional xlsx file name. If no file is passed, the object is not written to disk and only a workbook object is returned. #' @param as_table If `TRUE`, will write as a data table, instead of data. #' @inheritDotParams wb_workbook creator #' @inheritDotParams wb_add_worksheet sheet grid_lines tab_color zoom -#' @inheritDotParams wb_add_data_table start_col start_row col_names row_names na.strings +#' @inheritDotParams wb_add_data_table start_col start_row col_names row_names na.strings total_row #' @inheritDotParams wb_add_data start_col start_row col_names row_names na.strings #' @inheritDotParams wb_freeze_pane first_active_row first_active_col first_row first_col #' @inheritDotParams wb_set_col_widths widths @@ -49,7 +49,7 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { arguments <- c(ls(), "creator", "sheet_name", "grid_lines", "tab_color", "tab_colour", "zoom", "header", "footer", "even_header", "even_footer", "first_header", - "first_footer", "start_col", "start_row", + "first_footer", "start_col", "start_row", "total_row", "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", @@ -259,6 +259,11 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { tableStyle <- params$table_style } + totalRow <- FALSE + if ("total_row" %in% names(params)) { + totalRow <- params$total_row + } + na.strings <- if ("na.strings" %in% names(params)) { params$na.strings @@ -356,7 +361,8 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { table_style = tableStyle[[i]], table_name = NULL, with_filter = withFilter[[i]], - na.strings = na.strings + na.strings = na.strings, + total_row = totalRow ) } else { # TODO add_data()? @@ -440,6 +446,8 @@ write_xlsx <- function(x, file, as_table = FALSE, ...) { } } - wb_save(wb, file = file, overwrite = overwrite) + if (!missing(file)) + wb_save(wb, file = file, overwrite = overwrite) + invisible(wb) } diff --git a/inst/WORDLIST b/inst/WORDLIST index f13c88334..7be0d7e35 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -195,6 +195,7 @@ posixt pre printTitleCols printTitleRows +priori pugi pugixml queryTables @@ -239,6 +240,7 @@ tableStyle tablename textLength th +totalLabel totalRow totalsRowCount twoCell diff --git a/man/wbWorkbook.Rd b/man/wbWorkbook.Rd index 51c003d05..f3b826336 100644 --- a/man/wbWorkbook.Rd +++ b/man/wbWorkbook.Rd @@ -579,6 +579,7 @@ add a data table remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... )}\if{html}{\out{}} } @@ -625,6 +626,8 @@ add a data table \item{\code{inline_strings}}{write characters as inline strings} +\item{\code{total_row}}{write total rows to table} + \item{\code{...}}{additional arguments} } \if{html}{\out{}} @@ -972,8 +975,9 @@ Build table showColNames, tableStyle, tableName, - withFilter, + withFilter = TRUE, totalsRowCount = 0, + totalLabel = FALSE, showFirstColumn = 0, showLastColumn = 0, showRowStripes = 1, @@ -1000,6 +1004,8 @@ Build table \item{\code{totalsRowCount}}{totalsRowCount} +\item{\code{totalLabel}}{totalLabel} + \item{\code{showFirstColumn}}{showFirstColumn} \item{\code{showLastColumn}}{showLastColumn} diff --git a/man/wb_add_data_table.Rd b/man/wb_add_data_table.Rd index 22b20b9e9..31d557f7c 100644 --- a/man/wb_add_data_table.Rd +++ b/man/wb_add_data_table.Rd @@ -25,6 +25,7 @@ wb_add_data_table( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... ) } @@ -77,6 +78,8 @@ columns to a character vector e.g. \item{inline_strings}{write characters as inline strings} +\item{total_row}{logical. With the default \code{FALSE} no total row is added.} + \item{...}{additional arguments} } \description{ @@ -112,6 +115,28 @@ distinction is that the latter creates a table in the worksheet that can be used for different kind of formulas and can be sorted independently, though is less flexible than basic cell regions. } +\section{Modify total row argument}{ +It is possible to further tweak the total row. In addition to the default +\code{FALSE} possible values are \code{TRUE} (the xlsx file will create column sums +each variable). + +In addition it is possible to tweak this further using a character string +with one of the following functions for each variable: \code{"average"}, +\code{"count"}, \code{"countNums"}, \code{"max"}, \code{"min"}, \code{"stdDev"}, \code{"sum"}, \code{"var"}. +It is possible to leave the cell empty \code{"none"} or to create a text input +using a named character with name \code{text} like: \code{c(text = "Total")}. +It's also possible to pass other spreadsheet software functions if they +return a single value and hence \code{"SUM"} would work too. +} + +\examples{ +wb <- wb_workbook()$add_worksheet()$ + add_data_table( + x = as.data.frame(USPersonalExpenditure), + row_names = TRUE, + total_row = c(text = "Total", "none", "sum", "sum", "sum", "SUM") + ) +} \seealso{ Other worksheet content functions: \code{\link{col_widths-wb}}, diff --git a/man/wb_add_formula.Rd b/man/wb_add_formula.Rd index 370d4dfcf..0816dec40 100644 --- a/man/wb_add_formula.Rd +++ b/man/wb_add_formula.Rd @@ -67,7 +67,7 @@ take \code{dims} as a reference. For some formulas, the result will span multipl cells (see the \code{MMULT()} example below). For this type of formula, the output range must be known a priori and passed to \code{dims}, otherwise only the value of the first cell will be returned. This type of formula, whose result -extends over several cells, is only possible with scalar values. If a vector +extends over several cells, is only possible with single strings. If a vector is passed, it is only possible to return individual cells. } \examples{ diff --git a/man/write_datatable.Rd b/man/write_datatable.Rd index b1ab251ce..ba14e8b52 100644 --- a/man/write_datatable.Rd +++ b/man/write_datatable.Rd @@ -25,6 +25,7 @@ write_datatable( remove_cell_style = FALSE, na.strings = na_strings(), inline_strings = TRUE, + total_row = FALSE, ... ) } @@ -77,6 +78,8 @@ columns to a character vector e.g. \item{inline_strings}{write characters as inline strings} +\item{total_row}{logical. With the default \code{FALSE} no total row is added.} + \item{...}{additional arguments} } \description{ diff --git a/man/write_xlsx.Rd b/man/write_xlsx.Rd index 1f4419b88..d43efd540 100644 --- a/man/write_xlsx.Rd +++ b/man/write_xlsx.Rd @@ -7,9 +7,9 @@ write_xlsx(x, file, as_table = FALSE, ...) } \arguments{ -\item{x}{An object or a list of objects that can be handled by \code{\link[=wb_add_data]{wb_add_data()}} to write to file} +\item{x}{An object or a list of objects that can be handled by \code{\link[=wb_add_data]{wb_add_data()}} to write to file.} -\item{file}{An xlsx file name} +\item{file}{An optional xlsx file name. If no file is passed, the object is not written to disk and only a workbook object is returned.} \item{as_table}{If \code{TRUE}, will write as a data table, instead of data.} @@ -24,6 +24,7 @@ hidden.} \code{grDevices::colors()}) or a valid hex color beginning with "#".} \item{\code{zoom}}{The sheet zoom level, a numeric between 10 and 400 as a percentage. (A zoom value smaller than 10 will default to 10.)} + \item{\code{total_row}}{logical. With the default \code{FALSE} no total row is added.} \item{\code{start_col}}{A vector specifying the starting column to write \code{x} to.} \item{\code{start_row}}{A vector specifying the starting row to write \code{x} to.} \item{\code{col_names}}{If \code{TRUE}, column names of \code{x} are written.} diff --git a/tests/testthat/test-write.R b/tests/testthat/test-write.R index 2e167f6f8..84b7dd4f3 100644 --- a/tests/testthat/test-write.R +++ b/tests/testthat/test-write.R @@ -806,3 +806,74 @@ test_that("filter works with wb_add_data()", { expect_equal(exp, got) }) + +test_that("writing total row works", { + + # default row sums + wb <- wb_workbook()$add_worksheet()$add_data_table(x = mtcars, total_row = TRUE) + + exp <- data.frame( + A = "SUBTOTAL(109,Table1[mpg])", B = "SUBTOTAL(109,Table1[cyl])", + C = "SUBTOTAL(109,Table1[disp])", D = "SUBTOTAL(109,Table1[hp])", + E = "SUBTOTAL(109,Table1[drat])", F = "SUBTOTAL(109,Table1[wt])", + G = "SUBTOTAL(109,Table1[qsec])", H = "SUBTOTAL(109,Table1[vs])", + I = "SUBTOTAL(109,Table1[am])", J = "SUBTOTAL(109,Table1[gear])", + K = "SUBTOTAL(109,Table1[carb])" + ) + got <- wb_to_df(wb, dims = wb_dims(rows = 33, cols = "A:K"), + show_formula = TRUE, col_names = FALSE) + expect_equal(exp, got, ignore_attr = TRUE) + + # empty total row + wb <- wb_workbook()$add_worksheet()$add_data_table(x = mtcars, total_row = c("none")) + + exp <- data.frame( + A = NA_real_, B = NA_real_, C = NA_real_, D = NA_real_, + E = NA_real_, F = NA_real_, G = NA_real_, H = NA_real_, I = NA_real_, + J = NA_real_, K = NA_real_ + ) + got <- wb_to_df(wb, dims = wb_dims(rows = 33, cols = "A:K"), + show_formula = TRUE, col_names = FALSE) + expect_equal(exp, got, ignore_attr = TRUE) + + # total row with text only + wb <- wb_workbook()$add_worksheet()$add_data_table(x = cars, total_row = c(text = "Result", text = "sum")) + + exp <- data.frame(A = "Result", B = "sum") + got <- wb_to_df(wb, dims = wb_dims(rows = 51, cols = "A:B"), + show_formula = TRUE, col_names = FALSE) + expect_equal(exp, got, ignore_attr = TRUE) + + # total row with text and formula + wb <- wb_workbook()$add_worksheet()$add_data_table(x = cars, total_row = c(text = "Result", "sum")) + + exp <- data.frame(A = "Result", B = "SUBTOTAL(109,Table1[dist])") + got <- wb_to_df(wb, dims = wb_dims(rows = 51, cols = "A:B"), + show_formula = TRUE, col_names = FALSE) + expect_equal(exp, got, ignore_attr = TRUE) + + # total row with none and custom formula + wb <- wb_workbook()$add_worksheet()$add_data_table(x = cars, total_row = c("none", "COUNTA")) + + exp <- data.frame(A = NA_real_, B = "COUNTA(Table1[dist])") + got <- wb_to_df(wb, dims = wb_dims(rows = 51, cols = "A:B"), + show_formula = TRUE, col_names = FALSE) + expect_equal(exp, got, ignore_attr = TRUE) + + # with rownames + wb <- wb_workbook()$add_worksheet()$ + add_data_table( + x = as.data.frame(USPersonalExpenditure), + row_names = TRUE, + total_row = c(text = "Total", "none", "sum", "sum", "sum", "SUM") + ) + + exp <- data.frame( + A = "Total", B = NA_real_, C = "SUBTOTAL(109,Table1[1945])", + D = "SUBTOTAL(109,Table1[1950])", E = "SUBTOTAL(109,Table1[1955])", + F = "SUM(Table1[1960])" + ) + got <- wb_to_df(wb, dims = wb_dims(rows = 6, cols = "A:F"), col_names = FALSE, show_formula = TRUE) + expect_equal(exp, got, ignore_attr = TRUE) + +})