diff --git a/R/write.R b/R/write.R index 3e4b24ed6..78496516b 100644 --- a/R/write.R +++ b/R/write.R @@ -1,721 +1,278 @@ - -# `write_data()` --------------------------------------------------------------- -#' Write an object to a worksheet -#' -#' Write an object to worksheet with optional styling. +#' function to add missing cells to cc and rows #' -#' Formulae written using write_formula to a Workbook object will not get picked up by read_xlsx(). -#' This is because only the formula is written and left to Excel to evaluate the formula when the file is opened in Excel. -#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame contains this string, the output will be broken. +#' Create a cell in the workbook #' -#' @param wb A Workbook object containing a worksheet. -#' @param sheet The worksheet to write to. Can be the worksheet index or name. -#' @param x Object to be written. For classes supported look at the examples. -#' @param start_col A vector specifying the starting column to write to. -#' @param start_row A vector specifying the starting row to write to. -#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" -#' @param array A bool if the function written is of type array -#' @param col_names If `TRUE`, column names of x are written. -#' @param row_names If `TRUE`, data.frame row names of x are written. -#' @param with_filter If `TRUE`, add filters to the column name row. NOTE can only have one filter per worksheet. -#' @param sep Only applies to list columns. The separator used to collapse list columns to a character vector e.g. sapply(x$list_column, paste, collapse = sep). -#' @param name If not NULL, a named region is defined. -#' @param apply_cell_style apply styles when writing on the sheet -#' @param remove_cell_style if writing into existing cells, should the cell style be removed? +#' @param wb the workbook update +#' @param sheet_id the sheet to update +#' @param x the newly filled cc frame +#' @param rows the rows needed +#' @param cells_needed the cells needed +#' @param colNames has colNames (only in update_cell) +#' @param removeCellStyle remove the cell style (only in update_cell) #' @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 ... additional arguments -#' @seealso [write_datatable()] -#' @return invisible(0) -#' @examples -#' ## See formatting vignette for further examples. -#' -#' ## Options for default styling (These are the defaults) -#' options("openxlsx2.dateFormat" = "mm/dd/yyyy") -#' options("openxlsx2.datetimeFormat" = "yyyy-mm-dd hh:mm:ss") -#' options("openxlsx2.numFmt" = NULL) +#' `na_strings()` uses the special `#N/A` value within the workbook.#' @keywords internal +#' @noRd +inner_update <- function( + wb, + sheet_id, + x, + rows, + cells_needed, + colNames = FALSE, + removeCellStyle = FALSE, + na.strings = na_strings() +) { + + # 1) pull sheet to modify from workbook; 2) modify it; 3) push it back + cc <- wb$worksheets[[sheet_id]]$sheet_data$cc + row_attr <- wb$worksheets[[sheet_id]]$sheet_data$row_attr + + # workbooks contain only entries for values currently present. + # if A1 is filled, B1 is not filled and C1 is filled the sheet will only + # contain fields A1 and C1. + cells_in_wb <- cc$r + rows_in_wb <- row_attr$r + + # check if there are rows not available + if (!all(rows %in% rows_in_wb)) { + # message("row(s) not in workbook") + + missing_rows <- rows[!rows %in% rows_in_wb] + + # new row_attr + row_attr_missing <- empty_row_attr(n = length(missing_rows)) + row_attr_missing$r <- missing_rows + + row_attr <- rbind(row_attr, row_attr_missing) + + # order + row_attr <- row_attr[order(as.numeric(row_attr$r)), ] + + wb$worksheets[[sheet_id]]$sheet_data$row_attr <- row_attr + # provide output + rows_in_wb <- row_attr$r + + } + + if (!all(cells_needed %in% cells_in_wb)) { + # message("cell(s) not in workbook") + + missing_cells <- cells_needed[!cells_needed %in% cells_in_wb] + + # create missing cells + cc_missing <- create_char_dataframe(names(cc), length(missing_cells)) + cc_missing$r <- missing_cells + cc_missing$row_r <- gsub("[[:upper:]]", "", cc_missing$r) + cc_missing$c_r <- gsub("[[:digit:]]", "", cc_missing$r) + + # assign to cc + cc <- rbind(cc, cc_missing) + + # order cc (not really necessary, will be done when saving) + cc <- cc[order(as.integer(cc[, "row_r"]), col2int(cc[, "c_r"])), ] + + # update dimensions (only required if new cols and rows are added) ------ + all_rows <- as.numeric(unique(cc$row_r)) + all_cols <- col2int(unique(cc$c_r)) + + min_cell <- trimws(paste0(int2col(min(all_cols, na.rm = TRUE)), min(all_rows, na.rm = TRUE))) + max_cell <- trimws(paste0(int2col(max(all_cols, na.rm = TRUE)), max(all_rows, na.rm = TRUE))) + + # i know, i know, i'm lazy + wb$worksheets[[sheet_id]]$dimension <- paste0("") + } + + if (is_na_strings(na.strings)) { + na.strings <- NULL + } + + if (removeCellStyle) { + cell_style <- "c_s" + } else { + cell_style <- NULL + } + + replacement <- c("r", cell_style, "c_t", "c_cm", "c_ph", "c_vm", "v", + "f", "f_t", "f_ref", "f_ca", "f_si", "is", "typ") + + sel <- match(x$r, cc$r) + cc[sel, replacement] <- x[replacement] + + # avoid missings in cc + if (any(is.na(cc))) + cc[is.na(cc)] <- "" + + # push everything back to workbook + wb$worksheets[[sheet_id]]$sheet_data$cc <- cc + + wb +} + +#' Initialize data cell(s) #' -#' ############################################################################# -#' ## Create Workbook object and add worksheets -#' wb <- wb_workbook() +#' Create a cell in the workbook #' -#' ## Add worksheets -#' wb$add_worksheet("Cars") -#' wb$add_worksheet("Formula") +#' @param wb the workbook you want to update +#' @param sheet the sheet you want to update +#' @param new_cells the cell you want to update in Excel connotation e.g. "A1" #' -#' x <- mtcars[1:6, ] -#' wb$add_data("Cars", x, startCol = 2, startRow = 3, rowNames = TRUE) +#' @keywords internal +#' @noRd +initialize_cell <- function(wb, sheet, new_cells) { + + sheet_id <- wb$validate_sheet(sheet) + + # create artificial cc for the missing cells + x <- empty_sheet_data_cc(n = length(new_cells)) + x$r <- new_cells + x$row_r <- gsub("[[:upper:]]", "", new_cells) + x$c_r <- gsub("[[:digit:]]", "", new_cells) + + rows <- x$row_r + cells_needed <- new_cells + + inner_update(wb, sheet_id, x, rows, cells_needed) +} + +#' Replace data cell(s) #' -#' ############################################################################# -#' ## Hyperlinks -#' ## - vectors/columns with class 'hyperlink' are written as hyperlinks' +#' Minimal invasive update of cell(s) inside of imported workbooks. #' -#' v <- rep("https://CRAN.R-project.org/", 4) -#' names(v) <- paste0("Hyperlink", 1:4) # Optional: names will be used as display text -#' class(v) <- "hyperlink" -#' wb$add_data("Cars", x = v, dims = c("B32")) +#' @param x cc dataframe of the updated cells +#' @param wb the workbook you want to update +#' @param sheet the sheet you want to update +#' @param cell the cell you want to update in Excel connotation e.g. "A1" +#' @param colNames if TRUE colNames are passed down +#' @param removeCellStyle keep the cell style? +#' @param na.strings optional na.strings argument. if missing #N/A is used. If NULL no cell value is written, if character or numeric this is written (even if NA is part of numeric data) #' -#' ############################################################################# -#' ## Formulas -#' ## - vectors/columns with class 'formula' are written as formulas' +#' @keywords internal +#' @noRd +update_cell <- function(x, wb, sheet, cell, colNames = FALSE, + removeCellStyle = FALSE, na.strings) { + + if (missing(na.strings)) + na.strings <- substitute() + + sheet_id <- wb$validate_sheet(sheet) + + dims <- dims_to_dataframe(cell, fill = TRUE) + rows <- rownames(dims) + + cells_needed <- unname(unlist(dims)) + + inner_update(wb, sheet_id, x, rows, cells_needed, colNames, removeCellStyle, na.strings) +} + +#' dummy function to write data +#' @param wb workbook +#' @param sheet sheet +#' @param data data to export +#' @param name If not NULL, a named region is defined. +#' @param colNames include colnames? +#' @param rowNames include rownames? +#' @param startRow row to place it +#' @param startCol col to place it +#' @param applyCellStyle apply styles when writing on the sheet +#' @param removeCellStyle keep the cell style? +#' @param na.strings Value used for replacing `NA` values from `x`. Default +#' `na_strings()` uses the special `#N/A` value within the workbook. +#' @param data_table logical. if `TRUE` and `rowNames = TRUE`, do not write the cell containing `"_rowNames_"` +#' @param inline_strings write characters as inline strings +#' @details +#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame +#' contains this string, the output will be broken. #' -#' df <- data.frame( -#' x = 1:3, y = 1:3, -#' z = paste(paste0("A", 1:3 + 1L), paste0("B", 1:3 + 1L), sep = "+"), -#' stringsAsFactors = FALSE -#' ) +#' @examples +#' # create a workbook and add some sheets +#' wb <- wb_workbook() #' -#' class(df$z) <- c(class(df$z), "formula") +#' wb$add_worksheet("sheet1") +#' write_data2(wb, "sheet1", mtcars, colNames = TRUE, rowNames = TRUE) #' -#' wb$add_data(sheet = "Formula", x = df) +#' wb$add_worksheet("sheet2") +#' write_data2(wb, "sheet2", cars, colNames = FALSE) #' -#' ############################################################################# -#' # update cell range and add mtcars -#' xlsxFile <- system.file("extdata", "openxlsx2_example.xlsx", package = "openxlsx2") -#' wb2 <- wb_load(xlsxFile) +#' wb$add_worksheet("sheet3") +#' write_data2(wb, "sheet3", letters) #' -#' # read dataset with inlinestr -#' wb_to_df(wb2) -#' write_data(wb2, 1, mtcars, startCol = 4, startRow = 4) -#' wb_to_df(wb2) -#' @export -write_data <- function( +#' wb$add_worksheet("sheet4") +#' write_data2(wb, "sheet4", as.data.frame(Titanic), startRow = 2, startCol = 2) +#' @keywords internal +#' @noRd +write_data2 <- function( wb, sheet, - x, - dims = rowcol_to_dims(start_row, start_col), - start_col = 1, - start_row = 1, - array = FALSE, - col_names = TRUE, - row_names = FALSE, - with_filter = FALSE, - sep = ", ", - name = NULL, - apply_cell_style = TRUE, - remove_cell_style = FALSE, - na.strings = na_strings(), - inline_strings = TRUE, - ... + data, + name = NULL, + colNames = TRUE, + rowNames = FALSE, + startRow = 1, + startCol = 1, + applyCellStyle = TRUE, + removeCellStyle = FALSE, + na.strings = na_strings(), + data_table = FALSE, + inline_strings = TRUE ) { - standardize_case_names(...) + is_data_frame <- FALSE + #### prepare the correct data formats for openxml + dc <- openxlsx2_type(data) - write_data_table( - wb = wb, - sheet = sheet, - x = x, - dims = dims, - startCol = start_col, - startRow = start_row, - array = array, - colNames = col_names, - rowNames = row_names, - tableStyle = NULL, - tableName = NULL, - withFilter = with_filter, - sep = sep, - firstColumn = FALSE, - lastColumn = FALSE, - bandedRows = FALSE, - bandedCols = FALSE, - name = name, - applyCellStyle = apply_cell_style, - removeCellStyle = remove_cell_style, - data_table = FALSE, - na.strings = na.strings, - inline_strings = inline_strings - ) -} + # convert factor to character + if (any(dc == openxlsx2_celltype[["factor"]])) { + is_factor <- dc == openxlsx2_celltype[["factor"]] + fcts <- names(dc[is_factor]) + data[fcts] <- lapply(data[fcts], to_string) + # dc <- openxlsx2_type(data) + } -# write_formula() ------------------------------------------- -#' Write a character vector as an Excel Formula -#' -#' Write a a character vector containing Excel formula to a worksheet. -#' -#' @details -#' Currently only the English version of functions are supported. Please don't use the local translation. -#' The examples below show a small list of possible formulas: -#' * SUM(B2:B4) -#' * AVERAGE(B2:B4) -#' * MIN(B2:B4) -#' * MAX(B2:B4) -#' * ... -#' -#' @param wb A Workbook object containing a worksheet. -#' @param sheet The worksheet to write to. Can be the worksheet index or name. -#' @param x A character vector. -#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" -#' @param start_col A vector specifying the starting column to write to. -#' @param start_row A vector specifying the starting row to write to. -#' @param array A bool if the function written is of type array -#' @param cm A bool if the function is of type cm (array with hidden curly braces) -#' @param apply_cell_style apply styles when writing on the sheet -#' @param remove_cell_style if writing into existing cells, should the cell style be removed? -#' @param ... additional arguments -#' @seealso [write_data()] -#' @examples -#' ## There are 3 ways to write a formula -#' -#' wb <- wb_workbook() -#' wb$add_worksheet("Sheet 1") -#' wb$add_data("Sheet 1", x = iris) -#' -#' ## SEE `int2col()` to convert int to Excel column label -#' -#' ## 1. - As a character vector using write_formula -#' -#' v <- c("SUM(A2:A151)", "AVERAGE(B2:B151)") ## skip header row -#' write_formula(wb, sheet = 1, x = v, startCol = 10, startRow = 2) -#' write_formula(wb, 1, x = "A2 + B2", startCol = 10, startRow = 10) -#' -#' -#' ## 2. - As a data.frame column with class "formula" using write_data -#' -#' df <- data.frame( -#' x = 1:3, -#' y = 1:3, -#' z = paste(paste0("A", 1:3 + 1L), paste0("B", 1:3 + 1L), sep = " + "), -#' z2 = sprintf("ADDRESS(1,%s)", 1:3), -#' stringsAsFactors = FALSE -#' ) -#' -#' class(df$z) <- c(class(df$z), "formula") -#' class(df$z2) <- c(class(df$z2), "formula") -#' -#' wb$add_worksheet("Sheet 2") -#' wb$add_data(sheet = 2, x = df) -#' -#' -#' ## 3. - As a vector with class "formula" using write_data -#' -#' v2 <- c("SUM(A2:A4)", "AVERAGE(B2:B4)", "MEDIAN(C2:C4)") -#' class(v2) <- c(class(v2), "formula") -#' -#' wb$add_data(sheet = 2, x = v2, startCol = 10, startRow = 2) -#' -#' -#' ## 4. - Writing internal hyperlinks -#' -#' wb <- wb_workbook() -#' wb$add_worksheet("Sheet1") -#' wb$add_worksheet("Sheet2") -#' write_formula(wb, "Sheet1", x = '=HYPERLINK("#Sheet2!B3", "Text to Display - Link to Sheet2")') -#' -#' -#' ## 5. - Writing array formulas -#' -#' set.seed(123) -#' df <- data.frame(C = rnorm(10), D = rnorm(10)) -#' -#' wb <- wb_workbook() -#' wb <- wb_add_worksheet(wb, "df") -#' -#' wb$add_data("df", df, startCol = "C") -#' -#' write_formula(wb, "df", startCol = "E", startRow = "2", -#' x = "SUM(C2:C11*D2:D11)", -#' array = TRUE) -#' @export -write_formula <- function( - wb, - sheet, - x, - dims = rowcol_to_dims(start_row, start_col), - start_col = 1, - start_row = 1, - array = FALSE, - cm = FALSE, - apply_cell_style = TRUE, - remove_cell_style = FALSE, - ... -) { + hconvert_date1904 <- grepl('date1904="1"|date1904="true"', + stri_join(unlist(wb$workbook), collapse = ""), + ignore.case = TRUE) - standardize_case_names(...) + # TODO need to tell excel that we have a date, apply some kind of numFmt + data <- convert_to_excel_date(df = data, date1904 = hconvert_date1904) - assert_class(x, "character") - # remove xml encoding and reapply it afterwards. until v0.3 encoding was not enforced - x <- replaceXMLEntities(x) - x <- vapply(x, function(val) xml_value(xml_node_create("fml", val, escapes = TRUE), "fml"), NA_character_) - dfx <- data.frame("X" = x, stringsAsFactors = FALSE) + # backward compatible + if (!inherits(data, "data.frame") || inherits(data, "matrix")) { + data <- as.data.frame(data) + colNames <- FALSE + } - formula <- "formula" - if (array) formula <- "array_formula" - if (cm) { - # need to set cell metadata in wb$metadata - if (is.null(wb$metadata)) { + if (inherits(data, "data.frame") || inherits(data, "matrix")) { + is_data_frame <- TRUE - wb$append("Content_Types", "") + if (is.data.frame(data)) data <- as.data.frame(data) - wb$metadata <- # danger danger no clue what this means! - xml_node_create( - "metadata", - xml_attributes = c( - xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main", - "xmlns:xda" = "http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray" - ), - xml_children = read_xml( - " - - - - - - - - - - - , - ", - pointer = FALSE - ) - ) + sel <- !dc %in% c(4, 5, 10) + data[sel] <- lapply(data[sel], as.character) + + # add rownames + if (rowNames) { + data <- cbind("_rowNames_" = rownames(data), data) + dc <- c(c("_rowNames_" = openxlsx2_celltype[["character"]]), dc) } - ## TODO Not sure if there are more cases - # add new cell metadata record - cM <- xml_node(wb$metadata, "metadata", "cellMetadata") - cM <- xml_add_child(cM, xml_child = "") + # add colnames + if (colNames) + data <- rbind(colnames(data), data) + } - # we need to update count - cnt <- as_xml_attr(length(xml_node(cM, "cellMetadata", "bk"))) - cM <- xml_attr_mod(cM, xml_attributes = c(count = cnt)) - # remove current cellMetadata update new - wb$metadata <- xml_rm_child(wb$metadata, "cellMetadata") - wb$metadata <- xml_add_child(wb$metadata, cM) + sheetno <- wb_validate_sheet(wb, sheet) + # message("sheet no: ", sheetno) - attr(dfx, "c_cm") <- cnt - formula <- "cm_formula" - } + # create a data frame + if (!is_data_frame) + data <- as.data.frame(t(data)) - class(dfx$X) <- c("character", formula) - if (!is.null(dims)) { - if (array || cm) { - attr(dfx, "f_ref") <- dims - } - } + data_nrow <- NROW(data) + data_ncol <- NCOL(data) - if (any(grepl("=([\\s]*?)HYPERLINK\\(", x, perl = TRUE))) { - class(dfx$X) <- c("character", "formula", "hyperlink") - } - - write_data( - wb = wb, - sheet = sheet, - x = dfx, - start_col = start_col, - start_row = start_row, - dims = dims, - array = array, - col_names = FALSE, - row_names = FALSE, - apply_cell_style = apply_cell_style, - remove_cell_style = remove_cell_style - ) - -} - -# `write_datatable()` ---------------------- -#' Write to a worksheet as an Excel table -#' -#' Write to a worksheet and format as an Excel table -#' -#' @param wb A Workbook object containing a worksheet. -#' @param sheet The worksheet to write to. Can be the worksheet index or name. -#' @param x A data frame. -#' @param start_col A vector specifying the starting column to write df -#' @param start_row A vector specifying the starting row to write df -#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" -#' @param col_names If `TRUE`, column names of x are written. -#' @param row_names If `TRUE`, row names of x are written. -#' @param table_style Any excel table style name or "none" (see "formatting" vignette). -#' @param table_name name of table in workbook. The table name must be unique. -#' @param with_filter If `TRUE`, columns with have filters in the first row. -#' @param sep Only applies to list columns. The separator used to collapse list columns to a character vector e.g. sapply(x$list_column, paste, collapse = sep). -#' \cr\cr -#' \cr**The below options correspond to Excel table options:** -#' \cr -#' \if{html}{\figure{tableoptions.png}{options: width="40\%" alt="Figure: table_options.png"}} -#' \if{latex}{\figure{tableoptions.pdf}{options: width=7cm}} -#' -#' @param first_column logical. If TRUE, the first column is bold -#' @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 apply_cell_style apply styles when writing on the sheet -#' @param remove_cell_style if writing into existing cells, should the cell style be removed? -#' @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 ... additional arguments -#' @details columns of x with class Date/POSIXt, currency, accounting, -#' hyperlink, percentage are automatically styled as dates, currency, accounting, -#' hyperlinks, percentages respectively. -#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame -#' contains this string, the output will be broken. -#' @seealso -#' [wb_add_worksheet()] -#' [write_data()] -#' [wb_remove_tables()] -#' [wb_get_tables()] -#' @examples -#' ## see package vignettes for further examples. -#' -#' ##################################################################################### -#' ## Create Workbook object and add worksheets -#' wb <- wb_workbook() -#' wb$add_worksheet("S1") -#' wb$add_worksheet("S2") -#' wb$add_worksheet("S3") -#' -#' ##################################################################################### -#' ## -- write data.frame as an Excel table with column filters -#' ## -- default table style is "TableStyleMedium2" -#' -#' wb$add_data_table("S1", x = iris) -#' -#' wb$add_data_table("S2", -#' x = mtcars, dims = c("B3"), rowNames = TRUE, -#' tableStyle = "TableStyleLight9" -#' ) -#' -#' df <- data.frame( -#' "Date" = Sys.Date() - 0:19, -#' "T" = TRUE, "F" = FALSE, -#' "Time" = Sys.time() - 0:19 * 60 * 60, -#' "Cash" = paste("$", 1:20), "Cash2" = 31:50, -#' "hLink" = "https://CRAN.R-project.org/", -#' "Percentage" = seq(0, 1, length.out = 20), -#' "TinyNumbers" = runif(20) / 1E9, stringsAsFactors = FALSE -#' ) -#' -#' ## openxlsx will apply default Excel styling for these classes -#' class(df$Cash) <- c(class(df$Cash), "currency") -#' class(df$Cash2) <- c(class(df$Cash2), "accounting") -#' class(df$hLink) <- "hyperlink" -#' class(df$Percentage) <- c(class(df$Percentage), "percentage") -#' class(df$TinyNumbers) <- c(class(df$TinyNumbers), "scientific") -#' -#' wb$add_data_table("S3", x = df, startRow = 4, rowNames = TRUE, tableStyle = "TableStyleMedium9") -#' -#' ##################################################################################### -#' ## Additional Header Styling and remove column filters -#' -#' write_datatable(wb, -#' sheet = 1, -#' x = iris, -#' startCol = 7, -#' withFilter = FALSE, -#' firstColumn = TRUE, -#' lastColumn = TRUE, -#' bandedRows = TRUE, -#' bandedCols = TRUE -#' ) -#' -#' ##################################################################################### -#' ## Pre-defined table styles gallery -#' -#' wb <- wb_workbook(paste0("tableStylesGallery.xlsx")) -#' wb$add_worksheet("Style Samples") -#' for (i in 1:21) { -#' style <- paste0("TableStyleLight", i) -#' write_datatable(wb, -#' x = data.frame(style), sheet = 1, -#' tableStyle = style, startRow = 1, startCol = i * 3 - 2 -#' ) -#' } -#' -#' for (i in 1:28) { -#' style <- paste0("TableStyleMedium", i) -#' write_datatable(wb, -#' x = data.frame(style), sheet = 1, -#' tableStyle = style, startRow = 4, startCol = i * 3 - 2 -#' ) -#' } -#' -#' for (i in 1:11) { -#' style <- paste0("TableStyleDark", i) -#' write_datatable(wb, -#' x = data.frame(style), sheet = 1, -#' tableStyle = style, startRow = 7, startCol = i * 3 - 2 -#' ) -#' } -#' @export -write_datatable <- function( - wb, - sheet, - x, - dims = rowcol_to_dims(start_row, start_col), - start_col = 1, - start_row = 1, - col_names = TRUE, - row_names = FALSE, - table_style = "TableStyleLight9", - table_name = NULL, - with_filter = TRUE, - sep = ", ", - first_column = FALSE, - last_column = FALSE, - banded_rows = TRUE, - banded_cols = FALSE, - apply_cell_style = TRUE, - remove_cell_style = FALSE, - na.strings = na_strings(), - inline_strings = TRUE, - ... -) { - - standardize_case_names(...) - - write_data_table( - wb = wb, - sheet = sheet, - x = x, - startCol = start_col, - startRow = start_row, - dims = dims, - array = FALSE, - colNames = col_names, - rowNames = row_names, - tableStyle = table_style, - tableName = table_name, - withFilter = with_filter, - sep = sep, - firstColumn = first_column, - lastColumn = last_column, - bandedRows = banded_rows, - bandedCols = banded_cols, - name = NULL, - data_table = TRUE, - applyCellStyle = apply_cell_style, - removeCellStyle = remove_cell_style, - na.strings = na.strings, - inline_strings = inline_strings - ) -} -# Internal helpers of `write_data*` -------------------------------------------- -#' function to add missing cells to cc and rows -#' -#' Create a cell in the workbook -#' -#' @param wb the workbook update -#' @param sheet_id the sheet to update -#' @param x the newly filled cc frame -#' @param rows the rows needed -#' @param cells_needed the cells needed -#' @param colNames has colNames (only in update_cell) -#' @param removeCellStyle remove the cell style (only in update_cell) -#' @param na.strings Value used for replacing `NA` values from `x`. Default -#' `na_strings()` uses the special `#N/A` value within the workbook. -#' @noRd -inner_update <- function( - wb, - sheet_id, - x, - rows, - cells_needed, - colNames = FALSE, - removeCellStyle = FALSE, - na.strings = na_strings() -) { - - # 1) pull sheet to modify from workbook; 2) modify it; 3) push it back - cc <- wb$worksheets[[sheet_id]]$sheet_data$cc - row_attr <- wb$worksheets[[sheet_id]]$sheet_data$row_attr - - # workbooks contain only entries for values currently present. - # if A1 is filled, B1 is not filled and C1 is filled the sheet will only - # contain fields A1 and C1. - cells_in_wb <- cc$r - rows_in_wb <- row_attr$r - - # check if there are rows not available - if (!all(rows %in% rows_in_wb)) { - # message("row(s) not in workbook") - - missing_rows <- rows[!rows %in% rows_in_wb] - - # new row_attr - row_attr_missing <- empty_row_attr(n = length(missing_rows)) - row_attr_missing$r <- missing_rows - - row_attr <- rbind(row_attr, row_attr_missing) - - # order - row_attr <- row_attr[order(as.numeric(row_attr$r)), ] - - wb$worksheets[[sheet_id]]$sheet_data$row_attr <- row_attr - # provide output - rows_in_wb <- row_attr$r - - } - - if (!all(cells_needed %in% cells_in_wb)) { - # message("cell(s) not in workbook") - - missing_cells <- cells_needed[!cells_needed %in% cells_in_wb] - - # create missing cells - cc_missing <- create_char_dataframe(names(cc), length(missing_cells)) - cc_missing$r <- missing_cells - cc_missing$row_r <- gsub("[[:upper:]]", "", cc_missing$r) - cc_missing$c_r <- gsub("[[:digit:]]", "", cc_missing$r) - - # assign to cc - cc <- rbind(cc, cc_missing) - - # order cc (not really necessary, will be done when saving) - cc <- cc[order(as.integer(cc[, "row_r"]), col2int(cc[, "c_r"])), ] - - # update dimensions (only required if new cols and rows are added) ------ - all_rows <- as.numeric(unique(cc$row_r)) - all_cols <- col2int(unique(cc$c_r)) - - min_cell <- trimws(paste0(int2col(min(all_cols, na.rm = TRUE)), min(all_rows, na.rm = TRUE))) - max_cell <- trimws(paste0(int2col(max(all_cols, na.rm = TRUE)), max(all_rows, na.rm = TRUE))) - - # i know, i know, i'm lazy - wb$worksheets[[sheet_id]]$dimension <- paste0("") - } - - if (is_na_strings(na.strings)) { - na.strings <- NULL - } - - if (removeCellStyle) { - cell_style <- "c_s" - } else { - cell_style <- NULL - } - - replacement <- c("r", cell_style, "c_t", "c_cm", "c_ph", "c_vm", "v", - "f", "f_t", "f_ref", "f_ca", "f_si", "is", "typ") - - sel <- match(x$r, cc$r) - cc[sel, replacement] <- x[replacement] - - # avoid missings in cc - if (any(is.na(cc))) - cc[is.na(cc)] <- "" - - # push everything back to workbook - wb$worksheets[[sheet_id]]$sheet_data$cc <- cc - - wb -} -# `write_data2()` ------------------------------------- -#' dummy function to write data -#' -#' @param wb workbook -#' @param sheet sheet -#' @param data data to export -#' @param name If not NULL, a named region is defined. -#' @param colNames include colnames? -#' @param rowNames include rownames? -#' @param startRow row to place it -#' @param startCol col to place it -#' @param applyCellStyle apply styles when writing on the sheet -#' @param removeCellStyle keep the cell style? -#' @param na.strings Value used for replacing `NA` values from `x`. Default -#' `na_strings()` uses the special `#N/A` value within the workbook. -#' @param data_table logical. if `TRUE` and `rowNames = TRUE`, do not write the cell containing `"_rowNames_"` -#' @param inline_strings write characters as inline strings -#' @details -#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame -#' contains this string, the output will be broken. -#' -#' @examples -#' # create a workbook and add some sheets -#' wb <- wb_workbook() -#' -#' wb$add_worksheet("sheet1") -#' write_data2(wb, "sheet1", mtcars, colNames = TRUE, rowNames = TRUE) -#' -#' wb$add_worksheet("sheet2") -#' write_data2(wb, "sheet2", cars, colNames = FALSE) -#' -#' wb$add_worksheet("sheet3") -#' write_data2(wb, "sheet3", letters) -#' -#' wb$add_worksheet("sheet4") -#' write_data2(wb, "sheet4", as.data.frame(Titanic), startRow = 2, startCol = 2) -#' @noRd -write_data2 <- function( - wb, - sheet, - data, - name = NULL, - colNames = TRUE, - rowNames = FALSE, - startRow = 1, - startCol = 1, - applyCellStyle = TRUE, - removeCellStyle = FALSE, - na.strings = na_strings(), - data_table = FALSE, - inline_strings = TRUE -) { - - is_data_frame <- FALSE - #### prepare the correct data formats for openxml - dc <- openxlsx2_type(data) - - # convert factor to character - if (any(dc == openxlsx2_celltype[["factor"]])) { - is_factor <- dc == openxlsx2_celltype[["factor"]] - fcts <- names(dc[is_factor]) - data[fcts] <- lapply(data[fcts], to_string) - # dc <- openxlsx2_type(data) - } - - hconvert_date1904 <- grepl('date1904="1"|date1904="true"', - stri_join(unlist(wb$workbook), collapse = ""), - ignore.case = TRUE) - - # TODO need to tell excel that we have a date, apply some kind of numFmt - data <- convert_to_excel_date(df = data, date1904 = hconvert_date1904) - - # backward compatible - if (!inherits(data, "data.frame") || inherits(data, "matrix")) { - data <- as.data.frame(data) - colNames <- FALSE - } - - if (inherits(data, "data.frame") || inherits(data, "matrix")) { - is_data_frame <- TRUE - - if (is.data.frame(data)) data <- as.data.frame(data) - - sel <- !dc %in% c(4, 5, 10) - data[sel] <- lapply(data[sel], as.character) - - # add rownames - if (rowNames) { - data <- cbind("_rowNames_" = rownames(data), data) - dc <- c(c("_rowNames_" = openxlsx2_celltype[["character"]]), dc) - } - - # add colnames - if (colNames) - data <- rbind(colnames(data), data) - } - - - sheetno <- wb_validate_sheet(wb, sheet) - # message("sheet no: ", sheetno) - - # create a data frame - if (!is_data_frame) - data <- as.data.frame(t(data)) - - data_nrow <- NROW(data) - data_ncol <- NCOL(data) - - endRow <- (startRow - 1) + data_nrow - endCol <- (startCol - 1) + data_ncol + endRow <- (startRow - 1) + data_nrow + endCol <- (startCol - 1) + data_ncol dims <- paste0( int2col(startCol), startRow, @@ -1098,9 +655,9 @@ write_data2 <- function( return(wb) } # `write_data_table()` --------------------------------------------------------- +# `write_data_table()` an internal driver function to `write_data` and `write_data_table` ---- #' Write to a worksheet as an Excel table #' -#' `write_data_table()` an internal driver function to `write_data` and `write_datatable` #' Write to a worksheet and format as an Excel table #' #' @param wb A Workbook object containing a worksheet. @@ -1128,6 +685,7 @@ write_data2 <- function( #' `na_strings()` uses the special `#N/A` value within the workbook. #' @param inline_strings optional write strings as inline strings #' @noRd +#' @keywords internal write_data_table <- function( wb, sheet, @@ -1284,156 +842,600 @@ write_data_table <- function( } ### End: Only in data -------------------------------------------------------- - if (data_table) { - overwrite_nrows <- 1L - check_tab_head_only <- FALSE - error_msg <- "Cannot overwrite existing table with another table" - } else { - 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." - } + if (data_table) { + overwrite_nrows <- 1L + check_tab_head_only <- FALSE + error_msg <- "Cannot overwrite existing table with another table" + } else { + 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." + } + + ## 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), + check_table_header_only = check_tab_head_only, + 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, + removeCellStyle = removeCellStyle, + na.strings = na.strings, + data_table = data_table, + inline_strings = inline_strings + ) + + ### Beg: Only in datatable --------------------------------------------------- + + # if rowNames is set, write_data2 has added a rowNames column to the sheet. + # This has to be handled in colnames and in ref. + if (data_table) { + + ## replace invalid XML characters + col_names <- replace_legal_chars(colnames(x)) + if (rowNames) col_names <- c("_rowNames_", col_names) + + ## Table name validation + if (is.null(tableName)) { + tableName <- paste0("Table", as.character(NROW(wb$tables) + 1L)) + } else { + tableName <- wb_validate_table_name(wb, tableName) + } + + ## 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) + if (!tolower(tableStyle) %in% tolower(validNames)) { + stop("Invalid table style.") + } else { + tableStyle <- grep(paste0("^", tableStyle, "$"), validNames, ignore.case = TRUE, value = TRUE) + } + + tableStyle <- tableStyle[!is.na(tableStyle)] + if (length(tableStyle) == 0) { + stop("Unknown table style.") + } + + ## If zero rows, append an empty row (prevent XML from corrupting) + if (nrow(x) == 0) { + x <- rbind(as.data.frame(x), matrix("", nrow = 1, ncol = nCol, dimnames = list(character(), colnames(x)))) + names(x) <- colNames + } + + ref1 <- paste0(int2col(startCol), startRow) + ref2 <- paste0(int2col(startCol + nCol - !rowNames), startRow + nRow) + 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, + showColumnStripes = bandedCols + ) + } + + ### End: Only in datatable --------------------------------------------------- + + return(wb) +} + +# `write_data()` --------------------------------------------------------------- +#' Write an object to a worksheet +#' +#' Write an object to worksheet with optional styling. +#' +#' Formulae written using write_formula to a Workbook object will not get picked up by read_xlsx(). +#' This is because only the formula is written and left to Excel to evaluate the formula when the file is opened in Excel. +#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame contains this string, the output will be broken. +#' +#' @param wb A Workbook object containing a worksheet. +#' @param sheet The worksheet to write to. Can be the worksheet index or name. +#' @param x Object to be written. For classes supported look at the examples. +#' @param start_col A vector specifying the starting column to write to. +#' @param start_row A vector specifying the starting row to write to. +#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" +#' @param array A bool if the function written is of type array +#' @param col_names If `TRUE`, column names of x are written. +#' @param row_names If `TRUE`, data.frame row names of x are written. +#' @param with_filter If `TRUE`, add filters to the column name row. NOTE can only have one filter per worksheet. +#' @param sep Only applies to list columns. The separator used to collapse list columns to a character vector e.g. sapply(x$list_column, paste, collapse = sep). +#' @param name If not NULL, a named region is defined. +#' @param apply_cell_style apply styles when writing on the sheet +#' @param remove_cell_style if writing into existing cells, should the cell style be removed? +#' @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 ... additional arguments +#' @seealso [write_datatable()] +#' @return invisible(0) +#' @examples +#' ## See formatting vignette for further examples. +#' +#' ## Options for default styling (These are the defaults) +#' options("openxlsx2.dateFormat" = "mm/dd/yyyy") +#' options("openxlsx2.datetimeFormat" = "yyyy-mm-dd hh:mm:ss") +#' options("openxlsx2.numFmt" = NULL) +#' +#' ############################################################################# +#' ## Create Workbook object and add worksheets +#' wb <- wb_workbook() +#' +#' ## Add worksheets +#' wb$add_worksheet("Cars") +#' wb$add_worksheet("Formula") +#' +#' x <- mtcars[1:6, ] +#' wb$add_data("Cars", x, startCol = 2, startRow = 3, rowNames = TRUE) +#' +#' ############################################################################# +#' ## Hyperlinks +#' ## - vectors/columns with class 'hyperlink' are written as hyperlinks' +#' +#' v <- rep("https://CRAN.R-project.org/", 4) +#' names(v) <- paste0("Hyperlink", 1:4) # Optional: names will be used as display text +#' class(v) <- "hyperlink" +#' wb$add_data("Cars", x = v, dims = c("B32")) +#' +#' ############################################################################# +#' ## Formulas +#' ## - vectors/columns with class 'formula' are written as formulas' +#' +#' df <- data.frame( +#' x = 1:3, y = 1:3, +#' z = paste(paste0("A", 1:3 + 1L), paste0("B", 1:3 + 1L), sep = "+"), +#' stringsAsFactors = FALSE +#' ) +#' +#' class(df$z) <- c(class(df$z), "formula") +#' +#' wb$add_data(sheet = "Formula", x = df) +#' +#' ############################################################################# +#' # update cell range and add mtcars +#' xlsxFile <- system.file("extdata", "openxlsx2_example.xlsx", package = "openxlsx2") +#' wb2 <- wb_load(xlsxFile) +#' +#' # read dataset with inlinestr +#' wb_to_df(wb2) +#' write_data(wb2, 1, mtcars, startCol = 4, startRow = 4) +#' wb_to_df(wb2) +#' @export +write_data <- function( + wb, + sheet, + x, + dims = rowcol_to_dims(start_row, start_col), + start_col = 1, + start_row = 1, + array = FALSE, + col_names = TRUE, + row_names = FALSE, + with_filter = FALSE, + sep = ", ", + name = NULL, + apply_cell_style = TRUE, + remove_cell_style = FALSE, + na.strings = na_strings(), + inline_strings = TRUE, + ... +) { + + standardize_case_names(...) - ## 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), - check_table_header_only = check_tab_head_only, - error_msg = error_msg + write_data_table( + wb = wb, + sheet = sheet, + x = x, + dims = dims, + startCol = start_col, + startRow = start_row, + array = array, + colNames = col_names, + rowNames = row_names, + tableStyle = NULL, + tableName = NULL, + withFilter = with_filter, + sep = sep, + firstColumn = FALSE, + lastColumn = FALSE, + bandedRows = FALSE, + bandedCols = FALSE, + name = name, + applyCellStyle = apply_cell_style, + removeCellStyle = remove_cell_style, + data_table = FALSE, + na.strings = na.strings, + inline_strings = inline_strings ) +} - ## 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, - removeCellStyle = removeCellStyle, - na.strings = na.strings, - data_table = data_table, - inline_strings = inline_strings - ) +# write_formula() ------------------------------------------- +#' Write a character vector as an Excel Formula +#' +#' Write a a character vector containing Excel formula to a worksheet. +#' +#' @details +#' Currently only the English version of functions are supported. Please don't use the local translation. +#' The examples below show a small list of possible formulas: +#' * SUM(B2:B4) +#' * AVERAGE(B2:B4) +#' * MIN(B2:B4) +#' * MAX(B2:B4) +#' * ... +#' +#' @param wb A Workbook object containing a worksheet. +#' @param sheet The worksheet to write to. Can be the worksheet index or name. +#' @param x A character vector. +#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" +#' @param start_col A vector specifying the starting column to write to. +#' @param start_row A vector specifying the starting row to write to. +#' @param array A bool if the function written is of type array +#' @param cm A bool if the function is of type cm (array with hidden curly braces) +#' @param apply_cell_style apply styles when writing on the sheet +#' @param remove_cell_style if writing into existing cells, should the cell style be removed? +#' @param ... additional arguments +#' @seealso [write_data()] +#' @examples +#' ## There are 3 ways to write a formula +#' +#' wb <- wb_workbook() +#' wb$add_worksheet("Sheet 1") +#' wb$add_data("Sheet 1", x = iris) +#' +#' ## SEE `int2col()` to convert int to Excel column label +#' +#' ## 1. - As a character vector using write_formula +#' +#' v <- c("SUM(A2:A151)", "AVERAGE(B2:B151)") ## skip header row +#' write_formula(wb, sheet = 1, x = v, startCol = 10, startRow = 2) +#' write_formula(wb, 1, x = "A2 + B2", startCol = 10, startRow = 10) +#' +#' +#' ## 2. - As a data.frame column with class "formula" using write_data +#' +#' df <- data.frame( +#' x = 1:3, +#' y = 1:3, +#' z = paste(paste0("A", 1:3 + 1L), paste0("B", 1:3 + 1L), sep = " + "), +#' z2 = sprintf("ADDRESS(1,%s)", 1:3), +#' stringsAsFactors = FALSE +#' ) +#' +#' class(df$z) <- c(class(df$z), "formula") +#' class(df$z2) <- c(class(df$z2), "formula") +#' +#' wb$add_worksheet("Sheet 2") +#' wb$add_data(sheet = 2, x = df) +#' +#' +#' ## 3. - As a vector with class "formula" using write_data +#' +#' v2 <- c("SUM(A2:A4)", "AVERAGE(B2:B4)", "MEDIAN(C2:C4)") +#' class(v2) <- c(class(v2), "formula") +#' +#' wb$add_data(sheet = 2, x = v2, startCol = 10, startRow = 2) +#' +#' +#' ## 4. - Writing internal hyperlinks +#' +#' wb <- wb_workbook() +#' wb$add_worksheet("Sheet1") +#' wb$add_worksheet("Sheet2") +#' write_formula(wb, "Sheet1", x = '=HYPERLINK("#Sheet2!B3", "Text to Display - Link to Sheet2")') +#' +#' +#' ## 5. - Writing array formulas +#' +#' set.seed(123) +#' df <- data.frame(C = rnorm(10), D = rnorm(10)) +#' +#' wb <- wb_workbook() +#' wb <- wb_add_worksheet(wb, "df") +#' +#' wb$add_data("df", df, startCol = "C") +#' +#' write_formula(wb, "df", startCol = "E", startRow = "2", +#' x = "SUM(C2:C11*D2:D11)", +#' array = TRUE) +#' @export +write_formula <- function( + wb, + sheet, + x, + dims = rowcol_to_dims(start_row, start_col), + start_col = 1, + start_row = 1, + array = FALSE, + cm = FALSE, + apply_cell_style = TRUE, + remove_cell_style = FALSE, + ... +) { - ### Beg: Only in datatable --------------------------------------------------- + standardize_case_names(...) - # if rowNames is set, write_data2 has added a rowNames column to the sheet. - # This has to be handled in colnames and in ref. - if (data_table) { + assert_class(x, "character") + # remove xml encoding and reapply it afterwards. until v0.3 encoding was not enforced + x <- replaceXMLEntities(x) + x <- vapply(x, function(val) xml_value(xml_node_create("fml", val, escapes = TRUE), "fml"), NA_character_) + dfx <- data.frame("X" = x, stringsAsFactors = FALSE) - ## replace invalid XML characters - col_names <- replace_legal_chars(colnames(x)) - if (rowNames) col_names <- c("_rowNames_", col_names) + formula <- "formula" + if (array) formula <- "array_formula" + if (cm) { + # need to set cell metadata in wb$metadata + if (is.null(wb$metadata)) { - ## Table name validation - if (is.null(tableName)) { - tableName <- paste0("Table", as.character(NROW(wb$tables) + 1L)) - } else { - tableName <- wb_validate_table_name(wb, tableName) - } + wb$append("Content_Types", "") - ## 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) - if (!tolower(tableStyle) %in% tolower(validNames)) { - stop("Invalid table style.") - } else { - tableStyle <- grep(paste0("^", tableStyle, "$"), validNames, ignore.case = TRUE, value = TRUE) + wb$metadata <- # danger danger no clue what this means! + xml_node_create( + "metadata", + xml_attributes = c( + xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "xmlns:xda" = "http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray" + ), + xml_children = read_xml( + " + + + + + + + + + + + , + ", + pointer = FALSE + ) + ) } - tableStyle <- tableStyle[!is.na(tableStyle)] - if (length(tableStyle) == 0) { - stop("Unknown table style.") - } + ## TODO Not sure if there are more cases + # add new cell metadata record + cM <- xml_node(wb$metadata, "metadata", "cellMetadata") + cM <- xml_add_child(cM, xml_child = "") - ## If zero rows, append an empty row (prevent XML from corrupting) - if (nrow(x) == 0) { - x <- rbind(as.data.frame(x), matrix("", nrow = 1, ncol = nCol, dimnames = list(character(), colnames(x)))) - names(x) <- colNames - } + # we need to update count + cnt <- as_xml_attr(length(xml_node(cM, "cellMetadata", "bk"))) + cM <- xml_attr_mod(cM, xml_attributes = c(count = cnt)) + + # remove current cellMetadata update new + wb$metadata <- xml_rm_child(wb$metadata, "cellMetadata") + wb$metadata <- xml_add_child(wb$metadata, cM) + + attr(dfx, "c_cm") <- cnt + formula <- "cm_formula" + } - ref1 <- paste0(int2col(startCol), startRow) - ref2 <- paste0(int2col(startCol + nCol - !rowNames), startRow + nRow) - ref <- paste(ref1, ref2, sep = ":") + class(dfx$X) <- c("character", formula) + if (!is.null(dims)) { + if (array || cm) { + attr(dfx, "f_ref") <- dims + } + } - ## 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, - showColumnStripes = bandedCols - ) + if (any(grepl("=([\\s]*?)HYPERLINK\\(", x, perl = TRUE))) { + class(dfx$X) <- c("character", "formula", "hyperlink") } - ### End: Only in datatable --------------------------------------------------- + write_data( + wb = wb, + sheet = sheet, + x = dfx, + start_col = start_col, + start_row = start_row, + dims = dims, + array = array, + col_names = FALSE, + row_names = FALSE, + apply_cell_style = apply_cell_style, + remove_cell_style = remove_cell_style + ) - return(wb) } -# Cell(s) helpers -------------------------------------------- -#' Initialize data cell(s) + +# `write_datatable()` ---------------------- +#' Write to a worksheet as an Excel table #' -#' Create a cell in the workbook +#' Write to a worksheet and format as an Excel table #' -#' @param wb the workbook you want to update -#' @param sheet the sheet you want to update -#' @param new_cells the cell you want to update in Excel connotation e.g. "A1" +#' @param wb A Workbook object containing a worksheet. +#' @param sheet The worksheet to write to. Can be the worksheet index or name. +#' @param x A data frame. +#' @param start_col A vector specifying the starting column to write df +#' @param start_row A vector specifying the starting row to write df +#' @param dims Spreadsheet dimensions that will determine startCol and startRow: "A1", "A1:B2", "A:B" +#' @param col_names If `TRUE`, column names of x are written. +#' @param row_names If `TRUE`, row names of x are written. +#' @param table_style Any excel table style name or "none" (see "formatting" vignette). +#' @param table_name name of table in workbook. The table name must be unique. +#' @param with_filter If `TRUE`, columns with have filters in the first row. +#' @param sep Only applies to list columns. The separator used to collapse list columns to a character vector e.g. sapply(x$list_column, paste, collapse = sep). +#' \cr\cr +#' \cr**The below options correspond to Excel table options:** +#' \cr +#' \if{html}{\figure{tableoptions.png}{options: width="40\%" alt="Figure: table_options.png"}} +#' \if{latex}{\figure{tableoptions.pdf}{options: width=7cm}} #' -#' @noRd -initialize_cell <- function(wb, sheet, new_cells) { - - sheet_id <- wb$validate_sheet(sheet) - - # create artificial cc for the missing cells - x <- empty_sheet_data_cc(n = length(new_cells)) - x$r <- new_cells - x$row_r <- gsub("[[:upper:]]", "", new_cells) - x$c_r <- gsub("[[:digit:]]", "", new_cells) - - rows <- x$row_r - cells_needed <- new_cells - - inner_update(wb, sheet_id, x, rows, cells_needed) -} - -#' Replace data cell(s) +#' @param first_column logical. If TRUE, the first column is bold +#' @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 apply_cell_style apply styles when writing on the sheet +#' @param remove_cell_style if writing into existing cells, should the cell style be removed? +#' @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 ... additional arguments +#' @details columns of x with class Date/POSIXt, currency, accounting, +#' hyperlink, percentage are automatically styled as dates, currency, accounting, +#' hyperlinks, percentages respectively. +#' The string `"_openxlsx_NA"` is reserved for `openxlsx2`. If the data frame +#' contains this string, the output will be broken. +#' @seealso +#' [wb_add_worksheet()] +#' [write_data()] +#' [wb_remove_tables()] +#' [wb_get_tables()] +#' @examples +#' ## see package vignettes for further examples. #' -#' Minimal invasive update of cell(s) inside of imported workbooks. +#' ##################################################################################### +#' ## Create Workbook object and add worksheets +#' wb <- wb_workbook() +#' wb$add_worksheet("S1") +#' wb$add_worksheet("S2") +#' wb$add_worksheet("S3") #' -#' @param x cc dataframe of the updated cells -#' @param wb the workbook you want to update -#' @param sheet the sheet you want to update -#' @param cell the cell you want to update in Excel connotation e.g. "A1" -#' @param colNames if TRUE colNames are passed down -#' @param removeCellStyle keep the cell style? -#' @param na.strings optional na.strings argument. if missing #N/A is used. If NULL no cell value is written, if character or numeric this is written (even if NA is part of numeric data) +#' ##################################################################################### +#' ## -- write data.frame as an Excel table with column filters +#' ## -- default table style is "TableStyleMedium2" #' -#' @noRd -update_cell <- function(x, wb, sheet, cell, colNames = FALSE, - removeCellStyle = FALSE, na.strings) { - - if (missing(na.strings)) - na.strings <- substitute() - - sheet_id <- wb$validate_sheet(sheet) - - dims <- dims_to_dataframe(cell, fill = TRUE) - rows <- rownames(dims) +#' wb$add_data_table("S1", x = iris) +#' +#' wb$add_data_table("S2", +#' x = mtcars, dims = c("B3"), rowNames = TRUE, +#' tableStyle = "TableStyleLight9" +#' ) +#' +#' df <- data.frame( +#' "Date" = Sys.Date() - 0:19, +#' "T" = TRUE, "F" = FALSE, +#' "Time" = Sys.time() - 0:19 * 60 * 60, +#' "Cash" = paste("$", 1:20), "Cash2" = 31:50, +#' "hLink" = "https://CRAN.R-project.org/", +#' "Percentage" = seq(0, 1, length.out = 20), +#' "TinyNumbers" = runif(20) / 1E9, stringsAsFactors = FALSE +#' ) +#' +#' ## openxlsx will apply default Excel styling for these classes +#' class(df$Cash) <- c(class(df$Cash), "currency") +#' class(df$Cash2) <- c(class(df$Cash2), "accounting") +#' class(df$hLink) <- "hyperlink" +#' class(df$Percentage) <- c(class(df$Percentage), "percentage") +#' class(df$TinyNumbers) <- c(class(df$TinyNumbers), "scientific") +#' +#' wb$add_data_table("S3", x = df, startRow = 4, rowNames = TRUE, tableStyle = "TableStyleMedium9") +#' +#' ##################################################################################### +#' ## Additional Header Styling and remove column filters +#' +#' write_datatable(wb, +#' sheet = 1, +#' x = iris, +#' startCol = 7, +#' withFilter = FALSE, +#' firstColumn = TRUE, +#' lastColumn = TRUE, +#' bandedRows = TRUE, +#' bandedCols = TRUE +#' ) +#' +#' ##################################################################################### +#' ## Pre-defined table styles gallery +#' +#' wb <- wb_workbook(paste0("tableStylesGallery.xlsx")) +#' wb$add_worksheet("Style Samples") +#' for (i in 1:21) { +#' style <- paste0("TableStyleLight", i) +#' write_datatable(wb, +#' x = data.frame(style), sheet = 1, +#' tableStyle = style, startRow = 1, startCol = i * 3 - 2 +#' ) +#' } +#' +#' for (i in 1:28) { +#' style <- paste0("TableStyleMedium", i) +#' write_datatable(wb, +#' x = data.frame(style), sheet = 1, +#' tableStyle = style, startRow = 4, startCol = i * 3 - 2 +#' ) +#' } +#' +#' for (i in 1:11) { +#' style <- paste0("TableStyleDark", i) +#' write_datatable(wb, +#' x = data.frame(style), sheet = 1, +#' tableStyle = style, startRow = 7, startCol = i * 3 - 2 +#' ) +#' } +#' @export +write_datatable <- function( + wb, + sheet, + x, + dims = rowcol_to_dims(start_row, start_col), + start_col = 1, + start_row = 1, + col_names = TRUE, + row_names = FALSE, + table_style = "TableStyleLight9", + table_name = NULL, + with_filter = TRUE, + sep = ", ", + first_column = FALSE, + last_column = FALSE, + banded_rows = TRUE, + banded_cols = FALSE, + apply_cell_style = TRUE, + remove_cell_style = FALSE, + na.strings = na_strings(), + inline_strings = TRUE, + ... +) { - cells_needed <- unname(unlist(dims)) + standardize_case_names(...) - inner_update(wb, sheet_id, x, rows, cells_needed, colNames, removeCellStyle, na.strings) + write_data_table( + wb = wb, + sheet = sheet, + x = x, + startCol = start_col, + startRow = start_row, + dims = dims, + array = FALSE, + colNames = col_names, + rowNames = row_names, + tableStyle = table_style, + tableName = table_name, + withFilter = with_filter, + sep = sep, + firstColumn = first_column, + lastColumn = last_column, + bandedRows = banded_rows, + bandedCols = banded_cols, + name = NULL, + data_table = TRUE, + applyCellStyle = apply_cell_style, + removeCellStyle = remove_cell_style, + na.strings = na.strings, + inline_strings = inline_strings + ) }