diff --git a/R/pagination_utils.R b/R/pagination_utils.R index c53a1f5..28565e5 100644 --- a/R/pagination_utils.R +++ b/R/pagination_utils.R @@ -18,6 +18,10 @@ #' Providing a value (e.g., `"|"`, `""`, or `"—"`) automatically enables filling and uses that #' value for all cells in the empty rows. This helps maintain consistent vertical positioning #' across all pages. The target row count is the maximum page size across all pages. +#' Use `NA` to fill with missing values while preserving original column types. +#' If you need consistent types across all pages, use `NA`. +#' Because `fill_empty` is a character placeholder, pages are coerced to character +#' columns when filling is applied (except when `fill_empty = NA`). #' #' @return A list of data frames, one for each page. When `split_by` is used, the list #' is named with the group values. If a group spans multiple pages (when combined with @@ -177,8 +181,13 @@ paginate_table <- function( } } - if (!is.null(fill_empty) && (!is.character(fill_empty) || length(fill_empty) != 1)) { - stop("`fill_empty` must be NULL or a single character string.") + if (!is.null(fill_empty)) { + fill_is_single_na <- length(fill_empty) == 1 && isTRUE(is.na(fill_empty)) + fill_is_single_character <- is.character(fill_empty) && length(fill_empty) == 1 + + if (!(fill_is_single_na || fill_is_single_character)) { + stop("`fill_empty` must be NULL, NA, or a single character string.") + } } # Split data based on method @@ -231,12 +240,28 @@ paginate_table <- function( if (page_nrows < rows_per_page) { rows_difference <- rows_per_page - page_nrows - # Create empty rows with specified fill value - empty_df <- data.frame( - matrix(fill_empty, nrow = rows_difference, ncol = ncol(data)), - stringsAsFactors = FALSE - ) - colnames(empty_df) <- colnames(data) + if (isTRUE(is.na(fill_empty))) { + # Preserve original column classes when filling with missing values. + empty_df <- as.data.frame( + lapply(page, function(col) rep(col[NA_integer_], rows_difference)), + stringsAsFactors = FALSE, + check.names = FALSE + ) + } else { + # Character placeholder fill uses character columns across all fields. + page <- as.data.frame( + lapply(page, as.character), + stringsAsFactors = FALSE, + check.names = FALSE + ) + + empty_df <- data.frame( + matrix(fill_empty, nrow = rows_difference, ncol = ncol(data)), + stringsAsFactors = FALSE, + check.names = FALSE + ) + colnames(empty_df) <- colnames(data) + } # Append to page page <- rbind(page, empty_df) diff --git a/man/paginate_table.Rd b/man/paginate_table.Rd index 04ffefe..d165a2f 100644 --- a/man/paginate_table.Rd +++ b/man/paginate_table.Rd @@ -23,7 +23,11 @@ At least one of \code{rows_per_page} or \code{split_by} must be provided.} with empty rows to match the target row count. Default is \code{NULL} (no filling). Providing a value (e.g., \code{"|"}, \code{""}, or \code{"—"}) automatically enables filling and uses that value for all cells in the empty rows. This helps maintain consistent vertical positioning -across all pages. The target row count is the maximum page size across all pages.} +across all pages. The target row count is the maximum page size across all pages. +Use \code{NA} to fill with missing values while preserving original column types. +If you need consistent types across all pages, use \code{NA}. +Because \code{fill_empty} is a character placeholder, pages are coerced to character +columns when filling is applied (except when \code{fill_empty = NA}).} } \value{ A list of data frames, one for each page. When \code{split_by} is used, the list diff --git a/tests/testthat/test_paginate_table.R b/tests/testthat/test_paginate_table.R index 1249ec2..9b65a6a 100644 --- a/tests/testthat/test_paginate_table.R +++ b/tests/testthat/test_paginate_table.R @@ -60,12 +60,12 @@ test_that("paginate_table validates inputs correctly", { expect_error( paginate_table(mtcars, 10, fill_empty = TRUE), - "`fill_empty` must be NULL or a single character string" + "`fill_empty` must be NULL, NA, or a single character string" ) expect_error( paginate_table(mtcars, 10, fill_empty = c("|", "-")), - "`fill_empty` must be NULL or a single character string" + "`fill_empty` must be NULL, NA, or a single character string" ) expect_error( @@ -198,3 +198,34 @@ test_that("paginate_table split_by handles single group", { expect_equal(length(pages), 1) expect_equal(nrow(pages[[1]]), nrow(df_single)) }) + +test_that("paginate_table fill_empty works with Date columns", { + df <- data.frame( + aval = runif(40, 10, 50), + dt = as.Date(rep(c("2025-01-02", "2025-02-03", "2025-03-04", "2025-04-05"), 10)) + ) + + expect_no_error({ + pages <- paginate_table(data = df, rows_per_page = 7, fill_empty = " ") + expect_true(length(pages) > 0) + expect_equal(nrow(pages[[length(pages)]]), 7) + expect_type(pages[[length(pages)]]$dt, "character") + expect_equal(pages[[length(pages)]][7, "dt"], " ") + }) +}) + +test_that("paginate_table supports fill_empty = NA preserving column types", { + df <- data.frame( + aval = runif(40, 10, 50), + dt = as.Date(rep(c("2025-01-02", "2025-02-03", "2025-03-04", "2025-04-05"), 10)) + ) + + pages <- paginate_table(data = df, rows_per_page = 7, fill_empty = NA) + last_page <- pages[[length(pages)]] + + expect_equal(nrow(last_page), 7) + expect_type(last_page$aval, "double") + expect_s3_class(last_page$dt, "Date") + expect_true(anyNA(last_page[7, "aval"])) + expect_true(anyNA(last_page[7, "dt"])) +})