| 1 | ## #' Page Dimensions | |
| 2 | ## #' | |
| 3 | ## #' Dimensions for mapping page dimensions to text dimensions | |
| 4 | ## #' @references https://www.ietf.org/rfc/rfc0678.txt | |
| 5 | ## #' @export | |
| 6 | ## #' @rdname pagedims | |
| 7 | ## lpi_vert <- 6 | |
| 8 | ## #' @export | |
| 9 | ## #' @rdname pagedims | |
| 10 | ## cpi_horiz <- 10 | |
| 11 | ## #' @export | |
| 12 | ## #' @rdname pagedims | |
| 13 | ## horiz_margin_chars <- 13 | |
| 14 | ## #' @export | |
| 15 | ## #' @rdname pagedims | |
| 16 | ## horiz_margin_inches <- horiz_margin_chars / cpi_horiz | |
| 17 | ## #' @export | |
| 18 | ## #' @rdname pagedims | |
| 19 | ## vert_margin_lines <- 6 | |
| 20 | ## #' @export | |
| 21 | ## #' @rdname pagedims | |
| 22 | ## vert_margin_inches <- vert_margin_lines / lpi_vert | |
| 23 | ||
| 24 | ## #' Physical Page dimensions to chars x lines | |
| 25 | ## #' | |
| 26 | ## #' Calculate number of lines long and characters wide a page size is, | |
| 27 | ## #' after excluding margins | |
| 28 | ## #' @export | |
| 29 | ## #' @examples | |
| 30 | ## #' phys_page_to_lc() | |
| 31 | ## phys_page_to_lc <- function(width = 8.5, len = 11, | |
| 32 | ## h_margin = horiz_margin_inches, | |
| 33 | ##                             v_margin = vert_margin_inches) { | |
| 34 | ## lgl_width <- width - h_margin | |
| 35 | ## lgl_len <- len - v_margin | |
| 36 | ## c(chars_wide = floor(lgl_width * cpi_horiz), | |
| 37 | ## lines_long = floor(lgl_len * lpi_vert)) | |
| 38 | ## } | |
| 39 | ||
| 40 | #' Pagination | |
| 41 | #' | |
| 42 | #' @section Pagination Algorithm: | |
| 43 | #' | |
| 44 | #' Pagination is performed independently in the vertical and horizontal | |
| 45 | #' directions based solely on a *pagination data frame*, which includes the | |
| 46 | #' following information for each row/column: | |
| 47 | #' | |
| 48 | #' - Number of lines/characters rendering the row will take **after | |
| 49 | #' word-wrapping** (`self_extent`) | |
| 50 | #' - The indices (`reprint_inds`) and number of lines (`par_extent`) | |
| 51 | #' of the rows which act as **context** for the row | |
| 52 | #' - The row's number of siblings and position within its siblings | |
| 53 | #' | |
| 54 | #' Given `lpp` (`cpp`) is already adjusted for rendered elements which | |
| 55 | #' are not rows/columns and a data frame of pagination information, | |
| 56 | #' pagination is performed via the following algorithm with `start = 1`. | |
| 57 | #' | |
| 58 | #' Core Pagination Algorithm: | |
| 59 | #' | |
| 60 | #' 1. Initial guess for pagination position is `start + lpp` (`start + cpp`) | |
| 61 | #' 2. While the guess is not a valid pagination position, and `guess > start`, | |
| 62 | #' decrement guess and repeat. | |
| 63 | #' - An error is thrown if all possible pagination positions between | |
| 64 | #' `start` and `start + lpp` (`start + cpp`) would be `< start` | |
| 65 | #' after decrementing | |
| 66 | #' 3. Retain pagination index | |
| 67 | #' 4. If pagination point was less than `NROW(tt)` (`ncol(tt)`), set | |
| 68 | #' `start` to `pos + 1`, and repeat steps (1) - (4). | |
| 69 | #' | |
| 70 | #' Validating Pagination Position: | |
| 71 | #' | |
| 72 | #' Given an (already adjusted) `lpp` or `cpp` value, a pagination is invalid if: | |
| 73 | #' | |
| 74 | #' - The rows/columns on the page would take more than (adjusted) `lpp` lines/`cpp` | |
| 75 | #' characters to render **including**: | |
| 76 | #' - word-wrapping | |
| 77 | #' - (vertical only) context repetition | |
| 78 | #' - (vertical only) footnote messages and/or section divider lines | |
| 79 | #' take up too many lines after rendering rows | |
| 80 | #' - (vertical only) row is a label or content (row-group summary) row | |
| 81 | #' - (vertical only) row at the pagination point has siblings, and | |
| 82 | #' it has less than `min_siblings` preceding or following siblings | |
| 83 | #' - pagination would occur within a sub-table listed in `nosplitin` | |
| 84 | #' | |
| 85 | #' @name pagination_algo | |
| 86 | NULL | |
| 87 | ||
| 88 | #' Create a row of a pagination data frame | |
| 89 | #' | |
| 90 | #' @inheritParams open_font_dev | |
| 91 | #' @param nm (`string`)\cr name. | |
| 92 | #' @param lab (`string`)\cr label. | |
| 93 | #' @param rnum (`numeric(1)`)\cr absolute row number. | |
| 94 | #' @param pth (`character` or `NULL`)\cr path within larger table. | |
| 95 | #' @param sibpos (`integer(1)`)\cr position among sibling rows. | |
| 96 | #' @param nsibs (`integer(1)`)\cr number of siblings (including self). | |
| 97 | #' @param extent (`numeric(1)`)\cr number of lines required to print the row. | |
| 98 | #' @param colwidths (`numeric`)\cr column widths. | |
| 99 | #' @param repext (`integer(1)`)\cr number of lines required to reprint all context for this row if it appears directly | |
| 100 | #' after pagination. | |
| 101 | #' @param repind (`integer`)\cr vector of row numbers to be reprinted if this row appears directly after pagination. | |
| 102 | #' @param indent (`integer`)\cr indent. | |
| 103 | #' @param rclass (`string`)\cr class of row object. | |
| 104 | #' @param nrowrefs (`integer(1)`)\cr number of row referential footnotes for this row. | |
| 105 | #' @param ncellrefs (`integer(1)`)\cr number of cell referential footnotes for the cells in this row. | |
| 106 | #' @param nreflines (`integer(1)`)\cr total number of lines required by all referential footnotes. | |
| 107 | #' @param force_page (`flag`)\cr currently ignored. | |
| 108 | #' @param page_title (`flag`)\cr currently ignored. | |
| 109 | #' @param trailing_sep (`string`)\cr the string to use as a separator below this row during printing. | |
| 110 | #' If `NA_character_`, no separator is used. | |
| 111 | #' @param row (`ANY`)\cr object representing the row, which is used for default values of `nm`, `lab`, | |
| 112 | #' `extent`, and `rclass` if provided. Must have methods for `obj_name`, `obj_label`, and `nlines`, to retrieve | |
| 113 | #' default values of `nm`, `lab`, and `extent`, respectively. | |
| 114 | #' | |
| 115 | #' @return A single row `data.frame` with the appropriate columns for a pagination info data frame. | |
| 116 | #' | |
| 117 | #' @export | |
| 118 | pagdfrow <- function(row, | |
| 119 | nm = obj_name(row), | |
| 120 | lab = obj_label(row), | |
| 121 | rnum, | |
| 122 | pth, | |
| 123 | sibpos = NA_integer_, | |
| 124 | nsibs = NA_integer_, | |
| 125 | extent = nlines(row, colwidths, fontspec = fontspec), | |
| 126 | colwidths = NULL, | |
| 127 | repext = 0L, | |
| 128 | repind = integer(), | |
| 129 | indent = 0L, | |
| 130 | rclass = class(row), | |
| 131 | nrowrefs = 0L, | |
| 132 | ncellrefs = 0L, | |
| 133 | nreflines = 0L, | |
| 134 | # ref_df = .make_ref_df(NULL, NULL), | |
| 135 | force_page = FALSE, | |
| 136 | page_title = NA_character_, | |
| 137 | trailing_sep = NA_character_, | |
| 138 |                      fontspec) { | |
| 139 | 1447x | data.frame( | 
| 140 | 1447x | label = lab, | 
| 141 | 1447x | name = nm, | 
| 142 | 1447x | abs_rownumber = rnum, | 
| 143 | 1447x | path = I(list(pth)), | 
| 144 | 1447x | pos_in_siblings = sibpos, | 
| 145 | 1447x | n_siblings = nsibs, | 
| 146 | 1447x | self_extent = extent, | 
| 147 | 1447x | par_extent = repext, | 
| 148 | 1447x | reprint_inds = I(rep(list(unlist(repind)), length.out = length(nm))), | 
| 149 | 1447x | node_class = rclass, | 
| 150 | 1447x | indent = max(0L, indent), | 
| 151 | 1447x | nrowrefs = nrowrefs, | 
| 152 | 1447x | ncellrefs = ncellrefs, | 
| 153 | 1447x | nreflines = nreflines, | 
| 154 | # ref_info_df = I(list(ref_df)), | |
| 155 | 1447x | force_page = force_page, | 
| 156 | 1447x | page_title = page_title, | 
| 157 | 1447x | trailing_sep = trailing_sep, | 
| 158 | 1447x | stringsAsFactors = FALSE, | 
| 159 | 1447x | row.names = NULL, | 
| 160 | 1447x | check.names = FALSE, | 
| 161 | 1447x | fix.empty.names = FALSE | 
| 162 | ) | |
| 163 | } | |
| 164 | ||
| 165 | calc_ref_nlines_df <- function(pagdf) { | |
| 166 | ## XXX XXX XXX this is dangerous and wrong!!! | |
| 167 | 628x |   if (is.null(pagdf$ref_info_df) && sum(pagdf$nreflines) == 0) { | 
| 168 | 221x | return(ref_df_row()[0, ]) | 
| 169 | } | |
| 170 | 407x | refdf <- do.call(rbind.data.frame, pagdf$ref_info_df) | 
| 171 | 407x |   if (NROW(refdf) == 0) { | 
| 172 | 375x | return(ref_df_row()[0, ]) | 
| 173 | } | |
| 174 | 32x | unqsyms <- !duplicated(refdf$symbol) | 
| 175 | 32x | refdf[unqsyms, , drop = FALSE] | 
| 176 | } | |
| 177 | ||
| 178 | build_fail_msg <- function(row, lines, raw_rowlines, | |
| 179 | allowed_lines, lpp, decoration_lines, | |
| 180 | start, guess, rep_ext, n_reprint, | |
| 181 |                            reflines, n_refs, sectlines) { | |
| 182 | 254x |   if (row) { | 
| 183 | 104x | spacetype <- "lines" | 
| 184 | 104x | spacetype_abr <- "lns" | 
| 185 | 104x | structtype_abr <- "rws" | 
| 186 | 104x | sprintf( | 
| 187 | 104x | paste0( | 
| 188 | 104x | " FAIL: rows selected for pagination require %d %s while only %d are available from ", | 
| 189 | 104x | "lpp = %d and %d header/footers lines.\n", | 
| 190 | 104x | " details: [raw: %d %s (%d %s), rep. context: %d %s (%d %s), ", | 
| 191 | 104x | "refs: %d %s (%d) sect. divs: %d %s]." | 
| 192 | ), | |
| 193 | 104x | lines, | 
| 194 | 104x | spacetype, | 
| 195 | 104x | allowed_lines, | 
| 196 | 104x | lpp, | 
| 197 | 104x | decoration_lines, # header + footers | 
| 198 | 104x | raw_rowlines, | 
| 199 | 104x | spacetype_abr, | 
| 200 | 104x | guess - start + 1, # because it includes both start and guess | 
| 201 | 104x | structtype_abr, | 
| 202 | 104x | rep_ext, | 
| 203 | 104x | spacetype_abr, | 
| 204 | 104x | n_reprint, | 
| 205 | 104x | structtype_abr, | 
| 206 | 104x | reflines, | 
| 207 | 104x | spacetype_abr, | 
| 208 | 104x | n_refs, | 
| 209 | 104x | sectlines, | 
| 210 | 104x | spacetype_abr | 
| 211 | ) | |
| 212 |   } else { ## !row | |
| 213 | 150x | spacetype <- "chars" | 
| 214 | 150x | spacetype_abr <- "chars" | 
| 215 | 150x | structtype_abr <- "cols" | 
| 216 | 150x | raw_ncol <- guess - start + 1 | 
| 217 | 150x | tot_ncol <- raw_ncol + n_reprint | 
| 218 | 150x | rep_ext <- rep_ext | 
| 219 | 150x | sprintf( | 
| 220 | 150x | paste0( | 
| 221 | 150x | " FAIL: selected %d columns require %d %s, while only %d are available. \n", | 
| 222 | 150x | " details: [raw: %d %s (%d %s), rep. cols: %d %s (%d %s), tot. colgap: %d %s]." | 
| 223 | ), | |
| 224 | 150x | guess - start + 1, | 
| 225 | 150x | lines + rep_ext + sectlines, | 
| 226 | 150x | spacetype, | 
| 227 | 150x | lpp, | 
| 228 | 150x | lines, | 
| 229 | 150x | spacetype_abr, | 
| 230 | 150x | raw_ncol, | 
| 231 | 150x | structtype_abr, | 
| 232 | 150x | rep_ext, | 
| 233 | 150x | spacetype_abr, | 
| 234 | 150x | n_reprint, | 
| 235 | 150x | structtype_abr, | 
| 236 | 150x | sectlines, | 
| 237 | 150x | spacetype | 
| 238 | ) | |
| 239 | } | |
| 240 | } | |
| 241 | ||
| 242 | valid_pag <- function(pagdf, | |
| 243 | guess, | |
| 244 | start, | |
| 245 | rlpp, | |
| 246 | lpp, # for informational purposes only | |
| 247 | context_lpp, # for informational purposes only (headers/footers) | |
| 248 | min_sibs, | |
| 249 | nosplit = NULL, | |
| 250 | div_height = 1L, | |
| 251 | verbose = FALSE, | |
| 252 | row = TRUE, | |
| 253 | have_col_fnotes = FALSE, | |
| 254 | col_gap, | |
| 255 |                       has_rowlabels) { | |
| 256 | # FALSE output from this function means that another guess is taken till success or failure | |
| 257 | 628x | rw <- pagdf[guess, ] | 
| 258 | ||
| 259 | 628x |   if (verbose) { | 
| 260 | 382x | message( | 
| 261 | 382x | "-> Attempting pagination between ", start, " and ", guess, " ", | 
| 262 | 382x | paste(ifelse(row, "row", "column")) | 
| 263 | ) | |
| 264 | } | |
| 265 | ||
| 266 | # Fix for counting the right number of lines when there is wrapping on a keycols | |
| 267 | 628x |   if (.is_listing_mf(pagdf) && !is.null(pagdf$self_extent_page_break)) { | 
| 268 | 28x | pagdf$self_extent[start] <- pagdf$self_extent_page_break[start] | 
| 269 | } | |
| 270 | ||
| 271 | 628x | raw_rowlines <- sum(pagdf[start:guess, "self_extent"] - pagdf[start:guess, "nreflines"]) | 
| 272 | ||
| 273 | 628x | refdf_ii <- calc_ref_nlines_df(pagdf[start:guess, ]) | 
| 274 | 628x | reflines <- if (row) sum(refdf_ii$nlines, 0L) else 0L | 
| 275 | 628x |   if (reflines > 0 && !have_col_fnotes) { | 
| 276 | 32x | reflines <- reflines + div_height + 1L | 
| 277 | } | |
| 278 | ||
| 279 | ||
| 280 | 628x | rowlines <- raw_rowlines + reflines ## sum(pagdf[start:guess, "self_extent"]) - reflines | 
| 281 | ## self extent includes reflines | |
| 282 | ## self extent does ***not*** currently include trailing sep for rows | |
| 283 | ## self extent does ***not*** currently include col_gap for columns | |
| 284 | ## we don't include the trailing_sep for guess because if we paginate here it won't be printed | |
| 285 | 628x | ncols <- 0L | 
| 286 | 628x |   if (row) { | 
| 287 | 257x | sectlines <- if (start == guess) 0L else sum(!is.na(pagdf[start:(guess - 1), "trailing_sep"])) | 
| 288 |   } else { ## columns | |
| 289 | 371x | ncols <- guess - start + 1 + length(pagdf$reprint_inds[[start]]) ## +1 because its inclusive, 5-6 is 2 columns | 
| 290 | 371x | sectlines <- col_gap * (ncols - as.integer(!has_rowlabels)) ## -1 if no row labels | 
| 291 | } | |
| 292 | 628x | lines <- rowlines + sectlines | 
| 293 | 628x | rep_ext <- pagdf$par_extent[start] | 
| 294 | 628x |   if (lines > rlpp) { | 
| 295 | 382x |     if (verbose) { | 
| 296 | 254x | structtype <- ifelse(row, "rows", "columns") | 
| 297 | 254x | structtype_abr <- ifelse(row, "rows", "cols") | 
| 298 | 254x | spacetype <- ifelse(row, "lines", "chars") | 
| 299 | 254x | spacetype_abr <- ifelse(row, "lns", "chrs") | 
| 300 | 254x | msg <- build_fail_msg( | 
| 301 | 254x | row, lines, raw_rowlines, | 
| 302 | 254x | allowed_lines = rlpp, lpp = lpp, decoration_lines = context_lpp, | 
| 303 | 254x | start, guess, rep_ext, length(pagdf$reprint_inds[[start]]), | 
| 304 | 254x | reflines, NROW(refdf_ii), sectlines | 
| 305 | ) | |
| 306 | 254x | message(msg) | 
| 307 | } | |
| 308 | 382x | return(FALSE) | 
| 309 | } | |
| 310 | ||
| 311 | # Special cases: is it a label or content row? | |
| 312 | 246x |   if (rw[["node_class"]] %in% c("LabelRow", "ContentRow")) { | 
| 313 | # check if it has children; if no children then valid | |
| 314 | 7x | has_children <- rw$abs_rownumber %in% unlist(pagdf$reprint_inds) | 
| 315 | 7x |     if (rw$abs_rownumber == nrow(pagdf)) { | 
| 316 | 1x |       if (verbose) { | 
| 317 | 1x |         message("  EXCEPTION: last row is a label or content row but in lpp") | 
| 318 | } | |
| 319 | 6x |     } else if (!any(has_children)) { | 
| 320 | 6x |       if (verbose) { | 
| 321 | 6x | message( | 
| 322 | 6x | " EXCEPTION: last row is a label or content row\n", | 
| 323 | 6x | "but does not have rows and row groups depending on it" | 
| 324 | ) | |
| 325 | } | |
| 326 |     } else { | |
| 327 | ! |       if (verbose) { | 
| 328 | ! |         message("  FAIL: last row is a label or content row") | 
| 329 | } | |
| 330 | ! | return(FALSE) | 
| 331 | } | |
| 332 | } | |
| 333 | ||
| 334 | # Siblings handling | |
| 335 | 246x | sibpos <- rw[["pos_in_siblings"]] | 
| 336 | 246x | nsib <- rw[["n_siblings"]] | 
| 337 | # okpos <- min(min_sibs + 1, rw[["n_siblings"]]) | |
| 338 | 246x |   if (sibpos != nsib) { | 
| 339 | 99x | retfalse <- FALSE | 
| 340 | 99x |     if (sibpos < min_sibs + 1) { | 
| 341 | 25x | retfalse <- TRUE | 
| 342 | 25x |       if (verbose) { | 
| 343 | 25x | message( | 
| 344 | 25x | " FAIL: last row had only ", sibpos - 1, | 
| 345 | 25x | " preceding siblings, needed ", min_sibs | 
| 346 | ) | |
| 347 | } | |
| 348 | 74x |     } else if (nsib - sibpos < min_sibs + 1) { | 
| 349 | 7x | retfalse <- TRUE | 
| 350 | 7x |       if (verbose) { | 
| 351 | 4x | message( | 
| 352 | 4x | " FAIL: last row had only ", nsib - sibpos - 1, | 
| 353 | 4x | " following siblings, needed ", min_sibs | 
| 354 | ) | |
| 355 | } | |
| 356 | } | |
| 357 | 99x |     if (retfalse) { | 
| 358 | 32x | return(FALSE) | 
| 359 | } | |
| 360 | } | |
| 361 | 214x |   if (guess < nrow(pagdf) && length(nosplit > 0)) { | 
| 362 | ## paths end at the leaf name which is *always* different | |
| 363 | 16x | curpth <- head(unlist(rw$path), -1) | 
| 364 | 16x | nxtpth <- head(unlist(pagdf$path[[guess + 1]]), -1) | 
| 365 | ||
| 366 | 16x | inplay <- nosplit[(nosplit %in% intersect(curpth, nxtpth))] | 
| 367 | 16x |     if (length(inplay) > 0) { | 
| 368 | 16x |       ok_split <- vapply(inplay, function(var) { | 
| 369 | 16x | !identical(curpth[match(var, curpth) + 1], nxtpth[match(var, nxtpth) + 1]) | 
| 370 | 16x | }, TRUE) | 
| 371 | ||
| 372 | 16x | curvals <- curpth[match(inplay, curpth) + 1] | 
| 373 | 16x | nxtvals <- nxtpth[match(inplay, nxtpth) + 1] | 
| 374 | 16x |       if (!all(ok_split)) { | 
| 375 | 16x |         if (verbose) { | 
| 376 | 16x | message( | 
| 377 | 16x | " FAIL: nosplit variable [", | 
| 378 | 16x | inplay[min(which(!ok_split))], "] would be constant [", | 
| 379 | 16x | curvals, "] across this pagebreak." | 
| 380 | ) | |
| 381 | } | |
| 382 | 16x | return(FALSE) | 
| 383 | } | |
| 384 | } | |
| 385 | } | |
| 386 | ||
| 387 | # Usual output when found | |
| 388 | 198x |   if (verbose) { | 
| 389 | 83x |     message("  OK [", lines + rep_ext, if (row) " lines]" else " chars]") | 
| 390 | } | |
| 391 | 198x | TRUE | 
| 392 | } | |
| 393 | ||
| 394 | find_pag <- function(pagdf, | |
| 395 | current_page, | |
| 396 | start, | |
| 397 | guess, | |
| 398 | rlpp, | |
| 399 | lpp_or_cpp, | |
| 400 | context_lpp_or_cpp, | |
| 401 | min_siblings, | |
| 402 | nosplitin = character(), | |
| 403 | verbose = FALSE, | |
| 404 | row = TRUE, | |
| 405 | have_col_fnotes = FALSE, | |
| 406 | div_height = 1L, | |
| 407 | do_error = FALSE, | |
| 408 | col_gap, | |
| 409 |                      has_rowlabels) { | |
| 410 | 204x |   if (verbose) { | 
| 411 | 89x |     if (row) { | 
| 412 | 44x |       message("--------- ROW-WISE: Checking possible pagination for page ", current_page) | 
| 413 |     } else { | |
| 414 | 45x |       message("========= COLUMN-WISE: Checking possible pagination for page ", current_page) | 
| 415 | } | |
| 416 | } | |
| 417 | ||
| 418 | 204x | origuess <- guess | 
| 419 | 204x | while (guess >= start && !valid_pag( | 
| 420 | 204x | pagdf, guess, | 
| 421 | 204x | start = start, | 
| 422 | 204x | rlpp = rlpp, lpp = lpp_or_cpp, context_lpp = context_lpp_or_cpp, # only lpp goes to row pagination | 
| 423 | 204x | min_sibs = min_siblings, | 
| 424 | 204x | nosplit = nosplitin, verbose, row = row, | 
| 425 | 204x | have_col_fnotes = have_col_fnotes, | 
| 426 | 204x | div_height = div_height, | 
| 427 | 204x | col_gap = col_gap, | 
| 428 | 204x | has_rowlabels = has_rowlabels | 
| 429 |   )) { | |
| 430 | 430x | guess <- guess - 1 | 
| 431 | } | |
| 432 | 204x |   if (guess < start) { | 
| 433 | # Repeat pagination process to see what went wrong with verbose on | |
| 434 | 6x |     if (isFALSE(do_error) && isFALSE(verbose)) { | 
| 435 | ! | find_pag( | 
| 436 | ! | pagdf = pagdf, | 
| 437 | ! | current_page = current_page, | 
| 438 | ! | start = start, | 
| 439 | ! | guess = origuess, | 
| 440 | ! | rlpp = rlpp, lpp_or_cpp = lpp_or_cpp, context_lpp_or_cpp = context_lpp_or_cpp, | 
| 441 | ! | min_siblings = min_siblings, | 
| 442 | ! | nosplitin = nosplitin, | 
| 443 | ! | verbose = TRUE, | 
| 444 | ! | row = row, | 
| 445 | ! | have_col_fnotes = have_col_fnotes, | 
| 446 | ! | div_height = div_height, | 
| 447 | ! | do_error = TRUE, # only used to avoid loop | 
| 448 | ! | col_gap = col_gap, | 
| 449 | ! | has_rowlabels = has_rowlabels | 
| 450 | ) | |
| 451 | } | |
| 452 | 6x | stop( | 
| 453 | 6x | "-------------------------------------- Error Summary ----------------------------------------\n", | 
| 454 | 6x | "Unable to find any valid pagination split for page ", current_page, " between ", | 
| 455 | 6x | ifelse(row, "rows ", "columns "), start, " and ", origuess, ". \n", | 
| 456 | 6x | "Inserted ", ifelse(row, "lpp (row-space, lines per page) ", "cpp (column-space, content per page) "), | 
| 457 | 6x | ": ", lpp_or_cpp, "\n", | 
| 458 | 6x | "Context-relevant additional ", ifelse(row, "header/footers lines", "fixed column characters"), | 
| 459 | 6x | ": ", context_lpp_or_cpp, "\n", | 
| 460 | 6x | ifelse(row, | 
| 461 | 6x |         paste("Limit of allowed row lines per page:", rlpp, "\n"), | 
| 462 | 6x |         paste("Check the minimum allowed column characters per page in the last FAIL(ed) attempt. \n") | 
| 463 | ), | |
| 464 | 6x | "Note: take a look at the last FAIL(ed) attempt above to see what went wrong. It could be, for example, ", | 
| 465 | 6x | "that the inserted column width induces some wrapping, hence the inserted number of lines (lpp) is not enough." | 
| 466 | ) | |
| 467 | } | |
| 468 | 198x | guess | 
| 469 | } | |
| 470 | ||
| 471 | #' Find pagination indices from pagination info data frame | |
| 472 | #' | |
| 473 | #' Pagination methods should typically call the `make_row_df` method | |
| 474 | #' for their object and then call this function on the resulting | |
| 475 | #' pagination info `data.frame`. | |
| 476 | #' | |
| 477 | #' @param pagdf (`data.frame`)\cr a pagination info `data.frame` as created by | |
| 478 | #' either `make_rows_df` or `make_cols_df`. | |
| 479 | #' @param rlpp (`numeric`)\cr maximum number of *row* lines per page (not including header materials), including | |
| 480 | #' (re)printed header and context rows. | |
| 481 | #' @param lpp_or_cpp (`numeric`)\cr total maximum number of *row* lines or content (column-wise characters) per page | |
| 482 | #' (including header materials and context rows). This is only for informative results with `verbose = TRUE`. | |
| 483 | #' It will print `NA` if not specified by the pagination machinery. | |
| 484 | #' @param context_lpp_or_cpp (`numeric`)\cr total number of context *row* lines or content (column-wise characters) | |
| 485 | #' per page (including header materials). Uses `NA` if not specified by the pagination machinery and is only | |
| 486 | #' for informative results with `verbose = TRUE`. | |
| 487 | #' @param min_siblings (`numeric`)\cr minimum sibling rows which must appear on either side of pagination row for a | |
| 488 | #' mid-subtable split to be valid. Defaults to 2 for tables. It is automatically turned off (set to 0) for listings. | |
| 489 | #' @param nosplitin (`character`)\cr list of names of subtables where page breaks are not allowed, regardless of other | |
| 490 | #' considerations. Defaults to none. | |
| 491 | #' @param verbose (`flag`)\cr whether additional informative messages about the search for | |
| 492 | #' pagination breaks should be shown. Defaults to `FALSE`. | |
| 493 | #' @param row (`flag`)\cr whether pagination is happening in row space (`TRUE`, the default) or column | |
| 494 | #' space (`FALSE`). | |
| 495 | #' @param have_col_fnotes (`flag`)\cr whether the table-like object being rendered has column-associated | |
| 496 | #' referential footnotes. | |
| 497 | #' @param div_height (`numeric(1)`)\cr the height of the divider line when the associated object is rendered. | |
| 498 | #' Defaults to `1`. | |
| 499 | #' @param col_gap (`numeric(1)`)\cr width of gap between columns, in same units as extent in `pagdf` (spaces | |
| 500 | #' under a particular font specification). | |
| 501 | #' @param has_rowlabels (`logical(1)`)\cr whether the object being paginated has row labels. | |
| 502 | #' | |
| 503 | #' @details `pab_indices_inner` implements the core pagination algorithm (see below) | |
| 504 | #' for a single direction (vertical if `row = TRUE` (the default), horizontal otherwise) | |
| 505 | #' based on the pagination data frame and (already adjusted for non-body rows/columns) | |
| 506 | #' lines (or characters) per page. | |
| 507 | #' | |
| 508 | #' @inheritSection pagination_algo Pagination Algorithm | |
| 509 | #' | |
| 510 | #' @return A `list` containing a vector of row numbers, broken up by page. | |
| 511 | #' | |
| 512 | #' @examples | |
| 513 | #' mypgdf <- basic_pagdf(row.names(mtcars)) | |
| 514 | #' | |
| 515 | #' paginds <- pag_indices_inner(mypgdf, rlpp = 15, min_siblings = 0) | |
| 516 | #' lapply(paginds, function(x) mtcars[x, ]) | |
| 517 | #' | |
| 518 | #' @export | |
| 519 | pag_indices_inner <- function(pagdf, | |
| 520 | rlpp, | |
| 521 | lpp_or_cpp = NA_integer_, context_lpp_or_cpp = NA_integer_, # Context number of lines | |
| 522 | min_siblings, | |
| 523 | nosplitin = character(), | |
| 524 | verbose = FALSE, | |
| 525 | row = TRUE, | |
| 526 | have_col_fnotes = FALSE, | |
| 527 | div_height = 1L, | |
| 528 | col_gap = 3L, | |
| 529 |                               has_rowlabels) { | |
| 530 | 95x | start <- 1 | 
| 531 | 95x | current_page <- 1 | 
| 532 | 95x | nr <- nrow(pagdf) | 
| 533 | 95x | ret <- list() | 
| 534 | 95x |   while (start <= nr) { | 
| 535 | 205x | adjrlpp <- rlpp - pagdf$par_extent[start] | 
| 536 | 205x |     if (adjrlpp <= 0) { | 
| 537 | 1x |       if (row) { | 
| 538 | 1x |         stop("Lines of repeated context (plus header materials) larger than specified lines per page") | 
| 539 |       } else { | |
| 540 | ! |         stop("Width of row labels equal to or larger than specified characters per page.") | 
| 541 | } | |
| 542 | } | |
| 543 | 204x | guess <- min(nr, start + adjrlpp - 1) | 
| 544 | 204x | end <- find_pag( | 
| 545 | 204x | pagdf = pagdf, | 
| 546 | 204x | current_page = current_page, start = start, guess = guess, | 
| 547 | 204x | rlpp = adjrlpp, lpp_or_cpp = lpp_or_cpp, context_lpp_or_cpp = context_lpp_or_cpp, | 
| 548 | 204x | min_siblings = min_siblings, | 
| 549 | 204x | nosplitin = nosplitin, | 
| 550 | 204x | verbose = verbose, | 
| 551 | 204x | row = row, | 
| 552 | 204x | have_col_fnotes = have_col_fnotes, | 
| 553 | 204x | div_height = div_height, | 
| 554 | 204x | col_gap = col_gap, | 
| 555 | 204x | has_rowlabels = has_rowlabels | 
| 556 | ) | |
| 557 | 198x | ret <- c(ret, list(c( | 
| 558 | 198x | pagdf$reprint_inds[[start]], | 
| 559 | 198x | start:end | 
| 560 | ))) | |
| 561 | 198x | start <- end + 1 | 
| 562 | 198x | current_page <- current_page + 1 | 
| 563 | } | |
| 564 | 88x | ret | 
| 565 | } | |
| 566 | ||
| 567 | #' Find column indices for vertical pagination | |
| 568 | #' | |
| 569 | #' @inheritParams pag_indices_inner | |
| 570 | #' @inheritParams open_font_dev | |
| 571 | #' @inheritParams format_value | |
| 572 | #' @param obj (`ANY`)\cr object to be paginated. Must have a [matrix_form()] method. | |
| 573 | #' @param cpp (`numeric(1)`)\cr number of characters per page (width). | |
| 574 | #' @param colwidths (`numeric`)\cr vector of column widths (in characters) for use in vertical pagination. | |
| 575 | #' @param rep_cols (`numeric(1)`)\cr number of *columns* (not including row labels) to be repeated on every page. | |
| 576 | #' Defaults to 0. | |
| 577 | #' | |
| 578 | #' @return A `list` partitioning the vector of column indices into subsets for 1 or more horizontally paginated pages. | |
| 579 | #' | |
| 580 | #' @examples | |
| 581 | #' mf <- basic_matrix_form(df = mtcars) | |
| 582 | #' colpaginds <- vert_pag_indices(mf, fontspec = font_spec()) | |
| 583 | #' lapply(colpaginds, function(j) mtcars[, j, drop = FALSE]) | |
| 584 | #' | |
| 585 | #' @export | |
| 586 | vert_pag_indices <- function(obj, | |
| 587 | cpp = 40, | |
| 588 | colwidths = NULL, | |
| 589 | verbose = FALSE, | |
| 590 | rep_cols = 0L, | |
| 591 | fontspec, | |
| 592 | nosplitin = character(), | |
| 593 |                              round_type = c("iec", "sas")) { | |
| 594 | 45x |   if (is.list(nosplitin)) { | 
| 595 | ! | nosplitin <- nosplitin[["cols"]] | 
| 596 | } | |
| 597 | 45x | mf <- matrix_form(obj, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type) | 
| 598 | 45x | clwds <- colwidths %||% propose_column_widths(mf, fontspec = fontspec) | 
| 599 | 45x |   if (is.null(mf_cinfo(mf))) { ## like always, ugh. | 
| 600 | ! | mf <- mpf_infer_cinfo(mf, colwidths = clwds, rep_cols = rep_cols, fontspec = fontspec) | 
| 601 | } | |
| 602 | ||
| 603 | 45x | num_rep_cols(mf) <- rep_cols | 
| 604 | ||
| 605 | 45x | has_rlabs <- mf_has_rlabels(mf) | 
| 606 | 45x | rlabs_flag <- as.integer(has_rlabs) | 
| 607 | 45x | rlab_extent <- if (has_rlabs) clwds[1] else 0L | 
| 608 | ||
| 609 | # rep_extent <- pdf$par_extent[nrow(pdf)] | |
| 610 | 45x | rcpp <- cpp - table_inset(mf) - rlab_extent # rep_extent - table_inset(mf) - rlab_extent | 
| 611 | 45x |   if (verbose) { | 
| 612 | 14x | message( | 
| 613 | 14x | "Adjusted characters per page: ", rcpp, | 
| 614 | 14x | " [original: ", cpp, | 
| 615 | 14x |       ", table inset: ", table_inset(mf), if (has_rlabs) paste0(", row labels: ", clwds[1]), | 
| 616 | "]" | |
| 617 | ) | |
| 618 | } | |
| 619 | 45x | res <- pag_indices_inner(mf_cinfo(mf), | 
| 620 | 45x | rlpp = rcpp, lpp_or_cpp = cpp, context_lpp_or_cpp = cpp - rcpp, | 
| 621 | # cpp - sum(clwds[seq_len(rep_cols)]), | |
| 622 | 45x | verbose = verbose, | 
| 623 | 45x | min_siblings = 1, | 
| 624 | 45x | nosplitin = nosplitin, | 
| 625 | 45x | row = FALSE, | 
| 626 | 45x | col_gap = mf_colgap(mf), | 
| 627 | 45x | has_rowlabels = mf_has_rlabels(mf) | 
| 628 | ) | |
| 629 | 44x | res | 
| 630 | } | |
| 631 | ||
| 632 | mpf_infer_cinfo <- function(mf, colwidths = NULL, rep_cols = num_rep_cols(mf), fontspec, colpaths = NULL) { | |
| 633 | 58x |   if (!is.null(mf_cinfo(mf))) { | 
| 634 | 5x | return(mf_update_cinfo(mf, colwidths = colwidths)) | 
| 635 | } | |
| 636 | 53x | new_dev <- open_font_dev(fontspec) | 
| 637 | 53x |   if (new_dev) { | 
| 638 | 53x | on.exit(close_font_dev()) | 
| 639 | } | |
| 640 | 53x |   if (!is(rep_cols, "numeric") || is.na(rep_cols) || rep_cols < 0) { | 
| 641 | ! |     stop("got invalid number of columns to be repeated: ", rep_cols) | 
| 642 | } | |
| 643 | 53x | clwds <- (colwidths %||% mf_col_widths(mf)) %||% propose_column_widths(mf, fontspec = fontspec) | 
| 644 | 53x | has_rlabs <- mf_has_rlabels(mf) | 
| 645 | 53x | rlabs_flag <- as.integer(has_rlabs) | 
| 646 | 53x | rlab_extent <- if (has_rlabs) clwds[1] else 0L | 
| 647 | 53x | sqstart <- rlabs_flag + 1L # rep_cols + 1L | 
| 648 | ||
| 649 | 53x | pdfrows <- lapply( | 
| 650 | 53x | (sqstart):ncol(mf$strings), | 
| 651 | 53x |     function(i) { | 
| 652 | 301x | rownum <- i - rlabs_flag | 
| 653 | 301x | rep_inds <- seq_len(rep_cols)[seq_len(rep_cols) < rownum] | 
| 654 | 301x | rep_extent_i <- sum( | 
| 655 | 301x | 0L, | 
| 656 | 301x | clwds[rlabs_flag + rep_inds] | 
| 657 | 301x | ) ## colwidths | 
| 658 | 301x | pagdfrow( | 
| 659 | 301x | row = NA, | 
| 660 | 301x | nm = rownum, | 
| 661 | 301x | lab = rownum, | 
| 662 | 301x | rnum = rownum, | 
| 663 | 301x | pth = NA, | 
| 664 | 301x | extent = clwds[i], | 
| 665 | 301x | repext = rep_extent_i, # sum(clwds[rep_cols]) + mf$col_gap * max(0, (length(rep_cols) - 1)), | 
| 666 | 301x | repind = rep_inds, # rep_cols, | 
| 667 | 301x | rclass = "stuff", | 
| 668 | 301x | sibpos = 1 - 1, | 
| 669 | 301x | nsibs = 1 - 1, | 
| 670 | 301x | fontspec = fontspec | 
| 671 | ) | |
| 672 | } | |
| 673 | ) | |
| 674 | 53x | pdf <- do.call(rbind, pdfrows) | 
| 675 | ||
| 676 | 53x | refdf <- mf_fnote_df(mf) | 
| 677 | 53x | pdf <- splice_fnote_info_in(pdf, refdf, row = FALSE) | 
| 678 | 53x |   if (!is.null(colpaths)) { | 
| 679 | ! |     if (length(colpaths) != NROW(pdf)) { | 
| 680 | ## nocov start | |
| 681 | stop( | |
| 682 |         "Got non-null colpaths with length not equal to number of columns (", | |
| 683 | length(colpaths), | |
| 684 | "!=", | |
| 685 | NROW(pdf), | |
| 686 | ") during MatrixPrintForm construction. Please contact the maintainers." | |
| 687 | ) | |
| 688 | ## nocov end | |
| 689 | } | |
| 690 | ! | pdf[["path"]] <- colpaths | 
| 691 | } | |
| 692 | 53x | mf_cinfo(mf) <- pdf | 
| 693 | 53x | mf | 
| 694 | } | |
| 695 | ||
| 696 | #' Basic/spoof pagination info data frame | |
| 697 | #' | |
| 698 | #' Returns a minimal pagination info `data.frame` (with no info on siblings, footnotes, etc.). | |
| 699 | #' | |
| 700 | #' @inheritParams test_matrix_form | |
| 701 | #' @inheritParams open_font_dev | |
| 702 | #' @param rnames (`character`)\cr vector of row names. | |
| 703 | #' @param labs (`character`)\cr vector of row labels. Defaults to `rnames`. | |
| 704 | #' @param rnums (`integer`)\cr vector of row numbers. Defaults to `seq_along(rnames)`. | |
| 705 | #' @param extents (`integer`)\cr number of lines each row requires to print. Defaults to 1 for all rows. | |
| 706 | #' @param rclass (`character`)\cr class(es) for the rows. Defaults to `"DataRow"`. | |
| 707 | #' @param paths (`list`)\cr list of paths to the rows. Defaults to `lapply(rnames, function(x) c(parent_path, x))`. | |
| 708 | #' | |
| 709 | #' @return A `data.frame` suitable for use in both the `MatrixPrintForm` constructor and the pagination machinery. | |
| 710 | #' | |
| 711 | #' @examples | |
| 712 | #' basic_pagdf(c("hi", "there")) | |
| 713 | #' | |
| 714 | #' @export | |
| 715 | basic_pagdf <- function(rnames, | |
| 716 | labs = rnames, | |
| 717 | rnums = seq_along(rnames), | |
| 718 | extents = 1L, | |
| 719 | rclass = "DataRow", | |
| 720 | parent_path = NULL, | |
| 721 | paths = lapply(rnames, function(x) c(parent_path, x)), | |
| 722 |                         fontspec = font_spec()) { | |
| 723 | 52x | rws <- mapply(pagdfrow, | 
| 724 | 52x | nm = rnames, lab = labs, extent = extents, | 
| 725 | 52x | rclass = rclass, rnum = rnums, pth = paths, | 
| 726 | 52x | MoreArgs = list(fontspec = fontspec), | 
| 727 | 52x | SIMPLIFY = FALSE, nsibs = 1, sibpos = 1 | 
| 728 | ) | |
| 729 | 52x | res <- do.call(rbind.data.frame, rws) | 
| 730 | 52x | res$n_siblings <- nrow(res) | 
| 731 | 52x | res$pos_in_siblings <- seq_along(res$n_siblings) | 
| 732 | ||
| 733 | 52x |   if (!all(rclass == "DataRow")) { | 
| 734 | # These things are used in the simple case of a split, hence having labels. | |
| 735 | # To improve and extend to other cases | |
| 736 | 4x | res$pos_in_siblings <- NA | 
| 737 | 4x | res$pos_in_siblings[rclass == "DataRow"] <- 1 | 
| 738 | 4x | res$par_extent[rclass == "DataRow"] <- 1 # the rest is 0 | 
| 739 | 4x | res$n_siblings <- res$pos_in_siblings | 
| 740 | 4x | res$reprint_inds[which(rclass == "DataRow")] <- res$abs_rownumber[which(rclass == "DataRow") - 1] | 
| 741 | } | |
| 742 | 52x | res | 
| 743 | } | |
| 744 | ||
| 745 | ## write paginate() which operates **solely** on a MatrixPrintForm obj | |
| 746 | ||
| 747 | page_size_spec <- function(lpp, cpp, max_width, | |
| 748 | font_family, | |
| 749 | font_size, | |
| 750 | lineheight, | |
| 751 | fontspec = font_spec( | |
| 752 | font_family = font_family, | |
| 753 | font_size = font_size, | |
| 754 | lineheight = lineheight | |
| 755 |                            )) { | |
| 756 | 50x | structure(list( | 
| 757 | 50x | lpp = lpp, | 
| 758 | 50x | cpp = cpp, | 
| 759 | 50x | max_width = max_width, | 
| 760 | 50x | font_spec = fontspec | 
| 761 | 50x | ), class = "page_size_spec") | 
| 762 | } | |
| 763 | ||
| 764 | get_font_spec <- function(obj) { | |
| 765 | 48x |   if (!is(obj, "page_size_spec")) { | 
| 766 | ! |     stop("get_font_spec is only currently defined for page_size_spec objects") | 
| 767 | } | |
| 768 | 48x | obj$font_spec | 
| 769 | } | |
| 770 | ||
| 771 | 100x | non_null_na <- function(x) !is.null(x) && is.na(x) | 
| 772 | ||
| 773 | calc_lcpp <- function(page_type = NULL, | |
| 774 | landscape = FALSE, | |
| 775 | pg_width = page_dim(page_type)[if (landscape) 2 else 1], | |
| 776 | pg_height = page_dim(page_type)[if (landscape) 1 else 2], | |
| 777 | fontspec = font_spec(), | |
| 778 | ## font_family = "Courier", | |
| 779 | ## font_size = 8, # grid parameters | |
| 780 | cpp = NA_integer_, | |
| 781 | lpp = NA_integer_, | |
| 782 | tf_wrap = TRUE, | |
| 783 | max_width = NULL, | |
| 784 | ## lineheight = 1, | |
| 785 | margins = c(bottom = .5, left = .75, top = .5, right = .75), | |
| 786 | colwidths, | |
| 787 | col_gap, | |
| 788 |                       inset) { | |
| 789 | 50x | pg_lcpp <- page_lcpp( | 
| 790 | 50x | page_type = page_type, | 
| 791 | 50x | landscape = landscape, | 
| 792 | ## font_family = font_family, | |
| 793 | ## font_size = font_size, | |
| 794 | ## lineheight = lineheight, | |
| 795 | 50x | fontspec = fontspec, | 
| 796 | 50x | margins = margins, | 
| 797 | 50x | pg_width = pg_width, | 
| 798 | 50x | pg_height = pg_height | 
| 799 | ) | |
| 800 | ||
| 801 | 50x |   if (non_null_na(lpp)) { | 
| 802 | 29x | lpp <- pg_lcpp$lpp | 
| 803 | } | |
| 804 | 50x |   if (non_null_na(cpp)) { | 
| 805 | 22x | cpp <- pg_lcpp$cpp | 
| 806 | } | |
| 807 | 50x | stopifnot(!is.na(cpp)) | 
| 808 | ||
| 809 | 50x | max_width <- .handle_max_width(tf_wrap, max_width, cpp, colwidths, col_gap, inset) | 
| 810 | ||
| 811 | 50x | page_size_spec( | 
| 812 | 50x | lpp = lpp, cpp = cpp, max_width = max_width, | 
| 813 | ## font_family = font_family, | |
| 814 | ## font_size = font_size, | |
| 815 | ## lineheight = lineheight | |
| 816 | 50x | fontspec = fontspec | 
| 817 | ) | |
| 818 | } | |
| 819 | ||
| 820 | calc_rlpp <- function(pg_size_spec, mf, colwidths, tf_wrap, verbose) { | |
| 821 | 48x | lpp <- pg_size_spec$lpp | 
| 822 | 48x | max_width <- pg_size_spec$max_width | 
| 823 | 48x | fontspec <- get_font_spec(pg_size_spec) | 
| 824 | 48x | dh <- divider_height(mf) | 
| 825 | 48x |   if (any(nzchar(all_titles(mf)))) { | 
| 826 | ## +1 is for blank line between subtitles and divider | |
| 827 | ## dh is for divider line **between subtitles and column labels** | |
| 828 | ## other divider line is accounted for in cinfo_lines | |
| 829 | 24x |     if (!tf_wrap) { | 
| 830 | 12x | tlines <- length(all_titles(mf)) | 
| 831 |     } else { | |
| 832 | 12x | tlines <- sum(nlines(all_titles(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec)) | 
| 833 | } | |
| 834 | 24x | tlines <- tlines + dh + 1L | 
| 835 |   } else { | |
| 836 | 24x | tlines <- 0 | 
| 837 | } | |
| 838 | ||
| 839 | ## dh for divider line between column labels and table body | |
| 840 | 48x | cinfo_lines <- mf_nlheader(mf) + dh | 
| 841 | ||
| 842 | 48x |   if (verbose) { | 
| 843 | 17x | message( | 
| 844 | 17x | "Determining lines required for header content: ", | 
| 845 | 17x | tlines, " title and ", cinfo_lines, " table header lines" | 
| 846 | ) | |
| 847 | } | |
| 848 | ||
| 849 | 48x | refdf <- mf_fnote_df(mf) | 
| 850 | 48x | cfn_df <- refdf[is.na(refdf$row) & !is.na(refdf$col), ] | 
| 851 | ||
| 852 | 48x | flines <- 0L | 
| 853 | 48x | mnfoot <- main_footer(mf) | 
| 854 | 48x | havemn <- length(mnfoot) && any(nzchar(mnfoot)) | 
| 855 | 48x |   if (havemn) { | 
| 856 | 25x | flines <- nlines( | 
| 857 | 25x | mnfoot, | 
| 858 | 25x | colwidths = colwidths, | 
| 859 | 25x | max_width = max_width - table_inset(mf), | 
| 860 | 25x | fontspec = fontspec | 
| 861 | ) | |
| 862 | } | |
| 863 | 48x | prfoot <- prov_footer(mf) | 
| 864 | 48x |   if (length(prfoot) && any(nzchar(prfoot))) { | 
| 865 | 31x | flines <- flines + nlines(prov_footer(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec) | 
| 866 | 31x |     if (havemn) { | 
| 867 | 24x | flines <- flines + 1L | 
| 868 | } ## space between main and prov footer. | |
| 869 | } | |
| 870 | ## this time its for the divider between the footers and whatever is above them | |
| 871 | ## (either table body or referential footnotes) | |
| 872 | 48x |   if (flines > 0) { | 
| 873 | 32x | flines <- flines + dh + 1L | 
| 874 | } | |
| 875 | ## this time its for the divider between the referential footnotes and | |
| 876 | ## the table body IFF we have any, otherwise that divider+blanks pace doesn't get drawn | |
| 877 | 48x |   if (NROW(cfn_df) > 0) { | 
| 878 | ! | cinfo_lines <- cinfo_lines + sum(cfn_df$nlines) | 
| 879 | ! | flines <- flines + dh + 1L | 
| 880 | } | |
| 881 | ||
| 882 | 48x |   if (verbose) { | 
| 883 | 17x | message( | 
| 884 | 17x | "Determining lines required for footer content", | 
| 885 | 17x | if (NROW(cfn_df) > 0) " [column fnotes present]", | 
| 886 | 17x | ": ", flines, " lines" | 
| 887 | ) | |
| 888 | } | |
| 889 | ||
| 890 | 48x | ret <- lpp - flines - tlines - cinfo_lines | 
| 891 | ||
| 892 | 48x |   if (verbose) { | 
| 893 | 17x |     message("Lines per page available for tables rows: ", ret, " (original: ", lpp, ")") | 
| 894 | } | |
| 895 | 48x | ret | 
| 896 | } | |
| 897 | ||
| 898 | ## this is ok to be unchanged because by this point | |
| 899 | ## all of these are in terms of space widths | |
| 900 | calc_rcpp <- function(pg_size_spec, mf, colwidths) { | |
| 901 | ! | cpp <- pg_size_spec$cpp | 
| 902 | ||
| 903 | ! | cpp - table_inset(mf) - colwidths[1] - mf_colgap(mf) | 
| 904 | } | |
| 905 | ||
| 906 | splice_idx_lists <- function(lsts) { | |
| 907 | ! | list( | 
| 908 | ! | pag_row_indices = do.call(c, lapply(lsts, function(xi) xi$pag_row_indices)), | 
| 909 | ! | pag_col_indices = do.call(c, lapply(lsts, function(yi) yi$pag_col_indices)) | 
| 910 | ) | |
| 911 | } | |
| 912 | ||
| 913 | #' Paginate a table-like object for rendering | |
| 914 | #' | |
| 915 | #' These functions perform or diagnose bi-directional pagination on an object. | |
| 916 | #' | |
| 917 | #' `paginate_indices` renders `obj` into a `MatrixPrintForm` (MPF), then uses that representation to | |
| 918 | #' calculate the rows and columns of `obj` corresponding to each page of the pagination of `obj`, but | |
| 919 | #' simply returns these indices rather than paginating `obj` itself (see Details for an important caveat). | |
| 920 | #' | |
| 921 | #' `paginate_to_mpfs` renders `obj` into its MPF intermediate representation, then paginates that MPF into | |
| 922 | #' component MPFs each corresponding to an individual page and returns those in a `list`. | |
| 923 | #' | |
| 924 | #' `diagnose_pagination` attempts pagination via `paginate_to_mpfs`, then returns diagnostic information | |
| 925 | #' which explains why page breaks were positioned where they were, or alternatively why no valid pagination | |
| 926 | #' could be found. | |
| 927 | #' | |
| 928 | #' @details | |
| 929 | #' All three of these functions generally support all classes which have a corresponding [matrix_form()] | |
| 930 | #' method which returns a valid `MatrixPrintForm` object (including `MatrixPrintForm` objects themselves). | |
| 931 | #' | |
| 932 | #' `paginate_indices` is directly called by `paginate_to_mpfs` (and thus `diagnose_pagination`). For most | |
| 933 | #' classes, and most tables represented by supported classes, calling `paginate_to_mpfs` is equivalent to a | |
| 934 | #' manual `paginate_indices -> subset obj into pages -> matrix_form` workflow. | |
| 935 | #' | |
| 936 | #' The exception to this equivalence is objects which support "forced pagination", or pagination logic which | |
| 937 | #' is built into the object itself rather than being a function of space on a page. Forced pagination | |
| 938 | #' generally involves the creation of, e.g., page-specific titles which apply to these forced paginations. | |
| 939 | #' `paginate_to_mpfs` and `diagnose_pagination` support forced pagination by automatically calling the | |
| 940 | #' [do_forced_paginate()] generic on the object and then paginating each object returned by that generic | |
| 941 | #' separately. The assumption here, then, is that page-specific titles and such are handled by the class' | |
| 942 | #' [do_forced_paginate()] method. | |
| 943 | #' | |
| 944 | #' `paginate_indices`, on the other hand, *does not support forced pagination*, because it returns only a | |
| 945 | #' set of indices for row and column subsetting for each page, and thus cannot retain any changes, e.g., | |
| 946 | #' to titles, done within [do_forced_paginate()]. `paginate_indices` does call [do_forced_paginate()], but | |
| 947 | #' instead of continuing it throws an error in the case that the result is larger than a single "page". | |
| 948 | #' | |
| 949 | #' @inheritParams vert_pag_indices | |
| 950 | #' @inheritParams pag_indices_inner | |
| 951 | #' @inheritParams page_lcpp | |
| 952 | #' @inheritParams toString | |
| 953 | #' @inheritParams propose_column_widths | |
| 954 | #' @param lpp (`numeric(1)` or `NULL`)\cr lines per page. If `NA` (the default), this is calculated automatically | |
| 955 | #' based on the specified page size). `NULL` indicates no vertical pagination should occur. | |
| 956 | #' @param cpp (`numeric(1)` or `NULL`)\cr width (in characters) per page. If `NA` (the default), this is calculated | |
| 957 | #' automatically based on the specified page size). `NULL` indicates no horizontal pagination should occur. | |
| 958 | #' @param pg_size_spec (`page_size_spec`)\cr. a pre-calculated page size specification. Typically this is not set by | |
| 959 | #' end users. | |
| 960 | #' @param col_gap (`numeric(1)`)\cr The number of spaces to be placed between columns | |
| 961 | #' in the rendered table (and assumed for horizontal pagination). | |
| 962 | #' @param page_num (`string`)\cr placeholder string for page numbers. See [default_page_number] for more | |
| 963 | #' information. Defaults to `NULL`. | |
| 964 | #' | |
| 965 | #' @return | |
| 966 | #' * `paginate_indices` returns a `list` with two elements of the same length: `pag_row_indices` and `pag_col_indices`. | |
| 967 | #' * `paginate_to_mpfs` returns a `list` of `MatrixPrintForm` objects representing each individual page after | |
| 968 | #' pagination (including forced pagination if necessary). | |
| 969 | #' | |
| 970 | #' @examples | |
| 971 | #' mpf <- basic_matrix_form(mtcars) | |
| 972 | #' | |
| 973 | #' paginate_indices(mpf, pg_width = 5, pg_height = 3) | |
| 974 | #' | |
| 975 | #' paginate_to_mpfs(mpf, pg_width = 5, pg_height = 3) | |
| 976 | #' | |
| 977 | #' @aliases paginate pagination | |
| 978 | #' @export | |
| 979 | paginate_indices <- function(obj, | |
| 980 | page_type = "letter", | |
| 981 | font_family = "Courier", | |
| 982 | font_size = 8, | |
| 983 | lineheight = 1, | |
| 984 | landscape = FALSE, | |
| 985 | pg_width = NULL, | |
| 986 | pg_height = NULL, | |
| 987 | margins = c(top = .5, bottom = .5, left = .75, right = .75), | |
| 988 | lpp = NA_integer_, | |
| 989 | cpp = NA_integer_, | |
| 990 | min_siblings = 2, | |
| 991 | nosplitin = list( | |
| 992 | rows = character(), | |
| 993 | cols = character() | |
| 994 | ), | |
| 995 | colwidths = NULL, | |
| 996 | tf_wrap = FALSE, | |
| 997 | max_width = NULL, | |
| 998 | indent_size = 2, | |
| 999 | pg_size_spec = NULL, | |
| 1000 | rep_cols = num_rep_cols(obj), | |
| 1001 | col_gap = 3, | |
| 1002 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 1003 |                              round_type = c("iec", "sas"), | |
| 1004 |                              verbose = FALSE) { | |
| 1005 | ## this preserves backwards compatibility | |
| 1006 | ## could start deprecation cycle of char input | |
| 1007 | 50x |   if (is.character(nosplitin)) { | 
| 1008 | 47x | nosplitin <- list( | 
| 1009 | 47x | rows = nosplitin, | 
| 1010 | 47x | cols = character() | 
| 1011 | ) | |
| 1012 | } | |
| 1013 | 50x | newdev <- open_font_dev(fontspec) | 
| 1014 | 50x |   if (newdev) { | 
| 1015 | 3x | on.exit(close_font_dev()) | 
| 1016 | } | |
| 1017 | ## this MUST alsways return a list, inluding list(obj) when | |
| 1018 | ## no forced pagination is needed! otherwise stuff breaks for things | |
| 1019 | ## based on s3 classes that are lists underneath!!! | |
| 1020 | 50x | fpags <- do_forced_paginate(obj) | 
| 1021 | ## if we have more than one forced "page", | |
| 1022 | ## paginate each of them individually and return the result. | |
| 1023 | ## forced pagination is ***currently*** only vertical, so | |
| 1024 | ## we don't have to worry about divying up colwidths here, | |
| 1025 | ## but we will if we ever allow force_paginate to do horiz | |
| 1026 | ## pagination. | |
| 1027 | 50x |   if (length(fpags) > 1) { | 
| 1028 | 1x | stop( | 
| 1029 | 1x | "forced pagination is required for this object (class: ", class(obj)[1], | 
| 1030 | 1x | ") this is not supported in paginate_indices. Use paginate_to_mpfs or call ", | 
| 1031 | 1x | "do_forced_paginate on your object and paginate each returned section separately." | 
| 1032 | ) | |
| 1033 | } | |
| 1034 | ||
| 1035 | ## order is annoying here, since we won't actually need the mpf if | |
| 1036 | ## we run into forced pagination, but life is short and this should work fine. | |
| 1037 | 49x | mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) | 
| 1038 | 49x |   if (is.null(colwidths)) { | 
| 1039 | 2x | colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) | 
| 1040 |   } else { | |
| 1041 | 47x | mf_col_widths(mpf) <- colwidths | 
| 1042 | } | |
| 1043 | ||
| 1044 | 49x | mf_colgap(mpf) <- col_gap | 
| 1045 | 49x |   if (!is.null(rep_cols) && rep_cols != num_rep_cols(obj)) { | 
| 1046 | 3x | num_rep_cols(mpf) <- rep_cols | 
| 1047 | } | |
| 1048 | 49x |   if (NROW(mf_cinfo(mpf)) == 0) { | 
| 1049 | ! | mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) | 
| 1050 | } | |
| 1051 | ||
| 1052 | 49x |   if (is.null(pg_size_spec)) { | 
| 1053 | 2x | pg_size_spec <- calc_lcpp( | 
| 1054 | 2x | page_type = page_type, | 
| 1055 | ## font_family = font_family, | |
| 1056 | ## font_size = font_size, | |
| 1057 | ## lineheight = lineheight, | |
| 1058 | 2x | fontspec = fontspec, | 
| 1059 | 2x | landscape = landscape, | 
| 1060 | 2x | pg_width = pg_width, | 
| 1061 | 2x | pg_height = pg_height, | 
| 1062 | 2x | margins = margins, | 
| 1063 | 2x | lpp = lpp, | 
| 1064 | 2x | cpp = cpp, | 
| 1065 | 2x | tf_wrap = tf_wrap, | 
| 1066 | 2x | max_width = max_width, | 
| 1067 | 2x | colwidths = colwidths, | 
| 1068 | 2x | inset = table_inset(mpf), | 
| 1069 | 2x | col_gap = col_gap | 
| 1070 | ) | |
| 1071 | } | |
| 1072 | ||
| 1073 | ## we can't support forced pagination in paginate_indices because | |
| 1074 | ## forced pagination is generally going to set page titles, which | |
| 1075 | ## we can't preserve when just returning lists of indices. | |
| 1076 | ## Instead we make a hard assumption here that any forced pagination | |
| 1077 | ## has already occurred. | |
| 1078 | ||
| 1079 | ## this wraps the cell contents AND shoves referential footnote | |
| 1080 | ## info into mf_rinfo(mpf) | |
| 1081 | 49x | mpf <- do_cell_fnotes_wrap(mpf, colwidths, max_width, tf_wrap = tf_wrap, fontspec = fontspec) | 
| 1082 | ||
| 1083 | # rlistings note: if there is a wrapping in a keycol, it is not calculated correctly | |
| 1084 | # in the above call, so we need to keep this information in mf_rinfo | |
| 1085 | # and use it here. | |
| 1086 | 49x | mfri <- mf_rinfo(mpf) | 
| 1087 | 49x | keycols <- .get_keycols_from_listing(obj) | 
| 1088 | 49x |   if (NROW(mfri) > 1 && .is_listing_mf(mpf) && length(keycols) > 0) { | 
| 1089 | # Lets determine the groupings created by keycols | |
| 1090 | 12x | keycols_grouping_df <- NULL | 
| 1091 | 12x |     for (i in seq_along(keycols)) { | 
| 1092 | 24x | kcol <- keycols[i] | 
| 1093 | 24x |       if (is(obj, "MatrixPrintForm")) { | 
| 1094 | # This makes the function work also in the case we have only matrix form (mainly for testing purposes) | |
| 1095 | 24x | kcolvec <- mf_strings(obj)[, mf_strings(obj)[1, , drop = TRUE] == kcol][-1] | 
| 1096 | 24x |         while (any(kcolvec == "")) { | 
| 1097 | 284x | kcolvec[which(kcolvec == "")] <- kcolvec[which(kcolvec == "") - 1] | 
| 1098 | } | |
| 1099 |       } else { | |
| 1100 | ! | kcolvec <- obj[[kcol]] | 
| 1101 | ! | kcolvec <- vapply(kcolvec, format_value, "", format = obj_format(kcolvec), na_str = obj_na_str(kcolvec)) | 
| 1102 | } | |
| 1103 | 24x | groupings <- as.numeric(factor(kcolvec, levels = unique(kcolvec))) | 
| 1104 | 24x | where_they_start <- which(c(1, diff(groupings)) > 0) | 
| 1105 | 24x | keycols_grouping_df <- cbind( | 
| 1106 | 24x | keycols_grouping_df, | 
| 1107 | 24x | where_they_start[groupings] | 
| 1108 | 24x | ) # take the groupings | 
| 1109 | } | |
| 1110 | ||
| 1111 | # Creating the real self_extend for mf_rinfo (if the line is chosen for pagination start) | |
| 1112 | 12x | self_extent_df <- apply(keycols_grouping_df, 2, function(x) mfri$self_extent[x]) | 
| 1113 | 12x | mf_rinfo(mpf) <- cbind(mfri, "self_extent_page_break" = apply(self_extent_df, 1, max)) | 
| 1114 | } | |
| 1115 | ||
| 1116 | 49x |   if (is.null(pg_size_spec$lpp)) { | 
| 1117 | 1x | pag_row_indices <- list(seq_len(mf_nrow(mpf))) | 
| 1118 |   } else { | |
| 1119 | 48x | rlpp <- calc_rlpp( | 
| 1120 | 48x | pg_size_spec, mpf, | 
| 1121 | 48x | colwidths = colwidths, | 
| 1122 | 48x | tf_wrap = tf_wrap, verbose = verbose | 
| 1123 | ) | |
| 1124 | 48x | pag_row_indices <- pag_indices_inner( | 
| 1125 | 48x | pagdf = mf_rinfo(mpf), | 
| 1126 | 48x | rlpp = rlpp, | 
| 1127 | 48x | lpp_or_cpp = pg_size_spec$lpp, | 
| 1128 | 48x | context_lpp_or_cpp = pg_size_spec$lpp - rlpp, | 
| 1129 | 48x | verbose = verbose, | 
| 1130 | 48x | min_siblings = min_siblings, | 
| 1131 | 48x | nosplitin = nosplitin[["rows"]], | 
| 1132 | 48x | col_gap = col_gap, | 
| 1133 | 48x | has_rowlabels = mf_has_rlabels(mpf) | 
| 1134 | ) | |
| 1135 | } | |
| 1136 | ||
| 1137 | 44x |   if (is.null(pg_size_spec$cpp)) { | 
| 1138 | 1x | pag_col_indices <- list(seq_len(mf_ncol(mpf))) | 
| 1139 |   } else { | |
| 1140 | 43x | pag_col_indices <- vert_pag_indices( | 
| 1141 | 43x | mpf, | 
| 1142 | 43x | cpp = pg_size_spec$cpp, colwidths = colwidths, | 
| 1143 | 43x | rep_cols = rep_cols, fontspec = fontspec, | 
| 1144 | 43x | nosplitin = nosplitin[["cols"]], | 
| 1145 | 43x | round_type = round_type, | 
| 1146 | 43x | verbose = verbose | 
| 1147 | ) | |
| 1148 | } | |
| 1149 | ||
| 1150 | 43x | list(pag_row_indices = pag_row_indices, pag_col_indices = pag_col_indices) | 
| 1151 | } | |
| 1152 | ||
| 1153 | 47x | setGeneric("has_page_title", function(obj) standardGeneric("has_page_title")) | 
| 1154 | ||
| 1155 | 47x | setMethod("has_page_title", "ANY", function(obj) length(page_titles(obj)) > 0) | 
| 1156 | ||
| 1157 | #' @rdname paginate_indices | |
| 1158 | #' @export | |
| 1159 | paginate_to_mpfs <- function(obj, | |
| 1160 | page_type = "letter", | |
| 1161 | font_family = "Courier", | |
| 1162 | font_size = 8, | |
| 1163 | lineheight = 1, | |
| 1164 | landscape = FALSE, | |
| 1165 | pg_width = NULL, | |
| 1166 | pg_height = NULL, | |
| 1167 | margins = c(top = .5, bottom = .5, left = .75, right = .75), | |
| 1168 | lpp = NA_integer_, | |
| 1169 | cpp = NA_integer_, | |
| 1170 | min_siblings = 2, | |
| 1171 | nosplitin = character(), | |
| 1172 | colwidths = NULL, | |
| 1173 | tf_wrap = FALSE, | |
| 1174 | max_width = NULL, | |
| 1175 | indent_size = 2, | |
| 1176 | pg_size_spec = NULL, | |
| 1177 | page_num = default_page_number(), | |
| 1178 | rep_cols = NULL, | |
| 1179 | # rep_cols = num_rep_cols(obj), | |
| 1180 | # col_gap = 3, # this could be change in default - breaking change | |
| 1181 | col_gap = 3, | |
| 1182 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 1183 |                              round_type = c("iec", "sas"), | |
| 1184 |                              verbose = FALSE) { | |
| 1185 | 57x | newdev <- open_font_dev(fontspec) | 
| 1186 | 57x |   if (newdev) { | 
| 1187 | 32x | on.exit(close_font_dev()) | 
| 1188 | } | |
| 1189 | ||
| 1190 | 57x |   if (isTRUE(page_num)) { | 
| 1191 | 1x |     page_num <- "page {i}/{n}" | 
| 1192 | } | |
| 1193 | 57x | checkmate::assert_string(page_num, null.ok = TRUE, min.chars = 1) | 
| 1194 | ||
| 1195 | # We can return a list of paginated tables and listings | |
| 1196 | 57x |   if (.is_list_of_tables_or_listings(obj)) { | 
| 1197 | 8x | cur_call <- match.call(expand.dots = FALSE) | 
| 1198 | 8x | mpfs <- unlist( | 
| 1199 | 8x |       lapply(obj, function(obj_i) { | 
| 1200 | 15x | cur_call[["obj"]] <- obj_i | 
| 1201 | 15x | eval(cur_call, envir = parent.frame(3L)) | 
| 1202 | }), | |
| 1203 | 8x | recursive = FALSE | 
| 1204 | ) | |
| 1205 | ||
| 1206 | 7x |     if (!is.null(page_num)) { | 
| 1207 | 3x | extracted_cpp <- max( | 
| 1208 | 3x |         sapply(mpfs, function(mpf) { | 
| 1209 | 12x | pf <- prov_footer(mpf) | 
| 1210 | 12x | nchar(pf[length(pf)]) | 
| 1211 | }) | |
| 1212 | ) | |
| 1213 | 3x | mpfs <- .modify_footer_for_page_nums(mpfs, page_num, extracted_cpp) | 
| 1214 | } | |
| 1215 | ||
| 1216 | 7x | return(mpfs) | 
| 1217 | } | |
| 1218 | ||
| 1219 | 49x |   if (!is.null(page_num)) { | 
| 1220 | # Only adding a line for pagination -> lpp - 1 would have worked too | |
| 1221 | 14x | prov_footer(obj) <- c(prov_footer(obj), page_num) | 
| 1222 | } | |
| 1223 | ||
| 1224 | 49x | mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) | 
| 1225 | # For listings, keycols are mandatory rep_num_cols | |
| 1226 | 49x |   if (is.null(rep_cols)) { | 
| 1227 | 44x | rep_cols <- num_rep_cols(obj) | 
| 1228 | } | |
| 1229 | 49x | num_rep_cols(mpf) <- rep_cols | 
| 1230 | ||
| 1231 | # Turning off min_siblings for listings | |
| 1232 | 49x |   if (.is_listing_mf(mpf)) { | 
| 1233 | 13x | min_siblings <- 0 | 
| 1234 | } | |
| 1235 | ||
| 1236 | # Checking colwidths | |
| 1237 | 49x |   if (is.null(colwidths)) { | 
| 1238 | 33x | colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) | 
| 1239 |   } else { | |
| 1240 | 16x | cur_ncol <- ncol(mpf) | 
| 1241 | 16x |     if (!.is_listing_mf(mpf)) { | 
| 1242 | 10x | cur_ncol <- cur_ncol + as.numeric(mf_has_rlabels(mpf)) | 
| 1243 | } | |
| 1244 | 16x |     if (length(colwidths) != cur_ncol) { | 
| 1245 | 1x | stop( | 
| 1246 | 1x | "non-null colwidths argument must have length ncol(x) (+ 1 if row labels are present and if it is a table) [", | 
| 1247 | 1x | cur_ncol, "], got length ", length(colwidths) | 
| 1248 | ) | |
| 1249 | } | |
| 1250 | 15x | mf_col_widths(mpf) <- colwidths | 
| 1251 | } | |
| 1252 | ||
| 1253 | 48x |   if (NROW(mf_cinfo(mpf)) == 0) { | 
| 1254 | ! | mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) | 
| 1255 | } | |
| 1256 | ||
| 1257 | 48x |   if (is.null(pg_size_spec)) { | 
| 1258 | 46x | pg_size_spec <- calc_lcpp( | 
| 1259 | 46x | page_type = page_type, | 
| 1260 | ## font_family = font_family, | |
| 1261 | ## font_size = font_size, | |
| 1262 | ## lineheight = lineheight, | |
| 1263 | 46x | fontspec = fontspec, | 
| 1264 | 46x | landscape = landscape, | 
| 1265 | 46x | pg_width = pg_width, | 
| 1266 | 46x | pg_height = pg_height, | 
| 1267 | 46x | margins = margins, | 
| 1268 | 46x | lpp = lpp, | 
| 1269 | 46x | cpp = cpp, | 
| 1270 | 46x | tf_wrap = tf_wrap, | 
| 1271 | 46x | max_width = max_width, | 
| 1272 | 46x | colwidths = colwidths, | 
| 1273 | 46x | inset = table_inset(mpf), | 
| 1274 | 46x | col_gap = col_gap | 
| 1275 | ) | |
| 1276 | } | |
| 1277 | ## this MUST always return a list, including list(obj) when | |
| 1278 | ## no forced pagination is needed! otherwise stuff breaks for things | |
| 1279 | ## based on s3 classes that are lists underneath!!! | |
| 1280 | 48x | fpags <- do_forced_paginate(obj) | 
| 1281 | ||
| 1282 | ## if we have more than one forced "page", | |
| 1283 | ## paginate each of them individually and return the result. | |
| 1284 | ## forced pagination is ***currently*** only vertical, so | |
| 1285 | ## we don't have to worry about divying up colwidths here, | |
| 1286 | ## but we will if we ever allow force_paginate to do horiz | |
| 1287 | ## pagination. | |
| 1288 | 48x |   if (length(fpags) > 1) { | 
| 1289 | # Correction for case we are entering here (page_by) | |
| 1290 | 1x |     if (!is.null(page_num)) { | 
| 1291 | ! | prov_footer(obj) <- head(prov_footer(obj), -1) | 
| 1292 | ! |       fpags <- lapply(fpags, function(x) { | 
| 1293 | ! | prov_footer(x) <- head(prov_footer(x), -1) | 
| 1294 | ! | x | 
| 1295 | }) | |
| 1296 | } | |
| 1297 | # XXX to merge with listings and avoid recursive (after PR #296) | |
| 1298 | 1x | deep_pag <- paginate_to_mpfs( # what about the other parameters? | 
| 1299 | 1x | fpags, | 
| 1300 | 1x | pg_size_spec = pg_size_spec, | 
| 1301 | 1x | colwidths = colwidths, | 
| 1302 | 1x | min_siblings = min_siblings, | 
| 1303 | 1x | nosplitin = nosplitin, | 
| 1304 | 1x | fontspec = fontspec, | 
| 1305 | 1x | verbose = verbose, | 
| 1306 | 1x | rep_cols = rep_cols, | 
| 1307 | 1x | page_num = page_num, | 
| 1308 | 1x | round_type = round_type | 
| 1309 | ) | |
| 1310 | 1x | return(deep_pag) | 
| 1311 | 47x |   } else if (has_page_title(fpags[[1]])) { | 
| 1312 | ! | obj <- fpags[[1]] | 
| 1313 | } | |
| 1314 | ||
| 1315 | ## we run into forced pagination, but life is short and this should work fine. | |
| 1316 | 47x | mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) | 
| 1317 | 47x | num_rep_cols(mpf) <- rep_cols | 
| 1318 | 47x |   if (is.null(colwidths)) { | 
| 1319 | ! | colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) | 
| 1320 | } | |
| 1321 | 47x | mf_col_widths(mpf) <- colwidths | 
| 1322 | 47x | mf_colgap(mpf) <- col_gap | 
| 1323 | ||
| 1324 | 47x | page_indices <- paginate_indices( | 
| 1325 | 47x | obj = obj, | 
| 1326 | ## page_type = page_type, | |
| 1327 | ## font_family = font_family, | |
| 1328 | ## font_size = font_size, | |
| 1329 | ## lineheight = lineheight, | |
| 1330 | ## landscape = landscape, | |
| 1331 | ## pg_width = pg_width, | |
| 1332 | ## pg_height = pg_height, | |
| 1333 | ## margins = margins, | |
| 1334 | 47x | pg_size_spec = pg_size_spec, | 
| 1335 | ## lpp = lpp, | |
| 1336 | ## cpp = cpp, | |
| 1337 | 47x | min_siblings = min_siblings, | 
| 1338 | 47x | nosplitin = nosplitin, | 
| 1339 | 47x | colwidths = colwidths, | 
| 1340 | 47x | tf_wrap = tf_wrap, | 
| 1341 | ## max_width = max_width, | |
| 1342 | 47x | rep_cols = rep_cols, | 
| 1343 | 47x | verbose = verbose, | 
| 1344 | 47x | col_gap = col_gap, | 
| 1345 | 47x | fontspec = fontspec, | 
| 1346 | 47x | round_type = round_type | 
| 1347 | ) | |
| 1348 | ||
| 1349 | 43x |   pagmats <- lapply(page_indices$pag_row_indices, function(ii) { | 
| 1350 | 89x | mpf_subset_rows(mpf, ii, keycols = .get_keycols_from_listing(obj)) | 
| 1351 | }) | |
| 1352 | ## these chunks now carry around their (correctly subset) col widths... | |
| 1353 | 43x |   res <- lapply(pagmats, function(matii) { | 
| 1354 | 89x |     lapply(page_indices$pag_col_indices, function(jj) { | 
| 1355 | 220x | mpf_subset_cols(matii, jj, keycols = .get_keycols_from_listing(obj)) | 
| 1356 | }) | |
| 1357 | }) | |
| 1358 | ||
| 1359 | 43x | res <- unlist(res, recursive = FALSE) | 
| 1360 | ||
| 1361 | # Adding page numbers if needed | |
| 1362 | 43x |   if (!is.null(page_num)) { | 
| 1363 | 14x | res <- .modify_footer_for_page_nums( | 
| 1364 | 14x | mf_list = res, | 
| 1365 | 14x | page_num_format = page_num, | 
| 1366 | 14x | current_cpp = pg_size_spec$cpp | 
| 1367 | ) | |
| 1368 | } | |
| 1369 | ||
| 1370 | 42x | res | 
| 1371 | } | |
| 1372 | ||
| 1373 | .modify_footer_for_page_nums <- function(mf_list, page_num_format, current_cpp) { | |
| 1374 | 17x | total_pages <- length(mf_list) | 
| 1375 | 17x |   page_str <- gsub("\\{n\\}", total_pages, page_num_format) | 
| 1376 | 17x | page_nums <- vapply( | 
| 1377 | 17x | seq_len(total_pages), | 
| 1378 | 17x |     function(x) { | 
| 1379 | 135x |       gsub("\\{i\\}", x, page_str) | 
| 1380 | }, | |
| 1381 | 17x | FUN.VALUE = character(1) | 
| 1382 | ) | |
| 1383 | 17x |   page_footer <- sprintf(paste0("%", current_cpp, "s"), page_nums) | 
| 1384 | 17x |   if (any(nchar(page_footer) > current_cpp)) { | 
| 1385 | 1x |     stop("Page numbering string (page_num) is too wide to fit the desired page size width (cpp).") | 
| 1386 | } | |
| 1387 | ||
| 1388 | 16x |   lapply(seq_along(mf_list), function(pg_i) { | 
| 1389 | 69x | prov_footer(mf_list[[pg_i]]) <- c(head(prov_footer(mf_list[[pg_i]]), -1), page_footer[pg_i]) | 
| 1390 | 69x | mf_list[[pg_i]] | 
| 1391 | }) | |
| 1392 | } | |
| 1393 | ||
| 1394 | # This works only with matrix_form objects | |
| 1395 | .is_listing_mf <- function(mf) { | |
| 1396 | 1143x | all(mf_rinfo(mf)$node_class == "listing_df") | 
| 1397 | } | |
| 1398 | ||
| 1399 | # Extended copy of get_keycols | |
| 1400 | .get_keycols_from_listing <- function(obj) { | |
| 1401 | 88x |   if (is(obj, "listing_df")) { | 
| 1402 | ! | names(which(sapply(obj, is, class2 = "listing_keycol"))) | 
| 1403 | 88x |   } else if (is(obj, "MatrixPrintForm") && .is_listing_mf(obj)) { | 
| 1404 | 52x | obj$listing_keycols | 
| 1405 |   } else { | |
| 1406 | 36x | NULL # table case | 
| 1407 | } | |
| 1408 | } | |
| 1409 | ||
| 1410 | #' @importFrom utils capture.output | |
| 1411 | #' @details | |
| 1412 | #' `diagnose_pagination` attempts pagination and then, regardless of success or failure, returns diagnostic | |
| 1413 | #' information about pagination attempts (if any) after each row and column. | |
| 1414 | #' | |
| 1415 | #' The diagnostics data reflects the final time the pagination algorithm evaluated a page break at the | |
| 1416 | #' specified location, regardless of how many times the position was assessed in total. | |
| 1417 | #' | |
| 1418 | #' To get information about intermediate attempts, perform pagination with `verbose = TRUE` and inspect | |
| 1419 | #' the messages in order. | |
| 1420 | #' | |
| 1421 | #' @importFrom utils capture.output | |
| 1422 | #' | |
| 1423 | #' @return | |
| 1424 | #' * `diagnose_pagination` returns a `list` containing: | |
| 1425 | #' | |
| 1426 | #'   \describe{ | |
| 1427 | #'     \item{`lpp_diagnostics`}{Diagnostic information regarding lines per page.} | |
| 1428 | #'     \item{`row_diagnostics`}{Basic information about rows, whether pagination was attempted | |
| 1429 | #' after each row, and the final result of such an attempt, if made.} | |
| 1430 | #'     \item{`cpp_diagnostics`}{Diagnostic information regarding columns per page.} | |
| 1431 | #'     \item{`col_diagnostics`}{Very basic information about leaf columns, whether pagination | |
| 1432 | #' was attempted after each leaf column, ad the final result of such attempts, if made.} | |
| 1433 | #' } | |
| 1434 | #' | |
| 1435 | #' @note | |
| 1436 | #' For `diagnose_pagination`, the column labels are not displayed in the `col_diagnostics` element | |
| 1437 | #' due to certain internal implementation details; rather the diagnostics are reported in terms of | |
| 1438 | #' absolute (leaf) column position. This is a known limitation, and may eventually be changed, but the | |
| 1439 | #' information remains useful as it is currently reported. | |
| 1440 | #' | |
| 1441 | #' `diagnose_pagination` is intended for interactive debugging use and *should not be programmed against*, | |
| 1442 | #' as the exact content and form of the verbose messages it captures and returns is subject to change. | |
| 1443 | #' | |
| 1444 | #' Because `diagnose_pagination` relies on `capture.output(type = "message")`, it cannot be used within the | |
| 1445 | #' `testthat` (and likely other) testing frameworks, and likely cannot be used within `knitr`/`rmarkdown` | |
| 1446 | #' contexts either, as this clashes with those systems' capture of messages. | |
| 1447 | #' | |
| 1448 | #' @examples | |
| 1449 | #' diagnose_pagination(mpf, pg_width = 5, pg_height = 3) | |
| 1450 | #' clws <- propose_column_widths(mpf) | |
| 1451 | #' clws[1] <- floor(clws[1] / 3) | |
| 1452 | #' dgnost <- diagnose_pagination(mpf, pg_width = 5, pg_height = 3, colwidths = clws) | |
| 1453 | #' try(diagnose_pagination(mpf, pg_width = 1)) # fails | |
| 1454 | #' | |
| 1455 | #' @rdname paginate_indices | |
| 1456 | #' @export | |
| 1457 | diagnose_pagination <- function(obj, | |
| 1458 | page_type = "letter", | |
| 1459 | font_family = "Courier", | |
| 1460 | font_size = 8, | |
| 1461 | lineheight = 1, | |
| 1462 | landscape = FALSE, | |
| 1463 | pg_width = NULL, | |
| 1464 | pg_height = NULL, | |
| 1465 | margins = c(top = .5, bottom = .5, left = .75, right = .75), | |
| 1466 | lpp = NA_integer_, | |
| 1467 | cpp = NA_integer_, | |
| 1468 | min_siblings = 2, | |
| 1469 | nosplitin = character(), | |
| 1470 | colwidths = propose_column_widths(matrix_form(obj, TRUE, round_type = round_type), | |
| 1471 | fontspec = fontspec, round_type = round_type | |
| 1472 | ), | |
| 1473 | tf_wrap = FALSE, | |
| 1474 | max_width = NULL, | |
| 1475 | indent_size = 2, | |
| 1476 | pg_size_spec = NULL, | |
| 1477 | rep_cols = num_rep_cols(obj), | |
| 1478 | col_gap = 3, | |
| 1479 | verbose = FALSE, | |
| 1480 | fontspec = font_spec( | |
| 1481 | font_family, | |
| 1482 | font_size, | |
| 1483 | lineheight | |
| 1484 | ), | |
| 1485 |                                 round_type = c("iec", "sas"), | |
| 1486 |                                 ...) { | |
| 1487 | 6x | new_dev <- open_font_dev(fontspec) | 
| 1488 | 6x |   if (new_dev) { | 
| 1489 | 4x | on.exit(close_font_dev()) | 
| 1490 | } | |
| 1491 | 6x | fpag <- do_forced_paginate(obj) | 
| 1492 | 6x |   if (length(fpag) > 1) { | 
| 1493 | 1x | return(lapply( | 
| 1494 | 1x | fpag, | 
| 1495 | 1x | diagnose_pagination, | 
| 1496 | 1x | page_type = page_type, | 
| 1497 | 1x | landscape = landscape, | 
| 1498 | 1x | pg_width = pg_width, | 
| 1499 | 1x | pg_height = pg_height, | 
| 1500 | 1x | margins = margins, | 
| 1501 | 1x | lpp = lpp, | 
| 1502 | 1x | cpp = cpp, | 
| 1503 | 1x | tf_wrap = tf_wrap, | 
| 1504 | 1x | max_width = max_width, | 
| 1505 | 1x | colwidths = colwidths, | 
| 1506 | 1x | col_gap = col_gap, | 
| 1507 | 1x | min_siblings = min_siblings, | 
| 1508 | 1x | nosplitin = nosplitin, | 
| 1509 | 1x | fontspec = fontspec, | 
| 1510 | 1x | round_type = round_type | 
| 1511 | )) | |
| 1512 | } | |
| 1513 | ||
| 1514 | 5x | mpf <- matrix_form(obj, TRUE, fontspec = fontspec) | 
| 1515 | 5x | msgres <- capture.output( | 
| 1516 |     { | |
| 1517 | 5x | tmp <- try( | 
| 1518 | 5x | paginate_to_mpfs( | 
| 1519 | 5x | obj, | 
| 1520 | 5x | page_type = page_type, | 
| 1521 | 5x | landscape = landscape, | 
| 1522 | 5x | pg_width = pg_width, | 
| 1523 | 5x | pg_height = pg_height, | 
| 1524 | 5x | margins = margins, | 
| 1525 | 5x | lpp = lpp, | 
| 1526 | 5x | cpp = cpp, | 
| 1527 | 5x | tf_wrap = tf_wrap, | 
| 1528 | 5x | max_width = max_width, | 
| 1529 | 5x | colwidths = colwidths, | 
| 1530 | 5x | col_gap = col_gap, | 
| 1531 | 5x | min_siblings = min_siblings, | 
| 1532 | 5x | nosplitin = nosplitin, | 
| 1533 | 5x | fontspec = fontspec, | 
| 1534 | 5x | round_type = round_type, | 
| 1535 | 5x | verbose = TRUE | 
| 1536 | ) | |
| 1537 | ) | |
| 1538 | }, | |
| 1539 | 5x | type = "message" | 
| 1540 | ) | |
| 1541 | 5x |   if (is(tmp, "try-error") && grepl("Width of row labels equal to or larger", tmp)) { | 
| 1542 | ! | cond <- attr(tmp, "condition") | 
| 1543 | ! | stop(conditionMessage(cond), call. = conditionCall(cond)) | 
| 1544 | } | |
| 1545 | ||
| 1546 | 5x |   lpp_diagnostic <- grep("^(Determining lines|Lines per page available).*$", msgres, value = TRUE) | 
| 1547 | 5x |   cpp_diagnostic <- unique(grep("^Adjusted characters per page.*$", msgres, value = TRUE)) | 
| 1548 | ||
| 1549 | 5x | mpf <- do_cell_fnotes_wrap( | 
| 1550 | 5x | mpf, | 
| 1551 | 5x | widths = colwidths, max_width = max_width, tf_wrap = tf_wrap, | 
| 1552 | 5x | fontspec = font_spec(font_family, font_size, lineheight) | 
| 1553 | ) | |
| 1554 | 5x | mpf <- mpf_infer_cinfo(mpf, colwidths = colwidths, fontspec = fontspec) | 
| 1555 | ||
| 1556 | 5x |   rownls <- grep("Checking pagination after row", msgres, fixed = TRUE) | 
| 1557 | 5x |   rownum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[rownls])) | 
| 1558 | 5x |   rowmsgs <- vapply(unique(rownum), function(ii) { | 
| 1559 | ! | idx <- max(which(rownum == ii)) | 
| 1560 | ! |     gsub("\\t[.]*", "", msgres[rownls[idx] + 1]) | 
| 1561 | }, "") | |
| 1562 | ||
| 1563 | 5x | msgdf <- data.frame( | 
| 1564 | 5x | abs_rownumber = unique(rownum), | 
| 1565 | 5x | final_pag_result = rowmsgs, stringsAsFactors = FALSE | 
| 1566 | ) | |
| 1567 | 5x |   rdf <- mf_rinfo(mpf)[, c("abs_rownumber", "label", "self_extent", "par_extent", "node_class")] | 
| 1568 | 5x | rdf$pag_attempted <- rdf$abs_rownumber %in% rownum | 
| 1569 | 5x | row_diagnose <- merge(rdf, msgdf, by = "abs_rownumber", all.x = TRUE) | 
| 1570 | ||
| 1571 | 5x |   colnls <- grep("Checking pagination after column", msgres, fixed = TRUE) | 
| 1572 | 5x |   colnum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[colnls])) | 
| 1573 | 5x |   colmsgs <- vapply(unique(colnum), function(ii) { | 
| 1574 | ! | idx <- max(which(colnum == ii)) | 
| 1575 | ! |     gsub("\\t[.]*", "", msgres[colnls[idx] + 1]) | 
| 1576 | }, "") | |
| 1577 | ||
| 1578 | 5x | colmsgdf <- data.frame( | 
| 1579 | 5x | abs_rownumber = unique(colnum), | 
| 1580 | 5x | final_pag_result = colmsgs, | 
| 1581 | 5x | stringsAsFactors = FALSE | 
| 1582 | ) | |
| 1583 | 5x |   cdf <- mf_cinfo(mpf)[, c("abs_rownumber", "self_extent")] | 
| 1584 | 5x | cdf$pag_attempted <- cdf$abs_rownumber %in% colnum | 
| 1585 | 5x | col_diagnose <- merge(cdf, colmsgdf, by = "abs_rownumber", all.x = TRUE) | 
| 1586 | 5x |   names(col_diagnose) <- gsub("^abs_rownumber$", "abs_colnumber", names(col_diagnose)) | 
| 1587 | 5x | list( | 
| 1588 | 5x | lpp_diagnostics = lpp_diagnostic, | 
| 1589 | 5x | row_diagnostics = row_diagnose, | 
| 1590 | 5x | cpp_diagnostics = cpp_diagnostic, | 
| 1591 | 5x | col_diagnostics = col_diagnose | 
| 1592 | ) | |
| 1593 | } | 
| 1 | ### This file defines the generics which make up the interface `formatters` offers. | |
| 2 | ### Defining methods for these generics for a new table-like class should be fully | |
| 3 | ### sufficient for hooking that class up to the `formatters` pagination and rendering | |
| 4 | ### machinery. | |
| 5 | ||
| 6 | #' Make row layout summary data frames for use during pagination | |
| 7 | #' | |
| 8 | #' All relevant information about table rows (e.g. indentations) is summarized in a `data.frame`. | |
| 9 | #' This function works **only** on `rtables` and `rlistings` objects, and not on their `print` counterparts | |
| 10 | #' (like [`MatrixPrintForm`]). | |
| 11 | #' | |
| 12 | #' @inheritParams open_font_dev | |
| 13 | #' @inheritParams format_value | |
| 14 | #' @param tt (`ANY`)\cr object representing the table-like object to be summarized. | |
| 15 | #' @param visible_only (`flag`)\cr should only visible aspects of the table structure be reflected | |
| 16 | #' in this summary. Defaults to `TRUE`. May not be supported by all methods. | |
| 17 | #' @param incontent (`flag`)\cr internal detail, do not set manually. | |
| 18 | #' @param repr_ext (`integer(1)`)\cr internal detail, do not set manually. | |
| 19 | #' @param repr_inds (`integer`)\cr internal detail, do not set manually. | |
| 20 | #' @param sibpos (`integer(1)`)\cr internal detail, do not set manually. | |
| 21 | #' @param nsibs (`integer(1)`)\cr internal detail, do not set manually. | |
| 22 | #' @param rownum (`numeric(1)`)\cr internal detail, do not set manually. | |
| 23 | #' @param indent (`integer(1)`)\cr internal detail, do not set manually. | |
| 24 | #' @param colwidths (`numeric`)\cr internal detail, do not set manually. | |
| 25 | #' @param path (`character`)\cr path to the (sub)table represented by `tt`. Defaults to `character()`. | |
| 26 | #' @param max_width (`numeric(1)` or `NULL`)\cr maximum width for title/footer materials. | |
| 27 | #' @param col_gap (`numeric(1)`)\cr the gap to be assumed between columns, in number of spaces with | |
| 28 | #' font specified by `fontspec`. | |
| 29 | #' | |
| 30 | #' @import methods | |
| 31 | #' @include matrix_form.R | |
| 32 | #' | |
| 33 | #' @details | |
| 34 | #' When `visible_only` is `TRUE` (the default), methods should return a `data.frame` with exactly one | |
| 35 | #' row per visible row in the table-like object. This is useful when reasoning about how a table will | |
| 36 | #' print, but does not reflect the full pathing space of the structure (though the paths which are given | |
| 37 | #' will all work as is). | |
| 38 | #' | |
| 39 | #' If supported, when `visible_only` is `FALSE`, every structural element of the table (in row-space) | |
| 40 | #' will be reflected in the returned `data.frame`, meaning the full pathing-space will be represented | |
| 41 | #' but some rows in the layout summary will not represent printed rows in the table as it is displayed. | |
| 42 | #' | |
| 43 | #' Most arguments beyond `tt` and `visible_only` are present so that `make_row_df` methods can call | |
| 44 | #' `make_row_df` recursively and retain information, and should not be set during a top-level call. | |
| 45 | #' | |
| 46 | #' @return A `data.frame` of row/column-structure information used by the pagination machinery. | |
| 47 | #' | |
| 48 | #' @note The technically present root tree node is excluded from the summary returned by | |
| 49 | #' both `make_row_df` and `make_col_df` (see relevant functions in`rtables`), as it is the | |
| 50 | #' row/column structure of `tt` and thus not useful for pathing or pagination. | |
| 51 | #' | |
| 52 | #' @examples | |
| 53 | #' # Expected error with matrix_form. For real case examples consult {rtables} documentation | |
| 54 | #' mf <- basic_matrix_form(iris) | |
| 55 | #' # make_row_df(mf) # Use table obj instead | |
| 56 | #' | |
| 57 | #' @export | |
| 58 | #' @name make_row_df | |
| 59 | setGeneric("make_row_df", function(tt, colwidths = NULL, visible_only = TRUE, | |
| 60 | rownum = 0, | |
| 61 | indent = 0L, | |
| 62 | path = character(), | |
| 63 | incontent = FALSE, | |
| 64 | repr_ext = 0L, | |
| 65 | repr_inds = integer(), | |
| 66 | sibpos = NA_integer_, | |
| 67 | nsibs = NA_integer_, | |
| 68 | max_width = NULL, | |
| 69 | fontspec = font_spec(), | |
| 70 | col_gap = 3L, | |
| 71 |                                    round_type = c("iec", "sas")) { | |
| 72 | 1x |   standardGeneric("make_row_df") | 
| 73 | }) | |
| 74 | ||
| 75 | #' @rdname make_row_df | |
| 76 | setMethod("make_row_df", "MatrixPrintForm", function(tt, colwidths = NULL, visible_only = TRUE, | |
| 77 | rownum = 0, | |
| 78 | indent = 0L, | |
| 79 | path = character(), | |
| 80 | incontent = FALSE, | |
| 81 | repr_ext = 0L, | |
| 82 | repr_inds = integer(), | |
| 83 | sibpos = NA_integer_, | |
| 84 | nsibs = NA_integer_, | |
| 85 | max_width = NULL, | |
| 86 | fontspec = font_spec(), | |
| 87 | col_gap = mf_colgap(tt) %||% 3L, | |
| 88 |                                                      round_type = c("iec", "sas")) { | |
| 89 | 1x | msg <- paste0( | 
| 90 | 1x |     "make_row_df can be used only on {rtables} table objects, and not on `matrix_form`-", | 
| 91 | 1x | "generated objects (MatrixPrintForm)." | 
| 92 | ) | |
| 93 | 1x | stop(msg) | 
| 94 | }) | |
| 95 | ||
| 96 | #' Transform `rtable` to a list of matrices which can be used for outputting | |
| 97 | #' | |
| 98 | #' Although `rtable`s are represented as a tree data structure when outputting the table to ASCII or HTML, | |
| 99 | #' it is useful to map the `rtable` to an in-between state with the formatted cells in a matrix form. | |
| 100 | #' | |
| 101 | #' @inheritParams make_row_df | |
| 102 | #' @inheritParams format_value | |
| 103 | #' @param obj (`ANY`)\cr object to be transformed into a ready-to-render form (a [`MatrixPrintForm`] object). | |
| 104 | #' @param indent_rownames (`flag`)\cr if `TRUE`, the row names column in the `strings` matrix of `obj` | |
| 105 | #' will have indented row names (strings pre-fixed). | |
| 106 | #' @param expand_newlines (`flag`)\cr whether the generated matrix form should expand rows whose values | |
| 107 | #' contain newlines into multiple 'physical' rows (as they will appear when rendered into ASCII). Defaults | |
| 108 | #' to `TRUE`. | |
| 109 | #' @param indent_size (`numeric(1)`)\cr number of spaces to be used per level of indent (if supported by | |
| 110 | #' the relevant method). Defaults to 2. | |
| 111 | #' | |
| 112 | #' @return A [`MatrixPrintForm`] classed list with an additional `nrow_header` attribute indicating the | |
| 113 | #' number of pseudo "rows" the column structure defines, with the following elements: | |
| 114 | #'   \describe{ | |
| 115 | #'     \item{`strings`}{The content, as it should be printed, of the top-left material, column headers, row | |
| 116 | #' labels, and cell values of `tt`.} | |
| 117 | #'     \item{`spans`}{The column-span information for each print-string in the strings matrix.} | |
| 118 | #'     \item{`aligns`}{The text alignment for each print-string in the strings matrix.} | |
| 119 | #'     \item{`display`}{Whether each print-string in the strings matrix should be printed or not.} | |
| 120 | #'     \item{`row_info`}{The `data.frame` generated by [basic_pagdf()].} | |
| 121 | #' } | |
| 122 | #' | |
| 123 | #' @export | |
| 124 | setGeneric("matrix_form", function(obj, | |
| 125 | indent_rownames = FALSE, | |
| 126 | expand_newlines = TRUE, | |
| 127 | indent_size = 2, | |
| 128 | fontspec = NULL, | |
| 129 | col_gap = NULL, | |
| 130 |                                    round_type = c("iec", "sas")) { | |
| 131 | 373x |   standardGeneric("matrix_form") | 
| 132 | }) | |
| 133 | ||
| 134 | ||
| 135 | #' @rdname matrix_form | |
| 136 | #' @export | |
| 137 | setMethod("matrix_form", "MatrixPrintForm", function(obj, | |
| 138 | indent_rownames = FALSE, | |
| 139 | expand_newlines = TRUE, | |
| 140 | indent_size = 2, | |
| 141 | fontspec = NULL, | |
| 142 | col_gap = NULL, | |
| 143 |                                                      round_type = c("iec", "sas")) { | |
| 144 | 373x |   if (!is.null(fontspec)) { | 
| 145 | 368x | mf_fontspec(obj) <- fontspec | 
| 146 | } | |
| 147 | 373x |   if (!is.null(col_gap) && !isTRUE(all.equal(col_gap, mf_colgap(obj)))) { | 
| 148 | ! | mf_colgap(obj) <- col_gap | 
| 149 | } | |
| 150 | 373x | obj | 
| 151 | }) | |
| 152 | ||
| 153 | # Generics for `toString` and helper functions ----------------------------------------------------------- | |
| 154 | ||
| 155 | ## this is where we will take word wrapping | |
| 156 | ## into account when it is added | |
| 157 | ## | |
| 158 | ## ALL calculations of vertical space for pagination | |
| 159 | ## purposes must go through nlines and divider_height!!!!!!!! | |
| 160 | ||
| 161 | ## this will be customizable someday. I have foreseen it (spooky noises) | |
| 162 | #' Divider height | |
| 163 | #' | |
| 164 | #' @param obj (`ANY`)\cr object. | |
| 165 | #' | |
| 166 | #' @return The height, in lines of text, of the divider between header and body. Currently | |
| 167 | #' returns `1L` for the default method. | |
| 168 | #' | |
| 169 | #' @examples | |
| 170 | #' divider_height(mtcars) | |
| 171 | #' | |
| 172 | #' @export | |
| 173 | 49x | setGeneric("divider_height", function(obj) standardGeneric("divider_height")) | 
| 174 | ||
| 175 | #' @rdname divider_height | |
| 176 | #' @export | |
| 177 | setMethod( | |
| 178 | "divider_height", "ANY", | |
| 179 | 49x | function(obj) 1L | 
| 180 | ) | |
| 181 | ||
| 182 | # nlines --------------------------------------------------------------- | |
| 183 | ||
| 184 | #' Number of lines required to print a value | |
| 185 | #' | |
| 186 | #' @inheritParams open_font_dev | |
| 187 | #' @param x (`ANY`)\cr the object to be printed. | |
| 188 | #' @param colwidths (`numeric`)\cr column widths (if necessary). Principally used in `rtables`' | |
| 189 | #' method. | |
| 190 | #' @param max_width (`numeric(1)`)\cr width that strings should be wrapped to when | |
| 191 | #' determining how many lines they require. | |
| 192 | #' @param col_gap (`numeric(1)`)\cr width of gap between columns in number of spaces. | |
| 193 | #' Only used by methods which must calculate span widths after wrapping. | |
| 194 | #' | |
| 195 | #' @return The number of lines needed to render the object `x`. | |
| 196 | #' | |
| 197 | #' @export | |
| 198 | setGeneric( | |
| 199 | "nlines", | |
| 200 | ## XXX TODO come back and add fontspec default value once not having | |
| 201 | ## it has found all the disconnection breakages | |
| 202 | 50737x |   function(x, colwidths = NULL, max_width = NULL, fontspec, col_gap = NULL) standardGeneric("nlines") | 
| 203 | ) | |
| 204 | ||
| 205 | ## XXX beware. I think it is dangerous | |
| 206 | #' @export | |
| 207 | #' @rdname nlines | |
| 208 | setMethod( | |
| 209 | "nlines", "list", | |
| 210 |   function(x, colwidths, max_width, fontspec, col_gap = NULL) { | |
| 211 | 2x |     if (length(x) == 0) { | 
| 212 | 1x | 0L | 
| 213 |     } else { | |
| 214 | 1x | sum(unlist(vapply(x, nlines, NA_integer_, colwidths = colwidths, max_width = max_width, fontspec = fontspec))) | 
| 215 | } | |
| 216 | } | |
| 217 | ) | |
| 218 | ||
| 219 | #' @export | |
| 220 | #' @rdname nlines | |
| 221 | setMethod("nlines", "NULL", function(x, colwidths, max_width, fontspec, col_gap = NULL) 0L) | |
| 222 | ||
| 223 | #' @export | |
| 224 | #' @rdname nlines | |
| 225 | setMethod("nlines", "character", function(x, colwidths, max_width, fontspec, col_gap = NULL) { | |
| 226 | 50734x | splstr <- strsplit(x, "\n", fixed = TRUE) | 
| 227 | 50734x |   if (length(x) == 0) { | 
| 228 | 1x | return(0L) | 
| 229 | } | |
| 230 | ||
| 231 | 50733x | sum(vapply(splstr, | 
| 232 | 50733x |     function(xi, max_width) { | 
| 233 | 50782x |       if (length(xi) == 0) { | 
| 234 | 1522x | 1L | 
| 235 | 49260x |       } else if (length(max_width) == 0) { ## this happens with strsplit("", "\n") | 
| 236 | 49138x | length(xi) | 
| 237 |       } else { | |
| 238 | 122x | length(wrap_txt(xi, max_width, fontspec = fontspec)) | 
| 239 | } | |
| 240 | 50733x | }, 1L, | 
| 241 | 50733x | max_width = max_width | 
| 242 | )) | |
| 243 | }) | |
| 244 | ||
| 245 | #' Transform objects into string representations | |
| 246 | #' | |
| 247 | #' Transform a complex object into a string representation ready to be printed or written | |
| 248 | #' to a plain-text file. | |
| 249 | #' | |
| 250 | #' @param x (`ANY`)\cr object to be prepared for rendering. | |
| 251 | #' @param ... additional parameters passed to individual methods. | |
| 252 | #' | |
| 253 | #' @export | |
| 254 | #' @rdname tostring | |
| 255 | setGeneric("toString", function(x, ...) standardGeneric("toString")) | |
| 256 | ||
| 257 | ## preserve S3 behavior | |
| 258 | setMethod("toString", "ANY", base::toString) | |
| 259 | ||
| 260 |  | |
| 261 | #' | |
| 262 | #' Print an R object. See [print()]. | |
| 263 | #' | |
| 264 | #' @inheritParams base::print | |
| 265 | #' | |
| 266 | #' @rdname basemethods | |
| 267 | setMethod("print", "ANY", base::print) | |
| 268 | ||
| 269 | # General/"universal" property getter and setter generics and stubs -------------------------------------- | |
| 270 | ||
| 271 | #' Label, name, and format accessor generics | |
| 272 | #' | |
| 273 | #' Getters and setters for basic, relatively universal attributes of "table-like" objects. | |
| 274 | #' | |
| 275 | #' @param obj (`ANY`)\cr the object. | |
| 276 | #' @param value (`string` or `FormatSpec`)\cr the new value of the attribute. | |
| 277 | #' | |
| 278 | #' @return The name, format, or label of `obj` for getters, or `obj` after modification for setters. | |
| 279 | #' | |
| 280 | #' @export | |
| 281 | #' @name lab_name | |
| 282 | #' @aliases obj_name | |
| 283 | ||
| 284 | # obj_name --------------------------------------------------------------- | |
| 285 | ||
| 286 | ! | setGeneric("obj_name", function(obj) standardGeneric("obj_name")) | 
| 287 | ||
| 288 | #' @rdname lab_name | |
| 289 | #' @export | |
| 290 | ! | setGeneric("obj_name<-", function(obj, value) standardGeneric("obj_name<-")) | 
| 291 | ||
| 292 | # obj_label --------------------------------------------------------------- | |
| 293 | ||
| 294 | #' @seealso with_label | |
| 295 | #' @rdname lab_name | |
| 296 | #' @export | |
| 297 | 3x | setGeneric("obj_label", function(obj) standardGeneric("obj_label")) | 
| 298 | ||
| 299 | #' @rdname lab_name | |
| 300 | #' @param value character(1). The new label | |
| 301 | #' @export | |
| 302 | 2x | setGeneric("obj_label<-", function(obj, value) standardGeneric("obj_label<-")) | 
| 303 | ||
| 304 | #' @rdname lab_name | |
| 305 | #' @exportMethod obj_label | |
| 306 | 3x | setMethod("obj_label", "ANY", function(obj) attr(obj, "label")) | 
| 307 | ||
| 308 | #' @rdname lab_name | |
| 309 | #' @exportMethod obj_label<- | |
| 310 | setMethod( | |
| 311 | "obj_label<-", "ANY", | |
| 312 |   function(obj, value) { | |
| 313 | 2x | attr(obj, "label") <- value | 
| 314 | 2x | obj | 
| 315 | } | |
| 316 | ) | |
| 317 | ||
| 318 | # obj_format --------------------------------------------------------------- | |
| 319 | ||
| 320 | #' @rdname lab_name | |
| 321 | #' @export | |
| 322 | 301x | setGeneric("obj_format", function(obj) standardGeneric("obj_format")) | 
| 323 | ||
| 324 | ## this covers rcell, etc | |
| 325 | #' @rdname lab_name | |
| 326 | #' @exportMethod obj_format | |
| 327 | 299x | setMethod("obj_format", "ANY", function(obj) attr(obj, "format", exact = TRUE)) | 
| 328 | ||
| 329 | #' @rdname lab_name | |
| 330 | #' @export | |
| 331 | 2x | setMethod("obj_format", "fmt_config", function(obj) obj@format) | 
| 332 | ||
| 333 | #' @export | |
| 334 | #' @rdname lab_name | |
| 335 | 3x | setGeneric("obj_format<-", function(obj, value) standardGeneric("obj_format<-")) | 
| 336 | ||
| 337 | ## this covers rcell, etc | |
| 338 | #' @exportMethod obj_format<- | |
| 339 | #' @rdname lab_name | |
| 340 | setMethod("obj_format<-", "ANY", function(obj, value) { | |
| 341 | 2x | attr(obj, "format") <- value | 
| 342 | 2x | obj | 
| 343 | }) | |
| 344 | ||
| 345 | #' @rdname lab_name | |
| 346 | #' @export | |
| 347 | setMethod("obj_format<-", "fmt_config", function(obj, value) { | |
| 348 | 1x | obj@format <- value | 
| 349 | 1x | obj | 
| 350 | }) | |
| 351 | ||
| 352 | # obj_na_str --------------------------------------------------------------- | |
| 353 | ||
| 354 | #' @rdname lab_name | |
| 355 | #' @export | |
| 356 | 3x | setGeneric("obj_na_str", function(obj) standardGeneric("obj_na_str")) | 
| 357 | ||
| 358 | #' @rdname lab_name | |
| 359 | #' @exportMethod obj_na_str | |
| 360 | 1x | setMethod("obj_na_str", "ANY", function(obj) attr(obj, "format_na_str", exact = TRUE)) | 
| 361 | ||
| 362 | #' @rdname lab_name | |
| 363 | #' @export | |
| 364 | 2x | setMethod("obj_na_str", "fmt_config", function(obj) obj@format_na_str) | 
| 365 | ||
| 366 | #' @rdname lab_name | |
| 367 | #' @export | |
| 368 | 2x | setGeneric("obj_na_str<-", function(obj, value) standardGeneric("obj_na_str<-")) | 
| 369 | ||
| 370 | #' @exportMethod obj_na_str<- | |
| 371 | #' @rdname lab_name | |
| 372 | setMethod("obj_na_str<-", "ANY", function(obj, value) { | |
| 373 | 1x | attr(obj, "format_na_str") <- value | 
| 374 | 1x | obj | 
| 375 | }) | |
| 376 | ||
| 377 | #' @rdname lab_name | |
| 378 | #' @export | |
| 379 | setMethod("obj_na_str<-", "fmt_config", function(obj, value) { | |
| 380 | 1x | obj@format_na_str <- value | 
| 381 | 1x | obj | 
| 382 | }) | |
| 383 | ||
| 384 | # obj_align --------------------------------------------------------------- | |
| 385 | ||
| 386 | #' @rdname lab_name | |
| 387 | #' @export | |
| 388 | 3x | setGeneric("obj_align", function(obj) standardGeneric("obj_align")) | 
| 389 | ||
| 390 | #' @rdname lab_name | |
| 391 | #' @exportMethod obj_align | |
| 392 | 1x | setMethod("obj_align", "ANY", function(obj) attr(obj, "align", exact = TRUE)) | 
| 393 | ||
| 394 | #' @rdname lab_name | |
| 395 | #' @export | |
| 396 | 2x | setMethod("obj_align", "fmt_config", function(obj) obj@align) | 
| 397 | ||
| 398 | #' @rdname lab_name | |
| 399 | #' @export | |
| 400 | 2x | setGeneric("obj_align<-", function(obj, value) standardGeneric("obj_align<-")) | 
| 401 | ||
| 402 | #' @exportMethod obj_align<- | |
| 403 | #' @rdname lab_name | |
| 404 | setMethod("obj_align<-", "ANY", function(obj, value) { | |
| 405 | 1x | attr(obj, "align") <- value | 
| 406 | 1x | obj | 
| 407 | }) | |
| 408 | ||
| 409 | #' @rdname lab_name | |
| 410 | #' @export | |
| 411 | setMethod("obj_align<-", "fmt_config", function(obj, value) { | |
| 412 | 1x | obj@align <- value | 
| 413 | 1x | obj | 
| 414 | }) | |
| 415 | ||
| 416 | # main_title --------------------------------------------------------------- | |
| 417 | ||
| 418 | #' General title and footer accessors | |
| 419 | #' | |
| 420 | #' @param obj (`ANY`)\cr object to extract information from. | |
| 421 | #' | |
| 422 | #' @return A character scalar (`main_title`), character vector (`main_footer`), or | |
| 423 | #' vector of length zero or more (`subtitles`, `page_titles`, `prov_footer`) containing | |
| 424 | #' the relevant title/footer contents. | |
| 425 | #' | |
| 426 | #' @export | |
| 427 | #' @rdname title_footer | |
| 428 | 501x | setGeneric("main_title", function(obj) standardGeneric("main_title")) | 
| 429 | ||
| 430 | #' @export | |
| 431 | #' @rdname title_footer | |
| 432 | setMethod( | |
| 433 | "main_title", "MatrixPrintForm", | |
| 434 | 501x | function(obj) obj$main_title | 
| 435 | ) | |
| 436 | ||
| 437 | ##' @rdname title_footer | |
| 438 | ##' @export | |
| 439 | 17x | setGeneric("main_title<-", function(obj, value) standardGeneric("main_title<-")) | 
| 440 | ||
| 441 | ##' @rdname title_footer | |
| 442 | ##' @export | |
| 443 | setMethod( | |
| 444 | "main_title<-", "MatrixPrintForm", | |
| 445 |   function(obj, value) { | |
| 446 | 17x | obj$main_title <- value | 
| 447 | 17x | obj | 
| 448 | } | |
| 449 | ) | |
| 450 | ||
| 451 | # subtitles --------------------------------------------------------------- | |
| 452 | ||
| 453 | #' @export | |
| 454 | #' @rdname title_footer | |
| 455 | 500x | setGeneric("subtitles", function(obj) standardGeneric("subtitles")) | 
| 456 | ||
| 457 | #' @export | |
| 458 | #' @rdname title_footer | |
| 459 | setMethod( | |
| 460 | "subtitles", "MatrixPrintForm", | |
| 461 | 500x | function(obj) obj$subtitles | 
| 462 | ) | |
| 463 | ||
| 464 | ##' @rdname title_footer | |
| 465 | ##' @export | |
| 466 | 14x | setGeneric("subtitles<-", function(obj, value) standardGeneric("subtitles<-")) | 
| 467 | ||
| 468 | ##' @rdname title_footer | |
| 469 | ##' @export | |
| 470 | setMethod( | |
| 471 | "subtitles<-", "MatrixPrintForm", | |
| 472 |   function(obj, value) { | |
| 473 | 14x | obj$subtitles <- value | 
| 474 | 14x | obj | 
| 475 | } | |
| 476 | ) | |
| 477 | ||
| 478 | # page_titles --------------------------------------------------------------- | |
| 479 | ||
| 480 | #' @export | |
| 481 | #' @rdname title_footer | |
| 482 | 541x | setGeneric("page_titles", function(obj) standardGeneric("page_titles")) | 
| 483 | ||
| 484 | #' @export | |
| 485 | #' @rdname title_footer | |
| 486 | setMethod( | |
| 487 | "page_titles", "MatrixPrintForm", | |
| 488 | 541x | function(obj) obj$page_titles | 
| 489 | ) | |
| 490 | #' @rdname title_footer | |
| 491 | #' @export | |
| 492 | ! | setMethod("page_titles", "ANY", function(obj) NULL) | 
| 493 | ||
| 494 | ##' @rdname title_footer | |
| 495 | ##' @export | |
| 496 | 2x | setGeneric("page_titles<-", function(obj, value) standardGeneric("page_titles<-")) | 
| 497 | ||
| 498 | #' @export | |
| 499 | #' @rdname title_footer | |
| 500 | setMethod( | |
| 501 | "page_titles<-", "MatrixPrintForm", | |
| 502 |   function(obj, value) { | |
| 503 | 2x |     if (!is.character(value)) { | 
| 504 | ! |       stop("page titles must be in the form of a character vector, got object of class ", class(value)) | 
| 505 | } | |
| 506 | 2x | obj$page_titles <- value | 
| 507 | 2x | obj | 
| 508 | } | |
| 509 | ) | |
| 510 | ||
| 511 | # main_footer --------------------------------------------------------------- | |
| 512 | ||
| 513 | #' @export | |
| 514 | #' @rdname title_footer | |
| 515 | 481x | setGeneric("main_footer", function(obj) standardGeneric("main_footer")) | 
| 516 | ||
| 517 | #' @export | |
| 518 | #' @rdname title_footer | |
| 519 | setMethod( | |
| 520 | "main_footer", "MatrixPrintForm", | |
| 521 | 481x | function(obj) obj$main_footer | 
| 522 | ) | |
| 523 | ||
| 524 | #' @rdname title_footer | |
| 525 | #' @param value character. New value. | |
| 526 | #' @export | |
| 527 | 280x | setGeneric("main_footer<-", function(obj, value) standardGeneric("main_footer<-")) | 
| 528 | ||
| 529 | #' @export | |
| 530 | #' @rdname title_footer | |
| 531 | setMethod( | |
| 532 | "main_footer<-", "MatrixPrintForm", | |
| 533 |   function(obj, value) { | |
| 534 | 280x |     if (!is.character(value)) { | 
| 535 | ! |       stop("main footer must be a character vector. Got object of class ", class(value)) | 
| 536 | } | |
| 537 | 280x | obj$main_footer <- value | 
| 538 | 280x | obj | 
| 539 | } | |
| 540 | ) | |
| 541 | ||
| 542 | # prov_footer --------------------------------------------------------------- | |
| 543 | ||
| 544 | #' @export | |
| 545 | #' @rdname title_footer | |
| 546 | 606x | setGeneric("prov_footer", function(obj) standardGeneric("prov_footer")) | 
| 547 | ||
| 548 | #' @export | |
| 549 | #' @rdname title_footer | |
| 550 | setMethod( | |
| 551 | "prov_footer", "MatrixPrintForm", | |
| 552 | 606x | function(obj) obj$prov_footer | 
| 553 | ) | |
| 554 | ||
| 555 | #' @rdname title_footer | |
| 556 | #' @export | |
| 557 | 363x | setGeneric("prov_footer<-", function(obj, value) standardGeneric("prov_footer<-")) | 
| 558 | ||
| 559 | #' @export | |
| 560 | #' @rdname title_footer | |
| 561 | setMethod( | |
| 562 | "prov_footer<-", "MatrixPrintForm", | |
| 563 |   function(obj, value) { | |
| 564 | 363x |     if (!is.character(value)) { | 
| 565 | ! |       stop("provenance footer must be a character vector. Got object of class ", class(value)) | 
| 566 | } | |
| 567 | 363x | obj$prov_footer <- value | 
| 568 | 363x | obj | 
| 569 | } | |
| 570 | ) | |
| 571 | ||
| 572 | #' @rdname title_footer | |
| 573 | #' @export | |
| 574 | 1x | all_footers <- function(obj) c(main_footer(obj), prov_footer(obj)) | 
| 575 | ||
| 576 | #' @rdname title_footer | |
| 577 | #' @export | |
| 578 | 493x | all_titles <- function(obj) c(main_title(obj), subtitles(obj), page_titles(obj)) | 
| 579 | ||
| 580 | # table_inset --------------------------------------------------------------- | |
| 581 | ||
| 582 | #' Access or (recursively) set table inset | |
| 583 | #' | |
| 584 | #' Table inset is the amount of characters that the body of a table, referential footnotes, and | |
| 585 | #' main footer material are inset from the left-alignment of the titles and provenance | |
| 586 | #' footer materials. | |
| 587 | #' | |
| 588 | #' @param obj (`ANY`)\cr object to get or (recursively if necessary) set table inset for. | |
| 589 | #' @param value (`string`)\cr string to use as new header/body separator. | |
| 590 | #' | |
| 591 | #' @return | |
| 592 | #' * `table_inset` returns the integer value that the table body (including column heading | |
| 593 | #' information and section dividers), referential footnotes, and main footer should be inset | |
| 594 | #' from the left alignment of the titles and provenance footers during rendering. | |
| 595 | #' * `table_inset<-` returns `obj` with the new table_inset value applied recursively to it and | |
| 596 | #' all its subtables. | |
| 597 | #' | |
| 598 | #' @export | |
| 599 | 460x | setGeneric("table_inset", function(obj) standardGeneric("table_inset")) | 
| 600 | ||
| 601 | #' @rdname table_inset | |
| 602 | #' @export | |
| 603 | setMethod( | |
| 604 | "table_inset", "MatrixPrintForm", | |
| 605 | 460x | function(obj) obj$table_inset | 
| 606 | ) | |
| 607 | ||
| 608 | #' @rdname table_inset | |
| 609 | #' @export | |
| 610 | 4x | setGeneric("table_inset<-", function(obj, value) standardGeneric("table_inset<-")) | 
| 611 | ||
| 612 | #' @rdname table_inset | |
| 613 | #' @export | |
| 614 | setMethod( | |
| 615 | "table_inset<-", "MatrixPrintForm", | |
| 616 |   function(obj, value) { | |
| 617 | 4x | newval <- as.integer(value) | 
| 618 | 4x |     if (is.na(newval) || newval < 0) { | 
| 619 | 1x |       stop("Got invalid value for table_inset: ", newval) | 
| 620 | } | |
| 621 | 3x | obj$table_inset <- newval | 
| 622 | 3x | obj | 
| 623 | } | |
| 624 | ) | |
| 625 | ||
| 626 | # do_forced_paginate --------------------------------------------------------------- | |
| 627 | ||
| 628 | #' Generic for performing "forced" pagination | |
| 629 | #' | |
| 630 | #' Forced pagination is pagination which happens regardless of position on page. The object | |
| 631 | #' is expected to have all information necessary to locate such page breaks, and the | |
| 632 | #' `do_forced_pag` method is expected to fully perform those paginations. | |
| 633 | #' | |
| 634 | #' @param obj (`ANY`)\cr object to be paginated. The `ANY` method simply returns a list of | |
| 635 | #' length one, containing `obj`. | |
| 636 | #' | |
| 637 | #' @return A list of sub-objects, which will be further paginated by the standard pagination | |
| 638 | #' algorithm. | |
| 639 | #' | |
| 640 | #' @export | |
| 641 | 104x | setGeneric("do_forced_paginate", function(obj) standardGeneric("do_forced_paginate")) | 
| 642 | ||
| 643 | #' @export | |
| 644 | #' @rdname do_forced_paginate | |
| 645 | 101x | setMethod("do_forced_paginate", "ANY", function(obj) list(obj)) | 
| 646 | ||
| 647 | # num_rep_cols --------------------------------------------------------------- | |
| 648 | ||
| 649 | #' Number of repeated columns | |
| 650 | #' | |
| 651 | #' When called on a table-like object using the formatters framework, this method returns the | |
| 652 | #' number of columns which are mandatorily repeated after each horizontal pagination. | |
| 653 | #' | |
| 654 | #' Absent a class-specific method, this function returns 0, indicating no always-repeated columns. | |
| 655 | #' | |
| 656 | #' @param obj (`ANY`)\cr a table-like object. | |
| 657 | #' | |
| 658 | #' @return An integer. | |
| 659 | #' | |
| 660 | #' @note This number *does not* include row labels, the repetition of which is handled separately. | |
| 661 | #' | |
| 662 | #' @examples | |
| 663 | #' mpf <- basic_matrix_form(mtcars) | |
| 664 | #' num_rep_cols(mpf) | |
| 665 | #' lmpf <- basic_listing_mf(mtcars) | |
| 666 | #' num_rep_cols(lmpf) | |
| 667 | #' | |
| 668 | #' @export | |
| 669 | 662x | setGeneric("num_rep_cols", function(obj) standardGeneric("num_rep_cols")) | 
| 670 | ||
| 671 | #' @export | |
| 672 | #' @rdname num_rep_cols | |
| 673 | ! | setMethod("num_rep_cols", "ANY", function(obj) 0L) | 
| 674 | ||
| 675 | #' @export | |
| 676 | #' @rdname num_rep_cols | |
| 677 | 662x | setMethod("num_rep_cols", "MatrixPrintForm", function(obj) obj$num_rep_cols) | 
| 678 | ||
| 679 | #' @export | |
| 680 | #' @param value (`numeric(1)`)\cr the new number of columns to repeat. | |
| 681 | #' @rdname num_rep_cols | |
| 682 | 144x | setGeneric("num_rep_cols<-", function(obj, value) standardGeneric("num_rep_cols<-")) | 
| 683 | ||
| 684 | #' @export | |
| 685 | #' @rdname num_rep_cols | |
| 686 | ! | setMethod("num_rep_cols<-", "ANY", function(obj, value) stop("No num_rep_cols<- method for class ", class(obj))) | 
| 687 | ||
| 688 | #' @export | |
| 689 | #' @rdname num_rep_cols | |
| 690 | setMethod("num_rep_cols<-", "MatrixPrintForm", function(obj, value) { | |
| 691 | 144x | obj <- mf_update_cinfo(obj, colwidths = NULL, rep_cols = value) | 
| 692 | 144x | obj | 
| 693 | }) | |
| 694 | ||
| 695 | # header_section_div ----------------------------------------------------------- | |
| 696 | ||
| 697 | #' @keywords internal | |
| 698 | 156x | setGeneric("header_section_div", function(obj) standardGeneric("header_section_div")) | 
| 699 | ||
| 700 | #' @keywords internal | |
| 701 | setMethod( | |
| 702 | "header_section_div", "MatrixPrintForm", | |
| 703 | 156x | function(obj) obj$header_section_div | 
| 704 | ) | |
| 705 | ||
| 706 | #' @keywords internal | |
| 707 | ! | setGeneric("header_section_div<-", function(obj, value) standardGeneric("header_section_div<-")) | 
| 708 | ||
| 709 | #' @keywords internal | |
| 710 | setMethod( | |
| 711 | "header_section_div<-", "MatrixPrintForm", | |
| 712 |   function(obj, value) { | |
| 713 | ! | obj$header_section_div <- value | 
| 714 | ! | obj | 
| 715 | } | |
| 716 | ) | |
| 717 | ||
| 718 | # horizontal_sep --------------------------------------------------------------- | |
| 719 | ||
| 720 | #' @keywords internal | |
| 721 | 121x | setGeneric("horizontal_sep", function(obj) standardGeneric("horizontal_sep")) | 
| 722 | ||
| 723 | #' @keywords internal | |
| 724 | setMethod( | |
| 725 | "horizontal_sep", "MatrixPrintForm", | |
| 726 | 121x | function(obj) obj$horizontal_sep | 
| 727 | ) | |
| 728 | ||
| 729 | #' @keywords internal | |
| 730 | 1x | setGeneric("horizontal_sep<-", function(obj, value) standardGeneric("horizontal_sep<-")) | 
| 731 | ||
| 732 | #' @keywords internal | |
| 733 | setMethod( | |
| 734 | "horizontal_sep<-", "MatrixPrintForm", | |
| 735 |   function(obj, value) { | |
| 736 | 1x | obj$horizontal_sep <- value | 
| 737 | 1x | obj | 
| 738 | } | |
| 739 | ) | 
| 1 | ## state completely sucks and I hate it, but | |
| 2 | ## we need a pdf device open to calculate the | |
| 3 | ## print width of strings, and we can't be opening | |
| 4 | ## a new one every time we want to | |
| 5 | font_dev_state <- new.env() | |
| 6 | font_dev_state$open <- FALSE | |
| 7 | font_dev_state$fontspec <- list() | |
| 8 | font_dev_state$spacewidth <- NA_real_ | |
| 9 | font_dev_state$ismonospace <- NA | |
| 10 | font_dev_state$max_ratio <- NA_real_ | |
| 11 | font_dev_state$dev_num <- NA_integer_ | |
| 12 | font_dev_state$debug_active <- FALSE | |
| 13 | ||
| 14 | ||
| 15 | cwidth_inches_unsafe <- function(x) { | |
| 16 | 1136x | convertWidth(unit(1, "strwidth", x), "inches", valueOnly = TRUE) | 
| 17 | } | |
| 18 | ||
| 19 | ## returns whether it opened a new device or not | |
| 20 | #' Activate font state | |
| 21 | #' | |
| 22 | #' @param fontspec (`font_spec`)\cr a font_spec object specifying the font information to use for | |
| 23 | #' calculating string widths and heights, as returned by [font_spec()]. | |
| 24 | #' @param silent (`logical(1)`)\cr If `FALSE`, the default, a warning will be | |
| 25 | #' emitted if this function switches away from an active graphics device. | |
| 26 | #' | |
| 27 | #' @details The font device state is an environment with | |
| 28 | #' four variables guaranteed to be set: | |
| 29 | #' | |
| 30 | #' \describe{ | |
| 31 | #'   \item{`open`}{(`logical(1)`)\cr whether a device is already open with font info} | |
| 32 | #'   \item{`fontspec`}{(`font_spec`)\cr the font specification, if any, that is currently active (`list()` if none is).} | |
| 33 | #'   \item{`spacewidth`}{(`numeric(1)`)\cr the width of the space character in the currently active font.} | |
| 34 | #'   \item{`ismonospace`}{(`logical(1)`)\cr whether the specified font is monospaced.} | |
| 35 | #' } | |
| 36 | #' | |
| 37 | #' `open_font_dev` opens a pdf device with the specified font | |
| 38 | #' only if there is not one currently open with the same font. | |
| 39 | #' If a new device is opened, it caches `spacewidth` and | |
| 40 | #' `ismonospace` for use in `nchar_ttype`). | |
| 41 | #' | |
| 42 | #' `close_font_dev` closes any open font state device | |
| 43 | #' and clears the cached values. | |
| 44 | #' | |
| 45 | #' `debug_font_dev` and `undebug_font_dev` activate and deactivate, respectively, | |
| 46 | #' logging of where in the call stack font devices are being opened. | |
| 47 | #' | |
| 48 | #' @return | |
| 49 | #' - `open_font_dev` returns a logical value indicating whether a *new* pdf device was opened. | |
| 50 | #' - `close_font_dev`, `debug_font_dev` and `undebug_font_dev` return `NULL`. | |
| 51 | #' | |
| 52 | #' In all cases the value is returned invisibly. | |
| 53 | #' | |
| 54 | #' @examples | |
| 55 | #' open_font_dev(font_spec("Times")) | |
| 56 | #' nchar_ttype("Hiya there", font_spec("Times")) | |
| 57 | #' close_font_dev() | |
| 58 | #' | |
| 59 | #' @export | |
| 60 | open_font_dev <- function(fontspec, silent = FALSE) { | |
| 61 | 82300x |   if (is.null(fontspec)) { | 
| 62 | 1x | return(invisible(FALSE)) | 
| 63 | 82298x |   } else if (font_dev_is_open()) { | 
| 64 | 81788x |     if (identical(font_dev_state$fontspec, fontspec)) { | 
| 65 | 81787x |       if (!silent && dev.cur() != font_dev_state$dev_num) { | 
| 66 | 1x | warning( | 
| 67 | 1x | "formatters is switching to the font state graphics device ", | 
| 68 | 1x | "to perform string width calculations. You may need to switch ", | 
| 69 | 1x | "to your currently open graphics device, depending on whether ", | 
| 70 | 1x | "the font device is closed and what other devices you have open." | 
| 71 | ) | |
| 72 | 1x | dev.set(font_dev_state$dev_num) | 
| 73 | } | |
| 74 | 81787x | return(invisible(FALSE)) | 
| 75 |     } else { | |
| 76 | 1x | close_font_dev() | 
| 77 | } | |
| 78 | } | |
| 79 | 511x |   if (font_dev_state$debug_active && !font_dev_is_open()) { ## call debug_font_dev beforehand to get debugging info to helplocate places which aren't receiving/using the state properly # nolint | 
| 80 | ## dump the call stack any time we have cache misses | |
| 81 | ## and have to open a completely new font state device | |
| 82 | 1x | scalls <- sys.calls() | 
| 83 | 1x | msg <- sapply( | 
| 84 | 1x | scalls[2:length(scalls)], | 
| 85 | 1x |       function(sci) { | 
| 86 | 42x | toret <- deparse(sci[[1]], nlines = 3) | 
| 87 | 42x |         if (substr(toret[1], 1, 8) == "function") { | 
| 88 | ! | toret <- "anon function" | 
| 89 | } | |
| 90 | 42x | toret | 
| 91 | } | |
| 92 | ) | |
| 93 | 1x |     message(paste("\n***** START font dev debugging dump *****\n", | 
| 94 | 1x | paste(msg, collapse = " -> "), | 
| 95 | 1x | paste(capture.output(print(fontspec)), collapse = "\n"), | 
| 96 | 1x | sep = "\n" | 
| 97 | )) | |
| 98 | } | |
| 99 | 511x | tmppdf <- tempfile(fileext = ".pdf") | 
| 100 | 511x | pdf(tmppdf) | 
| 101 | 511x | grid.newpage() | 
| 102 | 511x | gp <- gpar_from_fspec(fontspec) | 
| 103 | 511x | pushViewport(plotViewport(gp = gp)) | 
| 104 | 511x |   spcwidth <- cwidth_inches_unsafe(" ") | 
| 105 | 511x |   assign("open", TRUE, envir = font_dev_state) | 
| 106 | 511x |   assign("fontspec", fontspec, envir = font_dev_state) | 
| 107 | 511x |   assign("spacewidth", spcwidth, envir = font_dev_state) | 
| 108 | 511x |   assign("ismonospace", spcwidth == cwidth_inches_unsafe("W"), | 
| 109 | 511x | envir = font_dev_state | 
| 110 | ) | |
| 111 | 511x |   assign("dev_num", dev.cur(), | 
| 112 | 511x | envir = font_dev_state | 
| 113 | ) | |
| 114 | 511x | invisible(TRUE) | 
| 115 | } | |
| 116 | ||
| 117 | #' @rdname open_font_dev | |
| 118 | #' @export | |
| 119 | close_font_dev <- function() { | |
| 120 | 511x |   if (font_dev_state$open) { | 
| 121 | 511x | dev.off(font_dev_state$dev_num) | 
| 122 | 511x |     assign("open", FALSE, envir = font_dev_state) | 
| 123 | 511x |     assign("fontspec", list(), envir = font_dev_state) | 
| 124 | 511x |     assign("spacewidth", NA_real_, envir = font_dev_state) | 
| 125 | 511x |     assign("ismonospace", NA, envir = font_dev_state) | 
| 126 | 511x |     assign("dev_num", NA_integer_, envir = font_dev_state) | 
| 127 | } | |
| 128 | 511x | invisible(NULL) | 
| 129 | } | |
| 130 | ||
| 131 | #' @rdname open_font_dev | |
| 132 | #' @export | |
| 133 | debug_font_dev <- function() { | |
| 134 | 1x |   message("debugging font device swapping. call undebug_font_dev() to turn debugging back off.") | 
| 135 | 1x | font_dev_state$debug_active <- TRUE | 
| 136 | 1x | invisible(NULL) | 
| 137 | } | |
| 138 | ||
| 139 | #' @rdname open_font_dev | |
| 140 | #' @export | |
| 141 | undebug_font_dev <- function() { | |
| 142 | 1x |   message("no longer debugging font device swapping.") | 
| 143 | 1x | font_dev_state$debug_active <- FALSE | 
| 144 | 1x | invisible(NULL) | 
| 145 | } | |
| 146 | ||
| 147 | ||
| 148 | ## can only be called when font_dev_state$open is TRUE | |
| 149 | get_space_width <- function() { | |
| 150 | 25x |   if (!font_dev_is_open()) { | 
| 151 | 1x | stop( | 
| 152 | 1x | "get_space_width called when font dev state is not open. ", | 
| 153 | 1x | "This shouldn't happen, please contact the maintainers." | 
| 154 | ) | |
| 155 | } | |
| 156 | 24x | font_dev_state$spacewidth | 
| 157 | } | |
| 158 | ||
| 159 | .open_fdev_is_monospace <- function() { | |
| 160 | 32671x |   if (!font_dev_is_open()) { | 
| 161 | 2x | stop( | 
| 162 | 2x | ".open_fdev_is_monospace called when font dev state is not open. ", | 
| 163 | 2x | "This shouldn't happen, please contact the maintainers." | 
| 164 | ) | |
| 165 | } | |
| 166 | 32669x | font_dev_state$ismonospace | 
| 167 | } | |
| 168 | ||
| 169 | ## safe wrapper around .open_fdev_is_monospace | |
| 170 | is_monospace <- function(fontspec = font_spec(font_family, font_size, lineheight), | |
| 171 | font_family = "Courier", | |
| 172 | font_size = 8, | |
| 173 |                          lineheight = 1) { | |
| 174 | 32670x |   if (is.null(fontspec)) { | 
| 175 | 1x | return(TRUE) | 
| 176 | } | |
| 177 | 32669x | new_dev <- open_font_dev(fontspec) | 
| 178 | 32669x |   if (new_dev) { | 
| 179 | 31x | on.exit(close_font_dev()) | 
| 180 | } | |
| 181 | 32669x | .open_fdev_is_monospace() | 
| 182 | } | |
| 183 | ||
| 184 | ## get_max_wratio <- function() { | |
| 185 | ##   if (!font_dev_state$open) { | |
| 186 | ## stop( | |
| 187 | ## "get_space_width called when font dev state is not open. ", | |
| 188 | ## "This shouldn't happen, please contact the maintainers." | |
| 189 | ## ) | |
| 190 | ## } | |
| 191 | ##   if (.open_fdev_is_monospace()) { | |
| 192 | ## 1 | |
| 193 | ##   } else { | |
| 194 | ## font_dev_state$maxratio | |
| 195 | ## } | |
| 196 | ## } | |
| 197 | ||
| 198 | gpar_from_fspec <- function(fontspec) { | |
| 199 | 517x | gpar( | 
| 200 | 517x | fontfamily = fontspec$family, | 
| 201 | 517x | fontsize = fontspec$size, | 
| 202 | 517x | lineheight = fontspec$lineheight | 
| 203 | ) | |
| 204 | } | |
| 205 | ||
| 206 | 114995x | font_dev_is_open <- function() font_dev_state$open | 
| 207 | ||
| 208 | #' Default horizontal separator | |
| 209 | #' | |
| 210 | #' The default horizontal separator character which can be displayed in the current | |
| 211 | #' charset for use in rendering table-like objects. | |
| 212 | #' | |
| 213 | #' @param hsep_char (`string`)\cr character that will be set in the R environment | |
| 214 | #' options as the default horizontal separator. Must be a single character. Use | |
| 215 | #'   `getOption("formatters_default_hsep")` to get its current value (`NULL` if not set). | |
| 216 | #' | |
| 217 | #' @return unicode 2014 (long dash for generating solid horizontal line) if in a | |
| 218 | #' locale that uses a UTF character set, otherwise an ASCII hyphen with a | |
| 219 | #' once-per-session warning. | |
| 220 | #' | |
| 221 | #' @examples | |
| 222 | #' default_hsep() | |
| 223 | #' set_default_hsep("o") | |
| 224 | #' default_hsep() | |
| 225 | #' | |
| 226 | #' @name default_horizontal_sep | |
| 227 | #' @export | |
| 228 | default_hsep <- function() { | |
| 229 |   system_default_hsep <- getOption("formatters_default_hsep") | |
| 230 | ||
| 231 |   if (is.null(system_default_hsep)) { | |
| 232 |     if (any(grepl("^UTF", utils::localeToCharset()))) { | |
| 233 | hsep <- "\u2014" | |
| 234 |     } else { | |
| 235 |       if (interactive()) { | |
| 236 | warning( | |
| 237 | "Detected non-UTF charset. Falling back to '-' ", | |
| 238 | "as default header/body separator. This warning ", | |
| 239 | "will only be shown once per R session." | |
| 240 | ) # nocov | |
| 241 | } # nocov | |
| 242 | hsep <- "-" # nocov | |
| 243 | } | |
| 244 |   } else { | |
| 245 | hsep <- system_default_hsep | |
| 246 | } | |
| 247 | hsep | |
| 248 | } | |
| 249 | ||
| 250 | #' @name default_horizontal_sep | |
| 251 | #' @export | |
| 252 | set_default_hsep <- function(hsep_char) { | |
| 253 | checkmate::assert_character(hsep_char, n.chars = 1, len = 1, null.ok = TRUE) | |
| 254 |   options("formatters_default_hsep" = hsep_char) | |
| 255 | } | |
| 256 | ||
| 257 | .calc_cell_widths <- function(mat, colwidths, col_gap) { | |
| 258 | 369x | spans <- mat$spans | 
| 259 | 369x | keep_mat <- mat$display | 
| 260 | 369x | body <- mat$strings | 
| 261 | ||
| 262 | 369x | nr <- nrow(body) | 
| 263 | ||
| 264 | 369x | cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE) | 
| 265 | 369x | nc <- ncol(cell_widths_mat) | 
| 266 | ||
| 267 | 369x |   for (i in seq_len(nrow(body))) { | 
| 268 | 6545x |     if (any(!keep_mat[i, ])) { # any spans? | 
| 269 | 6x | j <- 1 | 
| 270 | 6x |       while (j <= nc) { | 
| 271 | 10x | nj <- spans[i, j] | 
| 272 | 10x |         j <- if (nj > 1) { | 
| 273 | 6x | js <- seq(j, j + nj - 1) | 
| 274 | 6x | cell_widths_mat[i, js] <- sum(cell_widths_mat[i, js]) + col_gap * (nj - 1) | 
| 275 | 6x | j + nj | 
| 276 |         } else { | |
| 277 | 4x | j + 1 | 
| 278 | } | |
| 279 | } | |
| 280 | } | |
| 281 | } | |
| 282 | 369x | cell_widths_mat | 
| 283 | } | |
| 284 | ||
| 285 | # Main function that does the wrapping | |
| 286 | do_cell_fnotes_wrap <- function(mat, widths, max_width, tf_wrap, fontspec, expand_newlines = TRUE) { | |
| 287 | 213x | col_gap <- mf_colgap(mat) | 
| 288 | 213x | ncchar <- sum(widths) + (length(widths) - as.integer(mf_has_rlabels(mat))) * col_gap | 
| 289 | 213x | inset <- table_inset(mat) | 
| 290 | ||
| 291 | ## Text wrapping checks | |
| 292 | 213x |   if (tf_wrap) { | 
| 293 | 92x |     if (is.null(max_width)) { | 
| 294 | 24x |       max_width <- getOption("width", 80L) | 
| 295 | 68x |     } else if (is.character(max_width) && identical(max_width, "auto")) { | 
| 296 | ! | max_width <- ncchar + inset | 
| 297 | } | |
| 298 | 92x | assert_number(max_width, lower = 0) | 
| 299 | } | |
| 300 | ||
| 301 | ## Check for having the right number of widths | |
| 302 | 213x | stopifnot(length(widths) == ncol(mat$strings)) | 
| 303 | ||
| 304 | ## format the to ASCII | |
| 305 | 213x | cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) | 
| 306 | ||
| 307 | # Check that indentation is correct (it works only for body) | |
| 308 | 213x | .check_indentation(mat, row_col_width = cell_widths_mat[, 1, drop = TRUE]) | 
| 309 | 210x | mod_ind_list <- .modify_indentation(mat, cell_widths_mat, do_what = "remove") | 
| 310 | 210x | mfs <- mod_ind_list[["mfs"]] | 
| 311 | 210x | cell_widths_mat <- mod_ind_list[["cell_widths_mat"]] | 
| 312 | ||
| 313 | # Main wrapper | |
| 314 | 210x | mf_strings(mat) <- matrix( | 
| 315 | 210x | unlist(mapply(wrap_string, | 
| 316 | 210x | str = mfs, | 
| 317 | 210x | width = cell_widths_mat, | 
| 318 | 210x | collapse = "\n", | 
| 319 | 210x | MoreArgs = list(fontspec = fontspec) | 
| 320 | )), | |
| 321 | 210x | ncol = ncol(mfs) | 
| 322 | ) | |
| 323 | ||
| 324 | 210x |   if (expand_newlines) { | 
| 325 | ## XXXXX this is wrong and will break for listings cause we don't know when | |
| 326 | ## we need has_topleft to be FALSE!!!!!!!!!! | |
| 327 | 210x | mat <- mform_handle_newlines(mat) | 
| 328 | ||
| 329 | ## this updates extents in rinfo AND nlines in ref_fnotes_df | |
| 330 | ## mat already has fontspec on it so no need to pass that down | |
| 331 | 210x | mat <- update_mf_nlines(mat, max_width = max_width) | 
| 332 | ||
| 333 | # Re-indenting | |
| 334 | 210x | mf_strings(mat) <- .modify_indentation(mat, cell_widths_mat, do_what = "add")[["mfs"]] | 
| 335 | 210x | .check_indentation(mat) # all went well | 
| 336 | } | |
| 337 | 210x | mat | 
| 338 | } | |
| 339 | ||
| 340 | # Helper function to see if body indentation matches (minimum) | |
| 341 | # It sees if there is AT LEAST the indentation contained in rinfo | |
| 342 | .check_indentation <- function(mat, row_col_width = NULL) { | |
| 343 | # mf_nrheader(mat) # not useful | |
| 344 | 424x | mf_nlh <- mf_nlheader(mat) | 
| 345 | 424x | mf_lgrp <- mf_lgrouping(mat) | 
| 346 | 424x | mf_str <- mf_strings(mat) | 
| 347 | # we base everything on the groupings -> unique indentation identifiers | |
| 348 | 424x |   if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable() | 
| 349 | 424x | mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft | 
| 350 |   } else { | |
| 351 | ! | mf_ind <- rep(0, mf_nrheader(mat)) | 
| 352 | } | |
| 353 | 424x |   ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") | 
| 354 | ||
| 355 | # Expected indent (-x negative numbers should not appear at this stage) | |
| 356 | 424x | stopifnot(all(mf_ind >= 0)) | 
| 357 | 424x |   real_indent <- vapply(mf_ind, function(ii) { | 
| 358 | 7833x | paste0(rep(ind_std, ii), collapse = "") | 
| 359 | 424x | }, character(1)) | 
| 360 | ||
| 361 | 424x |   if (!is.null(row_col_width) && any(row_col_width > 0) && !is.null(mf_rinfo(mat))) { # third is rare case | 
| 362 | # Self consistency test for row_col_width (same groups should have same width) | |
| 363 | # This should not be necessary (nocov) | |
| 364 | 213x |     consistency_check <- vapply(unique(mf_lgrp), function(ii) { | 
| 365 | 3935x | width_per_grp <- row_col_width[which(mf_lgrp == ii)] | 
| 366 | 3935x | all(width_per_grp == width_per_grp[1]) | 
| 367 | 213x | }, logical(1)) | 
| 368 | 213x | stopifnot(all(consistency_check)) | 
| 369 | ||
| 370 | # Taking only one width for each indentation grouping | |
| 371 | 213x | unique_row_col_width <- row_col_width[match(unique(mf_lgrp), mf_lgrp)] | 
| 372 | ||
| 373 | # Exception for check: case with summarize_row_groups and (hence) content_rows | |
| 374 | 213x | nchar_real_indent <- nchar(real_indent) | 
| 375 | 213x | body_rows <- seq(mf_nrheader(mat) + 1, length(nchar_real_indent)) | 
| 376 | 213x | nchar_real_indent[body_rows] <- nchar_real_indent[body_rows] + | 
| 377 | 213x | as.numeric(mf_rinfo(mat)$node_class != "ContentRow") | 
| 378 | # xxx I think all of the above is a bit buggy honestly (check ContentRows!!!) | |
| 379 | ||
| 380 | 213x |     if (any(nchar_real_indent > unique_row_col_width)) { | 
| 381 | 2x | stop( | 
| 382 | 2x | "Inserted width for row label column is not wide enough. ", | 
| 383 | 2x | "We found the following rows that do not have at least indentation * ind_size + 1", | 
| 384 | 2x | " characters to allow text to be shown after indentation: ", | 
| 385 | 2x | paste0(which(nchar(real_indent) + 1 > unique_row_col_width), collapse = " ") | 
| 386 | ) | |
| 387 | } | |
| 388 | } | |
| 389 | ||
| 390 | # Main detector | |
| 391 | 422x |   correct_indentation <- vapply(seq_along(mf_lgrp), function(xx) { | 
| 392 | 8277x | grouping <- mf_lgrp[xx] | 
| 393 | 8277x |     if (nzchar(real_indent[grouping])) { | 
| 394 | 33x | has_correct_indentation <- stringi::stri_detect( | 
| 395 | 33x | mf_str[xx, 1], | 
| 396 | 33x |         regex = paste0("^", real_indent[grouping]) | 
| 397 | ) | |
| 398 | 33x | return(has_correct_indentation || !nzchar(mf_str[xx, 1])) # "" is still an ok indentation | 
| 399 | } | |
| 400 | # Cases where no indent are true by definition | |
| 401 | 8244x | TRUE | 
| 402 | 422x | }, logical(1)) | 
| 403 | ||
| 404 | 422x |   if (any(!correct_indentation)) { | 
| 405 | 1x | stop( | 
| 406 | 1x | "We discovered indentation mismatches between the matrix_form and the indentation", | 
| 407 | 1x | " predefined in mf_rinfo. This should not happen. Contact the maintainer." | 
| 408 | 1x | ) # nocov | 
| 409 | } | |
| 410 | } | |
| 411 | ||
| 412 | # Helper function that takes out or adds the proper indentation | |
| 413 | .modify_indentation <- function(mat, cell_widths_mat, do_what = c("remove", "add")) { | |
| 414 | # Extract info | |
| 415 | 420x | mfs <- mf_strings(mat) # we work on mfs | 
| 416 | 420x | mf_nlh <- mf_nlheader(mat) | 
| 417 | 420x | mf_l <- mf_lgrouping(mat) | 
| 418 | 420x |   if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable() | 
| 419 | 420x | mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft | 
| 420 |   } else { | |
| 421 | ! | mf_ind <- rep(0, mf_nrheader(mat)) | 
| 422 | } | |
| 423 | 420x | stopifnot(length(mf_ind) == length(unique(mf_l))) # Check for indentation and grouping | 
| 424 | 420x |   ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") # standard size of indent 1 | 
| 425 | ||
| 426 | # Create real indentation | |
| 427 | 420x | real_indent <- sapply(mf_ind, function(ii) paste0(rep(ind_std, ii), collapse = "")) | 
| 428 | ||
| 429 | # Use groupings to add or remove proper indentation | |
| 430 | 420x | lbl_row <- mfs[, 1, drop = TRUE] | 
| 431 | 420x |   for (ii in seq_along(lbl_row)) { | 
| 432 | 8268x | grp <- mf_l[ii] | 
| 433 | 8268x |     if (nzchar(real_indent[grp])) { | 
| 434 | # Update also the widths!! | |
| 435 | 29x |       if (do_what[1] == "remove") { | 
| 436 | 9x | cell_widths_mat[ii, 1] <- cell_widths_mat[ii, 1] - nchar(real_indent[grp]) | 
| 437 | 9x |         mfs[ii, 1] <- stringi::stri_replace(lbl_row[ii], "", regex = paste0("^", real_indent[grp])) | 
| 438 | 20x |       } else if (do_what[1] == "add") { | 
| 439 | 20x | mfs[ii, 1] <- paste0(real_indent[grp], lbl_row[ii]) | 
| 440 |       } else { | |
| 441 |         stop("do_what needs to be remove or add.") # nocov | |
| 442 | } | |
| 443 |     } else { | |
| 444 | 8239x | mfs[ii, 1] <- lbl_row[ii] | 
| 445 | } | |
| 446 | } | |
| 447 | # Final return | |
| 448 | 420x |   return(list("mfs" = mfs, "cell_widths_mat" = cell_widths_mat)) | 
| 449 | } | |
| 450 | ||
| 451 | ## take a character vector and return whether the value is | |
| 452 | ## a string version of a number or not | |
| 453 | is_number_str <- function(vec) { | |
| 454 | ! | is.na(as.numeric(vec)) | 
| 455 | } | |
| 456 | ||
| 457 | is_dec_align <- function(vec) { | |
| 458 | # "c" is not an alignment method we define in `formatters`, | |
| 459 | # but the reverse dependency package `tables` will need | |
| 460 | 604x | sdiff <- setdiff(vec, c(list_valid_aligns(), "c")) | 
| 461 | 604x |   if (length(sdiff) > 0) { | 
| 462 | ! |     stop("Invalid text-alignment(s): ", paste(sdiff, collapse = ", ")) | 
| 463 | } | |
| 464 | 604x |   grepl("dec", vec) | 
| 465 | } | |
| 466 | ||
| 467 | 459x | any_dec_align <- function(vec) any(is_dec_align(vec)) | 
| 468 | ||
| 469 | #' Decimal alignment | |
| 470 | #' | |
| 471 | #' Aligning decimal values of string matrix. Allowed alignments are: `dec_left`, `dec_right`, | |
| 472 | #' and `decimal`. | |
| 473 | #' | |
| 474 | #' @param string_mat (`character matrix`)\cr "string" matrix component of `MatrixPrintForm` object. | |
| 475 | #' @param align_mat (`character matrix`)\cr "aligns" matrix component of `MatrixPrintForm` object. | |
| 476 | #' Should contain either `dec_left`, `dec_right`, or `decimal` for values to be decimal aligned. | |
| 477 | #' | |
| 478 | #' @details Left and right decimal alignment (`dec_left` and `dec_right`) differ from center decimal | |
| 479 | #' alignment (`decimal`) only when there is padding present. This may occur if column widths are | |
| 480 | #' set wider via parameters `widths` in `toString` or `colwidths` in `paginate_*`. More commonly, | |
| 481 | #' it also occurs when column names are wider. Cell wrapping is not supported when decimal | |
| 482 | #' alignment is used. | |
| 483 | #' | |
| 484 | #' @return A processed string matrix of class `MatrixPrintForm` with decimal-aligned values. | |
| 485 | #' | |
| 486 | #' @seealso [toString()], [MatrixPrintForm()] | |
| 487 | #' | |
| 488 | #' @examples | |
| 489 | #' dfmf <- basic_matrix_form(mtcars[1:5, ]) | |
| 490 | #' aligns <- mf_aligns(dfmf) | |
| 491 | #' aligns[, -c(1)] <- "dec_left" | |
| 492 | #' decimal_align(mf_strings(dfmf), aligns) | |
| 493 | #' | |
| 494 | #' @export | |
| 495 | decimal_align <- function(string_mat, align_mat) { | |
| 496 | ## Evaluate if any values are to be decimal aligned | |
| 497 | 45x |   if (!any_dec_align(align_mat)) { | 
| 498 | ! | return(string_mat) | 
| 499 | } | |
| 500 | 45x |   for (i in seq(1, ncol(string_mat))) { | 
| 501 | ## Take a column and its decimal alignments | |
| 502 | 145x | col_i <- as.character(string_mat[, i]) | 
| 503 | 145x | align_col_i <- is_dec_align(align_mat[, i]) | 
| 504 | ||
| 505 | ## !( A || B) -> !A && !B DeMorgan's Law | |
| 506 | ## Are there any values to be decimal aligned? safe if | |
| 507 | 145x |     if (any(align_col_i) && any(!grepl("^[0-9]\\.", col_i))) { | 
| 508 | ## Extract values not to be aligned (NAs, non-numbers, | |
| 509 | ## doesn't say "decimal" in alignment matrix) | |
| 510 | ## XXX FIXME because this happens after formatting, we can't tell the difference between | |
| 511 | ## non-number strings which come from na_str+ NA value and strings which just aren't numbers. | |
| 512 | ## this is a problem that should eventually be fixed. | |
| 513 | 82x | nas <- vapply(col_i, is.na, FUN.VALUE = logical(1)) | 
| 514 | 82x |       nonnum <- !grepl("[0-9]", col_i) | 
| 515 |       ## No grepl("[a-zA-Z]", col_i) because this excludes N=xx, e.g. | |
| 516 | 82x | nonalign <- nas | nonnum | !align_col_i | 
| 517 | 82x | col_ia <- col_i[!nonalign] | 
| 518 | ||
| 519 | ## Do decimal alignment | |
| 520 | 82x |       if (length(col_ia) > 0) { | 
| 521 | # Special case: scientific notation | |
| 522 | 82x |         has_sc_not <- grepl("\\d+[e|E][\\+|\\-]\\d+", col_ia) | 
| 523 | 82x |         if (any(has_sc_not)) { | 
| 524 | 1x | stop( | 
| 525 | 1x | "Found values using scientific notation between the ones that", | 
| 526 | 1x | " needs to be decimal aligned (aligns is decimal, dec_left or dec_right).", | 
| 527 | 1x | " Please consider using format functions to get a complete decimal ", | 
| 528 | 1x | "(e.g. formatC)." | 
| 529 | ) | |
| 530 | } | |
| 531 | ||
| 532 | ## Count the number of numbers in the string | |
| 533 | 81x |         matches <- gregexpr("\\d+\\.\\d+|\\d+", col_ia) | 
| 534 | 81x |         more_than_one <- vapply(matches, function(x) { | 
| 535 | 697x | sum(attr(x, "match.length") > 0) > 1 | 
| 536 | 81x | }, logical(1)) | 
| 537 | ## Throw error in case any have more than 1 numbers | |
| 538 | 81x |         if (any(more_than_one)) { | 
| 539 | 2x | stop( | 
| 540 | 2x | "Decimal alignment is not supported for multiple values. ", | 
| 541 | 2x | "Found the following string with multiple numbers ", | 
| 542 | 2x | "(first 3 selected from column ", col_i[1], "): '", | 
| 543 | 2x | paste0(col_ia[more_than_one][seq(1, 3)], collapse = "', '"), | 
| 544 | "'" | |
| 545 | ) | |
| 546 | } | |
| 547 | ## General split (only one match -> the first) | |
| 548 | 79x |         main_regexp <- regexpr("\\d+", col_ia) | 
| 549 | 79x | left <- regmatches(col_ia, main_regexp, invert = FALSE) | 
| 550 | 79x | right <- regmatches(col_ia, main_regexp, invert = TRUE) | 
| 551 | 79x | right <- sapply(right, "[[", 2) | 
| 552 | 79x | something_left <- sapply(strsplit(col_ia, "\\d+"), "[[", 1) | 
| 553 | 79x | left <- paste0(something_left, left) | 
| 554 | 79x |         if (!checkmate::test_set_equal(paste0(left, right), col_ia)) { | 
| 555 | ! | stop( | 
| 556 | ! | "Split string list lost some piece along the way. This ", | 
| 557 | ! | "should not have happened. Please contact the maintainer." | 
| 558 | ) | |
| 559 | } # nocov | |
| 560 | 79x |         separator <- sapply(right, function(x) { | 
| 561 | 645x |           if (nzchar(x)) { | 
| 562 | 349x | substr(x, 1, 1) | 
| 563 |           } else { | |
| 564 | 296x |             c(" ") | 
| 565 | } | |
| 566 | 79x | }, USE.NAMES = FALSE) | 
| 567 | 79x |         right <- sapply(right, function(x) { | 
| 568 | 645x |           if (nchar(x) > 1) { | 
| 569 | 317x | substr(x, 2, nchar(x)) | 
| 570 |           } else { | |
| 571 | 328x |             c("") | 
| 572 | } | |
| 573 | 79x | }, USE.NAMES = FALSE) | 
| 574 | ## figure out whether we need space separators (at least one had a "." or not) | |
| 575 | 79x |         if (!any(grepl("[^[:space:]]", separator))) { | 
| 576 | 26x |           separator <- gsub("[[:space:]]*", "", separator) | 
| 577 | } | |
| 578 | ## modify the piece with spaces | |
| 579 | 79x | left_mod <- paste0(spaces(max(nchar(left), na.rm = TRUE) - nchar(left)), left) | 
| 580 | 79x | right_mod <- paste0(right, spaces(max(nchar(right), na.rm = TRUE) - nchar(right))) | 
| 581 | # Put everything together | |
| 582 | 79x | aligned <- paste(left_mod, separator, right_mod, sep = "") | 
| 583 | 79x | string_mat[!nonalign, i] <- aligned | 
| 584 | } | |
| 585 | } | |
| 586 | } | |
| 587 | 42x | string_mat | 
| 588 | } | |
| 589 | ||
| 590 | ## this gives the conversion from number of spaces to number of characters | |
| 591 | ## for use in, e.g., repping out divider lines. | |
| 592 | calc_str_adj <- function(str, fontspec) { | |
| 593 | 160x |   if (nchar(str) == 0) { | 
| 594 | ! | return(0) | 
| 595 | } | |
| 596 | 160x | nchar(str) / nchar_ttype(str, fontspec, raw = TRUE) | 
| 597 | } | |
| 598 | ||
| 599 | # toString --------------------------------------------------------------------- | |
| 600 | # main printing code for MatrixPrintForm | |
| 601 | ||
| 602 | #' @description | |
| 603 | #' All objects that are printed to console pass via `toString`. This function allows | |
| 604 | #' fundamental formatting specifications to be applied to final output, like column widths | |
| 605 | #' and relative wrapping (`width`), title and footer wrapping (`tf_wrap = TRUE` and | |
| 606 | #' `max_width`), and horizontal separator character (e.g. `hsep = "+"`). | |
| 607 | #' | |
| 608 | #' @inheritParams MatrixPrintForm | |
| 609 | #' @inheritParams open_font_dev | |
| 610 | #' @inheritParams format_value | |
| 611 | #' @param widths (`numeric` or `NULL`)\cr Proposed widths for the columns of `x`. The expected | |
| 612 | #' length of this numeric vector can be retrieved with `ncol(x) + 1` as the column of row names | |
| 613 | #' must also be considered. | |
| 614 | #' @param hsep (`string`)\cr character to repeat to create header/body separator line. If | |
| 615 | #' `NULL`, the object value will be used. If `" "`, an empty separator will be printed. See | |
| 616 | #' [default_hsep()] for more information. | |
| 617 | #' @param tf_wrap (`flag`)\cr whether the text for title, subtitles, and footnotes should be wrapped. | |
| 618 | #' @param max_width (`integer(1)`, `string` or `NULL`)\cr width that title and footer (including | |
| 619 | #' footnotes) materials should be word-wrapped to. If `NULL`, it is set to the current print width of the | |
| 620 | #'   session (`getOption("width")`). If set to `"auto"`, the width of the table (plus any table inset) is | |
| 621 | #' used. Parameter is ignored if `tf_wrap = FALSE`. | |
| 622 | #' @param ttype_ok (`logical(1)`)\cr should truetype (non-monospace) fonts be | |
| 623 | #' allowed via `fontspec`. Defaults to `FALSE`. This parameter is primarily | |
| 624 | #' for internal testing and generally should not be set by end users. | |
| 625 | #' | |
| 626 | #' @details | |
| 627 | #' Manual insertion of newlines is not supported when `tf_wrap = TRUE` and will result in a warning and | |
| 628 | #' undefined wrapping behavior. Passing vectors of already split strings remains supported, however in this | |
| 629 | #' case each string is word-wrapped separately with the behavior described above. | |
| 630 | #' | |
| 631 | #' @return A character string containing the ASCII rendering of the table-like object represented by `x`. | |
| 632 | #' | |
| 633 | #' @seealso [wrap_string()] | |
| 634 | #' | |
| 635 | #' @examples | |
| 636 | #' mform <- basic_matrix_form(mtcars) | |
| 637 | #' cat(toString(mform)) | |
| 638 | #' | |
| 639 | #' @rdname tostring | |
| 640 | #' @exportMethod toString | |
| 641 | setMethod("toString", "MatrixPrintForm", function(x, | |
| 642 | widths = NULL, | |
| 643 | tf_wrap = FALSE, | |
| 644 | max_width = NULL, | |
| 645 | col_gap = mf_colgap(x), | |
| 646 | hsep = NULL, | |
| 647 | fontspec = font_spec(), | |
| 648 | ttype_ok = FALSE, | |
| 649 |                                                   round_type = c("iec", "sas")) { | |
| 650 | 163x | checkmate::assert_flag(tf_wrap) | 
| 651 | ||
| 652 | ## we are going to use the pdf device and grid to understand the actual | |
| 653 | ## print width of things given our font family and font size | |
| 654 | 163x | new_dev <- open_font_dev(fontspec) | 
| 655 | 163x |   if (new_dev) { | 
| 656 | 153x | on.exit(close_font_dev()) | 
| 657 | } | |
| 658 | ||
| 659 | 163x |   if (!is_monospace(fontspec = fontspec) && !ttype_ok) { | 
| 660 | ! | stop( | 
| 661 | ! | "non-monospace font specified in toString call; this would result in cells contents not lining up exactly. ", | 
| 662 | ! | "If you truly want this behavior please set ttype_ok = TRUE in the call to toString/export_as_txt/export_as_pdf" | 
| 663 | ) | |
| 664 | } | |
| 665 | 163x | mat <- matrix_form(x, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type) | 
| 666 | ||
| 667 | # Check for \n in mat strings -> if there are any, matrix_form did not work | |
| 668 | 163x |   if (any(grepl("\n", mf_strings(mat)))) { | 
| 669 | ! | stop( | 
| 670 | ! | "Found newline characters (\\n) in string matrix produced by matrix_form. ", | 
| 671 | ! | "This is not supported and implies missbehavior on the first parsing (in matrix_form). ", | 
| 672 | ! | "Please contact the maintainer or file an issue." | 
| 673 | ! | ) # nocov | 
| 674 | } | |
| 675 | 163x |   if (any(grepl("\r", mf_strings(mat)))) { | 
| 676 | ! | stop( | 
| 677 | ! | "Found recursive special characters (\\r) in string matrix produced by matrix_form. ", | 
| 678 | ! | "This special character is not supported and should be removed." | 
| 679 | ! | ) # nocov | 
| 680 | } | |
| 681 | ||
| 682 | # Check that expansion worked for header -> should not happen | |
| 683 | 163x | if (!is.null(mf_rinfo(mat)) && # rare case of rtables::rtable() | 
| 684 | 163x | (length(mf_lgrouping(mat)) != nrow(mf_strings(mat)) || # non-unique grouping test # nolint | 
| 685 | 163x |       mf_nrheader(mat) + nrow(mf_rinfo(mat)) != length(unique(mf_lgrouping(mat))))) { # nolint | 
| 686 | ! | stop( | 
| 687 | ! | "The sum of the expected nrows header and nrows of content table does ", | 
| 688 | ! | "not match the number of rows in the string matrix. To our knowledge, ", | 
| 689 | ! | "this is usually of a problem in solving newline characters (\\n) in the header. ", | 
| 690 | ! | "Please contact the maintaner or file an issue." | 
| 691 | ! | ) # nocov | 
| 692 | } | |
| 693 | ||
| 694 | 163x | inset <- table_inset(mat) | 
| 695 | ||
| 696 | # if cells are decimal aligned, run propose column widths | |
| 697 | # if the provided widths is less than proposed width, return an error | |
| 698 | 163x |   if (any_dec_align(mf_aligns(mat))) { | 
| 699 | 22x | aligned <- propose_column_widths(x, fontspec = fontspec, round_type = round_type) | 
| 700 | ||
| 701 | # catch any columns that require widths more than what is provided | |
| 702 | 20x |     if (!is.null(widths)) { | 
| 703 | 9x | how_wide <- sapply(seq_along(widths), function(i) c(widths[i] - aligned[i])) | 
| 704 | 9x | too_wide <- how_wide < 0 | 
| 705 | 9x |       if (any(too_wide)) { | 
| 706 | 2x | desc_width <- paste(paste( | 
| 707 | 2x | names(which(too_wide)), | 
| 708 | 2x |           paste0("(", how_wide[too_wide], ")") | 
| 709 | 2x | ), collapse = ", ") | 
| 710 | 2x | stop( | 
| 711 | 2x | "Inserted width(s) for column(s) ", desc_width, | 
| 712 | 2x | " is(are) not wide enough for the desired alignment." | 
| 713 | ) | |
| 714 | } | |
| 715 | } | |
| 716 | } | |
| 717 | ||
| 718 | # Column widths are fixed here | |
| 719 | 159x |   if (is.null(widths)) { | 
| 720 | # if mf does not have widths -> propose them | |
| 721 | 133x | widths <- mf_col_widths(x) %||% propose_column_widths(x, fontspec = fontspec, round_type = round_type) | 
| 722 |   } else { | |
| 723 | 26x | mf_col_widths(x) <- widths | 
| 724 | } | |
| 725 | ||
| 726 | ## Total number of characters for the table | |
| 727 | ## col_gap (and table inset) are in number of spaces | |
| 728 | ## so we're ok here even in the truetype case | |
| 729 | 159x | ncchar <- sum(widths) + (length(widths) - 1) * col_gap | 
| 730 | ||
| 731 | ## max_width for wrapping titles and footers (not related to ncchar if not indirectly) | |
| 732 | 159x | max_width <- .handle_max_width( | 
| 733 | 159x | tf_wrap = tf_wrap, | 
| 734 | 159x | max_width = max_width, | 
| 735 | 159x | colwidths = widths, | 
| 736 | 159x | col_gap = col_gap, | 
| 737 | 159x | inset = inset | 
| 738 | ) | |
| 739 | ||
| 740 | # Main wrapper function for table core | |
| 741 | 159x | mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap, fontspec = fontspec) | 
| 742 | ||
| 743 | 156x | body <- mf_strings(mat) | 
| 744 | 156x | aligns <- mf_aligns(mat) | 
| 745 | 156x | keep_mat <- mf_display(mat) | 
| 746 | ## spans <- mat$spans | |
| 747 | 156x | mf_ri <- mf_rinfo(mat) | 
| 748 | 156x | ref_fnotes <- mf_rfnotes(mat) | 
| 749 | 156x | nl_header <- mf_nlheader(mat) | 
| 750 | ||
| 751 | 156x | cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) | 
| 752 | ||
| 753 | # decimal alignment | |
| 754 | 156x |   if (any_dec_align(aligns)) { | 
| 755 | 18x | body <- decimal_align(body, aligns) | 
| 756 | } | |
| 757 | ||
| 758 | # Content is a matrix of cells with the right amount of spaces | |
| 759 | 156x | content <- matrix( | 
| 760 | 156x | mapply(padstr, body, cell_widths_mat, aligns, MoreArgs = list(fontspec = fontspec)), | 
| 761 | 156x | ncol = ncol(body) | 
| 762 | ) | |
| 763 | 156x | content[!keep_mat] <- NA | 
| 764 | ||
| 765 | # Define gap string and divisor string | |
| 766 | 156x |   gap_str <- strrep(" ", col_gap) | 
| 767 | 156x |   if (is.null(hsep)) { | 
| 768 | 121x | hsep <- horizontal_sep(mat) | 
| 769 | } | |
| 770 | 156x | adj_hsep <- calc_str_adj(hsep, fontspec) | 
| 771 | 156x | div <- substr(strrep(hsep, ceiling(ncchar * adj_hsep)), 1, ceiling(ncchar * adj_hsep)) | 
| 772 | 156x | hsd <- header_section_div(mat) | 
| 773 | 156x |   if (!is.na(hsd)) { | 
| 774 | ! | adj_hsd <- calc_str_adj(hsd, fontspec) | 
| 775 | ! | hsd <- substr(strrep(hsd, ceiling(ncchar * adj_hsd)), 1, ceiling(ncchar * adj_hsd)) | 
| 776 |   } else { | |
| 777 | 156x | hsd <- NULL # no divisor | 
| 778 | } | |
| 779 | ||
| 780 | # text head (paste w/o NA content header and gap string) | |
| 781 | 156x | txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str) | 
| 782 | ||
| 783 | # txt body | |
| 784 | 156x |   sec_seps_df <- mf_ri[, c("abs_rownumber", "trailing_sep"), drop = FALSE] | 
| 785 | 156x |   if (!is.null(sec_seps_df) && any(!is.na(sec_seps_df$trailing_sep))) { | 
| 786 | 2x | bdy_cont <- tail(content, -nl_header) | 
| 787 | ## unfortunately we count "header rows" wrt line grouping so it | |
| 788 | ## doesn't match the real (i.e. body) rows as is | |
| 789 | 2x | row_grouping <- tail(mf_lgrouping(mat), -nl_header) - mf_nrheader(mat) | 
| 790 | 2x | nrbody <- NROW(bdy_cont) | 
| 791 | 2x | stopifnot(length(row_grouping) == nrbody) | 
| 792 | ## all rows with non-NA section divs and the final row (regardless of NA status) | |
| 793 | ## fixes #77 | |
| 794 | 2x | sec_seps_df <- sec_seps_df[unique(c( | 
| 795 | 2x | which(!is.na(sec_seps_df$trailing_sep)), | 
| 796 | 2x | NROW(sec_seps_df) | 
| 797 | )), ] | |
| 798 | 2x | txt_body <- character() | 
| 799 | 2x | sec_strt <- 1 | 
| 800 | 2x | section_rws <- sec_seps_df$abs_rownumber | 
| 801 | 2x |     for (i in seq_len(NROW(section_rws))) { | 
| 802 | 6x | cur_rownum <- section_rws[i] | 
| 803 | 6x | sec_end <- max(which(row_grouping == cur_rownum)) | 
| 804 | 6x | txt_body <- c( | 
| 805 | 6x | txt_body, | 
| 806 | 6x | apply(bdy_cont[seq(sec_strt, sec_end), , drop = FALSE], | 
| 807 | 6x | 1, | 
| 808 | 6x | .paste_no_na, | 
| 809 | 6x | collapse = gap_str | 
| 810 | ), | |
| 811 | ## don't print section dividers if they would be the last thing before the | |
| 812 | ## footer divider | |
| 813 | ## this also ensures an extraneous sec div won't be printed if we have non-sec-div | |
| 814 | ## rows after the last sec div row (#77) | |
| 815 | 6x |         if (sec_end < nrbody) { | 
| 816 | 4x | adj_i <- calc_str_adj(sec_seps_df$trailing_sep[i], fontspec) | 
| 817 | 4x | substr( | 
| 818 | 4x | strrep(sec_seps_df$trailing_sep[i], ceiling(ncchar * adj_i)), 1, | 
| 819 | 4x | ceiling((ncchar - inset) * adj_i) | 
| 820 | ) | |
| 821 | } | |
| 822 | ) | |
| 823 | 6x | sec_strt <- sec_end + 1 | 
| 824 | } | |
| 825 |   } else { | |
| 826 | # This is the usual default pasting | |
| 827 | 154x | txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str) | 
| 828 | } | |
| 829 | ||
| 830 | # retrieving titles and footers | |
| 831 | 156x | allts <- all_titles(mat) | 
| 832 | ||
| 833 | 156x | ref_fnotes <- reorder_ref_fnotes(ref_fnotes) | 
| 834 | # Fix for ref_fnotes with \n characters XXX this does not count in the pagination | |
| 835 | 156x |   if (any(grepl("\\n", ref_fnotes))) { | 
| 836 | 2x | ref_fnotes <- unlist(strsplit(ref_fnotes, "\n", fixed = TRUE)) | 
| 837 | } | |
| 838 | ||
| 839 | 156x | allfoots <- list( | 
| 840 | 156x | "main_footer" = main_footer(mat), | 
| 841 | 156x | "prov_footer" = prov_footer(mat), | 
| 842 | 156x | "ref_footnotes" = ref_fnotes | 
| 843 | ) | |
| 844 | 156x | allfoots <- allfoots[!sapply(allfoots, is.null)] | 
| 845 | ||
| 846 | ## Wrapping titles if they go beyond the horizontally allowed space | |
| 847 | 156x |   if (tf_wrap) { | 
| 848 | 68x | new_line_warning(allts) | 
| 849 | 68x | allts <- wrap_txt(allts, max_width, fontspec = fontspec) | 
| 850 | } | |
| 851 | 156x | titles_txt <- if (any(nzchar(allts))) c(allts, "", .do_inset(div, inset)) else NULL | 
| 852 | ||
| 853 | # Wrapping footers if they go beyond the horizontally allowed space | |
| 854 | 156x |   if (tf_wrap) { | 
| 855 | 68x | new_line_warning(allfoots) | 
| 856 | 68x | allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset, fontspec = fontspec) | 
| 857 | 68x | allfoots$ref_footnotes <- wrap_txt(allfoots$ref_footnotes, max_width - inset, fontspec = fontspec) | 
| 858 | ## no - inset here because the prov_footer is not inset | |
| 859 | 68x | allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width, fontspec = fontspec) | 
| 860 | } | |
| 861 | ||
| 862 | # Final return | |
| 863 | 156x | paste0( | 
| 864 | 156x | paste(c( | 
| 865 | 156x | titles_txt, # .do_inset(div, inset) happens if there are any titles | 
| 866 | 156x | .do_inset(txt_head, inset), | 
| 867 | 156x | .do_inset(div, inset), | 
| 868 | 156x | .do_inset(hsd, inset), # header_section_div if present | 
| 869 | 156x | .do_inset(txt_body, inset), | 
| 870 | 156x | .footer_inset_helper(allfoots, div, inset) | 
| 871 | 156x | ), collapse = "\n"), | 
| 872 | 156x | "\n" | 
| 873 | ) | |
| 874 | }) | |
| 875 | ||
| 876 | # Switcher for the 3 options for max_width (NULL, numeric, "auto")) | |
| 877 | .handle_max_width <- function(tf_wrap, max_width, | |
| 878 |                               cpp = NULL, # Defaults to getOption("width", 80L) | |
| 879 | # Things for auto | |
| 880 |                               inset = NULL, colwidths = NULL, col_gap = NULL) { | |
| 881 | 237x |   max_width <- if (!tf_wrap) { | 
| 882 | 117x |     if (!is.null(max_width)) { | 
| 883 | 1x |       warning("tf_wrap is FALSE - ignoring non-null max_width value.") | 
| 884 | } | |
| 885 | 117x | NULL | 
| 886 | 237x |   } else if (tf_wrap) { | 
| 887 | 120x |     if (is.null(max_width)) { | 
| 888 | 36x |       if (is.null(cpp) || is.na(cpp)) { | 
| 889 | 7x |         getOption("width", 80L) | 
| 890 |       } else { | |
| 891 | 29x | cpp | 
| 892 | } | |
| 893 | 84x |     } else if (is.numeric(max_width)) { | 
| 894 | 79x | max_width | 
| 895 | 5x |     } else if (is.character(max_width) && identical(max_width, "auto")) { | 
| 896 | # This should not happen, but just in case | |
| 897 | 4x |       if (any(sapply(list(inset, colwidths, col_gap), is.null))) { | 
| 898 | 1x |         stop("inset, colwidths, and col_gap must all be non-null when max_width is \"auto\".") | 
| 899 | } | |
| 900 | 3x | inset + sum(colwidths) + (length(colwidths) - 1) * col_gap | 
| 901 |     } else { | |
| 902 | 1x |       stop("max_width must be NULL, a numeric value, or \"auto\".") | 
| 903 | } | |
| 904 | } | |
| 905 | 235x | return(max_width) | 
| 906 | } | |
| 907 | ||
| 908 | .do_inset <- function(x, inset) { | |
| 909 | 1053x |   if (inset == 0 || !any(nzchar(x))) { | 
| 910 | 1034x | return(x) | 
| 911 | } | |
| 912 | 19x |   padding <- strrep(" ", inset) | 
| 913 | 19x |   if (is.character(x)) { | 
| 914 | 19x | x <- paste0(padding, x) | 
| 915 | ! |   } else if (is(x, "matrix")) { | 
| 916 | ! | x[, 1] <- .do_inset(x[, 1, drop = TRUE], inset) | 
| 917 | } | |
| 918 | 19x | x | 
| 919 | } | |
| 920 | ||
| 921 | .inset_div <- function(txt, div, inset) { | |
| 922 | 105x | c(.do_inset(div, inset), "", txt) | 
| 923 | } | |
| 924 | ||
| 925 | .footer_inset_helper <- function(footers_v, div, inset) { | |
| 926 | 156x | div_done <- FALSE # nolint | 
| 927 | 156x | fter <- footers_v$main_footer | 
| 928 | 156x | prvf <- footers_v$prov_footer | 
| 929 | 156x | rfn <- footers_v$ref_footnotes | 
| 930 | 156x | footer_txt <- .do_inset(rfn, inset) | 
| 931 | 156x |   if (any(nzchar(footer_txt))) { | 
| 932 | 14x | footer_txt <- .inset_div(footer_txt, div, inset) | 
| 933 | } | |
| 934 | 156x | if (any(vapply( | 
| 935 | 156x | footers_v, function(x) any(nzchar(x)), | 
| 936 | 156x | TRUE | 
| 937 |   ))) { | |
| 938 | 91x |     if (any(nzchar(prvf))) { | 
| 939 | 89x | provtxt <- c( | 
| 940 | 89x | if (any(nzchar(fter))) "", | 
| 941 | 89x | prvf | 
| 942 | ) | |
| 943 |     } else { | |
| 944 | 2x | provtxt <- character() | 
| 945 | } | |
| 946 | 91x | footer_txt <- c( | 
| 947 | 91x | footer_txt, | 
| 948 | 91x | .inset_div( | 
| 949 | 91x | c( | 
| 950 | 91x | .do_inset(fter, inset), | 
| 951 | 91x | provtxt | 
| 952 | ), | |
| 953 | 91x | div, | 
| 954 | 91x | inset | 
| 955 | ) | |
| 956 | ) | |
| 957 | } | |
| 958 | 156x | footer_txt | 
| 959 | } | |
| 960 | ||
| 961 | reorder_ref_fnotes <- function(fns) { | |
| 962 | 159x |   ind <- gsub("\\{(.*)\\}.*", "\\1", fns) | 
| 963 | 159x | ind_num <- suppressWarnings(as.numeric(ind)) | 
| 964 | 159x | is_num <- !is.na(ind_num) | 
| 965 | 159x | is_asis <- ind == fns | 
| 966 | ||
| 967 | 159x |   if (all(is_num)) { | 
| 968 | 143x | ord_num <- order(ind_num) | 
| 969 | 143x | ord_char <- NULL | 
| 970 | 143x | ord_other <- NULL | 
| 971 |   } else { | |
| 972 | 16x | ord_num <- order(ind_num[is_num]) | 
| 973 | 16x | ord_char <- order(ind[!is_num & !is_asis]) | 
| 974 | 16x | ord_other <- order(ind[is_asis]) | 
| 975 | } | |
| 976 | 159x | c(fns[is_num][ord_num], fns[!is_num & !is_asis][ord_char], ind[is_asis][ord_other]) | 
| 977 | } | |
| 978 | ||
| 979 | new_line_warning <- function(str_v) { | |
| 980 | 136x |   if (any(unlist(sapply(str_v, grepl, pattern = "\n")))) { | 
| 981 | ! | msg <- c( | 
| 982 | ! | "Detected manual newlines when automatic title/footer word-wrapping is on.", | 
| 983 | ! | "This is unsupported and will result in undefined behavior. Please either ", | 
| 984 | ! | "utilize automatic word-wrapping with newline characters inserted, or ", | 
| 985 | ! | "turn off automatic wrapping and wordwrap all contents manually by inserting ", | 
| 986 | ! | "newlines." | 
| 987 | ) | |
| 988 | ! | warning(paste0(msg, collapse = "")) | 
| 989 | } | |
| 990 | } | |
| 991 | ||
| 992 | #' Wrap a string to a precise width | |
| 993 | #' | |
| 994 | #' Core wrapping functionality that preserves whitespace. Newline character `"\n"` is not supported | |
| 995 | #' by core functionality [stringi::stri_wrap()]. This is usually solved beforehand by [matrix_form()]. | |
| 996 | #' If the width is smaller than any large word, these will be truncated after `width` characters. If | |
| 997 | #' the split leaves trailing groups of empty spaces, they will be dropped. | |
| 998 | #' | |
| 999 | #' @inheritParams open_font_dev | |
| 1000 | #' @param str (`string`, `character`, or `list`)\cr string to be wrapped. If it is a `vector` or | |
| 1001 | #' a `list`, it will be looped as a `list` and returned with `unlist(use.names = FALSE)`. | |
| 1002 | #' @param width (`numeric(1)`)\cr width, in characters, that the text should be wrapped to. | |
| 1003 | #' @param collapse (`string` or `NULL`)\cr collapse character used to separate segments of words that | |
| 1004 | #' have been split and should be pasted together. This is usually done internally with `"\n"` to update | |
| 1005 | #' the wrapping along with other internal values. | |
| 1006 | #' | |
| 1007 | #' @details Word wrapping happens similarly to [stringi::stri_wrap()] with the following difference: individual | |
| 1008 | #' words which are longer than `max_width` are broken up in a way that fits with other word wrapping. | |
| 1009 | #' | |
| 1010 | #' @return A string if `str` is one element and if `collapse = NULL`. Otherwise, a list of elements | |
| 1011 | #' (if `length(str) > 1`) that can contain strings or vectors of characters (if `collapse = NULL`). | |
| 1012 | #' | |
| 1013 | #' @examples | |
| 1014 | #' str <- list( | |
| 1015 | #' " , something really \\tnot very good", # \t needs to be escaped | |
| 1016 | #' " but I keep it12 " | |
| 1017 | #' ) | |
| 1018 | #' wrap_string(str, 5, collapse = "\n") | |
| 1019 | #' | |
| 1020 | #' @export | |
| 1021 | wrap_string <- function(str, width, collapse = NULL, fontspec = font_spec()) { | |
| 1022 | 36488x |   if (length(str) > 1) { | 
| 1023 | 114x | return( | 
| 1024 | 114x | unlist( | 
| 1025 | 114x | lapply(str, wrap_string, width = width, collapse = collapse, fontspec = fontspec), | 
| 1026 | 114x | use.names = FALSE | 
| 1027 | ) | |
| 1028 | ) | |
| 1029 | } | |
| 1030 | 36374x | str <- unlist(str, use.names = FALSE) # it happens is one list element | 
| 1031 | 36374x |   if (!length(str) || !nzchar(str) || is.na(str)) { | 
| 1032 | 3872x | return(str) | 
| 1033 | } | |
| 1034 | 32502x | checkmate::assert_character(str) | 
| 1035 | 32502x | checkmate::assert_int(width, lower = 1) | 
| 1036 | ||
| 1037 | 32502x |   if (any(grepl("\\n", str))) { | 
| 1038 | ! | stop( | 
| 1039 | ! | "Found \\n in a string that was meant to be wrapped. This should not happen ", | 
| 1040 | ! | "because matrix_form should take care of them before this step (toString, ", | 
| 1041 | ! | "i.e. the printing machinery). Please contact the maintaner or file an issue." | 
| 1042 | ) | |
| 1043 | } | |
| 1044 | ||
| 1045 | 32502x |   if (!is_monospace(fontspec)) { | 
| 1046 | 3x | return(wrap_string_ttype(str, width, fontspec, collapse = collapse)) | 
| 1047 | } | |
| 1048 | ||
| 1049 | # str can be also a vector or list. In this case simplify manages the output | |
| 1050 | 32499x | ret <- .go_stri_wrap(str, width) | 
| 1051 | ||
| 1052 | # Check if it went fine | |
| 1053 | 32499x |   if (any(nchar_ttype(ret, fontspec) > width)) { | 
| 1054 | 68x | which_exceeded <- which(nchar_ttype(ret, fontspec) > width) | 
| 1055 | ||
| 1056 | # Recursive for loop to take word interval | |
| 1057 | 68x |     while (length(which_exceeded) > 0) { | 
| 1058 | 75x | we_i <- which_exceeded[1] | 
| 1059 | # Is there space for some part of the next word? | |
| 1060 | 75x | char_threshold <- width * (2 / 3) + 0.01 # if too little space -> no previous word | 
| 1061 | 75x | smart_condition <- nchar_ttype(ret[we_i - 1], fontspec) + 1 < char_threshold # +1 is for spaces | 
| 1062 | 75x |       if (we_i - 1 > 0 && smart_condition) { | 
| 1063 | 6x | we_interval <- unique(c(we_i - 1, we_i)) | 
| 1064 | 6x | we_interval <- we_interval[ | 
| 1065 | 6x | (we_interval < (length(ret) + 1)) & | 
| 1066 | 6x | (we_interval > 0) | 
| 1067 | ] | |
| 1068 |       } else { | |
| 1069 | 69x | we_interval <- we_i | 
| 1070 | } | |
| 1071 | # Split words and collapse (needs unique afterwards) | |
| 1072 | 75x | ret[we_interval] <- split_words_by( | 
| 1073 | 75x | paste0(ret[we_interval], collapse = " "), | 
| 1074 | 75x | width | 
| 1075 | ) | |
| 1076 | # Taking out repetitions if there are more than one | |
| 1077 | 75x |       if (length(we_interval) > 1) { | 
| 1078 | 6x | ret <- ret[-we_interval[-1]] | 
| 1079 | 6x | we_interval <- we_interval[1] | 
| 1080 | } | |
| 1081 | # Paste together and rerun if it is not the same as original ret | |
| 1082 | 75x | ret_collapse <- paste0(ret, collapse = " ") | 
| 1083 | ||
| 1084 | # Checking if we are stuck in a loop | |
| 1085 | 75x | ori_wrapped_txt_v <- .go_stri_wrap(str, width) | 
| 1086 | 75x | cur_wrapped_txt_v <- .go_stri_wrap(ret_collapse, width) | 
| 1087 | 75x | broken_char_ori <- sum(nchar_ttype(ori_wrapped_txt_v, fontspec) > width) # how many issues there were | 
| 1088 | 75x | broken_char_cur <- sum(nchar_ttype(cur_wrapped_txt_v, fontspec) > width) # how many issues there are | 
| 1089 | ||
| 1090 | # if still broken, we did not solve the current issue! | |
| 1091 | 75x |       if (setequal(ori_wrapped_txt_v, cur_wrapped_txt_v) || broken_char_cur >= broken_char_ori) { | 
| 1092 | # help function: Very rare case where the recursion is stuck in a loop | |
| 1093 | 14x | ret_tmp <- force_split_words_by(ret[we_interval], width) # here we_interval is only one ind | 
| 1094 | 14x | ret <- append(ret, ret_tmp, we_interval)[-we_interval] | 
| 1095 | 14x | which_exceeded <- which(nchar_ttype(ret, fontspec) > width) | 
| 1096 |       } else { | |
| 1097 | 61x | return(wrap_string(str = ret_collapse, width = width, collapse = collapse)) | 
| 1098 | } | |
| 1099 | } | |
| 1100 | } | |
| 1101 | ||
| 1102 | 32438x |   if (!is.null(collapse)) { | 
| 1103 | 31987x | return(paste0(ret, collapse = collapse)) | 
| 1104 | } | |
| 1105 | ||
| 1106 | 451x | return(ret) | 
| 1107 | } | |
| 1108 | ||
| 1109 | .go_stri_wrap <- function(str, w) { | |
| 1110 | 32649x |   if (w < 1) { | 
| 1111 | ! | return(str) | 
| 1112 | } | |
| 1113 | 32649x | stringi::stri_wrap(str, | 
| 1114 | 32649x | width = w, | 
| 1115 | 32649x | normalize = FALSE, # keeps spaces | 
| 1116 | 32649x | simplify = TRUE, # If FALSE makes it a list with str elements | 
| 1117 | 32649x | indent = 0, | 
| 1118 | 32649x | use_length = FALSE # incase the defaul changes, use actual char widths | 
| 1119 | ) | |
| 1120 | } | |
| 1121 | ||
| 1122 | #' @rdname wrap_string_ttype | |
| 1123 | #' @export | |
| 1124 | split_word_ttype <- function(str, width, fontspec, min_ok_chars) { | |
| 1125 | 11x | chrs <- strsplit(str, "")[[1]] | 
| 1126 | 11x | nctt_chars <- nchar_ttype(chrs, fontspec, raw = TRUE) | 
| 1127 | 11x | ok <- which(cumsum(nctt_chars) <= width) | 
| 1128 | 11x |   if (length(ok) < min_ok_chars || length(chrs) - length(ok) < min_ok_chars) { | 
| 1129 | 3x | list( | 
| 1130 | 3x | ok = character(), | 
| 1131 | 3x | remainder = str | 
| 1132 | ) | |
| 1133 |   } else { | |
| 1134 | 8x | list( | 
| 1135 | 8x | ok = substr(str, 1, length(ok)), | 
| 1136 | 8x | remainder = substr(str, length(ok) + 1, nchar(str)) | 
| 1137 | ) | |
| 1138 | } | |
| 1139 | } | |
| 1140 | ||
| 1141 | ## need a separate path here because **the number of characters** | |
| 1142 | ## in each part is no longer going to be constant the way it | |
| 1143 | ## was for monospace | |
| 1144 | ## this is much slower but still shouldn't be a bottleneck, if it is we'll | |
| 1145 | ## have to do something else | |
| 1146 | #' wrap string given a Truetype font | |
| 1147 | #' | |
| 1148 | #' @inheritParams wrap_string | |
| 1149 | #' @param min_ok_chars (`numeric(1)`)\cr number of minimum characters that remain | |
| 1150 | #' on either side when a word is split. | |
| 1151 | #' @param wordbreak_ok (`logical(1)`)\cr should breaking within a word be allowed? If, `FALSE`, | |
| 1152 | #' attempts to wrap a string to a width narrower than its widest word will result | |
| 1153 | #' in an error. | |
| 1154 | #' @return `str`, broken up into a word-wrapped vector | |
| 1155 | #' @export | |
| 1156 | wrap_string_ttype <- function(str, | |
| 1157 | width, | |
| 1158 | fontspec, | |
| 1159 | collapse = NULL, | |
| 1160 | min_ok_chars = min(floor(nchar(str) / 2), 4, floor(width / 2)), | |
| 1161 |                               wordbreak_ok = TRUE) { | |
| 1162 | 12x | newdev <- open_font_dev(fontspec) | 
| 1163 | 11x |   if (newdev) { | 
| 1164 | ! | on.exit(close_font_dev()) | 
| 1165 | } | |
| 1166 | ||
| 1167 | 11x | rawspls <- strsplit(str, "[[:space:]](?=[^[:space:]])", perl = TRUE)[[1]] # preserve all but one space | 
| 1168 | 11x | nctt <- nchar_ttype(rawspls, fontspec, raw = TRUE) | 
| 1169 | 11x | pts <- which(cumsum(nctt) <= width) | 
| 1170 | 11x |   if (length(pts) == length(rawspls)) { ## no splitting needed | 
| 1171 | 3x | return(str) | 
| 1172 | 8x |   } else if (length(pts) == 0) { ## no spaces, all one word, split it and keep going | 
| 1173 | 7x |     if (wordbreak_ok) { | 
| 1174 | 7x | inner_res <- list() | 
| 1175 | 7x | min_ok_inner <- min_ok_chars | 
| 1176 | 7x |       while (length(inner_res$ok) == 0) { | 
| 1177 | 10x | inner_res <- split_word_ttype(rawspls[1], width, fontspec, min_ok_inner) # min_ok_chars) | 
| 1178 | 10x | min_ok_inner <- floor(min_ok_inner / 2) | 
| 1179 | } | |
| 1180 | 7x | done <- inner_res$ok | 
| 1181 | 7x | remainder <- paste(c(inner_res$remainder, rawspls[-1]), collapse = " ") | 
| 1182 |     } else { | |
| 1183 | ! | stop( | 
| 1184 | ! | "Unable to find word wrapping solution without breaking word: ", | 
| 1185 | ! | rawspls[[1]], " [requires ", nchar_ttype(rawspls[[1]], fontspec), " spaces of width, out of ", | 
| 1186 | ! | width, " available]." | 
| 1187 | ) | |
| 1188 | } | |
| 1189 |   } else { ## some words fit, and some words don't | |
| 1190 | 1x | done_tmp <- paste(rawspls[pts], collapse = " ") | 
| 1191 | 1x | tospl_tmp <- rawspls[length(pts) + 1] | 
| 1192 | 1x | width_tmp <- width - sum(nctt[pts]) | 
| 1193 | 1x |     if (wordbreak_ok && width_tmp / width > .33) { | 
| 1194 | 1x | inner_res <- split_word_ttype(tospl_tmp, width_tmp, fontspec, | 
| 1195 | 1x | min_ok_chars = min_ok_chars | 
| 1196 | ) | |
| 1197 |     } else { | |
| 1198 | ! | inner_res <- list(done = "", remainder = tospl_tmp) | 
| 1199 | } | |
| 1200 | 1x | done <- paste(c(rawspls[pts], inner_res$ok), | 
| 1201 | 1x | collapse = " " | 
| 1202 | ) | |
| 1203 | 1x | remainder <- paste( | 
| 1204 | 1x | c( | 
| 1205 | 1x | inner_res$remainder, | 
| 1206 | 1x | if (length(rawspls) > length(pts) + 1) tail(rawspls, -(length(pts) + 1)) | 
| 1207 | ), | |
| 1208 | 1x | collapse = " " | 
| 1209 | ) | |
| 1210 | } | |
| 1211 | 8x | ret <- c( | 
| 1212 | 8x | done, | 
| 1213 | 8x | wrap_string_ttype(remainder, width, fontspec) | 
| 1214 | ) | |
| 1215 | 8x |   if (!is.null(collapse)) { | 
| 1216 | ! | ret <- paste(ret, collapse = collapse) | 
| 1217 | } | |
| 1218 | 8x | ret | 
| 1219 | } | |
| 1220 | ||
| 1221 | # help function: Very rare case where the recursion is stuck in a loop | |
| 1222 | force_split_words_by <- function(ret, width) { | |
| 1223 | 14x | which_exceeded <- which(nchar(ret) > width) | 
| 1224 | 14x | ret_tmp <- NULL | 
| 1225 | 14x |   for (ii in seq_along(ret)) { | 
| 1226 | 14x |     if (ii %in% which_exceeded) { | 
| 1227 | 14x | wrd_i <- ret[ii] | 
| 1228 | 14x | init_v <- seq(1, nchar(wrd_i), by = width) | 
| 1229 | 14x | end_v <- c(init_v[-1] - 1, nchar(wrd_i)) | 
| 1230 | 14x | str_v_tmp <- stringi::stri_sub(wrd_i, from = init_v, to = end_v) | 
| 1231 | 14x |       ret_tmp <- c(ret_tmp, str_v_tmp[!grepl("^\\s+$", str_v_tmp) & nzchar(str_v_tmp)]) | 
| 1232 |     } else { | |
| 1233 | ! | ret_tmp <- c(ret_tmp, ret[ii]) | 
| 1234 | } | |
| 1235 | } | |
| 1236 | 14x | ret_tmp | 
| 1237 | } | |
| 1238 | ||
| 1239 | # Helper fnc to split the words and collapse them with space | |
| 1240 | split_words_by <- function(wrd, width) { | |
| 1241 | 75x |   vapply(wrd, function(wrd_i) { | 
| 1242 | 75x | init_v <- seq(1, nchar(wrd_i), by = width) | 
| 1243 | 75x | end_v <- c(init_v[-1] - 1, nchar(wrd_i)) | 
| 1244 | 75x | fin_str_v <- substring(wrd_i, init_v, end_v) | 
| 1245 | 75x |     is_only_spaces <- grepl("^\\s+$", fin_str_v) | 
| 1246 | # We pop only spaces at this point | |
| 1247 | 75x |     if (all(is_only_spaces)) { | 
| 1248 | ! | fin_str_v <- fin_str_v[1] # keep only one width-sized empty | 
| 1249 |     } else { | |
| 1250 | 75x | fin_str_v <- fin_str_v[!is_only_spaces] # hybrid text + \s | 
| 1251 | } | |
| 1252 | ||
| 1253 | # Collapse the string | |
| 1254 | 75x | paste0(fin_str_v, collapse = " ") | 
| 1255 | 75x | }, character(1), USE.NAMES = FALSE) | 
| 1256 | } | |
| 1257 | ||
| 1258 | #' @describeIn wrap_string Deprecated function. Please use [wrap_string()] instead. | |
| 1259 | #' | |
| 1260 | #' @examples | |
| 1261 | #' wrap_txt(str, 5, collapse = NULL) | |
| 1262 | #' | |
| 1263 | #' @export | |
| 1264 | wrap_txt <- function(str, width, collapse = NULL, fontspec = font_spec()) { | |
| 1265 | 396x | new_dev <- open_font_dev(fontspec) | 
| 1266 | 396x |   if (new_dev) { | 
| 1267 | 2x | on.exit(close_font_dev()) | 
| 1268 | } | |
| 1269 | ||
| 1270 | 396x | unlist(wrap_string(str, width, collapse, fontspec = fontspec), use.names = FALSE) | 
| 1271 | } | |
| 1272 | ||
| 1273 | pad_vert_top <- function(x, len, default = "") { | |
| 1274 | 5546x | c(x, rep(default, len - length(x))) | 
| 1275 | } | |
| 1276 | ||
| 1277 | pad_vert_bottom <- function(x, len, default = "") { | |
| 1278 | 362x | c(rep(default, len - length(x)), x) | 
| 1279 | } | |
| 1280 | ||
| 1281 | pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) { | |
| 1282 | 741x | dat <- unlist(lapply(vec[-1], cpadder, len = len)) | 
| 1283 | 741x | dat <- c(rlpadder(vec[[1]], len = len), dat) | 
| 1284 | 741x | matrix(dat, nrow = len) | 
| 1285 | } | |
| 1286 | ||
| 1287 | rep_vec_to_len <- function(vec, len, ...) { | |
| 1288 | 698x | matrix(unlist(lapply(vec, rep, times = len)), | 
| 1289 | 698x | nrow = len | 
| 1290 | ) | |
| 1291 | } | |
| 1292 | ||
| 1293 | safe_strsplit <- function(x, split, ...) { | |
| 1294 | 990x | ret <- strsplit(x, split, ...) | 
| 1295 | 990x | lapply(ret, function(reti) if (length(reti) == 0) "" else reti) | 
| 1296 | } | |
| 1297 | ||
| 1298 | .expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) { | |
| 1299 | 1439x | leni <- row_nlines[i] | 
| 1300 | 1439x | rw <- mat[i, ] | 
| 1301 | 1439x |   if (is.character(rw)) { | 
| 1302 | 990x | rw <- safe_strsplit(rw, "\n", fixed = TRUE) | 
| 1303 | } | |
| 1304 | 1439x | expfun(rw, len = leni, ...) | 
| 1305 | } | |
| 1306 | ||
| 1307 | expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) { | |
| 1308 | 280x | rinds <- seq_len(nrow(mat)) | 
| 1309 | 280x | exprows <- lapply(rinds, .expand_mat_rows_inner, | 
| 1310 | 280x | mat = mat, | 
| 1311 | 280x | row_nlines = row_nlines, | 
| 1312 | 280x | expfun = expfun, | 
| 1313 | ... | |
| 1314 | ) | |
| 1315 | 280x | do.call(rbind, exprows) | 
| 1316 | } | |
| 1317 | ||
| 1318 | #' Transform a vector of spans (with duplication) into a visibility vector | |
| 1319 | #' | |
| 1320 | #' @param spans (`numeric`)\cr a vector of spans, with each span value repeated | |
| 1321 | #' for the cells it covers. | |
| 1322 | #' | |
| 1323 | #' @details | |
| 1324 | #' The values of `spans` are assumed to be repeated such that each individual position covered by the | |
| 1325 | #' span has the repeated value. | |
| 1326 | #' | |
| 1327 | #' This means that each block of values in `spans` must be of a length at least equal to its value | |
| 1328 | #' (i.e. two 2s, three 3s, etc). | |
| 1329 | #' | |
| 1330 | #' This function correctly handles cases where two spans of the same size are next to each other; | |
| 1331 | #' i.e., a block of four 2s represents two large cells each of which spans two individual cells. | |
| 1332 | #' | |
| 1333 | #' @return A logical vector the same length as `spans` indicating whether the contents of a string vector | |
| 1334 | #' with those spans is valid. | |
| 1335 | #' | |
| 1336 | #' @note | |
| 1337 | #' Currently no checking or enforcement is done to verify that the vector of spans is valid according to | |
| 1338 | #' the specifications described in the Details section above. | |
| 1339 | #' | |
| 1340 | #' @examples | |
| 1341 | #' spans_to_viscell(c(2, 2, 2, 2, 1, 3, 3, 3)) | |
| 1342 | #' | |
| 1343 | #' @export | |
| 1344 | spans_to_viscell <- function(spans) { | |
| 1345 | 2x |   if (!is.vector(spans)) { | 
| 1346 | ! | spans <- as.vector(spans) | 
| 1347 | } | |
| 1348 | 2x | myrle <- rle(spans) | 
| 1349 | 2x | unlist( | 
| 1350 | 2x | mapply( | 
| 1351 | 2x |       function(vl, ln) { | 
| 1352 | 4x | rep(c(TRUE, rep(FALSE, vl - 1L)), times = ln / vl) | 
| 1353 | }, | |
| 1354 | 2x | SIMPLIFY = FALSE, | 
| 1355 | 2x | vl = myrle$values, | 
| 1356 | 2x | ln = myrle$lengths | 
| 1357 | ), | |
| 1358 | 2x | recursive = FALSE | 
| 1359 | ) | |
| 1360 | } | |
| 1361 | ||
| 1362 | #' Propose column widths based on the `MatrixPrintForm` of an object | |
| 1363 | #' | |
| 1364 | #' Row names are also considered a column for the output. | |
| 1365 | #' | |
| 1366 | #' @inheritParams open_font_dev | |
| 1367 | #' @inheritParams format_value | |
| 1368 | #' @param x (`ANY`)\cr a `MatrixPrintForm` object, or an object with a `matrix_form` method. | |
| 1369 | #' @param indent_size (`numeric(1)`)\cr indent size, in characters. Ignored when `x` is already | |
| 1370 | #' a `MatrixPrintForm` object in favor of information there. | |
| 1371 | #' | |
| 1372 | #' @return A vector of column widths based on the content of `x` for use in printing and pagination. | |
| 1373 | #' | |
| 1374 | #' @examples | |
| 1375 | #' mf <- basic_matrix_form(mtcars) | |
| 1376 | #' propose_column_widths(mf) | |
| 1377 | #' | |
| 1378 | #' @export | |
| 1379 | propose_column_widths <- function(x, | |
| 1380 | indent_size = 2, | |
| 1381 | fontspec = font_spec(), | |
| 1382 |                                   round_type = c("iec", "sas")) { | |
| 1383 | 95x | new_dev <- open_font_dev(fontspec) | 
| 1384 | 95x |   if (new_dev) { | 
| 1385 | 65x | on.exit(close_font_dev()) | 
| 1386 | } | |
| 1387 | ||
| 1388 | 95x |   if (!is(x, "MatrixPrintForm")) { | 
| 1389 | ! | x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) | 
| 1390 | } | |
| 1391 | 95x | body <- mf_strings(x) | 
| 1392 | 95x | spans <- mf_spans(x) | 
| 1393 | 95x | aligns <- mf_aligns(x) | 
| 1394 | 95x | display <- mf_display(x) | 
| 1395 | ||
| 1396 | # compute decimal alignment if asked in alignment matrix | |
| 1397 | 95x |   if (any_dec_align(aligns)) { | 
| 1398 | 27x | body <- decimal_align(body, aligns) | 
| 1399 | } | |
| 1400 | ||
| 1401 | ## chars <- nchar(body) #old monospace assumption | |
| 1402 |   ## we now use widths in terms of the printwidth of the space (" ") | |
| 1403 | ## character. This collapses to the same thing in the monospace | |
| 1404 | ## case but allows us to reasonably support truetype fonts | |
| 1405 | 92x | chars <- nchar_ttype(body, fontspec) | 
| 1406 | ||
| 1407 | # first check column widths without colspan | |
| 1408 | 92x | has_spans <- spans != 1 | 
| 1409 | 92x | chars_ns <- chars | 
| 1410 | 92x | chars_ns[has_spans] <- 0 | 
| 1411 | 92x | widths <- apply(chars_ns, 2, max) | 
| 1412 | ||
| 1413 | # now check if the colspans require extra width | |
| 1414 | 92x |   if (any(has_spans)) { | 
| 1415 | 1x | has_row_spans <- apply(has_spans, 1, any) | 
| 1416 | ||
| 1417 | 1x | chars_sp <- chars[has_row_spans, , drop = FALSE] | 
| 1418 | 1x | spans_sp <- spans[has_row_spans, , drop = FALSE] | 
| 1419 | 1x | disp_sp <- display[has_row_spans, , drop = FALSE] | 
| 1420 | ||
| 1421 | 1x | nc <- ncol(spans) | 
| 1422 | 1x |     for (i in seq_len(nrow(chars_sp))) { | 
| 1423 | 1x |       for (j in seq_len(nc)) { | 
| 1424 | 2x |         if (disp_sp[i, j] && spans_sp[i, j] != 1) { | 
| 1425 | 1x | i_cols <- seq(j, j + spans_sp[i, j] - 1) | 
| 1426 | ||
| 1427 | 1x | nchar_i <- chars_sp[i, j] | 
| 1428 | 1x | cw_i <- widths[i_cols] | 
| 1429 | 1x | available_width <- sum(cw_i) | 
| 1430 | ||
| 1431 | 1x |           if (nchar_i > available_width) { | 
| 1432 | # need to update widths to fit content with colspans | |
| 1433 | # spread width among columns | |
| 1434 | ! | widths[i_cols] <- cw_i + spread_integer(nchar_i - available_width, length(cw_i)) | 
| 1435 | } | |
| 1436 | } | |
| 1437 | } | |
| 1438 | } | |
| 1439 | } | |
| 1440 | 92x | widths | 
| 1441 | } | |
| 1442 | ||
| 1443 | ## "number of characters" width in terms of | |
| 1444 | ## width of " " for the chosen font family | |
| 1445 | ||
| 1446 | ## pdf device with font specification MUST already be open | |
| 1447 | ||
| 1448 | #' Calculate font-specific string width | |
| 1449 | #' | |
| 1450 | #' This function returns the width of each element `x` | |
| 1451 | #' *as a multiple of the width of the space character | |
| 1452 | #' for in declared font*, rounded up to the nearest | |
| 1453 | #' integer. This is used extensively in the text rendering | |
| 1454 | #' ([toString()]) and pagination machinery for | |
| 1455 | #' calculating word wrapping, default column widths, | |
| 1456 | #' lines per page, etc. | |
| 1457 | #' | |
| 1458 | #' @param x (`character`)\cr the string(s) to calculate width(s) for. | |
| 1459 | #' @param fontspec (`font_spec` or `NULL`)\cr if non-NULL, the font to use for | |
| 1460 | #' the calculations (as returned by [font_spec()]). Defaults to "Courier", | |
| 1461 | #' which is a monospace font. If NULL, the width will be returned | |
| 1462 | #' in number of characters by calling `nchar` directly. | |
| 1463 | #' @param tol (`numeric(1)`)\cr the tolerance to use when determining | |
| 1464 | #' if a multiple needs to be rounded up to the next integer. See | |
| 1465 | #' Details. | |
| 1466 | #' @param raw (`logical(1)`)\cr whether unrounded widths should be returned. Defaults to `FALSE`. | |
| 1467 | #' | |
| 1468 | #' @details String width is defined in terms of spaces within | |
| 1469 | #' the specified font. For monospace fonts, this definition | |
| 1470 | #' collapses to the number of characters in the string | |
| 1471 | #' ([nchar()]), but for truetype fonts it does not. | |
| 1472 | #' | |
| 1473 | #' For `raw = FALSE`, non-integer values (the norm in a truetype | |
| 1474 | #' setting) for the number of spaces a string takes up is rounded | |
| 1475 | #' up, *unless the multiple is less than `tol` above the last integer | |
| 1476 | #' before it*. E.g., if `k - num_spaces < tol` for an integer | |
| 1477 | #' `k`, `k` is returned instead of `k+1`. | |
| 1478 | #' | |
| 1479 | #' @seealso [font_spec()] | |
| 1480 | #' | |
| 1481 | #' @examples | |
| 1482 | #' nchar_ttype("hi there!") | |
| 1483 | #' | |
| 1484 | #' nchar_ttype("hi there!", font_spec("Times")) | |
| 1485 | #' | |
| 1486 | #' @export | |
| 1487 | nchar_ttype <- function(x, fontspec = font_spec(), tol = sqrt(.Machine$double.eps), raw = FALSE) { | |
| 1488 | ## escape hatch because sometimes we need to call, e.g. make_row_df | |
| 1489 | ## but we dont' care about getting the word wrapping right and the | |
| 1490 | ## performance penalty was KILLING us. Looking at you | |
| 1491 | ## rtables::update_ref_indexing @.@ | |
| 1492 | 48730x |   if (is.null(fontspec)) { | 
| 1493 | 1x | return(nchar(x)) | 
| 1494 | } | |
| 1495 | 48729x | new_dev <- open_font_dev(fontspec) | 
| 1496 | 48729x |   if (new_dev) { | 
| 1497 | 149x | on.exit(close_font_dev()) | 
| 1498 | } | |
| 1499 | 48729x |   if (font_dev_state$ismonospace) { ## WAY faster if we can do it | 
| 1500 | 48705x | return(nchar(x)) | 
| 1501 | } | |
| 1502 | 24x | space_width <- get_space_width() | 
| 1503 | ## cwidth_inches_unsafe is ok here because if we don't | |
| 1504 | ## have a successfully opened state (somehow), get_space_width | |
| 1505 | ## above will error. | |
| 1506 | 24x | num_inches_raw <- vapply(x, cwidth_inches_unsafe, 1.0) | 
| 1507 | 24x | num_spaces_raw <- num_inches_raw / space_width | 
| 1508 | 24x |   if (!raw) { | 
| 1509 | 1x | num_spaces_ceil <- ceiling(num_spaces_raw) | 
| 1510 | ## we don't want to add one when the answer is e.g, 3.0000000000000953 | |
| 1511 | 1x | within_tol <- which(num_spaces_raw + 1 - num_spaces_ceil <= tol) | 
| 1512 | 1x | ret <- num_spaces_ceil | 
| 1513 | 1x |     if (length(within_tol) == 0L) { | 
| 1514 | 1x | ret[within_tol] <- floor(num_spaces_raw[within_tol]) | 
| 1515 | } | |
| 1516 |   } else { | |
| 1517 | 23x | ret <- num_spaces_raw | 
| 1518 | } | |
| 1519 | 24x |   if (!is.null(dim(x))) { | 
| 1520 | ! | dim(ret) <- dim(x) | 
| 1521 |   } else { | |
| 1522 | 24x | names(ret) <- NULL | 
| 1523 | } | |
| 1524 | 24x | ret | 
| 1525 | } | |
| 1526 | ||
| 1527 | #' Pad a string and align within string | |
| 1528 | #' | |
| 1529 | #' @inheritParams open_font_dev | |
| 1530 | #' @param x (`string`)\cr a string. | |
| 1531 | #' @param n (`integer(1)`)\cr number of characters in the output string. If `n < nchar(x)`, an error is thrown. | |
| 1532 | #' @param just (`string`)\cr text alignment justification to use. Defaults to `"center"`. Must be one of | |
| 1533 | #' `"center"`, `"right"`, `"left"`, `"dec_right"`, `"dec_left"`, or `"decimal"`. | |
| 1534 | #' | |
| 1535 | #' @return `x`, padded to be a string of length `n`. | |
| 1536 | #' | |
| 1537 | #' @examples | |
| 1538 | #' padstr("abc", 3) | |
| 1539 | #' padstr("abc", 4) | |
| 1540 | #' padstr("abc", 5) | |
| 1541 | #' padstr("abc", 5, "left") | |
| 1542 | #' padstr("abc", 5, "right") | |
| 1543 | #' | |
| 1544 | #' \dontrun{ | |
| 1545 | #' # Expect error: "abc" has more than 1 characters | |
| 1546 | #' padstr("abc", 1) | |
| 1547 | #' } | |
| 1548 | #' | |
| 1549 | #' @export | |
| 1550 | padstr <- function(x, n, just = list_valid_aligns(), fontspec = font_spec()) { | |
| 1551 | 15649x | just <- match.arg(just) | 
| 1552 | ||
| 1553 | 1x |   if (length(x) != 1) stop("length of x needs to be 1 and not", length(x)) | 
| 1554 | 1x |   if (is.na(n) || !is.numeric(n) || n < 0) stop("n needs to be numeric and > 0") | 
| 1555 | ||
| 1556 | 2x | if (is.na(x)) x <- "<NA>" | 
| 1557 | ||
| 1558 | 15647x | nc <- nchar_ttype(x, fontspec) | 
| 1559 | ! |   if (n < nc) stop("\"", x, "\" has more than ", n, " characters") | 
| 1560 | ||
| 1561 | 15647x | switch(just, | 
| 1562 |     center = { | |
| 1563 | 13742x | pad <- (n - nc) / 2 | 
| 1564 | 13742x | paste0(spaces(floor(pad)), x, spaces(ceiling(pad))) | 
| 1565 | }, | |
| 1566 | 1754x | left = paste0(x, spaces(n - nc)), | 
| 1567 | 10x | right = paste0(spaces(n - nc), x), | 
| 1568 |     decimal = { | |
| 1569 | 61x | pad <- (n - nc) / 2 | 
| 1570 | 61x | paste0(spaces(floor(pad)), x, spaces(ceiling(pad))) | 
| 1571 | }, | |
| 1572 | 45x | dec_left = paste0(x, spaces(n - nc)), | 
| 1573 | 35x | dec_right = paste0(spaces(n - nc), x) | 
| 1574 | ) | |
| 1575 | } | |
| 1576 | ||
| 1577 | spaces <- function(n) { | |
| 1578 | 29608x |   strrep(" ", n) | 
| 1579 | } | |
| 1580 | ||
| 1581 | .paste_no_na <- function(x, ...) { | |
| 1582 | 2408x | paste(na.omit(x), ...) | 
| 1583 | } | |
| 1584 | ||
| 1585 | #' Spread an integer to a given length | |
| 1586 | #' | |
| 1587 | #' @param x (`integer(1)`)\cr number to spread. | |
| 1588 | #' @param len (`integer(1)`)\cr number of times to repeat `x`. | |
| 1589 | #' | |
| 1590 | #' @return If `x` is a scalar whole number value (see [is.wholenumber()]), the value `x` is repeated `len` times. | |
| 1591 | #' Otherwise, an error is thrown. | |
| 1592 | #' | |
| 1593 | #' @examples | |
| 1594 | #' spread_integer(3, 1) | |
| 1595 | #' spread_integer(0, 3) | |
| 1596 | #' spread_integer(1, 3) | |
| 1597 | #' spread_integer(2, 3) | |
| 1598 | #' spread_integer(3, 3) | |
| 1599 | #' spread_integer(4, 3) | |
| 1600 | #' spread_integer(5, 3) | |
| 1601 | #' spread_integer(6, 3) | |
| 1602 | #' spread_integer(7, 3) | |
| 1603 | #' | |
| 1604 | #' @export | |
| 1605 | spread_integer <- function(x, len) { | |
| 1606 | 2x | stopifnot( | 
| 1607 | 2x | is.wholenumber(x), length(x) == 1, x >= 0, | 
| 1608 | 2x | is.wholenumber(len), length(len) == 1, len >= 0, | 
| 1609 | 2x | !(len == 0 && x > 0) | 
| 1610 | ) | |
| 1611 | ||
| 1612 | 1x |   if (len == 0) { | 
| 1613 | ! | integer(0) | 
| 1614 |   } else { | |
| 1615 | 1x | y <- rep(floor(x / len), len) | 
| 1616 | 1x | i <- 1 | 
| 1617 | 1x |     while (sum(y) < x) { | 
| 1618 | 1x | y[i] <- y[i] + 1 | 
| 1619 | 1x |       if (i == len) { | 
| 1620 | ! | i <- 1 | 
| 1621 |       } else { | |
| 1622 | 1x | i <- i + 1 | 
| 1623 | } | |
| 1624 | } | |
| 1625 | 1x | y | 
| 1626 | } | |
| 1627 | } | |
| 1628 | ||
| 1629 | #' Check if a value is a whole number | |
| 1630 | #' | |
| 1631 | #' @param x (`numeric(1)`)\cr a numeric value. | |
| 1632 | #' @param tol (`numeric(1)`)\cr a precision tolerance. | |
| 1633 | #' | |
| 1634 | #' @return `TRUE` if `x` is within `tol` of zero, `FALSE` otherwise. | |
| 1635 | #' | |
| 1636 | #' @examples | |
| 1637 | #' is.wholenumber(5) | |
| 1638 | #' is.wholenumber(5.00000000000000001) | |
| 1639 | #' is.wholenumber(.5) | |
| 1640 | #' | |
| 1641 | #' @export | |
| 1642 | is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) { | |
| 1643 | 3x | abs(x - round(x)) < tol | 
| 1644 | } | 
| 1 | ## until we do it for real | |
| 2 | ||
| 3 | #' Class for Matrix Print Form | |
| 4 | #' | |
| 5 | #' The `MatrixPrintForm` class, an intermediate representation for ASCII table printing. | |
| 6 | #' | |
| 7 | #' @name MatrixPrintForm-class | |
| 8 | #' @rdname MatrixPrintForm_class | |
| 9 | #' @exportClass MatrixPrintForm | |
| 10 | setOldClass(c("MatrixPrintForm", "list")) | |
| 11 | ||
| 12 | mform_handle_newlines <- function(matform) { | |
| 13 | # Retrieving relevant information | |
| 14 | 265x | has_topleft <- mf_has_topleft(matform) | 
| 15 | 265x | strmat <- mf_strings(matform) | 
| 16 | 265x | frmmat <- mf_formats(matform) | 
| 17 | 265x | spamat <- mf_spans(matform) | 
| 18 | 265x | alimat <- mf_aligns(matform) | 
| 19 | 265x | nr_header <- mf_nrheader(matform) | 
| 20 | 265x | nl_inds_header <- seq(mf_nlheader(matform)) | 
| 21 | 265x | hdr_inds <- seq(nr_header) | 
| 22 | ||
| 23 | # hack that is necessary only if top-left is bottom aligned (default) | |
| 24 | 265x | topleft_has_nl_char <- FALSE | 
| 25 | ||
| 26 | # Exract top-left information | |
| 27 | 265x | tl <- strmat[nl_inds_header, 1, drop = TRUE] | 
| 28 | 265x | has_topleft <- has_topleft && any(nzchar(tl)) # update topleft info if there is any | 
| 29 | ||
| 30 | 265x |   if (has_topleft) { | 
| 31 | # removes it from the header (temporary) - done so the header can be top aligned | |
| 32 | 6x | strmat[nl_inds_header, 1] <- "" | 
| 33 | ||
| 34 | # remove top empty strings (because they are not topleft) and assign topleft to add back | |
| 35 | 6x |     if (any(!nzchar(tl)) && length(tl) > 1) { # needed if ever has_top_left is true but only empties | 
| 36 | # values that are "" before topleft information | |
| 37 | 2x | which_is_fist_nzchar <- which(nzchar(tl))[1] | 
| 38 | 2x | tl <- tl[-(seq(which_is_fist_nzchar) - 1)] # -1 because we take out "" from beginning | 
| 39 | } | |
| 40 | 6x |     topleft_has_nl_char <- any(grepl("\n", tl)) | 
| 41 | 6x | tl_to_add_back <- strsplit(paste0(tl, collapse = "\n"), split = "\n", fixed = TRUE)[[1]] | 
| 42 | 6x | tl_how_many_nl <- length(tl_to_add_back) | 
| 43 | } | |
| 44 | ||
| 45 | # pre-proc in case of wrapping and \n | |
| 46 | 265x | line_grouping <- mf_lgrouping(matform) | 
| 47 | 265x | strmat <- .compress_mat(strmat, line_grouping, "nl") | 
| 48 | 265x | frmmat <- .compress_mat(frmmat, line_grouping, "unique") # never not unique | 
| 49 | 265x | spamat <- .compress_mat(spamat, line_grouping, "unique") | 
| 50 | 265x | alimat <- .compress_mat(alimat, line_grouping, "unique") | 
| 51 | 265x | line_grouping <- unique(line_grouping) | 
| 52 | ||
| 53 | # nlines detects if there is a newline character | |
| 54 | # colwidths = NULL, max_width = NULL, fontspec = NULL | |
| 55 | # because we don't care about wrapping here we're counting lines | |
| 56 | # TODO probably better if we had a nlines_nowrap fun to be more explicit | |
| 57 | ||
| 58 | 265x | row_nlines <- apply( # tells how many nlines for each row | 
| 59 | 265x | strmat, | 
| 60 | 265x | 1, | 
| 61 | 265x |     function(x) { | 
| 62 | 5126x | max( | 
| 63 | 5126x | vapply(x, | 
| 64 | 5126x | nlines, | 
| 65 | 5126x | colwidths = NULL, | 
| 66 | 5126x | max_width = NULL, | 
| 67 | 5126x | fontspec = NULL, 1L | 
| 68 | ), | |
| 69 | 5126x | 1L | 
| 70 | ) | |
| 71 | } | |
| 72 | ) | |
| 73 | ||
| 74 | # Correction for the case where there are more lines for topleft material than for cols | |
| 75 | 265x |   if (has_topleft && (sum(row_nlines[hdr_inds]) < tl_how_many_nl)) { | 
| 76 | 1x | row_nlines[1] <- row_nlines[1] + tl_how_many_nl - sum(row_nlines[hdr_inds]) | 
| 77 | } | |
| 78 | ||
| 79 | # There is something to change | |
| 80 | 265x |   if (any(row_nlines > 1) || topleft_has_nl_char) { | 
| 81 | # False: Padder should be bottom aligned if no topleft (case of rlistings) | |
| 82 | # It is always bottom: tl_padder <- ifelse(has_topleft, pad_vert_top, pad_vert_bottom) | |
| 83 | ||
| 84 | 40x | newstrmat <- rbind( | 
| 85 | 40x | cbind( | 
| 86 | 40x | expand_mat_rows(strmat[hdr_inds, 1, drop = FALSE], | 
| 87 | 40x | row_nlines[hdr_inds], | 
| 88 | 40x | cpadder = pad_vert_bottom # topleft info is NOT top aligned | 
| 89 | ), | |
| 90 | 40x | expand_mat_rows(strmat[hdr_inds, -1, drop = FALSE], | 
| 91 | 40x | row_nlines[hdr_inds], | 
| 92 | 40x | cpadder = pad_vert_bottom # colnames are bottom aligned | 
| 93 | ) | |
| 94 | ), | |
| 95 | 40x | expand_mat_rows(strmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) | 
| 96 | ) | |
| 97 | 40x | colnames(newstrmat) <- colnames(strmat) | 
| 98 | ||
| 99 | 40x | newfrmmat <- rbind( | 
| 100 | 40x | expand_mat_rows( | 
| 101 | 40x | frmmat[hdr_inds, , drop = FALSE], | 
| 102 | 40x | row_nlines[hdr_inds], | 
| 103 | 40x | cpadder = pad_vert_bottom | 
| 104 | ), | |
| 105 | 40x | expand_mat_rows(frmmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) | 
| 106 | ) | |
| 107 | ||
| 108 | 40x |     if (has_topleft) { | 
| 109 | 6x |       starts_from_ind <- if (sum(row_nlines[hdr_inds]) - tl_how_many_nl > 0) { | 
| 110 | 4x | sum(row_nlines[hdr_inds]) - tl_how_many_nl | 
| 111 |       } else { | |
| 112 | 2x | 0 | 
| 113 | } | |
| 114 | 6x | newstrmat[starts_from_ind + seq_along(tl_to_add_back), 1] <- tl_to_add_back | 
| 115 | } | |
| 116 | ||
| 117 | 40x | mf_strings(matform) <- newstrmat | 
| 118 | 40x | mf_formats(matform) <- newfrmmat | 
| 119 | 40x | mf_spans(matform) <- expand_mat_rows(spamat, row_nlines, rep_vec_to_len) | 
| 120 | 40x | mf_aligns(matform) <- expand_mat_rows(alimat, row_nlines, rep_vec_to_len) | 
| 121 | ## mf_display(matform) <- expand_mat_rows(mf_display(matform), row_nlines, rep_vec_to_len) | |
| 122 | 40x | mf_lgrouping(matform) <- rep(line_grouping, times = row_nlines) | 
| 123 | } | |
| 124 | ||
| 125 | # Solve \n in titles | |
| 126 | 265x |   if (any(grepl("\n", all_titles(matform)))) { | 
| 127 | 2x |     if (any(grepl("\n", main_title(matform)))) { | 
| 128 | 2x | tmp_title_vec <- .quick_handle_nl(main_title(matform)) | 
| 129 | 2x | main_title(matform) <- tmp_title_vec[1] | 
| 130 | 2x | subtitles(matform) <- c(tmp_title_vec[-1], .quick_handle_nl(subtitles(matform))) | 
| 131 |     } else { | |
| 132 | ! | subtitles(matform) <- .quick_handle_nl(subtitles(matform)) | 
| 133 | } | |
| 134 | } | |
| 135 | ||
| 136 | # Solve \n in footers | |
| 137 | 265x | main_footer(matform) <- .quick_handle_nl(main_footer(matform)) | 
| 138 | 265x | prov_footer(matform) <- .quick_handle_nl(prov_footer(matform)) | 
| 139 | ||
| 140 | # xxx \n in page titles are not working atm (I think) | |
| 141 | 265x | matform | 
| 142 | } | |
| 143 | ||
| 144 | .quick_handle_nl <- function(str_v) { | |
| 145 | 534x |   if (any(grepl("\n", str_v))) { | 
| 146 | 4x | return(unlist(strsplit(str_v, "\n", fixed = TRUE))) | 
| 147 |   } else { | |
| 148 | 530x | return(str_v) | 
| 149 | } | |
| 150 | } | |
| 151 | ||
| 152 | # Helper function to recompact the lines following line groupings to then have them expanded again | |
| 153 | .compress_mat <- function(mat, line_grouping, collapse_method = c("nl", "unique")) { | |
| 154 | 1060x |   list_compacted_mat <- lapply(unique(line_grouping), function(lg) { | 
| 155 | 20504x |     apply(mat, 2, function(mat_cols) { | 
| 156 | 175880x | col_vec <- mat_cols[which(line_grouping == lg)] | 
| 157 | 175880x |       if (collapse_method[1] == "nl") { | 
| 158 | 43970x | paste0(col_vec, collapse = "\n") | 
| 159 |       } else { | |
| 160 | 131910x | val <- unique(col_vec) | 
| 161 | 131910x | val <- val[nzchar(val)] | 
| 162 | 131910x |         if (length(val) > 1) { | 
| 163 | 20504x |           stop("Problem in linegroupings! Some do not have the same values.") # nocov | 
| 164 | 131910x |         } else if (length(val) < 1) { | 
| 165 | 5133x | val <- "" # Case in which it is only "" | 
| 166 | } | |
| 167 | 131910x | val[[1]] | 
| 168 | } | |
| 169 | }) | |
| 170 | }) | |
| 171 | 1060x |   do.call("rbind", list_compacted_mat) | 
| 172 | } | |
| 173 | ||
| 174 | disp_from_spans <- function(spans) { | |
| 175 | 410x | display <- matrix(rep(TRUE, length(spans)), ncol = ncol(spans)) | 
| 176 | ||
| 177 | 410x | print_cells_mat <- spans == 1L | 
| 178 | 410x |   if (!all(print_cells_mat)) { | 
| 179 | 1x | display_rws <- lapply( | 
| 180 | 1x | seq_len(nrow(spans)), | 
| 181 | 1x |       function(i) { | 
| 182 | 2x | print_cells <- print_cells_mat[i, ] | 
| 183 | 2x | row <- spans[i, ] | 
| 184 |         ##         display <- t(apply(spans, 1, function(row) { | |
| 185 | ## print_cells <- row == 1 | |
| 186 | ||
| 187 | 2x |         if (!all(print_cells)) { | 
| 188 | ## need to calculate which cell need to be printed | |
| 189 | 1x | print_cells <- spans_to_viscell(row) | 
| 190 | } | |
| 191 | 2x | print_cells | 
| 192 | } | |
| 193 | ) | |
| 194 | 1x | display <- do.call(rbind, display_rws) | 
| 195 | } | |
| 196 | 410x | display | 
| 197 | } | |
| 198 | ||
| 199 | #' Constructor for Matrix Print Form | |
| 200 | #' | |
| 201 | #' Constructor for `MatrixPrintForm`, an intermediate representation for ASCII table printing. | |
| 202 | #' | |
| 203 | #' @inheritParams open_font_dev | |
| 204 | #' @param strings (`character matrix`)\cr matrix of formatted, ready-to-display strings | |
| 205 | #' organized as they will be positioned when rendered. Elements that span more than one | |
| 206 | #' column must be followed by the correct number of placeholders (typically either empty | |
| 207 | #' strings or repeats of the value). | |
| 208 | #' @param spans (`numeric matrix`)\cr matrix of same dimension as `strings` giving the | |
| 209 | #' spanning information for each element. Must be repeated to match placeholders in `strings`. | |
| 210 | #' @param aligns (`character matrix`)\cr matrix of same dimension as `strings` giving the text | |
| 211 | #' alignment information for each element. Must be repeated to match placeholders in `strings`. | |
| 212 | #' Must be a supported text alignment. See [decimal_align] for allowed values. | |
| 213 | #' @param formats (`matrix`)\cr matrix of same dimension as `strings` giving the text format | |
| 214 | #' information for each element. Must be repeated to match placeholders in `strings`. | |
| 215 | #' @param row_info (`data.frame`)\cr data frame with row-information necessary for pagination (see | |
| 216 | #' [basic_pagdf()] for more details). | |
| 217 | #' @param colpaths (`list` or `NULL`)\cr `NULL`, or a list of paths to each leaf column, | |
| 218 | #' for use during horizontal pagination. | |
| 219 | #' @param line_grouping (`integer`)\cr sequence of integers indicating how print lines correspond | |
| 220 | #' to semantic rows in the object. Typically this should not be set manually unless | |
| 221 | #' `expand_newlines` is set to `FALSE`. | |
| 222 | #' @param ref_fnotes (`list`)\cr referential footnote information, if applicable. | |
| 223 | #' @param nlines_header (`numeric(1)`)\cr number of lines taken up by the values of the header | |
| 224 | #' (i.e. not including the divider). | |
| 225 | #' @param nrow_header (`numeric(1)`)\cr number of *rows* corresponding to the header. | |
| 226 | #' @param has_topleft (`flag`)\cr does the corresponding table have "top left information" | |
| 227 | #' which should be treated differently when expanding newlines. Ignored if `expand_newlines` | |
| 228 | #' is `FALSE`. | |
| 229 | #' @param has_rowlabs (`flag`)\cr do the matrices (`strings`, `spans`, `aligns`) each contain a | |
| 230 | #' column that corresponds with row labels (rather than with table cell values). Defaults to `TRUE`. | |
| 231 | #' @param main_title (`string`)\cr main title as a string. | |
| 232 | #' @param subtitles (`character`)\cr subtitles, as a character vector. | |
| 233 | #' @param page_titles (`character`)\cr page-specific titles, as a character vector. | |
| 234 | #' @param main_footer (`character`)\cr main footer, as a character vector. | |
| 235 | #' @param prov_footer (`character`)\cr provenance footer information, as a character vector. | |
| 236 | #' @param listing_keycols (`character`)\cr. if matrix form of a listing, this contains | |
| 237 | #' the key columns as a character vector. | |
| 238 | #' @param header_section_div (`string`)\cr divider to be used between header and body sections. | |
| 239 | #' @param horizontal_sep (`string`)\cr horizontal separator to be used for printing divisors | |
| 240 | #' between header and table body and between different footers. | |
| 241 | #' @param expand_newlines (`flag`)\cr whether the matrix form generated should expand rows whose | |
| 242 | #' values contain newlines into multiple 'physical' rows (as they will appear when rendered into | |
| 243 | #' ASCII). Defaults to `TRUE`. | |
| 244 | #' @param col_gap (`numeric(1)`)\cr space (in characters) between columns. | |
| 245 | #' @param table_inset (`numeric(1)`)\cr table inset. See [table_inset()]. | |
| 246 | #' @param colwidths (`numeric` or `NULL`)\cr column rendering widths. If non-`NULL`, must have length | |
| 247 | #' equal to `ncol(strings)`. | |
| 248 | #' @param indent_size (`numeric(1)`)\cr number of spaces to be used per level of indent (if supported by | |
| 249 | #' the relevant method). Defaults to 2. | |
| 250 | #' @param rep_cols (`numeric(1)`)\cr number of columns to be repeated as context during horizontal pagination. | |
| 251 | #' | |
| 252 | #' @return An object of class `MatrixPrintForm`. Currently this is implemented as an S3 class inheriting | |
| 253 | #' from list with the following elements: | |
| 254 | #'   \describe{ | |
| 255 | #'     \item{`strings`}{see argument.} | |
| 256 | #'     \item{`spans`}{see argument.} | |
| 257 | #'     \item{`aligns`}{see argument.} | |
| 258 | #'     \item{`display`}{logical matrix of same dimension as `strings` that specifies whether an element | |
| 259 | #' in `strings` will be displayed when the table is rendered.} | |
| 260 | #'     \item{`formats`}{see argument.} | |
| 261 | #'     \item{`row_info`}{see argument.} | |
| 262 | #'     \item{`line_grouping`}{see argument.} | |
| 263 | #'     \item{`ref_footnotes`}{see argument.} | |
| 264 | #'     \item{`main_title`}{see argument.} | |
| 265 | #'     \item{`subtitles`}{see argument.} | |
| 266 | #'     \item{`page_titles`}{see argument.} | |
| 267 | #'     \item{`main_footer`}{see argument.} | |
| 268 | #'     \item{`prov_footer`}{see argument.} | |
| 269 | #'     \item{`header_section_div`}{see argument.} | |
| 270 | #'     \item{`horizontal_sep`}{see argument.} | |
| 271 | #'     \item{`col_gap`}{see argument.} | |
| 272 | #'     \item{`table_inset`}{see argument.} | |
| 273 | #' } | |
| 274 | #' | |
| 275 | #' as well as the following attributes: | |
| 276 | #' | |
| 277 | #'   \describe{ | |
| 278 | #'     \item{`nlines_header`}{see argument.} | |
| 279 | #'     \item{`nrow_header`}{see argument.} | |
| 280 | #'     \item{`ncols`}{number of columns *of the table*, not including any row names/row labels} | |
| 281 | #' } | |
| 282 | #' | |
| 283 | #' @note The bare constructor for the `MatrixPrintForm` should generally | |
| 284 | #' only be called by `matrix_form` custom methods, and almost never from other code. | |
| 285 | #' | |
| 286 | #' @examples | |
| 287 | #' basic_matrix_form(iris) # calls matrix_form which calls this constructor | |
| 288 | #' | |
| 289 | #' @export | |
| 290 | MatrixPrintForm <- function(strings = NULL, | |
| 291 | spans, | |
| 292 | aligns, | |
| 293 | formats, | |
| 294 | row_info, | |
| 295 | colpaths = NULL, | |
| 296 | line_grouping = seq_len(NROW(strings)), | |
| 297 | ref_fnotes = list(), | |
| 298 | nlines_header, | |
| 299 | nrow_header, | |
| 300 | has_topleft = TRUE, | |
| 301 | has_rowlabs = has_topleft, | |
| 302 | expand_newlines = TRUE, | |
| 303 | main_title = "", | |
| 304 | subtitles = character(), | |
| 305 | page_titles = character(), | |
| 306 | listing_keycols = NULL, | |
| 307 | main_footer = "", | |
| 308 | prov_footer = character(), | |
| 309 | header_section_div = NA_character_, | |
| 310 | horizontal_sep = default_hsep(), | |
| 311 | col_gap = 3, | |
| 312 | table_inset = 0L, | |
| 313 | colwidths = NULL, | |
| 314 | indent_size = 2, | |
| 315 | fontspec = font_spec(), | |
| 316 |                             rep_cols = 0L) { | |
| 317 | 53x | display <- disp_from_spans(spans) | 
| 318 | ||
| 319 | 53x | ncs <- if (has_rowlabs) ncol(strings) - 1 else ncol(strings) | 
| 320 | 53x | ret <- structure( | 
| 321 | 53x | list( | 
| 322 | 53x | strings = strings, | 
| 323 | 53x | spans = spans, | 
| 324 | 53x | aligns = aligns, | 
| 325 | 53x | display = display, | 
| 326 | 53x | formats = formats, | 
| 327 | 53x | row_info = row_info, | 
| 328 | 53x | line_grouping = line_grouping, | 
| 329 | 53x | ref_footnotes = ref_fnotes, | 
| 330 | 53x | main_title = main_title, | 
| 331 | 53x | subtitles = subtitles, | 
| 332 | 53x | page_titles = page_titles, | 
| 333 | 53x | main_footer = main_footer, | 
| 334 | 53x | prov_footer = prov_footer, | 
| 335 | 53x | header_section_div = header_section_div, | 
| 336 | 53x | horizontal_sep = horizontal_sep, | 
| 337 | 53x | col_gap = col_gap, | 
| 338 | 53x | listing_keycols = listing_keycols, | 
| 339 | 53x | table_inset = as.integer(table_inset), | 
| 340 | 53x | has_topleft = has_topleft, | 
| 341 | 53x | indent_size = indent_size, | 
| 342 | 53x | col_widths = colwidths, | 
| 343 | 53x | fontspec = fontspec, | 
| 344 | 53x | num_rep_cols = rep_cols | 
| 345 | ), | |
| 346 | 53x | nrow_header = nrow_header, | 
| 347 | 53x | ncols = ncs, | 
| 348 | 53x |     class = c("MatrixPrintForm", "list") | 
| 349 | ) | |
| 350 | ||
| 351 | ## .do_mat_expand(ret) | |
| 352 | 53x |   if (expand_newlines) { | 
| 353 | 53x | ret <- mform_handle_newlines(ret) | 
| 354 | } | |
| 355 | ||
| 356 | ## ret <- shove_refdf_into_rowinfo(ret) | |
| 357 | 53x |   if (is.null(colwidths)) { | 
| 358 | 53x | colwidths <- propose_column_widths(ret, fontspec = fontspec) | 
| 359 | } | |
| 360 | 53x | mf_col_widths(ret) <- colwidths | 
| 361 | 53x | ret <- mform_build_refdf(ret) | 
| 362 | 53x | ret <- mpf_infer_cinfo(ret, colpaths = colpaths, fontspec = fontspec) | 
| 363 | ||
| 364 | 53x | ret | 
| 365 | } | |
| 366 | ||
| 367 | mf_update_cinfo <- function(mf, colwidths = NULL, rep_cols = NULL) { | |
| 368 | 563x | need_update <- FALSE | 
| 369 | 563x |   if (!is.null(colwidths)) { | 
| 370 | 419x | mf$col_widths <- colwidths | 
| 371 | 419x | need_update <- TRUE | 
| 372 | } | |
| 373 | ||
| 374 | 563x |   if (!is.null(rep_cols)) { | 
| 375 | 144x | mf$num_rep_cols <- rep_cols | 
| 376 | 144x | need_update <- TRUE | 
| 377 | } | |
| 378 | ||
| 379 | 563x |   if (need_update && !is.null(mf_cinfo(mf))) { | 
| 380 | 510x | cinfo <- mf_cinfo(mf) | 
| 381 | 510x | r_colwidths <- mf_col_widths(mf) | 
| 382 | 510x | has_rlabs <- mf_has_rlabels(mf) | 
| 383 | 510x |     if (has_rlabs) { | 
| 384 | 398x | r_colwidths <- r_colwidths[-1] ## row label widths | 
| 385 | } | |
| 386 | 510x | cinfo$self_extent <- r_colwidths | 
| 387 | 510x | nrepcols <- num_rep_cols(mf) | 
| 388 | 510x | rep_seq <- seq_len(nrepcols) | 
| 389 | 510x | is_listing <- !is.null(mf$listing_keycols) | 
| 390 | ||
| 391 | # empty listing | |
| 392 | 510x |     if (is_listing && length(mf$listing_keycols) == 1 && length(r_colwidths) - nrepcols < 1) { | 
| 393 | ! | cinfo$par_extent <- 0 | 
| 394 | # listing with all key columns | |
| 395 | 510x |     } else if (is_listing && mf_ncol(mf) == length(mf$listing_keycols)) { | 
| 396 | 1x | cinfo$par_extent <- cumsum(c(0, cinfo$self_extent[seq_len(nrepcols - 1)])) | 
| 397 |     } else { | |
| 398 | 509x | cinfo$par_extent <- cumsum(c(0, cinfo$self_extent[seq_len(nrepcols)], rep(0, length(r_colwidths) - nrepcols - 1))) | 
| 399 | } | |
| 400 | ||
| 401 | 510x | cinfo$reprint_inds <- I(lapply(seq_len(NROW(cinfo)), function(i) rep_seq[rep_seq < i])) | 
| 402 | 510x | mf_cinfo(mf) <- cinfo | 
| 403 | } | |
| 404 | 563x | mf | 
| 405 | } | |
| 406 | ||
| 407 | #' Create a row for a referential footnote information data frame | |
| 408 | #' | |
| 409 | #' @inheritParams nlines | |
| 410 | #' @param row_path (`character`)\cr row path (or `NA_character_` for none). | |
| 411 | #' @param col_path (`character`)\cr column path (or `NA_character_` for none). | |
| 412 | #' @param row (`integer(1)`)\cr integer position of the row. | |
| 413 | #' @param col (`integer(1)`)\cr integer position of the column. | |
| 414 | #' @param symbol (`string`)\cr symbol for the reference. `NA_character_` to use the | |
| 415 | #' `ref_index` automatically. | |
| 416 | #' @param ref_index (`integer(1)`)\cr index of the footnote, used for ordering even when | |
| 417 | #' symbol is not `NA`. | |
| 418 | #' @param msg (`string`)\cr the string message, not including the symbol portion (`{symbol} - `) | |
| 419 | #' | |
| 420 | #' @return A single row data frame with the appropriate columns. | |
| 421 | #' | |
| 422 | #' @export | |
| 423 | ref_df_row <- function(row_path = NA_character_, | |
| 424 | col_path = NA_character_, | |
| 425 | row = NA_integer_, | |
| 426 | col = NA_integer_, | |
| 427 | symbol = NA_character_, | |
| 428 | ref_index = NA_integer_, | |
| 429 | msg = NA_character_, | |
| 430 |                        max_width = NULL) { | |
| 431 | 6356x | nlines <- nlines(msg, max_width = max_width) | 
| 432 | 6356x | data.frame( | 
| 433 | 6356x | row_path = I(list(row_path)), | 
| 434 | 6356x | col_path = I(list(col_path)), | 
| 435 | 6356x | row = row, | 
| 436 | 6356x | col = col, | 
| 437 | 6356x | symbol = symbol, | 
| 438 | 6356x | ref_index = ref_index, | 
| 439 | 6356x | msg = msg, | 
| 440 | 6356x | nlines = nlines, | 
| 441 | 6356x | stringsAsFactors = FALSE | 
| 442 | ) | |
| 443 | } | |
| 444 | ||
| 445 | ## this entire thing is a hatchetjob of a hack which should not be necessary. | |
| 446 | ## mf_rinfo(mform) should have the relevant info in it and | |
| 447 | ## mf_cinfo(mform) should be non-null (!!!) and have the info in it | |
| 448 | ## in which case this becomes silly and dumb, but here we are, so here we go. | |
| 449 | infer_ref_info <- function(mform, colspace_only) { | |
| 450 | 212x |   if (colspace_only) { | 
| 451 | 106x | idx <- seq_len(mf_nlheader(mform)) | 
| 452 |   } else { | |
| 453 | 106x | idx <- seq_len(nrow(mf_strings(mform))) | 
| 454 | } | |
| 455 | ||
| 456 | 212x | hasrlbs <- mf_has_rlabels(mform) | 
| 457 | ||
| 458 | 212x | strs <- mf_strings(mform)[idx, , drop = FALSE] | 
| 459 | ||
| 460 | ## they're nested so \\2 is the inner one, without the brackets | |
| 461 |   ## include space in front of { so we don't catch \{ when | |
| 462 | ## rtfs want to pass markup through | |
| 463 | 212x |   refs <- gsub("^[^{]*([{]([^}]+)[}]){0,1}$", "\\2", strs) | 
| 464 | ## handle spanned values | |
| 465 | 212x | refs[!mf_display(mform)[idx, ]] <- "" | 
| 466 | ||
| 467 | ## we want to count across rows first, not down columns, cause | |
| 468 | ## thats how footnote numbering works | |
| 469 | 212x | refs_inorder <- as.vector(t(refs)) | 
| 470 | 212x | keepem <- nzchar(refs_inorder) | 
| 471 | 212x |   if (sum(keepem) == 0) { | 
| 472 | 210x | return(ref_df_row()[0, ]) | 
| 473 | } | |
| 474 | ||
| 475 | 2x | refs_spl <- strsplit(refs_inorder[keepem], ", ", fixed = TRUE) | 
| 476 | 2x | runvec <- vapply(refs_spl, length, 1L) | 
| 477 | ||
| 478 | 2x | row_index <- as.vector( | 
| 479 | 2x | t(do.call(cbind, replicate(ncol(strs), list(mf_lgrouping(mform)[idx] - mf_nlheader(mform))))) | 
| 480 | 2x | )[keepem] | 
| 481 | 2x | row_index[row_index < 1] <- NA_integer_ | 
| 482 | 2x | c_torep <- if (hasrlbs) c(NA_integer_, seq(1, ncol(strs) - 1)) else seq_len(ncol(strs)) | 
| 483 | 2x | col_index <- rep(c_torep, nrow(strs))[keepem] | 
| 484 | ||
| 485 | 2x | ret <- data.frame( | 
| 486 | 2x | symbol = unlist(refs_spl), | 
| 487 | 2x | row_path = I(mf_rinfo(mform)$path[rep(row_index, times = runvec)]), | 
| 488 | 2x | row = rep(row_index, times = runvec), | 
| 489 | 2x | col = rep(col_index, times = runvec) | 
| 490 | ) | |
| 491 | 2x |   ret$msg <- vapply(ret$symbol, function(sym) { | 
| 492 | 16x |     fullmsg <- unique(grep(paste0("{", sym, "}"), fixed = TRUE, mf_rfnotes(mform), value = TRUE)) | 
| 493 | 16x |     gsub("^[{][^}]+[}] - ", "", fullmsg) | 
| 494 | }, "") | |
| 495 | ||
| 496 | 2x | col_pths <- mf_col_paths(mform) | 
| 497 | 2x | ret$col_path <- replicate(nrow(ret), list(NA_character_)) | 
| 498 | 2x | non_na_col <- !is.na(ret$col) | 
| 499 | 2x | ret$col_path[non_na_col] <- col_pths[ret$col[non_na_col]] | 
| 500 | 2x | ret$ref_index <- match(ret$symbol, unique(ret$symbol)) | 
| 501 | ## | |
| 502 | 2x |   ret$nlines <- vapply(paste0("{", ret$symbol, "} - ", ret$msg), nlines, 1L) | 
| 503 | 2x | ret <- ret[, names(ref_df_row())] | 
| 504 | 2x | ret | 
| 505 | } | |
| 506 | ||
| 507 | mform_build_refdf <- function(mform) { | |
| 508 | 106x | rdf <- mf_rinfo(mform) | 
| 509 | 106x | cref_rows <- infer_ref_info(mform, colspace_only = TRUE) | 
| 510 | ## this will recheck sometimes but its safer and shouldn't | |
| 511 | ## be too prohibitively costly | |
| 512 | 106x |   if (NROW(rdf$ref_info_df) > 0 && sum(sapply(rdf$ref_info_df, NROW)) > 0) { | 
| 513 | ! | cref_rows <- infer_ref_info(mform, colspace_only = TRUE) | 
| 514 | ! | rref_rows <- rdf$ref_info_df | 
| 515 |   } else { | |
| 516 | 106x | cref_rows <- infer_ref_info(mform, colspace_only = FALSE) | 
| 517 | 106x | rref_rows <- list() | 
| 518 | } | |
| 519 | 106x | mf_fnote_df(mform) <- do.call(rbind.data.frame, c(list(cref_rows), rref_rows)) | 
| 520 | 106x | update_mf_nlines(mform, colwidths = mf_col_widths(mform), max_width = NULL) | 
| 521 | } | |
| 522 | ||
| 523 | ## hide the implementation behind abstraction in case we decide we want a real class someday | |
| 524 | #' Getters and setters for aspects of `MatrixPrintForm` objects | |
| 525 | #' | |
| 526 | #' Most of these functions, particularly the setters, are intended almost exclusively for | |
| 527 | #' internal use in, e.g., [`matrix_form`] methods, and should generally not be called by end users. | |
| 528 | #' | |
| 529 | #' @param mf (`MatrixPrintForm`)\cr a `MatrixPrintForm` object. | |
| 530 | #' @param value (`ANY`)\cr the new value for the component in question. | |
| 531 | #' | |
| 532 | #' @return | |
| 533 | #' * Getters return the associated element of `mf`. | |
| 534 | #' * Setters return the modified `mf` object. | |
| 535 | #' | |
| 536 | #' @export | |
| 537 | #' @rdname mpf_accessors | |
| 538 | 4878x | mf_strings <- function(mf) mf$strings | 
| 539 | ||
| 540 | #' @export | |
| 541 | #' @rdname mpf_accessors | |
| 542 | ||
| 543 | 678x | mf_spans <- function(mf) mf$spans | 
| 544 | #' @export | |
| 545 | #' @rdname mpf_accessors | |
| 546 | ||
| 547 | 1011x | mf_aligns <- function(mf) mf$aligns | 
| 548 | ||
| 549 | #' @export | |
| 550 | #' @rdname mpf_accessors | |
| 551 | 463x | mf_display <- function(mf) mf$display | 
| 552 | ||
| 553 | #' @export | |
| 554 | #' @rdname mpf_accessors | |
| 555 | 583x | mf_formats <- function(mf) mf$formats | 
| 556 | ||
| 557 | #' @export | |
| 558 | #' @rdname mpf_accessors | |
| 559 | 4717x | mf_rinfo <- function(mf) mf$row_info | 
| 560 | ||
| 561 | #' @export | |
| 562 | #' @rdname mpf_accessors | |
| 563 | 1767x | mf_cinfo <- function(mf) mf$col_info | 
| 564 | ||
| 565 | ||
| 566 | #' @export | |
| 567 | #' @rdname mpf_accessors | |
| 568 | 265x | mf_has_topleft <- function(mf) mf$has_topleft | 
| 569 | ||
| 570 | #' @export | |
| 571 | #' @rdname mpf_accessors | |
| 572 | 5821x | mf_lgrouping <- function(mf) mf$line_grouping | 
| 573 | ||
| 574 | #' @export | |
| 575 | #' @rdname mpf_accessors | |
| 576 | 173x | mf_rfnotes <- function(mf) mf$ref_footnotes | 
| 577 | ||
| 578 | #' @export | |
| 579 | #' @rdname mpf_accessors | |
| 580 | 2734x | mf_nlheader <- function(mf) sum(mf_lgrouping(mf) <= mf_nrheader(mf)) | 
| 581 | ||
| 582 | #' @export | |
| 583 | #' @rdname mpf_accessors | |
| 584 | 4660x | mf_nrheader <- function(mf) attr(mf, "nrow_header", exact = TRUE) | 
| 585 | ||
| 586 | #' @export | |
| 587 | #' @rdname mpf_accessors | |
| 588 | 365x | mf_colgap <- function(mf) mf$col_gap | 
| 589 | ||
| 590 | #' @export | |
| 591 | #' @rdname mpf_accessors | |
| 592 | 7x | mf_fontspec <- function(mf) mf$fontspec | 
| 593 | ||
| 594 | #' @export | |
| 595 | #' @rdname mpf_accessors | |
| 596 | `mf_fontspec<-` <- function(mf, value) { | |
| 597 | 368x | mf$fontspec <- value | 
| 598 | 368x | mf | 
| 599 | } | |
| 600 | ||
| 601 | ## XXX should this be exported? not sure if there's a point | |
| 602 | mf_col_paths <- function(mf) { | |
| 603 | 2x |   if (!is.null(mf_cinfo(mf))) { | 
| 604 | 2x | mf_cinfo(mf)$path | 
| 605 |   } else { | |
| 606 | ! |     as.list(paste0("col", seq_len(nrow(mf_strings(mf)) - mf_has_topleft(mf)))) | 
| 607 | } | |
| 608 | } | |
| 609 | ||
| 610 | mf_col_widths <- function(mf) { | |
| 611 | 1175x | mf$col_widths | 
| 612 | } | |
| 613 | ||
| 614 | `mf_col_widths<-` <- function(mf, value) { | |
| 615 | 414x |   if (!is.null(value) && length(value) != NCOL(mf_strings(mf))) { | 
| 616 | ! | stop( | 
| 617 | ! |       "Number of column widths (", length(value), ") does not match ", | 
| 618 | ! |       "number of columns in strings matrix (", NCOL(mf_strings(mf)), ")." | 
| 619 | ) | |
| 620 | } | |
| 621 | 414x | mf <- mf_update_cinfo(mf, colwidths = value, rep_cols = NULL) | 
| 622 | 414x | mf | 
| 623 | } | |
| 624 | ||
| 625 | mf_fnote_df <- function(mf) { | |
| 626 | 1710x | mf$ref_fnote_df | 
| 627 | } | |
| 628 | ||
| 629 | `mf_fnote_df<-` <- function(mf, value) { | |
| 630 | 449x | stopifnot(is.null(value) || (is.data.frame(value) && identical(names(value), names(ref_df_row())))) | 
| 631 | 449x | mf$ref_fnote_df <- value | 
| 632 | 449x | mf | 
| 633 | } | |
| 634 | ||
| 635 | splice_fnote_info_in <- function(df, refdf, row = TRUE) { | |
| 636 | 396x |   if (NROW(df) == 0) { | 
| 637 | ! | return(df) | 
| 638 | } | |
| 639 | ||
| 640 | 396x | colnm <- ifelse(row, "row", "col") | 
| 641 | 396x | refdf <- refdf[!is.na(refdf[[colnm]]), ] | 
| 642 | ||
| 643 | 396x | refdf_spl <- split(refdf, refdf[[colnm]]) | 
| 644 | 396x | df$ref_info_df <- replicate(nrow(df), list(ref_df_row()[0, ])) | 
| 645 | 396x | df$ref_info_df[as.integer(names(refdf_spl))] <- refdf_spl | 
| 646 | 396x | df | 
| 647 | } | |
| 648 | ||
| 649 | shove_refdf_into_rowinfo <- function(mform) { | |
| 650 | 343x | refdf <- mf_fnote_df(mform) | 
| 651 | 343x | rowinfo <- mf_rinfo(mform) | 
| 652 | 343x | mf_rinfo(mform) <- splice_fnote_info_in(rowinfo, refdf) | 
| 653 | 343x | mform | 
| 654 | } | |
| 655 | ||
| 656 | update_mf_nlines <- function(mform, colwidths, max_width) { | |
| 657 | 316x | mform <- update_mf_ref_nlines(mform, max_width = max_width) | 
| 658 | 316x | mform <- update_mf_rinfo_extents(mform) | 
| 659 | ||
| 660 | 316x | mform | 
| 661 | } | |
| 662 | ||
| 663 | update_mf_rinfo_extents <- function(mform) { | |
| 664 | 316x | rinfo <- mf_rinfo(mform) | 
| 665 | 316x | refdf_all <- mf_fnote_df(mform) | 
| 666 | 316x | refdf_rows <- refdf_all[!is.na(refdf_all$row), ] | 
| 667 | 316x |   if (NROW(rinfo) == 0) { | 
| 668 | ! | return(mform) | 
| 669 | } | |
| 670 | 316x | lgrp <- mf_lgrouping(mform) - mf_nrheader(mform) | 
| 671 | 316x | lgrp <- lgrp[lgrp > 0] | 
| 672 | 316x |   rf_nlines <- vapply(seq_len(max(lgrp)), function(ii) { | 
| 673 | 5969x | refdfii <- refdf_rows[refdf_rows$row == ii, ] | 
| 674 | 5969x | refdfii <- refdfii[!duplicated(refdfii$symbol), ] | 
| 675 | 5969x |     if (NROW(refdfii) == 0L) { | 
| 676 | 5873x | return(0L) | 
| 677 | } | |
| 678 | 96x | sum(refdfii$nlines) | 
| 679 | 316x | }, 1L) | 
| 680 | ||
| 681 | 316x | raw_self_exts <- vapply(split(lgrp, lgrp), length, 0L) | 
| 682 | 316x | stopifnot(length(raw_self_exts) == length(rf_nlines)) | 
| 683 | 316x | new_exts <- raw_self_exts + rf_nlines | 
| 684 | ||
| 685 | 316x | mapdf <- data.frame( | 
| 686 | 316x | row_num = as.integer(names(new_exts)), | 
| 687 | 316x | raw_extent = raw_self_exts | 
| 688 | ) | |
| 689 | 316x | stopifnot(all(mapdf$row_num == rinfo$abs_rownumber)) | 
| 690 | ||
| 691 | 316x |   new_par_exts <- vapply(rinfo$reprint_inds, function(idx) { | 
| 692 | 5969x | sum(0L, mapdf$raw_extent[mapdf$row_num %in% idx]) | 
| 693 | 316x | }, 1L) | 
| 694 | ||
| 695 | 316x | rinfo$self_extent <- new_exts | 
| 696 | 316x | rinfo$par_extent <- new_par_exts | 
| 697 | 316x | rinfo$nreflines <- rf_nlines | 
| 698 | 316x | mf_rinfo(mform) <- rinfo | 
| 699 | 316x | mform | 
| 700 | } | |
| 701 | ||
| 702 | update_mf_ref_nlines <- function(mform, max_width) { | |
| 703 | 316x | refdf <- mf_fnote_df(mform) | 
| 704 | 316x |   if (NROW(refdf) == 0) { | 
| 705 | 289x | return(mform) | 
| 706 | } | |
| 707 | ||
| 708 | 27x | refdf$nlines <- vapply( | 
| 709 | 27x |     paste0("{", refdf$symbol, "} - ", refdf$msg), | 
| 710 | 27x | nlines, | 
| 711 | 27x | max_width = max_width, | 
| 712 | 27x | fontspec = mf_fontspec(mform), | 
| 713 | 27x | 1L | 
| 714 | ) | |
| 715 | 27x | mf_fnote_df(mform) <- refdf | 
| 716 | 27x | shove_refdf_into_rowinfo(mform) | 
| 717 | } | |
| 718 | ||
| 719 | #' @export | |
| 720 | #' @rdname mpf_accessors | |
| 721 | `mf_strings<-` <- function(mf, value) { | |
| 722 | 805x | mf$strings <- value | 
| 723 | 805x | mf | 
| 724 | } | |
| 725 | ||
| 726 | .chkdim_and_replace <- function(mf, value, component) { | |
| 727 | 1086x | strdim <- dim(mf_strings(mf)) | 
| 728 | 1086x | vdim <- dim(value) | 
| 729 | 1086x |   if (!is.null(strdim) && !identical(strdim, vdim)) { | 
| 730 | 1x | stop( | 
| 731 | 1x |       "Dimensions of new '", component, "' value (", | 
| 732 | 1x | vdim[1], ", ", vdim[2], # nocov | 
| 733 | 1x |       ") do not match dimensions of existing 'strings' component (", # nocov | 
| 734 | 1x | strdim[1], ", ", strdim[2], ")." # nocov | 
| 735 | ) | |
| 736 | } | |
| 737 | 1085x | mf[[component]] <- value | 
| 738 | 1085x | mf | 
| 739 | } | |
| 740 | ||
| 741 | #' @export | |
| 742 | #' @rdname mpf_accessors | |
| 743 | `mf_spans<-` <- function(mf, value) { | |
| 744 | 358x | mf <- .chkdim_and_replace(mf, value, component = "spans") | 
| 745 | 357x | mf$display <- disp_from_spans(value) | 
| 746 | 357x | mf | 
| 747 | } | |
| 748 | ||
| 749 | #' @export | |
| 750 | #' @rdname mpf_accessors | |
| 751 | `mf_aligns<-` <- function(mf, value) { | |
| 752 | 371x | .chkdim_and_replace(mf, value, component = "aligns") | 
| 753 | } | |
| 754 | ||
| 755 | #' @export | |
| 756 | #' @rdname mpf_accessors | |
| 757 | `mf_display<-` <- function(mf, value) { | |
| 758 | ! |   stop("display is now a derived element of the matrix print form, modify it via `mf_spans<-`") | 
| 759 | ! | .chkdim_and_replace(mf, value, component = "display") | 
| 760 | } | |
| 761 | ||
| 762 | #' @export | |
| 763 | #' @rdname mpf_accessors | |
| 764 | `mf_formats<-` <- function(mf, value) { | |
| 765 | 357x | .chkdim_and_replace(mf, value, component = "formats") | 
| 766 | } | |
| 767 | ||
| 768 | ## NB NROW(v) == length(v) for atomic vectors so this is ok for lgrouping as wellas rinfo | |
| 769 | .chknrow_and_replace <- function(mf, value, component, noheader = FALSE) { | |
| 770 | 357x | strdim <- NROW(mf_strings(mf)) - if (noheader) mf_nlheader(mf) else 0L | 
| 771 | 357x | vdim <- NROW(value) | 
| 772 | 357x |   if (!is.null(strdim) && !identical(strdim, vdim)) { | 
| 773 | ! | stop( | 
| 774 | ! |       "Number of rows/length of new '", component, "' value (", | 
| 775 | ! | vdim[1], | 
| 776 | ! |       ") does not match existing 'strings' component (", | 
| 777 | ! | strdim[1], ")." | 
| 778 | ) | |
| 779 | } | |
| 780 | 357x | mf[[component]] <- value | 
| 781 | 357x | mf | 
| 782 | } | |
| 783 | ||
| 784 | #' @export | |
| 785 | #' @rdname mpf_accessors | |
| 786 | `mf_rinfo<-` <- function(mf, value) { | |
| 787 | ## this can someijtmes be called after expanding newlines so in general | |
| 788 | ## we should not expect it to match the number of rows in the strings matrix | |
| 789 | ## .chknrow_and_replace(mf, value, component = "row_info", noheader = TRUE) | |
| 790 | 775x | lgrps <- mf_lgrouping(mf) | 
| 791 | 775x | nrs <- length(unique(lgrps[-seq_len(mf_nlheader(mf))])) | 
| 792 | 775x |   if (NROW(value) != nrs) { | 
| 793 | 1x | stop( | 
| 794 | 1x |       "Rows in new row_info component (", | 
| 795 | 1x | NROW(value), | 
| 796 | 1x |       ") does not match number of rows reflected in line_grouping component (", | 
| 797 | 1x | nrs, ")" | 
| 798 | ) | |
| 799 | } | |
| 800 | 774x | mf$row_info <- value | 
| 801 | 774x | mf | 
| 802 | } | |
| 803 | ||
| 804 | #' @export | |
| 805 | #' @rdname mpf_accessors | |
| 806 | `mf_cinfo<-` <- function(mf, value) { | |
| 807 | 783x |   if (NROW(value) > 0 && NROW(value) != mf_ncol(mf)) { | 
| 808 | ! | stop( | 
| 809 | ! |       "Number of rows in new cinfo (", NROW(value), ") does not match ", | 
| 810 | ! |       "number of columns (", mf_ncol(mf), ")" | 
| 811 | ) | |
| 812 | } | |
| 813 | 783x | mf$col_info <- value | 
| 814 | 783x | mf | 
| 815 | } | |
| 816 | ||
| 817 | #' @export | |
| 818 | #' @rdname mpf_accessors | |
| 819 | `mf_lgrouping<-` <- function(mf, value) { | |
| 820 | 357x | .chknrow_and_replace(mf, value, component = "line_grouping") | 
| 821 | } | |
| 822 | ||
| 823 | #' @export | |
| 824 | #' @rdname mpf_accessors | |
| 825 | `mf_rfnotes<-` <- function(mf, value) { | |
| 826 | 321x | mf$ref_footnotes <- value | 
| 827 | 321x | mf | 
| 828 | } | |
| 829 | ||
| 830 | #' @export | |
| 831 | #' @rdname mpf_accessors | |
| 832 | `mf_nrheader<-` <- function(mf, value) { | |
| 833 | 2x | attr(mf, "nrow_header") <- value | 
| 834 | 2x | mf | 
| 835 | } | |
| 836 | ||
| 837 | #' @export | |
| 838 | #' @rdname mpf_accessors | |
| 839 | `mf_colgap<-` <- function(mf, value) { | |
| 840 | 96x | mf$col_gap <- value | 
| 841 | 96x | mf | 
| 842 | } | |
| 843 | ||
| 844 | #' @export | |
| 845 | #' @rdname mpf_accessors | |
| 846 | 2801x | mf_ncol <- function(mf) attr(mf, "ncols", exact = TRUE) | 
| 847 | ||
| 848 | #' @export | |
| 849 | #' @rdname mpf_accessors | |
| 850 | 10x | mf_nrow <- function(mf) max(mf_lgrouping(mf)) - mf_nrheader(mf) | 
| 851 | ||
| 852 | #' @export | |
| 853 | #' @rdname mpf_accessors | |
| 854 | `mf_ncol<-` <- function(mf, value) { | |
| 855 | 440x | stopifnot(is.numeric(value)) | 
| 856 | 440x | attr(mf, "ncols") <- value | 
| 857 | 440x | mf | 
| 858 | } | |
| 859 | ||
| 860 | #' @param x `MatrixPrintForm`. The object. | |
| 861 | #' @export | |
| 862 | #' @rdname mpf_accessors | |
| 863 | setMethod( | |
| 864 | "ncol", "MatrixPrintForm", | |
| 865 | 27x | function(x) mf_ncol(x) | 
| 866 | ) | |
| 867 | ||
| 868 | #' @export | |
| 869 | #' @rdname mpf_accessors | |
| 870 | mpf_has_rlabels <- function(mf) { | |
| 871 | ! |   .Deprecated("mf_has_rlabels") | 
| 872 | ! | mf_has_rlabels(mf) | 
| 873 | } | |
| 874 | ||
| 875 | #' @export | |
| 876 | #' @rdname mpf_accessors | |
| 877 | 1359x | mf_has_rlabels <- function(mf) ncol(mf$strings) > mf_ncol(mf) | 
| 878 | ||
| 879 | #' Create spoof matrix form from a data frame | |
| 880 | #' | |
| 881 | #' Useful functions for writing tests and examples, and a starting point for | |
| 882 | #' more sophisticated custom `matrix_form` methods. | |
| 883 | #' | |
| 884 | #' @inheritParams open_font_dev | |
| 885 | #' @param df (`data.frame`)\cr a data frame. | |
| 886 | #' @param indent_rownames (`flag`)\cr whether row names should be indented. Being this | |
| 887 | #' used for testing purposes, it defaults to `FALSE`. If `TRUE`, it assigns label rows | |
| 888 | #' on even lines (also format is `"-"` and value strings are `""`). Indentation works | |
| 889 | #' only if split labels are used (see parameters `split_labels` and `data_labels`). | |
| 890 | #' @param parent_path (`string`)\cr parent path that all rows should be "children of". | |
| 891 | #' Defaults to `NULL`, as usually this is not needed. It may be necessary to use `"root"`, | |
| 892 | #' for some specific scenarios. | |
| 893 | #' @param ignore_rownames (`flag`)\cr whether row names should be ignored. | |
| 894 | #' @param add_decoration (`flag`)\cr whether adds title and footer decorations should | |
| 895 | #' be added to the matrix form. | |
| 896 | #' @param split_labels (`string`)\cr indicates which column to use as split labels. If | |
| 897 | #' `NULL`, no split labels are used. | |
| 898 | #' @param data_labels (`string`)\cr indicates which column to use as data labels. It is | |
| 899 | #' ignored if no `split_labels` is present and is automatically assigned to | |
| 900 | #' `"Analysis method"` when `split_labels` is present, but `data_labels` is `NULL`. | |
| 901 | #' Its direct column name is used as node name in `"DataRow"` pathing. See [mf_rinfo()] | |
| 902 | #' for more information. | |
| 903 | #' @param num_rep_cols (`numeric(1)`)\cr Number of columns to be treated as repeating columns. | |
| 904 | #' Defaults to `0` for `basic_matrix_form` and `length(keycols)` for | |
| 905 | #' `basic_listing_mf`. Note repeating columns are separate from row labels if present. | |
| 906 | #' | |
| 907 | #' @return A valid `MatrixPrintForm` object representing `df` that is ready for | |
| 908 | #' ASCII rendering. | |
| 909 | #' | |
| 910 | #' @details | |
| 911 | #' If some of the column has a [obj_format] assigned, it will be respected for all column | |
| 912 | #' values except for label rows, if present (see parameter `split_labels`). | |
| 913 | #' | |
| 914 | #' @examples | |
| 915 | #' mform <- basic_matrix_form(mtcars) | |
| 916 | #' cat(toString(mform)) | |
| 917 | #' | |
| 918 | #' @examplesIf require("dplyr") | |
| 919 | #' # Advanced test case with label rows | |
| 920 | #' library(dplyr) | |
| 921 | #' iris_output <- iris %>% | |
| 922 | #' group_by(Species) %>% | |
| 923 | #'   summarize("all obs" = round(mean(Petal.Length), 2)) %>% | |
| 924 | #'   mutate("DataRow_label" = "Mean") | |
| 925 | #' mf <- basic_matrix_form(iris_output, | |
| 926 | #' indent_rownames = TRUE, | |
| 927 | #' split_labels = "Species", data_labels = "DataRow_label" | |
| 928 | #' ) | |
| 929 | #' cat(toString(mf)) | |
| 930 | #' | |
| 931 | #' @name test_matrix_form | |
| 932 | #' @export | |
| 933 | basic_matrix_form <- function(df, | |
| 934 | indent_rownames = FALSE, | |
| 935 | parent_path = NULL, | |
| 936 | ignore_rownames = FALSE, | |
| 937 | add_decoration = FALSE, | |
| 938 | fontspec = font_spec(), | |
| 939 | split_labels = NULL, | |
| 940 | data_labels = NULL, | |
| 941 |                               num_rep_cols = 0L) { | |
| 942 | 51x | checkmate::assert_data_frame(df) | 
| 943 | 51x | checkmate::assert_flag(indent_rownames) | 
| 944 | 51x | checkmate::assert_character(parent_path, null.ok = TRUE) | 
| 945 | 51x | checkmate::assert_flag(ignore_rownames) | 
| 946 | 51x | checkmate::assert_flag(add_decoration) | 
| 947 | 51x | checkmate::assert_character(split_labels, null.ok = TRUE) | 
| 948 | 51x | checkmate::assert_character(data_labels, null.ok = TRUE) | 
| 949 | ||
| 950 | # Some defaults | |
| 951 | 51x | row_classes <- "DataRow" # Default for all rows | 
| 952 | 51x | data_row_format <- "xx" # Default if no labels are used | 
| 953 | 51x | indent_size <- 2 | 
| 954 | 51x |   indent_space <- paste0(rep(" ", indent_size), collapse = "") | 
| 955 | ||
| 956 | # Pre-processing the fake split | |
| 957 | 51x |   if (!is.null(split_labels)) { | 
| 958 | 4x | checkmate::assert_choice(split_labels, colnames(df)) | 
| 959 | 4x | label_rows <- as.character(df[[split_labels]]) | 
| 960 | 4x |     if (is.null(data_labels)) { | 
| 961 | ! |       data_rows <- rep("Analysis Method", nrow(df)) | 
| 962 | ! | data_labels <- "Analyzed Variable" | 
| 963 |     } else { | |
| 964 | 4x | checkmate::assert_choice(data_labels, colnames(df)) | 
| 965 | 4x | data_rows <- as.character(df[[data_labels]]) | 
| 966 | } | |
| 967 | 4x | rnms_special <- c(rbind(label_rows, data_rows)) | 
| 968 | 4x | row_classes <- c(rbind( | 
| 969 | 4x |       rep("LabelRow", length(label_rows)), | 
| 970 | 4x |       rep("DataRow", length(data_rows)) | 
| 971 | )) | |
| 972 | 4x | data_colnm <- setdiff(colnames(df), c(split_labels, data_labels)) | 
| 973 | 4x | tmp_df <- NULL | 
| 974 | 4x |     for (col_i in seq_along(data_colnm)) { | 
| 975 | 8x |       lbl_and_dt <- c(rbind(rep("", length(label_rows)), df[[data_colnm[col_i]]])) | 
| 976 | 8x | tmp_df <- cbind(tmp_df, lbl_and_dt) | 
| 977 | } | |
| 978 | 4x | colnames(tmp_df) <- data_colnm | 
| 979 | 4x | rownames(tmp_df) <- NULL | 
| 980 | 4x | df <- as.data.frame(tmp_df) | 
| 981 | 4x | ignore_rownames <- FALSE | 
| 982 | } | |
| 983 | ||
| 984 | # Formats | |
| 985 | 51x |   fmts <- lapply(df, function(x) { | 
| 986 | 297x |     if (is.null(obj_format(x))) { | 
| 987 | 297x | fmt_tmp <- data_row_format | 
| 988 |     } else { | |
| 989 | ! | fmt_tmp <- obj_format(x) # Can be assigned for each column | 
| 990 | } | |
| 991 | 297x | out <- rep(fmt_tmp, NROW(df)) | 
| 992 | 297x |     if (!is.null(split_labels)) { | 
| 993 | 8x | out[row_classes == "LabelRow"] <- "-" | 
| 994 | } | |
| 995 | 297x | out | 
| 996 | }) | |
| 997 | ||
| 998 | 51x |   formats <- rbind("", data.frame(fmts)) | 
| 999 | 51x |   if (!ignore_rownames) { | 
| 1000 | 38x |     formats <- cbind("rnms" = "", formats) | 
| 1001 | } | |
| 1002 | ||
| 1003 | # Strings | |
| 1004 | 51x |   bodystrs <- mapply(function(x, coli_fmt) { | 
| 1005 | 297x | coli_fmt[coli_fmt == "-"] <- "xx" | 
| 1006 | 297x |     sapply(seq_along(x), function(y) { | 
| 1007 | 8704x | format_value(x[y], format = coli_fmt[y]) | 
| 1008 | }) | |
| 1009 | 51x | }, x = df, coli_fmt = fmts) | 
| 1010 | ||
| 1011 | 51x |   if (!ignore_rownames) { | 
| 1012 | 38x | rnms <- row.names(df) | 
| 1013 | 38x |     if (!is.null(split_labels)) { | 
| 1014 | # This overload is done because identical rownames not allowed (e.g. Mean.1 Mean.2) | |
| 1015 | 4x | rnms <- rnms_special | 
| 1016 | 34x |     } else if (is.null(rnms)) { | 
| 1017 | ! | rnms <- as.character(seq_len(NROW(df))) | 
| 1018 | } | |
| 1019 | } | |
| 1020 | ||
| 1021 | 51x | strings <- rbind(colnames(df), bodystrs) | 
| 1022 | ||
| 1023 | 51x | rownames(strings) <- NULL | 
| 1024 | 51x |   if (!ignore_rownames) { | 
| 1025 | 38x |     strings <- cbind("rnms" = c("", rnms), strings) | 
| 1026 | } | |
| 1027 | # colnames(strings) <- NULL # to add after fixing basic_mf for listings | |
| 1028 | ||
| 1029 | # Spans | |
| 1030 | 51x | spans <- matrix(1, nrow = nrow(strings), ncol = ncol(strings)) | 
| 1031 | ||
| 1032 | # Aligns | |
| 1033 | # Default alignment is left for rownames column and center for the rest | |
| 1034 | 51x |   aligns <- matrix("center", | 
| 1035 | 51x | nrow = NROW(strings), | 
| 1036 | 51x | ncol = NCOL(strings) - as.numeric(!ignore_rownames) | 
| 1037 | ) | |
| 1038 | 51x |   if (!ignore_rownames) { | 
| 1039 | 38x |     aligns <- cbind("left", aligns) | 
| 1040 | } | |
| 1041 | ||
| 1042 | # Row Info: build up fake pagination df | |
| 1043 | 51x | charcols <- which(sapply(df, is.character)) | 
| 1044 | 51x |   if (length(charcols) > 0) { | 
| 1045 | 14x | exts <- apply(df[, charcols, drop = FALSE], 1, function(x) max(vapply(x, nlines, fontspec = fontspec, 1L))) | 
| 1046 |   } else { | |
| 1047 | 37x | exts <- rep(1L, NROW(df)) | 
| 1048 | } | |
| 1049 | # Constructing path roughly | |
| 1050 | 51x |   if (!is.null(split_labels)) { | 
| 1051 | 4x | paths <- lapply( | 
| 1052 | 4x | seq_along(rnms), | 
| 1053 | 4x |       function(row_path_i) { | 
| 1054 | 24x |         if (row_classes[row_path_i] == "DataRow") { | 
| 1055 | 12x | c( | 
| 1056 | 12x | split_labels, | 
| 1057 | 12x | rnms[row_path_i - 1], # LabelRow before | 
| 1058 | 12x | data_labels, | 
| 1059 | 12x | rnms[row_path_i] | 
| 1060 | ) | |
| 1061 |         } else { | |
| 1062 | 12x | c(split_labels, rnms[row_path_i]) | 
| 1063 | } | |
| 1064 | } | |
| 1065 | ) | |
| 1066 |   } else { | |
| 1067 | 47x | rnms <- row.names(df) | 
| 1068 | 47x |     if (is.null(rnms)) { | 
| 1069 | ! | rnms <- as.character(seq_len(NROW(df))) | 
| 1070 | } | |
| 1071 | 47x | paths <- lapply(rnms, function(x) c(parent_path, x)) | 
| 1072 | } | |
| 1073 | 51x | rowdf <- basic_pagdf( | 
| 1074 | 51x | rnames = rnms, | 
| 1075 | 51x | extents = exts, | 
| 1076 | 51x | rclass = row_classes, | 
| 1077 | 51x | parent_path = NULL, # Overloaded by above parent_path lapply | 
| 1078 | 51x | paths = paths | 
| 1079 | ) | |
| 1080 | ||
| 1081 | # Indentation happens last so to be sure we have all ready (only strings and formats change) | |
| 1082 | 51x |   if (indent_rownames && !is.null(split_labels)) { | 
| 1083 | 2x | where_to_indent <- which(row_classes == "DataRow") + 1 # +1 because of colnames | 
| 1084 | 2x | strings[where_to_indent, 1] <- paste0(indent_space, strings[where_to_indent, 1]) | 
| 1085 | 2x | formats[where_to_indent, 1] <- paste0(indent_space, formats[where_to_indent, 1]) # Needs fixing | 
| 1086 | 2x | rowdf$indent[where_to_indent - 1] <- 1 # -1 because only rows | 
| 1087 | } | |
| 1088 | ||
| 1089 | 51x | ret <- MatrixPrintForm( | 
| 1090 | 51x | strings = strings, | 
| 1091 | 51x | aligns = aligns, | 
| 1092 | 51x | spans = spans, | 
| 1093 | 51x |     formats = formats, ## matrix("xx", nrow = fnr, ncol = fnc), | 
| 1094 | 51x | row_info = rowdf, | 
| 1095 | 51x | has_topleft = FALSE, | 
| 1096 | 51x | nlines_header = 1, | 
| 1097 | 51x | nrow_header = 1, | 
| 1098 | 51x | has_rowlabs = isFALSE(ignore_rownames), | 
| 1099 | 51x | fontspec = fontspec, | 
| 1100 | 51x | col_gap = 3, | 
| 1101 | 51x | indent_size = indent_size, | 
| 1102 | 51x | rep_cols = num_rep_cols | 
| 1103 | ) | |
| 1104 | ||
| 1105 | # Check for ncols | |
| 1106 | 51x | stopifnot(mf_has_rlabels(ret) == isFALSE(ignore_rownames)) | 
| 1107 | ||
| 1108 | 51x | ret <- mform_build_refdf(ret) | 
| 1109 | ||
| 1110 | 51x |   if (add_decoration) { | 
| 1111 | 7x | main_title(ret) <- "main title" | 
| 1112 | 7x |     main_footer(ret) <- c("main", "  footer") | 
| 1113 | 7x | prov_footer(ret) <- "prov footer" | 
| 1114 | 7x |     subtitles(ret) <- c("sub", "titles") | 
| 1115 | } | |
| 1116 | ||
| 1117 | 51x | ret | 
| 1118 | } | |
| 1119 | ||
| 1120 | #' @describeIn test_matrix_form Create a `MatrixPrintForm` object from data frame `df` that | |
| 1121 | #' respects the default formats for a listing object. | |
| 1122 | #' | |
| 1123 | #' @param keycols (`character`)\cr a vector of `df` column names that are printed first and for which | |
| 1124 | #' repeated values are assigned `""`. This format is characteristic of a listing matrix form. | |
| 1125 | #' | |
| 1126 | #' @return A valid `MatrixPrintForm` object representing `df` as a listing that is ready for ASCII | |
| 1127 | #' rendering. | |
| 1128 | #' | |
| 1129 | #' @examples | |
| 1130 | #' mform <- basic_listing_mf(mtcars) | |
| 1131 | #' cat(toString(mform)) | |
| 1132 | #' | |
| 1133 | #' @export | |
| 1134 | basic_listing_mf <- function(df, | |
| 1135 | keycols = names(df)[1], | |
| 1136 | add_decoration = TRUE, | |
| 1137 |                              fontspec = font_spec()) { | |
| 1138 | 8x | checkmate::assert_data_frame(df) | 
| 1139 | 8x | checkmate::assert_subset(keycols, colnames(df)) | 
| 1140 | ||
| 1141 | 8x | dfmf <- basic_matrix_form( | 
| 1142 | 8x | df = df, | 
| 1143 | 8x | indent_rownames = FALSE, | 
| 1144 | 8x | ignore_rownames = TRUE, | 
| 1145 | 8x | add_decoration = add_decoration, | 
| 1146 | 8x | num_rep_cols = length(keycols), | 
| 1147 | 8x | fontspec = fontspec | 
| 1148 | ) | |
| 1149 | ||
| 1150 | # keycols addition to MatrixPrintForm (should happen in the constructor) | |
| 1151 | 8x | dfmf$listing_keycols <- keycols | 
| 1152 | ||
| 1153 | # Modifications needed for making it a listings | |
| 1154 | 8x | mf_strings(dfmf)[1, ] <- colnames(mf_strings(dfmf)) # set colnames | 
| 1155 | ||
| 1156 | 8x |   if (!is.null(keycols)) { | 
| 1157 | 8x | str_dfmf <- mf_strings(dfmf)[-1, ] | 
| 1158 | # Ordering | |
| 1159 | 8x | ord <- do.call( | 
| 1160 | 8x | order, | 
| 1161 | 8x | as.list( | 
| 1162 | 8x | data.frame( | 
| 1163 | 8x | str_dfmf[, keycols] | 
| 1164 | ) | |
| 1165 | ) | |
| 1166 | ) | |
| 1167 | 8x | str_dfmf <- str_dfmf[ord, ] | 
| 1168 | # Making keycols with empties | |
| 1169 | 8x | curkey <- "" | 
| 1170 | 8x |     for (i in seq_along(keycols)) { | 
| 1171 | 15x | kcol <- keycols[i] | 
| 1172 | 15x | kcolvec <- str_dfmf[, kcol] # -1 is col label row | 
| 1173 | 15x | str_dfmf[, kcol] <- "" | 
| 1174 | 15x | kcolvec <- vapply(kcolvec, format_value, "", format = NULL, na_str = "NA") | 
| 1175 | 15x | curkey <- paste0(curkey, kcolvec) | 
| 1176 | 15x | disp <- c(TRUE, tail(curkey, -1) != head(curkey, -1)) | 
| 1177 | 15x | str_dfmf[disp, kcol] <- kcolvec[disp] | 
| 1178 | } | |
| 1179 | 8x | mf_strings(dfmf)[-1, ] <- str_dfmf | 
| 1180 | # keycols as first | |
| 1181 | 8x | mf_strings(dfmf) <- cbind( | 
| 1182 | 8x | mf_strings(dfmf)[, keycols, drop = FALSE], | 
| 1183 | 8x | mf_strings(dfmf)[, !colnames(mf_strings(dfmf)) %in% keycols, drop = FALSE] | 
| 1184 | ) | |
| 1185 | } | |
| 1186 | ||
| 1187 | 8x | dfmf$aligns[seq(2, nrow(dfmf$aligns)), ] <- "center" # the default for listings | 
| 1188 | ||
| 1189 | # the default for listings is a 1 double?? | |
| 1190 | 8x | dfmf$formats <- matrix(1, nrow = nrow(dfmf$formats), ncol = ncol(dfmf$formats)) | 
| 1191 | ||
| 1192 | # row info | |
| 1193 | 8x | ri <- dfmf$row_info | 
| 1194 | 8x | rownames(ri) <- ri$abs_rownumber | 
| 1195 | 8x | ri$label <- ri$name <- "" | 
| 1196 | 8x | ri$path <- as.list(NA_character_) # same format of listings | 
| 1197 | 8x | ri$node_class <- "listing_df" | 
| 1198 | # l_ri$pos_in_siblings # why is it like this in rlistings?? also n_siblings | |
| 1199 | 8x | class(ri$path) <- "AsIs" # Artifact from I() | 
| 1200 | 8x | dfmf$row_info <- ri | 
| 1201 | ||
| 1202 | # colwidths need to be sorted too!! | |
| 1203 | 8x | dfmf$col_widths <- dfmf$col_widths[colnames(mf_strings(dfmf))] | 
| 1204 | ||
| 1205 | 8x |   if (!add_decoration) { | 
| 1206 | # This is probably a forced behavior in the original matrix_form in rlistings | |
| 1207 | 2x | main_title(dfmf) <- character() | 
| 1208 | 2x | main_footer(dfmf) <- character() | 
| 1209 | } | |
| 1210 | ||
| 1211 | 8x | dfmf | 
| 1212 | } | |
| 1213 | ||
| 1214 | map_to_new <- function(old, map) { | |
| 1215 | 412x | inds <- match(old, map$old_idx) | 
| 1216 | 412x | map$new_idx[inds] | 
| 1217 | } | |
| 1218 | ||
| 1219 | reconstruct_basic_fnote_list <- function(mf) { | |
| 1220 | 318x | refdf <- mf_fnote_df(mf) | 
| 1221 | 318x |   if (NROW(refdf) == 0) { | 
| 1222 | 278x | return(NULL) | 
| 1223 | } | |
| 1224 | 40x | refdf <- refdf[!duplicated(refdf$symbol), ] | 
| 1225 | 40x |   paste0("{", refdf$symbol, "} - ", refdf$msg) | 
| 1226 | } | |
| 1227 | ||
| 1228 | .mf_subset_core_mats <- function(mf, i, keycols = NULL, row = TRUE) { | |
| 1229 | 316x | fillnum <- if (row) nrow(mf_strings(mf)) - mf_nlheader(mf) else mf_ncol(mf) | 
| 1230 | 316x |   if (is.logical(i) || all(i < 0)) { | 
| 1231 | ! | i <- seq_len(fillnum)[i] | 
| 1232 | } | |
| 1233 | 316x | nlh <- mf_nlheader(mf) | 
| 1234 | ||
| 1235 | 316x |   if (row) { | 
| 1236 | 96x | ncolrows <- mf_nrheader(mf) | 
| 1237 | 96x | i_mat <- c(seq_len(nlh), which(mf_lgrouping(mf) %in% (i + ncolrows))) | 
| 1238 | 96x | j_mat <- seq_len(ncol(mf_strings(mf))) | 
| 1239 |   } else { | |
| 1240 | 220x | nlabcol <- as.integer(mf_has_rlabels(mf)) | 
| 1241 | 220x | i_mat <- seq_len(nrow(mf_strings(mf))) | 
| 1242 | 220x | j_mat <- c(seq_len(nlabcol), i + nlabcol) | 
| 1243 | } | |
| 1244 | ||
| 1245 | 316x | tmp_strmat <- mf_strings(mf)[i_mat, j_mat, drop = FALSE] | 
| 1246 | ||
| 1247 | # Only for listings - Fix pagination with empty values in key columns | |
| 1248 | 316x |   if (nrow(tmp_strmat) > 0 && .is_listing_mf(mf)) { # safe check for empty listings | 
| 1249 | 39x | ind_keycols <- which(colnames(tmp_strmat) %in% keycols) | 
| 1250 | ||
| 1251 | # Fix for missing labels in key columns (only for rlistings) | |
| 1252 | 39x | empty_keycols <- !nzchar(tmp_strmat[-seq_len(nlh), ind_keycols, drop = FALSE][1, ]) | 
| 1253 | ||
| 1254 | 39x |     if (any(empty_keycols)) { # only if there are missing keycol labels | 
| 1255 | # find the first non-empty label in the key columns | |
| 1256 | 6x | keycols_needed <- mf_strings(mf)[, empty_keycols, drop = FALSE] | 
| 1257 | 6x |       first_nonempty <- apply(keycols_needed, 2, function(x) { | 
| 1258 | 16x | section_ind <- i_mat[-seq_len(nlh)][1] | 
| 1259 | 16x | sec_ind_no_header <- seq_len(section_ind)[-seq_len(nlh)] | 
| 1260 | 16x | tail(x[sec_ind_no_header][nzchar(x[sec_ind_no_header])], 1) | 
| 1261 | }) | |
| 1262 | ||
| 1263 | # if there are only "" the previous returns character() | |
| 1264 | 6x |       any_chr_empty <- if (length(first_nonempty) > 1) { | 
| 1265 | 6x | vapply(first_nonempty, length, numeric(1)) | 
| 1266 |       } else { | |
| 1267 | ! | length(first_nonempty) | 
| 1268 | } | |
| 1269 | 6x |       if (any(any_chr_empty == 0L)) { | 
| 1270 | ! | warning( | 
| 1271 | ! | "There are empty key columns in the listing. ", | 
| 1272 | ! | "We keep empty strings for each page." | 
| 1273 | ) | |
| 1274 | ! | first_nonempty[any_chr_empty == 0L] <- "" | 
| 1275 | } | |
| 1276 | ||
| 1277 | # replace the empty labels with the first non-empty label | |
| 1278 | 6x | tmp_strmat[nlh + 1, empty_keycols] <- unlist(first_nonempty) | 
| 1279 | } | |
| 1280 | } | |
| 1281 | ||
| 1282 | 316x | mf_strings(mf) <- tmp_strmat | 
| 1283 | ||
| 1284 | 316x | mf_lgrouping(mf) <- as.integer(as.factor(mf_lgrouping(mf)[i_mat])) | 
| 1285 | ||
| 1286 | 316x |   if (!row) { | 
| 1287 | 220x | newspans <- truncate_spans(mf_spans(mf), j_mat) # 'i' is the columns here, bc row is FALSE | 
| 1288 |   } else { | |
| 1289 | 96x | newspans <- mf_spans(mf)[i_mat, j_mat, drop = FALSE] | 
| 1290 | } | |
| 1291 | ||
| 1292 | 316x | mf_spans(mf) <- newspans | 
| 1293 | 316x | mf_formats(mf) <- mf_formats(mf)[i_mat, j_mat, drop = FALSE] | 
| 1294 | ||
| 1295 | 316x | mf_aligns(mf) <- mf_aligns(mf)[i_mat, j_mat, drop = FALSE] | 
| 1296 | 316x |   if (!row) { | 
| 1297 | 220x | mf_ncol(mf) <- length(i) | 
| 1298 | 220x |     if (!is.null(mf_cinfo(mf))) { | 
| 1299 | 220x | mf_cinfo(mf) <- mf_cinfo(mf)[i, ] | 
| 1300 | } | |
| 1301 | 220x |     if (!is.null(mf_col_widths(mf))) { | 
| 1302 | 220x | mf_col_widths(mf) <- mf_col_widths(mf)[j_mat] | 
| 1303 | } | |
| 1304 | } | |
| 1305 | 316x | mf | 
| 1306 | } | |
| 1307 | ||
| 1308 | ## ugh. spans are **way** more of a pain than I expected x.x | |
| 1309 | truncate_one_span <- function(spanrow, j) { | |
| 1310 | 3793x | i <- 1 | 
| 1311 | 3793x | len <- length(spanrow) | 
| 1312 | 3793x |   while (i < len) { | 
| 1313 | 41757x | spnlen <- spanrow[i] | 
| 1314 | 41757x | inds <- seq(i, i + spnlen - 1) | 
| 1315 | 41757x | newspnlen <- sum(inds %in% j) | 
| 1316 | 41757x | spanrow[inds] <- newspnlen | 
| 1317 | 41757x | i <- i + spnlen | 
| 1318 | } | |
| 1319 | 3793x | spanrow[j] | 
| 1320 | } | |
| 1321 | ||
| 1322 | truncate_spans <- function(spans, j) { | |
| 1323 | 220x |   if (length(spans[1, ]) == 1 || length(j) == 1) { | 
| 1324 | ! | as.matrix(apply(spans, 1, truncate_one_span, j = j)) | 
| 1325 |   } else { | |
| 1326 | 220x | t(apply(spans, 1, truncate_one_span, j = j)) | 
| 1327 | } | |
| 1328 | } | |
| 1329 | ||
| 1330 | mpf_subset_rows <- function(mf, i, keycols = NULL) { | |
| 1331 | 96x | nlh <- mf_nlheader(mf) | 
| 1332 | 96x | lgrps <- mf_lgrouping(mf) | 
| 1333 | 96x | row_lgrps <- tail(lgrps, -1 * nlh) | 
| 1334 | 96x | nrs <- length(unique(row_lgrps)) | 
| 1335 | 96x | ncolrows <- length(unique(lgrps[seq_len(nlh)])) | 
| 1336 | ||
| 1337 | 96x | ncs <- mf_ncol(mf) | 
| 1338 | 96x | mf <- .mf_subset_core_mats(mf, i, keycols = keycols, row = TRUE) | 
| 1339 | 96x | map <- data.frame( | 
| 1340 | 96x | old_idx = c(seq_len(ncolrows), i + ncolrows), | 
| 1341 | 96x | new_idx = c(seq_len(ncolrows), ncolrows + order(i)) | 
| 1342 | ) | |
| 1343 | ||
| 1344 | 96x | row_map <- data.frame(old_idx = i, new_idx = order(i)) | 
| 1345 | ||
| 1346 | 96x | refdf <- mf_fnote_df(mf) | 
| 1347 | ||
| 1348 | 96x | old_nas <- is.na(refdf$row) | 
| 1349 | 96x | refdf$row <- map_to_new(refdf$row, row_map) | 
| 1350 | 96x | refdf <- refdf[old_nas | !is.na(refdf$row), ] | 
| 1351 | 96x | mf_fnote_df(mf) <- refdf | 
| 1352 | ||
| 1353 | 96x | rinfo <- mf_rinfo(mf) | 
| 1354 | ||
| 1355 | 96x | rinfo <- rinfo[rinfo$abs_rownumber %in% i, ] | 
| 1356 | ||
| 1357 | 96x | rinfo$abs_rownumber <- map_to_new(rinfo$abs_rownumber, row_map) | 
| 1358 | 96x | mf_rinfo(mf) <- rinfo | 
| 1359 | ||
| 1360 | 96x | mf <- shove_refdf_into_rowinfo(mf) | 
| 1361 | 96x | mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) | 
| 1362 | 96x | mf | 
| 1363 | } | |
| 1364 | ||
| 1365 | ## we only care about referential footnotes, cause | |
| 1366 | ## they are currently the only place we're tracking | |
| 1367 | ## column information that will need to be touched up | |
| 1368 | ## but lets be careful and do a bit more anyway | |
| 1369 | mpf_subset_cols <- function(mf, j, keycols = NULL) { | |
| 1370 | 220x | nc <- mf_ncol(mf) | 
| 1371 | 220x |   if (is.logical(j) || all(j < 0)) { | 
| 1372 | ! | j <- seq_len(nc)[j] | 
| 1373 | } | |
| 1374 | 220x |   if (any(j < 0)) { | 
| 1375 | ! |     stop("cannot mix negative and positive indices") | 
| 1376 | } | |
| 1377 | ||
| 1378 | 220x |   if (length(unique(j)) != length(j)) { | 
| 1379 | ! |     stop("duplicated columns are not allowed when subsetting a matrix print form objects") | 
| 1380 | } | |
| 1381 | ||
| 1382 | # j_mat <- c(if(mf_has_topleft(mf)) seq_len(nlabcol), j + nlabcol) | |
| 1383 | 220x | map <- data.frame(old_idx = j, new_idx = order(j)) | 
| 1384 | ||
| 1385 | ## this has to happen before the remap inher | |
| 1386 | 220x | refdf <- mf_fnote_df(mf) | 
| 1387 | ||
| 1388 | 220x | mf <- .mf_subset_core_mats(mf, j, keycols = keycols, row = FALSE) | 
| 1389 | ||
| 1390 | ## future proofing (pipe dreams) | |
| 1391 | ## uncomment if we ever manage to have col info information on MPFs | |
| 1392 |   ## if(!is.null(mf_cinfo(mf))) { | |
| 1393 | ## cinfo <- mf_cinfo(mf) | |
| 1394 | ## cinfo <- cinfo[j, , drop = FALSE] | |
| 1395 | ## cinfo$abs_pos <- map_to_new(cinfo$abs_pos, map) | |
| 1396 | ## mf_cinfo(mf) <- mf | |
| 1397 | ## } | |
| 1398 | ||
| 1399 | 220x | keep <- is.na(refdf$col) | refdf$col %in% j | 
| 1400 | 220x | refdf <- refdf[keep, , drop = FALSE] | 
| 1401 | ||
| 1402 | 220x | refdf$col <- map_to_new(refdf$col, map) | 
| 1403 | 220x | mf_fnote_df(mf) <- refdf | 
| 1404 | 220x | mf <- shove_refdf_into_rowinfo(mf) | 
| 1405 | 220x | mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) | 
| 1406 | 220x | mf_ncol(mf) <- length(j) | 
| 1407 | 220x | mf | 
| 1408 | } | 
| 1 | fun_takes <- function(f, nm) { | |
| 2 | 15x | nm %in% names(formals(f)) | 
| 3 | } | |
| 4 | ||
| 5 | call_format_fun <- function(f, | |
| 6 | value, | |
| 7 | na_str, | |
| 8 | round_type, | |
| 9 |                             output) { | |
| 10 | 5x | args <- c( | 
| 11 | 5x | list(value), | 
| 12 | 5x | if (fun_takes(f, "na_str")) list(na_str = na_str), | 
| 13 | 5x | if (fun_takes(f, "round_type")) list(round_type = round_type), | 
| 14 | 5x | if (fun_takes(f, "output")) list(output = output) | 
| 15 | ) | |
| 16 | 5x | do.call(f, args) | 
| 17 | } | |
| 18 | ||
| 19 | formats_1d <- c( | |
| 20 | "xx", "xx.", "xx.x", "xx.xx", "xx.xxx", "xx.xxxx", | |
| 21 | "xx%", "xx.%", "xx.x%", "xx.xx%", "xx.xxx%", "(N=xx)", "N=xx", ">999.9", ">999.99", | |
| 22 | "x.xxxx | (<0.0001)" | |
| 23 | ) | |
| 24 | ||
| 25 | formats_2d <- c( | |
| 26 | "xx / xx", "xx. / xx.", "xx.x / xx.x", "xx.xx / xx.xx", "xx.xxx / xx.xxx", | |
| 27 | "N=xx (xx%)", "xx (xx%)", "xx (xx.%)", "xx (xx.x%)", "xx (xx.xx%)", | |
| 28 | "xx. (xx.%)", "xx.x (xx.x%)", "xx.xx (xx.xx%)", | |
| 29 | "(xx, xx)", "(xx., xx.)", "(xx.x, xx.x)", "(xx.xx, xx.xx)", | |
| 30 | "(xx.xxx, xx.xxx)", "(xx.xxxx, xx.xxxx)", | |
| 31 | "xx - xx", "xx.x - xx.x", "xx.xx - xx.xx", | |
| 32 | "xx (xx)", "xx. (xx.)", "xx.x (xx.x)", "xx.xx (xx.xx)", | |
| 33 | "xx (xx.)", "xx (xx.x)", "xx (xx.xx)", | |
| 34 | "xx. (xx.x)", "xx.x (xx.xx)", "xx.xx (xx.xxx)", | |
| 35 | "xx, xx", "xx.x, xx.x", "xx.xx, xx.xx", | |
| 36 | "xx.x to xx.x" | |
| 37 | ) | |
| 38 | ||
| 39 | formats_3d <- c( | |
| 40 | "xx. (xx. - xx.)", | |
| 41 | "xx.x (xx.x - xx.x)", | |
| 42 | "xx.xx (xx.xx - xx.xx)", | |
| 43 | "xx.xxx (xx.xxx - xx.xxx)", | |
| 44 | "xx / xx (xx.%)", | |
| 45 | "xx / xx (xx.x%)", | |
| 46 | "xx / xx (xx.xx%)" | |
| 47 | ) | |
| 48 | ||
| 49 | #' List of currently supported formats and vertical alignments | |
| 50 | #' | |
| 51 | #' @description We support `xx` style format labels grouped by 1d, 2d, and 3d. | |
| 52 | #' Currently valid format labels cannot be added dynamically. Format functions | |
| 53 | #' must be used for special cases. | |
| 54 | #' | |
| 55 | #' @return | |
| 56 | #' * `list_valid_format_labels()` returns a nested list, with elements listing the supported 1d, 2d, | |
| 57 | #' and 3d format strings. | |
| 58 | #' | |
| 59 | #' @examples | |
| 60 | #' list_valid_format_labels() | |
| 61 | #' | |
| 62 | #' @name list_formats | |
| 63 | #' @export | |
| 64 | list_valid_format_labels <- function() { | |
| 65 | 64x | structure( | 
| 66 | 64x | list( | 
| 67 | 64x | "1d" = formats_1d, | 
| 68 | 64x | "2d" = formats_2d, | 
| 69 | 64x | "3d" = formats_3d | 
| 70 | ), | |
| 71 | 64x | info = "xx does not modify the element, and xx. rounds a number to 0 digits" | 
| 72 | ) | |
| 73 | } | |
| 74 | ||
| 75 | #' @return | |
| 76 | #' * `list_valid_aligns()` returns a character vector of valid vertical alignments. | |
| 77 | #' | |
| 78 | #' @examples | |
| 79 | #' list_valid_aligns() | |
| 80 | #' | |
| 81 | #' @name list_formats | |
| 82 | #' @export | |
| 83 | list_valid_aligns <- function() { | |
| 84 | 16259x |   c("left", "right", "center", "decimal", "dec_right", "dec_left") | 
| 85 | } | |
| 86 | ||
| 87 | #' Check if a format or alignment is supported | |
| 88 | #' | |
| 89 | #' @description Utility functions for checking formats and alignments. | |
| 90 | #' | |
| 91 | #' @param x (`string` or `function`)\cr format string or an object returned by [sprintf_format()] | |
| 92 | #' @param stop_otherwise (`flag`)\cr whether an error should be thrown if `x` is not a valid format. | |
| 93 | #' | |
| 94 | #' @return | |
| 95 | #' * `is_valid_format` returns `TRUE` if `x` is `NULL`, a supported format string, or a function, and | |
| 96 | #' `FALSE` otherwise. | |
| 97 | #' | |
| 98 | #' @note If `x` is a function, no check is performed to verify that it returns a valid format. | |
| 99 | #' | |
| 100 | #' @examples | |
| 101 | #' is_valid_format("xx.x") | |
| 102 | #' is_valid_format("fakeyfake") | |
| 103 | #' | |
| 104 | #' @name check_formats | |
| 105 | #' @export | |
| 106 | is_valid_format <- function(x, stop_otherwise = FALSE) { | |
| 107 | 60x | is_valid <- is.null(x) || (length(x) == 1 && (is.function(x) || x %in% unlist(list_valid_format_labels()))) | 
| 108 | ||
| 109 | 60x |   if (stop_otherwise && !is_valid) { | 
| 110 | ! |     stop("format needs to be a format label, sprintf_format object, a function, or NULL") | 
| 111 | } | |
| 112 | ||
| 113 | 60x | is_valid | 
| 114 | } | |
| 115 | ||
| 116 | #' @param algn (`character`)\cr a character vector that indicates the requested cell alignments. | |
| 117 | #' | |
| 118 | #' @return | |
| 119 | #' * `check_aligns` returns `TRUE` if the provided alignments are supported, otherwise, an error is thrown. | |
| 120 | #' | |
| 121 | #' @examples | |
| 122 | #' check_aligns(c("decimal", "dec_right")) | |
| 123 | #' | |
| 124 | #' @name check_formats | |
| 125 | #' @export | |
| 126 | check_aligns <- function(algn) { | |
| 127 | 3x |   if (anyNA(algn)) { | 
| 128 | 1x |     stop("Got missing-value for text alignment.") | 
| 129 | } | |
| 130 | 2x | invalid <- setdiff(algn, list_valid_aligns()) | 
| 131 | 2x |   if (length(invalid) > 0) { | 
| 132 | 1x |     stop("Unsupported text-alignment(s): ", paste(invalid, collapse = ", ")) | 
| 133 | } | |
| 134 | 1x | invisible(TRUE) | 
| 135 | } | |
| 136 | ||
| 137 | #' Specify text format via a `sprintf` format string | |
| 138 | #' | |
| 139 | #' @param format (`string`)\cr a format string passed to [sprintf()]. | |
| 140 | #' | |
| 141 | #' @return A formatting function which wraps and applies the specified `sprintf`-style format | |
| 142 | #' to string `format`. | |
| 143 | #' | |
| 144 | #' @seealso [sprintf()] | |
| 145 | #' | |
| 146 | #' @examples | |
| 147 | #' fmtfun <- sprintf_format("(N=%i") | |
| 148 | #' format_value(100, format = fmtfun) | |
| 149 | #' | |
| 150 | #' fmtfun2 <- sprintf_format("%.4f - %.2f") | |
| 151 | #' format_value(list(12.23456, 2.724)) | |
| 152 | #' | |
| 153 | #' @export | |
| 154 | sprintf_format <- function(format) { | |
| 155 | 1x |   function(x, ...) { | 
| 156 | 1x | do.call(sprintf, c(list(fmt = format), x)) | 
| 157 | } | |
| 158 | } | |
| 159 | ||
| 160 | #' Round and prepare a value for display | |
| 161 | #' | |
| 162 | #' This function is used within [format_value()] to prepare numeric values within | |
| 163 | #' cells for formatting and display. | |
| 164 | #' | |
| 165 | #' @param x (`numeric(1)`)\cr value to format. | |
| 166 | #' @param digits (`numeric(1)`)\cr number of digits to round to, or `NA` to convert to a | |
| 167 | #' character value with no rounding. | |
| 168 | #' @param na_str (`string`)\cr the value to return if `x` is `NA`. | |
| 169 | #' @param round_type (`"iec"` or `"sas"`)\cr the type of rounding to perform. iec, | |
| 170 | #' the default, peforms rounding compliant with IEC 60559 (see details), while | |
| 171 | #' sas performs nearest-value rounding consistent with rounding within SAS. | |
| 172 | #' | |
| 173 | #' @details | |
| 174 | #' This function combines rounding behavior with the strict decimal display of | |
| 175 | #' [sprintf()]. By default, R's standards-compliant [round()] | |
| 176 | #' function (see the Details section of that documentation) is used. The exact | |
| 177 | #' behavior is as follows: | |
| 178 | #' | |
| 179 | #' \enumerate{ | |
| 180 | #'   \item{If `x` is `NA`, the value of `na_str` is returned.} | |
| 181 | #'   \item{If `x` is non-`NA` but `digits` is `NA`, `x` is converted to a character and returned.} | |
| 182 | #'   \item{If `x` and `digits` are both non-NA, [round()] is called first, and then [sprintf()] | |
| 183 | #' is used to convert the rounded value to a character with the appropriate number of trailing | |
| 184 | #' zeros enforced.} | |
| 185 | #' } | |
| 186 | #' | |
| 187 | #' @return A character value representing the value after rounding, containing any trailing zeros | |
| 188 | #' required to display *exactly* `digits` elements. | |
| 189 | #' | |
| 190 | #' @note | |
| 191 | #' This differs from the base R [round()] function in that `NA` digits indicate `x` should be converted | |
| 192 | #' to character and returned unchanged whereas `round(x, digits=NA)` returns `NA` for all values of `x`. | |
| 193 | #' | |
| 194 | #' This behavior will differ from `as.character(round(x, digits = digits))` in the case where there are | |
| 195 | #' not at least `digits` significant digits after the decimal that remain after rounding. It *may* differ from | |
| 196 | #' `sprintf("\%.Nf", x)` for values ending in `5` after the decimal place on many popular operating systems | |
| 197 | #' due to `round`'s stricter adherence to the IEC 60559 standard, particularly for R versions > 4.0.0 (see | |
| 198 | #' warning in [round()] documentation). | |
| 199 | #' | |
| 200 | #' @seealso [format_value()], [round()], [sprintf()] | |
| 201 | #' | |
| 202 | #' @examples | |
| 203 | #' round_fmt(0, digits = 3) | |
| 204 | #' round_fmt(.395, digits = 2) | |
| 205 | #' round_fmt(NA, digits = 1) | |
| 206 | #' round_fmt(NA, digits = 1, na_str = "-") | |
| 207 | #' round_fmt(2.765923, digits = NA) | |
| 208 | #' round_fmt(0.845, digits = 2) | |
| 209 | #' round_fmt(0.845, digits = 2, round_type = "sas") | |
| 210 | #' | |
| 211 | #' @export | |
| 212 | #' @aliases rounding | |
| 213 | round_fmt <- function(x, digits, na_str = "NA", round_type = c("iec", "sas")) { | |
| 214 | 1220x | round_type <- match.arg(round_type) | 
| 215 | 1220x |   if (!is.na(digits) && digits < 0) { | 
| 216 | ! |     stop("round_fmt currently does not support non-missing values of digits < 0") | 
| 217 | } | |
| 218 | 1220x |   if (is.na(x)) { | 
| 219 | 11x | na_str | 
| 220 | 1209x |   } else if (is.na(digits)) { | 
| 221 | 321x | paste0(x) | 
| 222 |   } else { | |
| 223 | 888x | rndx <- switch(round_type, | 
| 224 | 888x | iec = round(x, digits), | 
| 225 | 888x | sas = round_sas(x, digits) | 
| 226 | ) | |
| 227 | 888x |     sprfmt <- paste0("%.", digits, "f") | 
| 228 | 888x | sprintf(fmt = sprfmt, rndx) | 
| 229 | } | |
| 230 | } | |
| 231 | ||
| 232 | ## https://stackoverflow.com/questions/12688717/round-up-from-5 | |
| 233 | ||
| 234 | #' @inheritParams round_fmt | |
| 235 | #' | |
| 236 | round_sas <- function(x, | |
| 237 |                       digits = 0) { | |
| 238 | # perform SAS rounding ---------------------------------------------------- | |
| 239 | 317x | posneg <- sign(x) | 
| 240 | 317x | z <- abs(x) * 10^digits | 
| 241 | 317x | z <- z + 0.5 + sqrt(.Machine$double.eps) | 
| 242 | 317x | z <- trunc(z) | 
| 243 | 317x | z <- z / 10^digits | 
| 244 | 317x | z <- z * posneg | 
| 245 | ## return numeric vector of rounded values | |
| 246 | 317x | z | 
| 247 | } | |
| 248 | ||
| 249 | ||
| 250 | val_pct_helper <- function(x, dig1, dig2, na_str, pct = TRUE, round_type = c("iec", "sas")) { | |
| 251 | 200x |   if (pct) { | 
| 252 | 90x | x[2] <- x[2] * 100 | 
| 253 | } | |
| 254 | 200x |   if (length(na_str) == 1) { | 
| 255 | ! | na_str <- rep(na_str, 2) | 
| 256 | } | |
| 257 | 200x | paste0( | 
| 258 | 200x | round_fmt(x[1], digits = dig1, na_str = na_str[1], round_type = round_type), | 
| 259 |     " (", | |
| 260 | 200x | round_fmt(x[2], digits = dig2, na_str = na_str[2], round_type = round_type), | 
| 261 | 200x | if (pct) "%", ")" | 
| 262 | ) | |
| 263 | } | |
| 264 | ||
| 265 | sep_2d_helper <- function(x, dig1, dig2, sep, na_str, wrap = NULL, round_type = c("iec", "sas")) { | |
| 266 | 282x | ret <- paste(mapply(round_fmt, x = x, digits = c(dig1, dig2), na_str = na_str, round_type = round_type), | 
| 267 | 282x | collapse = sep | 
| 268 | ) | |
| 269 | 282x |   if (!is.null(wrap)) { | 
| 270 | 114x | ret <- paste(c(wrap[1], ret, wrap[2]), collapse = "") | 
| 271 | } | |
| 272 | 282x | ret | 
| 273 | } | |
| 274 | ||
| 275 | ## na_or_round <- function(x, digits, na_str) { | |
| 276 | ## if(is.na(x)) | |
| 277 | ## na_str | |
| 278 | ## else | |
| 279 | ## round(x, digits = digits) | |
| 280 | ## } | |
| 281 | ||
| 282 | #' Converts a (possibly compound) value into a string using the `format` information | |
| 283 | #' | |
| 284 | #' @inheritParams round_fmt | |
| 285 | #' @param x (`ANY`)\cr the value to be formatted. | |
| 286 | #' @param format (`string` or `function`)\cr the format label (string) or formatter function to | |
| 287 | #' apply to `x`. | |
| 288 | #' @param na_str (`character`)\cr character vector to display when the values of `x` are missing. | |
| 289 | #' If only one string is provided, it is applied for all missing values. Defaults to `"NA"`. | |
| 290 | #' @param output (`string`)\cr output type. | |
| 291 | #' | |
| 292 | #' @details A length-zero value for `na_str` will be interpreted as `"NA"`. | |
| 293 | #' | |
| 294 | #' @return Formatted text representing the cell `x`. | |
| 295 | #' | |
| 296 | #' @seealso [round_fmt()] | |
| 297 | #' | |
| 298 | #' @examples | |
| 299 | #' x <- format_value(pi, format = "xx.xx") | |
| 300 | #' x | |
| 301 | #' | |
| 302 | #' format_value(x, output = "ascii") | |
| 303 | #' | |
| 304 | #' # na_str works with multiple values | |
| 305 | #' format_value(c(NA, 1, NA), format = "xx.x (xx.x - xx.x)", na_str = c("NE", "<missing>")) | |
| 306 | #' | |
| 307 | #' @export | |
| 308 | format_value <- function(x, format = NULL, output = c("ascii", "html"), na_str = "NA", round_type = c("iec", "sas")) { | |
| 309 | ## if(is(x, "CellValue")) | |
| 310 | ## x = x[[1]] | |
| 311 | ||
| 312 | 9742x |   if (length(x) == 0) { | 
| 313 | 1x |     return("") | 
| 314 | } | |
| 315 | ||
| 316 | 9741x | output <- match.arg(output) | 
| 317 | 9741x | round_type <- match.arg(round_type) | 
| 318 | ||
| 319 | # Checks for NAs in the input | |
| 320 | 9741x |   if (length(na_str) == 0) { | 
| 321 | 1x | na_str <- "NA" | 
| 322 | } | |
| 323 | 9741x |   if (any(is.na(na_str))) { | 
| 324 | 1x | na_str[is.na(na_str)] <- "NA" | 
| 325 | } | |
| 326 | 9741x |   if (length(na_str) == 1) { | 
| 327 | 9737x |     if (!all(is.na(x))) { | 
| 328 | ## array adds an unneeded dim attribute which causes problems | |
| 329 | 9707x | na_str <- rep(na_str, length(x)) | 
| 330 | } | |
| 331 |   } else { # length(na_str) > 1 | |
| 332 | 4x |     tmp_na_str <- rep("NA", length(x)) | 
| 333 | 4x | tmp_na_str[is.na(x)] <- na_str[seq(sum(is.na(x)))] | 
| 334 | 4x | na_str <- tmp_na_str | 
| 335 | } | |
| 336 |   # if (length(na_str) < sum(is.na(x))) { # not a fun of vec recycling | |
| 337 | # na_str <- rep(na_str, length.out = sum(is.na(x))) | |
| 338 | # } | |
| 339 | ||
| 340 | 9741x |   txt <- if (all(is.na(x)) && length(na_str) == 1L) { | 
| 341 | 30x | na_str | 
| 342 | 9741x |   } else if (is.null(format)) { | 
| 343 | 302x | toString(x) | 
| 344 | 9741x |   } else if (is.function(format)) { | 
| 345 | 5x | call_format_fun(f = format, value = x, na_str = na_str, round_type = round_type, output = output) | 
| 346 | 9741x |   } else if (is.character(format)) { | 
| 347 | 9404x |     l <- if (format %in% formats_1d) { | 
| 348 | 8919x | 1 | 
| 349 | 9404x |     } else if (format %in% formats_2d) { | 
| 350 | 403x | 2 | 
| 351 | 9404x |     } else if (format %in% formats_3d) { | 
| 352 | 81x | 3 | 
| 353 |     } else { | |
| 354 | 1x | stop( | 
| 355 | 1x | "Unknown format label: '", format, | 
| 356 | 1x | "'. Run `list_valid_format_labels()` to get a list of all available formats." | 
| 357 | ) | |
| 358 | } | |
| 359 | 9403x |     if (format != "xx" && length(x) != l) { | 
| 360 | 2x | stop( | 
| 361 | 2x | "Cell contents <", paste(x, collapse = ", "), "> and format '", | 
| 362 | 2x |         format, "' are of different lengths (", length(x), " vs ", l, ")." | 
| 363 | ) | |
| 364 | } | |
| 365 | 9401x | switch(format, | 
| 366 | 8708x | "xx" = as.character(x), | 
| 367 | 12x | "xx." = round_fmt(x, digits = 0, na_str = na_str, round_type = round_type), | 
| 368 | 15x | "xx.x" = round_fmt(x, digits = 1, na_str = na_str, round_type = round_type), | 
| 369 | 14x | "xx.xx" = round_fmt(x, digits = 2, na_str = na_str, round_type = round_type), | 
| 370 | 12x | "xx.xxx" = round_fmt(x, digits = 3, na_str = na_str, round_type = round_type), | 
| 371 | 12x | "xx.xxxx" = round_fmt(x, digits = 4, na_str = na_str, round_type = round_type), | 
| 372 | 11x | "xx%" = paste0(round_fmt(x * 100, digits = NA, na_str = na_str, round_type = round_type), "%"), | 
| 373 | 22x | "xx.%" = paste0(round_fmt(x * 100, digits = 0, na_str = na_str, round_type = round_type), "%"), | 
| 374 | 22x | "xx.x%" = paste0(round_fmt(x * 100, digits = 1, na_str = na_str, round_type = round_type), "%"), | 
| 375 | 22x | "xx.xx%" = paste0(round_fmt(x * 100, digits = 2, na_str = na_str, round_type = round_type), "%"), | 
| 376 | 11x | "xx.xxx%" = paste0(round_fmt(x * 100, digits = 3, na_str = na_str, round_type = round_type), "%"), | 
| 377 | 11x |       "(N=xx)" = paste0("(N=", round_fmt(x, digits = NA, na_str = na_str, round_type = round_type), ")"), | 
| 378 | 11x |       "N=xx" = paste0("N=", round_fmt(x, digits = NA, na_str = na_str, round_type = round_type)), | 
| 379 | 12x | ">999.9" = ifelse(x > 999.9, ">999.9", round_fmt(x, digits = 1, na_str = na_str, round_type = round_type)), | 
| 380 | 12x | ">999.99" = ifelse(x > 999.99, ">999.99", round_fmt(x, digits = 2, na_str = na_str, round_type = round_type)), | 
| 381 | 12x | "x.xxxx | (<0.0001)" = ifelse(x < 0.0001, "<0.0001", | 
| 382 | 12x | round_fmt(x, | 
| 383 | 12x | digits = 4, na_str = na_str, | 
| 384 | 12x | round_type = round_type | 
| 385 | ) | |
| 386 | ), | |
| 387 | 11x | "xx / xx" = sep_2d_helper(x, dig1 = NA, dig2 = NA, sep = " / ", na_str = na_str, round_type = round_type), | 
| 388 | 11x | "xx. / xx." = sep_2d_helper(x, dig1 = 0, dig2 = 0, sep = " / ", na_str = na_str, round_type = round_type), | 
| 389 | 11x | "xx.x / xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " / ", na_str = na_str, round_type = round_type), | 
| 390 | 11x | "xx.xx / xx.xx" = sep_2d_helper(x, dig1 = 2, dig2 = 2, sep = " / ", na_str = na_str, round_type = round_type), | 
| 391 | 11x | "xx.xxx / xx.xxx" = sep_2d_helper(x, dig1 = 3, dig2 = 3, sep = " / ", na_str = na_str, round_type = round_type), | 
| 392 | 11x |       "N=xx (xx%)" = paste0("N=", val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str, round_type = round_type)), | 
| 393 | 12x | "xx (xx%)" = val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str, round_type = round_type), | 
| 394 | 11x | "xx (xx.%)" = val_pct_helper(x, dig1 = NA, dig2 = 0, na_str = na_str, round_type = round_type), | 
| 395 | 11x | "xx (xx.x%)" = val_pct_helper(x, dig1 = NA, dig2 = 1, na_str = na_str, round_type = round_type), | 
| 396 | 11x | "xx (xx.xx%)" = val_pct_helper(x, dig1 = NA, dig2 = 2, na_str = na_str, round_type = round_type), | 
| 397 | 11x | "xx. (xx.%)" = val_pct_helper(x, dig1 = 0, dig2 = 0, na_str = na_str, round_type = round_type), | 
| 398 | 12x | "xx.x (xx.x%)" = val_pct_helper(x, dig1 = 1, dig2 = 1, na_str = na_str, round_type = round_type), | 
| 399 | 11x | "xx.xx (xx.xx%)" = val_pct_helper(x, dig1 = 2, dig2 = 2, na_str = na_str, round_type = round_type), | 
| 400 | 11x | "(xx, xx)" = sep_2d_helper(x, | 
| 401 | 11x | dig1 = NA, dig2 = NA, sep = ", ", | 
| 402 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 403 | 11x | round_type = round_type | 
| 404 | ), | |
| 405 | 11x | "(xx., xx.)" = sep_2d_helper(x, | 
| 406 | 11x | dig1 = 0, dig2 = 0, sep = ", ", | 
| 407 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 408 | 11x | round_type = round_type | 
| 409 | ), | |
| 410 | 11x | "(xx.x, xx.x)" = sep_2d_helper(x, | 
| 411 | 11x | dig1 = 1, dig2 = 1, sep = ", ", | 
| 412 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 413 | 11x | round_type = round_type | 
| 414 | ), | |
| 415 | 11x | "(xx.xx, xx.xx)" = sep_2d_helper(x, | 
| 416 | 11x | dig1 = 2, dig2 = 2, sep = ", ", | 
| 417 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 418 | 11x | round_type = round_type | 
| 419 | ), | |
| 420 | 11x | "(xx.xxx, xx.xxx)" = sep_2d_helper(x, | 
| 421 | 11x | dig1 = 3, dig2 = 3, sep = ", ", | 
| 422 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 423 | 11x | round_type = round_type | 
| 424 | ), | |
| 425 | 11x | "(xx.xxxx, xx.xxxx)" = sep_2d_helper(x, | 
| 426 | 11x | dig1 = 4, dig2 = 4, sep = ", ", | 
| 427 | 11x |         na_str = na_str, wrap = c("(", ")"), | 
| 428 | 11x | round_type = round_type | 
| 429 | ), | |
| 430 | 11x | "xx - xx" = sep_2d_helper(x, dig1 = NA, dig2 = NA, sep = " - ", na_str = na_str, round_type = round_type), | 
| 431 | 14x | "xx.x - xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " - ", na_str = na_str, round_type = round_type), | 
| 432 | 11x | "xx.xx - xx.xx" = sep_2d_helper(x, dig1 = 2, dig2 = 2, sep = " - ", na_str = na_str, round_type = round_type), | 
| 433 | 11x | "xx (xx)" = val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 434 | 11x | "xx. (xx.)" = val_pct_helper(x, dig1 = 0, dig2 = 0, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 435 | 11x | "xx.x (xx.x)" = val_pct_helper(x, dig1 = 1, dig2 = 1, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 436 | 11x | "xx.xx (xx.xx)" = val_pct_helper(x, dig1 = 2, dig2 = 2, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 437 | 11x | "xx (xx.)" = val_pct_helper(x, dig1 = NA, dig2 = 0, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 438 | 11x | "xx (xx.x)" = val_pct_helper(x, dig1 = NA, dig2 = 1, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 439 | 11x | "xx (xx.xx)" = val_pct_helper(x, dig1 = NA, dig2 = 2, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 440 | 11x | "xx. (xx.x)" = val_pct_helper(x, dig1 = 0, dig2 = 1, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 441 | 11x | "xx.x (xx.xx)" = val_pct_helper(x, dig1 = 1, dig2 = 2, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 442 | 11x | "xx.xx (xx.xxx)" = val_pct_helper(x, dig1 = 2, dig2 = 3, na_str = na_str, pct = FALSE, round_type = round_type), | 
| 443 | 11x | "xx, xx" = sep_2d_helper(x, dig1 = NA, dig2 = NA, sep = ", ", na_str = na_str, round_type = round_type), | 
| 444 | 11x | "xx.x, xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = ", ", na_str = na_str, round_type = round_type), | 
| 445 | 11x | "xx.xx, xx.xx" = sep_2d_helper(x, dig1 = 2, dig2 = 2, sep = ", ", na_str = na_str, round_type = round_type), | 
| 446 | 11x | "xx.x to xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " to ", na_str = na_str, round_type = round_type), | 
| 447 | 11x | "xx.xx (xx.xx - xx.xx)" = paste0( | 
| 448 | 11x | round_fmt(x[1], digits = 2, na_str = na_str[1], round_type = round_type), " ", | 
| 449 | 11x | sep_2d_helper(x[2:3], | 
| 450 | 11x | dig1 = 2, dig2 = 2, | 
| 451 | 11x | sep = " - ", na_str = na_str[2:3], | 
| 452 | 11x |           wrap = c("(", ")"), | 
| 453 | 11x | round_type = round_type | 
| 454 | ) | |
| 455 | ), | |
| 456 | 11x | "xx. (xx. - xx.)" = paste0( | 
| 457 | 11x | round_fmt(x[1], digits = 0, na_str = na_str[1], round_type = round_type), " ", | 
| 458 | 11x | sep_2d_helper(x[2:3], | 
| 459 | 11x | dig1 = 0, dig2 = 0, | 
| 460 | 11x | sep = " - ", na_str = na_str[2:3], | 
| 461 | 11x |           wrap = c("(", ")"), | 
| 462 | 11x | round_type = round_type | 
| 463 | ) | |
| 464 | ), | |
| 465 | 15x | "xx.x (xx.x - xx.x)" = paste0( | 
| 466 | 15x | round_fmt(x[1], digits = 1, na_str = na_str[1], round_type = round_type), " ", | 
| 467 | 15x | sep_2d_helper(x[2:3], | 
| 468 | 15x | dig1 = 1, dig2 = 1, | 
| 469 | 15x | sep = " - ", na_str = na_str[2:3], | 
| 470 | 15x |           wrap = c("(", ")"), | 
| 471 | 15x | round_type = round_type | 
| 472 | ) | |
| 473 | ), | |
| 474 | 11x | "xx.xxx (xx.xxx - xx.xxx)" = paste0( | 
| 475 | 11x | round_fmt(x[1], digits = 3, na_str = na_str[1], round_type = round_type), " ", | 
| 476 | 11x | sep_2d_helper(x[2:3], | 
| 477 | 11x | dig1 = 3, dig2 = 3, | 
| 478 | 11x | sep = " - ", na_str = na_str[2:3], | 
| 479 | 11x |           wrap = c("(", ")"), | 
| 480 | 11x | round_type = round_type | 
| 481 | ) | |
| 482 | ), | |
| 483 | 11x | "xx / xx (xx.%)" = paste0( | 
| 484 | 11x | sep_2d_helper(x[1:2], dig1 = NA, dig2 = NA, sep = " / ", round_type = round_type, na_str = na_str[1:2]), " ", | 
| 485 | 11x | paste0( | 
| 486 |           "(", | |
| 487 | 11x | format_value(x[3], format = "xx.%", output = "ascii", na_str = na_str[3], round_type = round_type), | 
| 488 | ")" | |
| 489 | ) | |
| 490 | ), | |
| 491 | 11x | "xx / xx (xx.x%)" = paste0( | 
| 492 | 11x | sep_2d_helper(x[1:2], dig1 = NA, dig2 = NA, sep = " / ", round_type = round_type, na_str = na_str[1:2]), " ", | 
| 493 | 11x | paste0( | 
| 494 |           "(", | |
| 495 | 11x | format_value(x[3], format = "xx.x%", output = "ascii", na_str = na_str[3], round_type = round_type), | 
| 496 | ")" | |
| 497 | ) | |
| 498 | ), | |
| 499 | 11x | "xx / xx (xx.xx%)" = paste0( | 
| 500 | 11x | sep_2d_helper(x[1:2], dig1 = NA, dig2 = NA, sep = " / ", round_type = round_type, na_str = na_str[1:2]), " ", | 
| 501 | 11x | paste0( | 
| 502 |           "(", | |
| 503 | 11x | format_value(x[3], format = "xx.xx%", output = "ascii", na_str = na_str[3], round_type = round_type), | 
| 504 | ")" | |
| 505 | ) | |
| 506 | ), | |
| 507 | ## we already checked for label validity so this can only happen when something is | |
| 508 | ## added to the valid labels list but left unimplemented. | |
| 509 | 9741x |       stop(paste("valid format string", format, "not implemented")) # nocov | 
| 510 | ) | |
| 511 | } | |
| 512 | # Check that probably never happens as it is almost always already text | |
| 513 | 9738x | txt[is.na(txt)] <- na_str[1] | 
| 514 | ||
| 515 | ||
| 516 | 9738x |   if (output == "ascii") { | 
| 517 | 9737x | txt | 
| 518 | 1x |   } else if (output == "html") { | 
| 519 | ## convert to tagList | |
| 520 | ## convert \n to <br/> | |
| 521 | ||
| 522 | 1x |     if (identical(txt, "")) { | 
| 523 | ! | txt | 
| 524 |     } else { | |
| 525 | 1x | els <- unlist(strsplit(txt, "\n", fixed = TRUE)) | 
| 526 | 1x |       Map(function(el, is.last) { | 
| 527 | 1x | tagList(el, if (!is.last) tags$br() else NULL) | 
| 528 | 1x | }, els, c(rep(FALSE, length(els) - 1), TRUE)) | 
| 529 | } | |
| 530 |   } else { | |
| 531 | ! | txt | 
| 532 | } | |
| 533 | } | |
| 534 | ||
| 535 | setClassUnion("FormatSpec", c("NULL", "character", "function", "list")) | |
| 536 | setClassUnion("characterOrNULL", c("NULL", "character")) | |
| 537 | setClass("fmt_config", | |
| 538 | slots = c( | |
| 539 | format = "FormatSpec", | |
| 540 | format_na_str = "characterOrNULL", | |
| 541 | align = "characterOrNULL" | |
| 542 | ) | |
| 543 | ) | |
| 544 | ||
| 545 | #' Format configuration | |
| 546 | #' | |
| 547 | #' @param format (`string` or `function`)\cr a format label (string) or formatter function. | |
| 548 | #' @param na_str (`string`)\cr string that should be displayed in place of missing values. | |
| 549 | #' @param align (`string`)\cr alignment values should be rendered with. | |
| 550 | #' | |
| 551 | #' @return An object of class `fmt_config` which contains the following elements: | |
| 552 | #' * `format` | |
| 553 | #' * `na_str` | |
| 554 | #' * `align` | |
| 555 | #' | |
| 556 | #' @examples | |
| 557 | #' fmt_config(format = "xx.xx", na_str = "-", align = "left") | |
| 558 | #' fmt_config(format = "xx.xx - xx.xx", align = "right") | |
| 559 | #' | |
| 560 | #' @export | |
| 561 | fmt_config <- function(format = NULL, na_str = "NA", align = "center") { | |
| 562 | 2x |   new("fmt_config", format = format, format_na_str = na_str, align = align) | 
| 563 | } | 
| 1 | .need_pag <- function(page_type, pg_width, pg_height, cpp, lpp) { | |
| 2 | ! | !(is.null(page_type) && is.null(pg_width) && is.null(pg_height) && is.null(cpp) && is.null(lpp)) | 
| 3 | } | |
| 4 | ||
| 5 | #' Export a table-like object to plain (ASCII) text with page breaks | |
| 6 | #' | |
| 7 | #' This function converts `x` to a `MatrixPrintForm` object via [matrix_form()], paginates it | |
| 8 | #' via [paginate_to_mpfs()], converts each page to ASCII text via [toString()], and outputs | |
| 9 | #' the strings, separated by `page_break`, to `file`. | |
| 10 | #' | |
| 11 | #' @inheritParams paginate_indices | |
| 12 | #' @inheritParams toString | |
| 13 | #' @inheritParams propose_column_widths | |
| 14 | #' @param x (`ANY`)\cr a table-like object to export. Must have an applicable `matrix_form` method. | |
| 15 | #' @param file (`string` or `NULL`)\cr if non-`NULL`, the path to write a text file to | |
| 16 | #' containing `x` rendered as ASCII text. | |
| 17 | #' @param page_break (`string`)\cr page break symbol (defaults to `"\\n\\s"`). | |
| 18 | #' @param paginate (`flag`)\cr whether pagination should be performed. Defaults to `TRUE` | |
| 19 | #' if page size is specified (including the default). | |
| 20 | #' @param ... additional parameters passed to [paginate_to_mpfs()]. | |
| 21 | #' | |
| 22 | #' @details | |
| 23 | #' If `x` has a `num_rep_cols` method, the value returned by it will be used for `rep_cols` by | |
| 24 | #' default. Otherwise, 0 will be used. | |
| 25 | #' | |
| 26 | #' If `x` has an applicable `do_forced_paginate` method, it will be invoked during the | |
| 27 | #' pagination process. | |
| 28 | #' | |
| 29 | #' @return If `file` is `NULL`, the full paginated and concatenated string value is returned, | |
| 30 | #' otherwise the output is written to `file` and no value (invisible `NULL`) is returned. | |
| 31 | #' | |
| 32 | #' @examples | |
| 33 | #' export_as_txt(basic_matrix_form(mtcars), pg_height = 5, pg_width = 4) | |
| 34 | #' | |
| 35 | #' @export | |
| 36 | export_as_txt <- function(x, | |
| 37 | file = NULL, | |
| 38 | page_type = NULL, | |
| 39 | landscape = FALSE, | |
| 40 | pg_width = page_dim(page_type)[if (landscape) 2 else 1], | |
| 41 | pg_height = page_dim(page_type)[if (landscape) 1 else 2], | |
| 42 | font_family = "Courier", | |
| 43 | font_size = 8, # grid parameters | |
| 44 | lineheight = 1L, | |
| 45 | margins = c(top = .5, bottom = .5, left = .75, right = .75), | |
| 46 | paginate = TRUE, | |
| 47 | cpp = NA_integer_, | |
| 48 | lpp = NA_integer_, | |
| 49 | ..., | |
| 50 | hsep = NULL, | |
| 51 | indent_size = 2, | |
| 52 | tf_wrap = paginate, | |
| 53 | max_width = NULL, | |
| 54 | colwidths = NULL, | |
| 55 | min_siblings = 2, | |
| 56 | nosplitin = character(), | |
| 57 | rep_cols = NULL, | |
| 58 | verbose = FALSE, | |
| 59 | page_break = "\\s\\n", | |
| 60 | page_num = default_page_number(), | |
| 61 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 62 | col_gap = 3, | |
| 63 |                           round_type = c("iec", "sas")) { | |
| 64 | # Processing lists of tables or listings | |
| 65 | 15x |   if (.is_list_of_tables_or_listings(x)) { | 
| 66 | 5x |     if (isFALSE(paginate)) { | 
| 67 | 1x | warning( | 
| 68 | 1x | "paginate is FALSE, but x is a list of tables or listings, ", | 
| 69 | 1x | "so paginate will automatically be updated to TRUE" | 
| 70 | ) | |
| 71 | } | |
| 72 | 5x | paginate <- TRUE | 
| 73 | } | |
| 74 | ||
| 75 | 15x |   if (paginate) { | 
| 76 | 15x | pages <- paginate_to_mpfs( | 
| 77 | 15x | x, | 
| 78 | 15x | page_type = page_type, | 
| 79 | ## font_family = font_family, | |
| 80 | ## font_size = font_size, | |
| 81 | ## lineheight = lineheight, | |
| 82 | 15x | landscape = landscape, | 
| 83 | 15x | pg_width = pg_width, | 
| 84 | 15x | pg_height = pg_height, | 
| 85 | 15x | margins = margins, | 
| 86 | 15x | lpp = lpp, | 
| 87 | 15x | cpp = cpp, | 
| 88 | 15x | min_siblings = min_siblings, | 
| 89 | 15x | nosplitin = nosplitin, | 
| 90 | 15x | colwidths = colwidths, | 
| 91 | 15x | tf_wrap = tf_wrap, | 
| 92 | 15x | max_width = max_width, | 
| 93 | 15x | indent_size = indent_size, | 
| 94 | 15x | verbose = verbose, | 
| 95 | 15x | rep_cols = rep_cols, | 
| 96 | 15x | page_num = page_num, | 
| 97 | 15x | fontspec = fontspec, | 
| 98 | 15x | col_gap = col_gap, | 
| 99 | 15x | round_type = round_type | 
| 100 | ) | |
| 101 |   } else { | |
| 102 | ! | mf <- matrix_form( | 
| 103 | ! | x, | 
| 104 | ! | TRUE, | 
| 105 | ! | TRUE, | 
| 106 | ! | indent_size = indent_size, | 
| 107 | ! | fontspec = fontspec, | 
| 108 | ! | col_gap = col_gap, | 
| 109 | ! | round_type = round_type | 
| 110 | ) | |
| 111 | ! | mf_col_widths(mf) <- colwidths %||% propose_column_widths(mf, fontspec = fontspec, round_type = round_type) | 
| 112 | ! | pages <- list(mf) | 
| 113 | } | |
| 114 | ||
| 115 | # Needs to be here because of adding cpp if it is not "auto" | |
| 116 | 14x |   if (!is.character(max_width)) { | 
| 117 | 14x | max_width <- .handle_max_width( | 
| 118 | 14x | tf_wrap = tf_wrap, | 
| 119 | 14x | max_width = max_width, | 
| 120 | 14x | cpp = cpp | 
| 121 | ) | |
| 122 | } | |
| 123 | ||
| 124 | ## we don't set widths here because we already put that info in mpf | |
| 125 | ## so its on each of the pages. | |
| 126 | 14x | strings <- vapply( | 
| 127 | 14x | pages, toString, "", | 
| 128 | 14x | widths = NULL, | 
| 129 | 14x | hsep = hsep, tf_wrap = tf_wrap, max_width = max_width, col_gap = col_gap | 
| 130 | ) | |
| 131 | ||
| 132 | 14x | res <- paste(strings, collapse = page_break) | 
| 133 | ||
| 134 | 14x |   if (is.null(file)) { | 
| 135 | 12x | res | 
| 136 |   } else { | |
| 137 | 2x | cat(res, file = file) | 
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | .is_list_of_tables_or_listings <- function(a_list) { | |
| 142 | 80x |   if (is(a_list, "list")) { | 
| 143 | 80x | all_matrix_forms <- FALSE | 
| 144 | 80x | obj_are_tables_or_listings <- FALSE | 
| 145 | ||
| 146 | 80x |     if (is(a_list[[1]], "MatrixPrintForm")) { | 
| 147 | 15x | all_matrix_forms <- all(sapply(a_list, is, class2 = "MatrixPrintForm")) | 
| 148 |     } else { | |
| 149 | 65x | obj_are_tables_or_listings <- all( | 
| 150 | 65x |         sapply(a_list, function(list_i) { | 
| 151 | 1625x | is(list_i, "listing_df") || is(list_i, "VTableTree") | 
| 152 | }) | |
| 153 | ) | |
| 154 | } | |
| 155 | 80x | out <- obj_are_tables_or_listings || all_matrix_forms | 
| 156 |   } else { | |
| 157 | ! | out <- FALSE | 
| 158 | } | |
| 159 | ||
| 160 | 80x | out | 
| 161 | } | |
| 162 | ||
| 163 | # RTF support ------------------------------------------------------------------ | |
| 164 | ||
| 165 | ## In use, must be tested | |
| 166 | prep_header_line <- function(mf, i) { | |
| 167 | 4x | ret <- mf$strings[i, mf$display[i, , drop = TRUE], drop = TRUE] | 
| 168 | 4x | ret | 
| 169 | } | |
| 170 | ||
| 171 | ## margin_lines_to_in <- function(margins, font_size, font_family) { | |
| 172 | ## tmpfile <- tempfile(fileext = ".pdf") | |
| 173 | ## gp_plot <- gpar(fontsize = font_size, fontfamily = font_family) | |
| 174 | ## pdf(file = tmpfile, width = 20, height = 20) | |
| 175 | ##   on.exit({ | |
| 176 | ## dev.off() | |
| 177 | ## file.remove(tmpfile) | |
| 178 | ## }) | |
| 179 | ## grid.newpage() | |
| 180 | ## pushViewport(plotViewport(margins = margins, gp = gp_plot)) | |
| 181 | ## c( | |
| 182 | ## bottom = convertHeight(unit(margins["bottom"], "lines"), "inches", valueOnly = TRUE), | |
| 183 | ##     left = convertWidth(unit(1, "strwidth", strrep("m", margins["left"])), "inches", valueOnly = TRUE), | |
| 184 | ## top = convertHeight(unit(margins["top"], "lines"), "inches", valueOnly = TRUE), | |
| 185 | ##     right = convertWidth(unit(1, "strwidth", strrep("m", margins["right"])), "inches", valueOnly = TRUE) | |
| 186 | ## ) | |
| 187 | ## } | |
| 188 | ||
| 189 | mpf_to_dfbody <- function(mpf, colwidths, fontspec, round_type = c("iec", "sas")) { | |
| 190 | 4x | mf <- matrix_form(mpf, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type) | 
| 191 | 4x | nlr <- mf_nlheader(mf) | 
| 192 | 4x |   if (is.null(colwidths)) { | 
| 193 | ! | colwidths <- propose_column_widths(mf, fontspec = fontspec, round_type = round_type) | 
| 194 | } | |
| 195 | 4x | mf$strings[1:nlr, 1] <- ifelse(nzchar(mf$strings[1:nlr, 1, drop = TRUE]), | 
| 196 | 4x | mf$strings[1:nlr, 1, drop = TRUE], | 
| 197 | 4x |     strrep(" ", colwidths) | 
| 198 | ) | |
| 199 | ||
| 200 | ||
| 201 | 4x | myfakedf <- as.data.frame(tail(mf$strings, -nlr)) | 
| 202 | 4x | myfakedf | 
| 203 | } | |
| 204 | ||
| 205 | #' Transform `MatrixPrintForm` to RTF | |
| 206 | #' | |
| 207 | #' Experimental export to rich text format (RTF) via the `r2rtf` package. | |
| 208 | #' | |
| 209 | #' @inheritParams page_lcpp | |
| 210 | #' @inheritParams toString | |
| 211 | #' @inheritParams grid::plotViewport | |
| 212 | #' @param mpf (`MatrixPrintForm`)\cr a `MatrixPrintForm` object. | |
| 213 | #' @param colwidths (`numeric`)\cr column widths. | |
| 214 | #' | |
| 215 | #' @details | |
| 216 | #' This function provides a low-level coercion of a `MatrixPrintForm` object into | |
| 217 | #' text containing the corresponding table in RTF. Currently, no pagination is done | |
| 218 | #' at this level, and should be done prior to calling this function, though that | |
| 219 | #' may change in the future. | |
| 220 | #' | |
| 221 | #' @return An RTF object. | |
| 222 | #' | |
| 223 | #' @export | |
| 224 | mpf_to_rtf <- function(mpf, | |
| 225 | colwidths = NULL, | |
| 226 | page_type = "letter", | |
| 227 | pg_width = page_dim(page_type)[if (landscape) 2 else 1], | |
| 228 | pg_height = page_dim(page_type)[if (landscape) 1 else 2], | |
| 229 | landscape = FALSE, | |
| 230 | margins = c(4, 4, 4, 4), | |
| 231 | font_family = "Courier", | |
| 232 | font_size = 8, | |
| 233 | lineheight = 1, | |
| 234 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 235 | round_type = round_type, | |
| 236 |                        ...) { | |
| 237 | 4x |   if (!requireNamespace("r2rtf")) { | 
| 238 | ! |     stop("RTF export requires the 'r2rtf' package, please install it.") | 
| 239 | } | |
| 240 | 4x |   if (fontspec$family != "Courier") { | 
| 241 | ! |     stop("Experimental RTF export does not currently support fonts other than Courier") | 
| 242 | } | |
| 243 | 4x | mpf <- matrix_form(mpf, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type) | 
| 244 | 4x | nlr <- mf_nlheader(mpf) | 
| 245 | 4x |   if (is.null(colwidths)) { | 
| 246 | ! | colwidths <- propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) | 
| 247 | } | |
| 248 | 4x | mpf$strings[1:nlr, 1] <- ifelse(nzchar(mpf$strings[1:nlr, 1, drop = TRUE]), | 
| 249 | 4x | mpf$strings[1:nlr, 1, drop = TRUE], | 
| 250 | 4x |     strrep(" ", colwidths) | 
| 251 | ) | |
| 252 | ||
| 253 | 4x | myfakedf <- mpf_to_dfbody(mpf, colwidths, fontspec = fontspec, round_type = round_type) | 
| 254 | ||
| 255 | 4x | rtfpg <- r2rtf::rtf_page(myfakedf, | 
| 256 | 4x | width = pg_width, | 
| 257 | 4x | height = pg_height, | 
| 258 | 4x | orientation = if (landscape) "landscape" else "portrait", | 
| 259 | 4x | margin = c(0.1, 0.1, 0.1, 0.1, 0.1, 0.1), | 
| 260 | 4x | nrow = 10000L | 
| 261 | 4x | ) ## dont allow r2rtf to restrict lines per page beyond actual real eastate | 
| 262 | 4x | rtfpg <- r2rtf::rtf_title(rtfpg, main_title(mpf), subtitles(mpf), text_font = 1) | 
| 263 | 4x |   for (i in seq_len(nlr)) { | 
| 264 | 4x | hdrlndat <- prep_header_line(mpf, i) | 
| 265 | 4x | rtfpg <- r2rtf::rtf_colheader(rtfpg, | 
| 266 | 4x | paste(hdrlndat, collapse = " | "), | 
| 267 | 4x | col_rel_width = unlist(tapply(colwidths, | 
| 268 | 4x | cumsum(mpf$display[i, , drop = TRUE]), | 
| 269 | 4x | sum, | 
| 270 | 4x | simplify = FALSE | 
| 271 | )), | |
| 272 | 4x |       border_top = c("", rep(if (i > 1) "single" else "", length(hdrlndat) - 1)), | 
| 273 | 4x | text_font = 9, ## this means Courier New for some insane reason | 
| 274 | 4x | text_font_size = font_size | 
| 275 | ) | |
| 276 | } | |
| 277 | ||
| 278 | 4x | rtfpg <- r2rtf::rtf_body(rtfpg, | 
| 279 | 4x | col_rel_width = colwidths, | 
| 280 | 4x |     text_justification = c("l", rep("c", ncol(myfakedf) - 1)), | 
| 281 | 4x | text_format = "", | 
| 282 | 4x | text_font = 9, | 
| 283 | 4x | text_font_size = font_size | 
| 284 | ) | |
| 285 | ||
| 286 | 4x |   for (i in seq_along(mpf$ref_footnotes)) { | 
| 287 | 4x | rtfpg <- r2rtf::rtf_footnote(rtfpg, | 
| 288 | 4x | mpf$ref_footnotes[i], | 
| 289 | 4x | border_top = if (i == 1) "single" else "", | 
| 290 | 4x | border_bottom = if (i == length(mpf$ref_footnotes)) "single" else "", | 
| 291 | 4x | text_font = 9 | 
| 292 | ) | |
| 293 | } | |
| 294 | ||
| 295 | 4x |   if (length(main_footer(mpf)) > 0) { | 
| 296 | 4x | rtfpg <- r2rtf::rtf_footnote(rtfpg, main_footer(mpf), text_font = 9) | 
| 297 | } | |
| 298 | 4x |   if (length(prov_footer(mpf)) > 0) { | 
| 299 | 4x | rtfpg <- r2rtf::rtf_source(rtfpg, prov_footer(mpf), text_font = 9) | 
| 300 | } | |
| 301 | ||
| 302 | 4x | rtfpg | 
| 303 | } | |
| 304 | ||
| 305 | ## Not currently in use, previous alternate ways to get to RTF | |
| 306 | ||
| 307 | ## ## XXX Experimental. Not to be exported without approval | |
| 308 | ## mpf_to_huxtable <- function(obj) { | |
| 309 | ##   if (!requireNamespace("huxtable")) { | |
| 310 | ##     stop("mpf_to_huxtable requires the huxtable package") | |
| 311 | ## } | |
| 312 | ## mf <- matrix_form(obj, indent_rownames = TRUE) | |
| 313 | ## nlr <- mf_nlheader(mf) | |
| 314 | ## myfakedf <- as.data.frame(tail(mf$strings, -nlr)) | |
| 315 | ## ret <- huxtable::as_hux(myfakedf, add_colnames = FALSE) | |
| 316 | ## mf$strings[!mf$display] <- "" | |
| 317 | ##   for (i in seq_len(nlr)) { | |
| 318 | ## arglist <- c( | |
| 319 | ## list(ht = ret, after = i - 1), | |
| 320 | ## as.list(mf$strings[i, ]) | |
| 321 | ## ) | |
| 322 | ## ret <- do.call(huxtable::insert_row, arglist) | |
| 323 | ||
| 324 | ## spanspl <- split( | |
| 325 | ## seq_len(ncol(mf$strings)), | |
| 326 | ## cumsum(mf$display[i, ]) | |
| 327 | ## ) | |
| 328 | ||
| 329 | ##     for (j in seq_along(spanspl)) { | |
| 330 | ##       if (length(spanspl[[j]]) > 1) { | |
| 331 | ## ret <- huxtable::merge_cells(ret, row = i, col = spanspl[[j]]) | |
| 332 | ## } | |
| 333 | ## } | |
| 334 | ## } | |
| 335 | ## ret <- huxtable::set_header_rows(ret, seq_len(nlr), TRUE) | |
| 336 | ## huxtable::font(ret) <- "courier" | |
| 337 | ## huxtable::font_size(ret) <- 6 | |
| 338 | ## huxtable::align(ret)[ | |
| 339 | ## seq_len(nrow(ret)), | |
| 340 | ## seq_len(ncol(ret)) | |
| 341 | ## ] <- mf$aligns | |
| 342 | ## ret | |
| 343 | ## } | |
| 344 | ||
| 345 | ## ## XXX Experimental. Not to be exported without approval | |
| 346 | ## mpf_to_rtf <- function(obj, ..., file) { | |
| 347 | ## huxt <- mpf_to_huxtable(obj) | |
| 348 | ## ## a bunch more stuff here | |
| 349 | ## huxtable::quick_rtf(huxt, ..., file = file) | |
| 350 | ## } | |
| 351 | ||
| 352 | ## ## XXX Experimental. Not to be exported without approval | |
| 353 | ## mpf_to_gt <- function(obj) { | |
| 354 | ##   requireNamespace("gt") | |
| 355 | ## mf <- matrix_form(obj, indent_rownames = TRUE) | |
| 356 | ## nlh <- mf_nlheader(mf) | |
| 357 | ## body_df <- as.data.frame(mf$strings[-1 * seq_len(nlh), ]) | |
| 358 | ## varnamerow <- mf_nrheader(mf) | |
| 359 | ## ## detect if we have counts | |
| 360 | ##   if (any(nzchar(mf$formats[seq_len(nlh), ]))) { | |
| 361 | ## varnamerow <- varnamerow - 1 | |
| 362 | ## } | |
| 363 | ||
| 364 | ## rlbl_lst <- as.list(mf$strings[nlh, , drop = TRUE]) | |
| 365 | ## names(rlbl_lst) <- names(body_df) | |
| 366 | ||
| 367 | ## ret <- gt::gt(body_df, rowname_col = "V1") | |
| 368 | ## ret <- gt::cols_label(ret, .list = rlbl_lst) | |
| 369 | ##   if (nlh > 1) { | |
| 370 | ##     for (i in 1:(nlh - 1)) { | |
| 371 | ## linedat <- mf$strings[i, , drop = TRUE] | |
| 372 | ## splvec <- cumsum(mf$display[i, , drop = TRUE]) | |
| 373 | ## spl <- split(seq_along(linedat), splvec) | |
| 374 | ##       for (j in seq_along(spl)) { | |
| 375 | ## vns <- names(body_df)[spl[[j]]] | |
| 376 | ## labval <- linedat[spl[[j]][1]] | |
| 377 | ## ret <- gt::tab_spanner(ret, | |
| 378 | ## label = labval, | |
| 379 | ##           columns = {{ vns }}, | |
| 380 | ## level = nlh - i, | |
| 381 | ## id = paste0(labval, j) | |
| 382 | ## ) | |
| 383 | ## } | |
| 384 | ## } | |
| 385 | ## } | |
| 386 | ||
| 387 | ##   ret <- gt::opt_css(ret, css = "th.gt_left { white-space:pre;}") | |
| 388 | ||
| 389 | ## ret | |
| 390 | ## } | |
| 391 | ||
| 392 | #' Export as RTF | |
| 393 | #' | |
| 394 | #' Experimental export to the rich text format (RTF) format. | |
| 395 | #' | |
| 396 | #' @details RTF export occurs via the following steps: | |
| 397 | #' * The table is paginated to the specified page size (vertically and horizontally). | |
| 398 | #' * Each separate page is converted to a `MatrixPrintForm` object and then to | |
| 399 | #' RTF-encoded text. | |
| 400 | #' * Separate RTF text chunks are combined and written to a single RTF file. | |
| 401 | #' | |
| 402 | #' Conversion of `MatrixPrintForm` objects to RTF is done via [mpf_to_rtf()]. | |
| 403 | #' | |
| 404 | #' @inheritParams export_as_txt | |
| 405 | #' @inheritParams toString | |
| 406 | #' @inheritParams grid::plotViewport | |
| 407 | #' @inheritParams paginate_to_mpfs | |
| 408 | #' | |
| 409 | #' @export | |
| 410 | export_as_rtf <- function(x, | |
| 411 | file = NULL, | |
| 412 | colwidths = NULL, | |
| 413 | page_type = "letter", | |
| 414 | pg_width = page_dim(page_type)[if (landscape) 2 else 1], | |
| 415 | pg_height = page_dim(page_type)[if (landscape) 1 else 2], | |
| 416 | landscape = FALSE, | |
| 417 | margins = c(bottom = .5, left = .75, top = .5, right = .75), | |
| 418 | font_family = "Courier", | |
| 419 | font_size = 8, | |
| 420 | lineheight = 1, | |
| 421 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 422 | paginate = TRUE, | |
| 423 |                           ...) { | |
| 424 | # Processing lists of tables or listings | |
| 425 | 2x |   if (.is_list_of_tables_or_listings(x)) { | 
| 426 | ! |     if (isFALSE(paginate)) { | 
| 427 | ! | warning( | 
| 428 | ! | "paginate is FALSE, but x is a list of tables or listings, ", | 
| 429 | ! | "so paginate will automatically be updated to TRUE" | 
| 430 | ) | |
| 431 | } | |
| 432 | ! | paginate <- TRUE | 
| 433 | } | |
| 434 | ||
| 435 | 2x |   if (!requireNamespace("r2rtf")) { | 
| 436 | ! |     stop("RTF export requires the r2rtf package, please install it.") | 
| 437 | } | |
| 438 | 2x |   if (fontspec$family != "Courier") { | 
| 439 | ! |     stop("Experimental RTF export does not currently support fonts other than Courier") | 
| 440 | } | |
| 441 | ||
| 442 | 2x |   if (is.null(names(margins))) { | 
| 443 | ! | names(margins) <- marg_order | 
| 444 | } | |
| 445 | ||
| 446 | # NEEDS TO BE INTO paginate_to_mpfs so to have this check once for all paginations | |
| 447 | # fullmf <- matrix_form(x, indent_rownames = TRUE, fontspec = fontspec) | |
| 448 | # req_ncols <- ncol(fullmf) + as.numeric(mf_has_rlabels(fullmf)) | |
| 449 |   # if (!is.null(colwidths) && length(colwidths) != req_ncols) { | |
| 450 | # stop( | |
| 451 | # "non-null colwidths argument must have length ncol(x) (+ 1 if row labels are present) [", | |
| 452 | # req_ncols, "], got length ", length(colwidths) | |
| 453 | # ) | |
| 454 | # } | |
| 455 | ||
| 456 | 2x |   true_width <- pg_width - sum(margins[c("left", "right")]) | 
| 457 | 2x |   true_height <- pg_height - sum(margins[c("top", "bottom")]) | 
| 458 | ||
| 459 | 2x | mpfs <- paginate_to_mpfs( | 
| 460 | 2x | x, | 
| 461 | 2x | fontspec = fontspec, | 
| 462 | 2x | pg_width = true_width, | 
| 463 | 2x | pg_height = true_height, | 
| 464 | 2x | margins = c(bottom = 0, left = 0, top = 0, right = 0), | 
| 465 | 2x | lineheight = 1.25, | 
| 466 | 2x | colwidths = colwidths, | 
| 467 | ... | |
| 468 | ) | |
| 469 | ||
| 470 | 2x |   rtftxts <- lapply(mpfs, function(mf) { | 
| 471 | 4x | r2rtf::rtf_encode(mpf_to_rtf(mf, | 
| 472 | 4x | colwidths = mf_col_widths(mf), | 
| 473 | 4x | page_type = page_type, | 
| 474 | 4x | pg_width = pg_width, | 
| 475 | 4x | pg_height = pg_height, | 
| 476 | 4x | font_size = fontspec$size, | 
| 477 | 4x | margins = c(top = 0, left = 0, bottom = 0, right = 0) | 
| 478 | )) | |
| 479 | }) | |
| 480 | 2x | restxt <- paste( | 
| 481 | 2x | rtftxts[[1]]$start, | 
| 482 | 2x | paste( | 
| 483 | 2x | sapply(rtftxts, function(x) x$body), | 
| 484 | 2x |       collapse = "\n{\\pard\\fs2\\par}\\page{\\pard\\fs2\\par}\n" | 
| 485 | ), | |
| 486 | 2x | rtftxts[[1]]$end | 
| 487 | ) | |
| 488 | 2x |   if (!is.null(file)) { | 
| 489 | 2x | cat(restxt, file = file) | 
| 490 |   } else { | |
| 491 | ! | restxt | 
| 492 | } | |
| 493 | } | |
| 494 | ||
| 495 | ||
| 496 | # PDF support ------------------------------------------------------------------ | |
| 497 | ||
| 498 | #' Export as PDF | |
| 499 | #' | |
| 500 | #' The PDF output from this function is based on the ASCII output created with [toString()]. | |
| 501 | #' | |
| 502 | #' @inheritParams export_as_txt | |
| 503 | #' @inheritParams toString | |
| 504 | #' @param file (`string`)\cr file to write to, must have `.pdf` extension. | |
| 505 | #' @param width `r lifecycle::badge("deprecated")` Please use the `pg_width` argument or specify | |
| 506 | #' `page_type` instead. | |
| 507 | #' @param height `r lifecycle::badge("deprecated")` Please use the `pg_height` argument or | |
| 508 | #' specify `page_type` instead. | |
| 509 | #' @param fontsize `r lifecycle::badge("deprecated")` Please use the `font_size` argument instead. | |
| 510 | #' @param margins (`numeric(4)`)\cr the number of lines/characters of the margin on the bottom, | |
| 511 | #' left, top, and right sides of the page, respectively. | |
| 512 | #' | |
| 513 | #' @importFrom grDevices pdf | |
| 514 | #' @importFrom grid textGrob grid.newpage gpar pushViewport plotViewport unit grid.draw | |
| 515 | #' convertWidth convertHeight grobHeight grobWidth | |
| 516 | #' @importFrom grid textGrob get.gpar | |
| 517 | #' @importFrom grDevices dev.off | |
| 518 | #' @importFrom tools file_ext | |
| 519 | #' | |
| 520 | #' @details | |
| 521 | #' By default, pagination is performed with default `cpp` and `lpp` defined by specified page | |
| 522 | #' dimensions and margins. User-specified `lpp` and `cpp` values override this, and should | |
| 523 | #' be used with caution. | |
| 524 | #' | |
| 525 | #' Title and footer materials are also word-wrapped by default (unlike when printed to the | |
| 526 | #' terminal), with `cpp` (as defined above) as the default `max_width`. | |
| 527 | #' | |
| 528 | #' @seealso [export_as_txt()] | |
| 529 | #' | |
| 530 | #' @examples | |
| 531 | #' \dontrun{ | |
| 532 | #' tf <- tempfile(fileext = ".pdf") | |
| 533 | #' export_as_pdf(basic_matrix_form(mtcars), file = tf, pg_height = 4) | |
| 534 | #' | |
| 535 | #' tf <- tempfile(fileext = ".pdf") | |
| 536 | #' export_as_pdf(basic_matrix_form(mtcars), file = tf, lpp = 8) | |
| 537 | #' } | |
| 538 | #' | |
| 539 | #' @export | |
| 540 | export_as_pdf <- function(x, | |
| 541 | file, | |
| 542 | page_type = "letter", | |
| 543 | landscape = FALSE, | |
| 544 | pg_width = page_dim(page_type)[if (landscape) 2 else 1], | |
| 545 | pg_height = page_dim(page_type)[if (landscape) 1 else 2], | |
| 546 | width = lifecycle::deprecated(), | |
| 547 | height = lifecycle::deprecated(), | |
| 548 | margins = c(4, 4, 4, 4), | |
| 549 | min_siblings = 2, | |
| 550 | font_family = "Courier", | |
| 551 | font_size = 8, | |
| 552 | fontsize = font_size, | |
| 553 | lineheight = 1.2, ## XXX this matches legacy behavior but differs from default everywhere else | |
| 554 | paginate = TRUE, | |
| 555 | page_num = default_page_number(), | |
| 556 | lpp = NULL, | |
| 557 | cpp = NULL, | |
| 558 | hsep = "-", | |
| 559 | indent_size = 2, | |
| 560 | rep_cols = NULL, | |
| 561 | tf_wrap = TRUE, | |
| 562 | max_width = NULL, | |
| 563 | colwidths = NULL, | |
| 564 | fontspec = font_spec(font_family, font_size, lineheight), | |
| 565 | ttype_ok = FALSE, | |
| 566 |                           round_type = c("iec", "sas")) { | |
| 567 | ## this has to happen at the very beginning before the first use of fontspec | |
| 568 | ## which happens in the default value of colwidths. yay lazy evaluation... | |
| 569 | 6x |   if (missing(font_size) && !missing(fontsize)) { | 
| 570 | ! | font_size <- fontsize | 
| 571 | } | |
| 572 | ||
| 573 | 6x | stopifnot(tools::file_ext(file) != ".pdf") | 
| 574 | ||
| 575 | # Processing lists of tables or listings | |
| 576 | 6x |   if (.is_list_of_tables_or_listings(x)) { | 
| 577 | 2x |     if (isFALSE(paginate)) { | 
| 578 | 2x | warning( | 
| 579 | 2x | "paginate is FALSE, but x is a list of tables or listings, ", | 
| 580 | 2x | "so paginate will automatically be updated to TRUE" | 
| 581 | ) | |
| 582 | } | |
| 583 | 2x | paginate <- TRUE | 
| 584 | } | |
| 585 | ||
| 586 | 6x | gp_plot <- gpar_from_fspec(fontspec) | 
| 587 | ||
| 588 | 6x |   if (lifecycle::is_present(width)) { | 
| 589 | 1x |     lifecycle::deprecate_warn("0.5.5", "export_as_pdf(width)", "export_as_pdf(pg_width)") | 
| 590 | 1x | pg_width <- width | 
| 591 | } | |
| 592 | 6x |   if (lifecycle::is_present(height)) { | 
| 593 | 1x |     lifecycle::deprecate_warn("0.5.5", "export_as_pdf(height)", "export_as_pdf(pg_height)") | 
| 594 | 1x | pg_height <- height | 
| 595 | } | |
| 596 | ||
| 597 | 6x | gp_plot <- grid::gpar(fontsize = font_size, fontfamily = font_family) | 
| 598 | ||
| 599 | 6x | pdf(file = file, width = pg_width, height = pg_height) | 
| 600 | 6x | out_dev_num <- dev.cur() | 
| 601 | 6x | on.exit(dev.off(out_dev_num), add = TRUE) | 
| 602 | 6x | grid::grid.newpage() | 
| 603 | 6x | grid::pushViewport(grid::plotViewport(margins = margins, gp = gp_plot)) | 
| 604 | ||
| 605 | 6x | cur_gpar <- grid::get.gpar() | 
| 606 | 6x |   if (is.null(lpp)) { | 
| 607 | 6x | lpp <- floor(grid::convertHeight(grid::unit(1, "npc"), "lines", valueOnly = TRUE) / | 
| 608 | 6x | (cur_gpar$cex * cur_gpar$lineheight)) ## - sum(margins[c(1, 3)]) # bottom, top # nolint | 
| 609 | } | |
| 610 | 6x |   if (is.null(cpp)) { | 
| 611 | 4x | cpp <- floor(grid::convertWidth(grid::unit(1, "npc"), "inches", valueOnly = TRUE) * | 
| 612 | 4x | font_lcpi(fontspec$family, fontspec$size, cur_gpar$lineheight)$cpi) - sum(margins[c(2, 4)]) # left, right # nolint | 
| 613 | } | |
| 614 | 6x |   if (tf_wrap && is.null(max_width)) { | 
| 615 | 6x | max_width <- cpp | 
| 616 | } | |
| 617 | ||
| 618 | 6x | newdev <- open_font_dev(fontspec, silent = TRUE) ## cause we know there's another dev open... | 
| 619 | 6x |   if (newdev) { | 
| 620 | 6x | on.exit(close_font_dev(), add = TRUE) | 
| 621 | } | |
| 622 | ||
| 623 | 6x |   if (paginate) { | 
| 624 | 4x | tbls <- paginate_to_mpfs( | 
| 625 | 4x | x, | 
| 626 | 4x | page_type = page_type, | 
| 627 | 4x | fontspec = fontspec, | 
| 628 | 4x | landscape = landscape, | 
| 629 | 4x | pg_width = pg_width, | 
| 630 | 4x | pg_height = pg_height, | 
| 631 | 4x | margins = margins, | 
| 632 | 4x | lpp = lpp, | 
| 633 | 4x | cpp = cpp, | 
| 634 | 4x | min_siblings = min_siblings, | 
| 635 | 4x | nosplitin = character(), | 
| 636 | 4x | colwidths = colwidths, | 
| 637 | 4x | tf_wrap = tf_wrap, | 
| 638 | 4x | max_width = max_width, | 
| 639 | 4x | indent_size = indent_size, | 
| 640 | 4x | verbose = FALSE, | 
| 641 | 4x | rep_cols = rep_cols, | 
| 642 | 4x | page_num = page_num, | 
| 643 | 4x | round_type = round_type | 
| 644 | ) | |
| 645 |   } else { | |
| 646 | 2x | mf <- matrix_form(x, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) | 
| 647 | 2x | mf_col_widths(mf) <- colwidths %||% propose_column_widths(mf, fontspec = fontspec) | 
| 648 | 2x | tbls <- list(mf) | 
| 649 | } | |
| 650 | ||
| 651 | # Needs to be here because of adding cpp if it is not "auto" | |
| 652 | 6x |   if (!is.character(max_width)) { | 
| 653 | 6x | max_width <- .handle_max_width( | 
| 654 | 6x | tf_wrap = tf_wrap, | 
| 655 | 6x | max_width = max_width, | 
| 656 | 6x | cpp = cpp | 
| 657 | ) | |
| 658 | } | |
| 659 | ||
| 660 | 6x |   tbl_txts <- lapply(tbls, function(tbli) { | 
| 661 | 10x | toString( | 
| 662 | 10x | tbli, | 
| 663 | 10x | widths = tbli$col_widths + 1, | 
| 664 | 10x | hsep = hsep, | 
| 665 | 10x | tf_wrap = tf_wrap, | 
| 666 | 10x | max_width = max_width, | 
| 667 | 10x | fontspec = fontspec, | 
| 668 | 10x | ttype_ok = ttype_ok | 
| 669 | ) | |
| 670 | }) | |
| 671 | ||
| 672 | ## switch back to our output pdf device | |
| 673 | 6x | dev.set(out_dev_num) | 
| 674 | 6x |   gtbls <- lapply(tbl_txts, function(txt) { | 
| 675 | 10x | grid::textGrob( | 
| 676 | 10x | txt, | 
| 677 | 10x | x = grid::unit(0, "npc"), y = grid::unit(1, "npc"), | 
| 678 | 10x |       just = c("left", "top") | 
| 679 | ) | |
| 680 | }) | |
| 681 | ||
| 682 | 6x | npages <- length(gtbls) | 
| 683 | 6x | exceeds_width <- rep(FALSE, npages) | 
| 684 | 6x | exceeds_height <- rep(FALSE, npages) | 
| 685 | ||
| 686 | 6x |   for (i in seq_along(gtbls)) { | 
| 687 | 10x | g <- gtbls[[i]] | 
| 688 | ||
| 689 | 10x |     if (i > 1) { | 
| 690 | 4x | grid::grid.newpage() | 
| 691 | 4x | grid::pushViewport(grid::plotViewport(margins = margins, gp = gp_plot)) | 
| 692 | } | |
| 693 | ||
| 694 | 10x | if (grid::convertHeight(grid::grobHeight(g), "inches", valueOnly = TRUE) > | 
| 695 | 10x |       grid::convertHeight(grid::unit(1, "npc"), "inches", valueOnly = TRUE)) { # nolint | 
| 696 | 1x | exceeds_height[i] <- TRUE | 
| 697 | 1x |       warning("height of page ", i, " exceeds the available space") | 
| 698 | } | |
| 699 | 10x | if (grid::convertWidth(grid::grobWidth(g), "inches", valueOnly = TRUE) > | 
| 700 | 10x |       grid::convertWidth(grid::unit(1, "npc"), "inches", valueOnly = TRUE)) { # nolint | 
| 701 | 4x | exceeds_width[i] <- TRUE | 
| 702 | 4x |       warning("width of page ", i, " exceeds the available space") | 
| 703 | } | |
| 704 | ||
| 705 | 10x | grid::grid.draw(g) | 
| 706 | } | |
| 707 | 6x | list( | 
| 708 | 6x | file = file, npages = npages, exceeds_width = exceeds_width, exceeds_height = exceeds_height, | 
| 709 | 6x | lpp = lpp, cpp = cpp | 
| 710 | ) | |
| 711 | } | 
| 1 | #' Return an object with a label attribute | |
| 2 | #' | |
| 3 | #' @param x (`ANY`)\cr an object. | |
| 4 | #' @param label (`string`)\cr label attribute to attach to `x`. | |
| 5 | #' | |
| 6 | #' @return `x` labeled by `label`. Note that the exact mechanism of labeling should be considered | |
| 7 | #' an internal implementation detail, but the label can always be retrieved via `obj_label`. | |
| 8 | #' | |
| 9 | #' @examples | |
| 10 | #' x <- with_label(c(1, 2, 3), label = "Test") | |
| 11 | #' obj_label(x) | |
| 12 | #' | |
| 13 | #' @export | |
| 14 | with_label <- function(x, label) { | |
| 15 | 1x | obj_label(x) <- label | 
| 16 | 1x | x | 
| 17 | } | |
| 18 | ||
| 19 | #' Get label attributes of variables in a `data.frame` | |
| 20 | #' | |
| 21 | #' Variable labels can be stored as a `label` attribute for each variable. | |
| 22 | #' This functions returns a named character vector with the variable labels | |
| 23 | #' (or empty strings if not specified). | |
| 24 | #' | |
| 25 | #' @param x (`data.frame`)\cr a data frame object. | |
| 26 | #' @param fill (`flag`)\cr whether variable names should be returned for variables for | |
| 27 | #' which the `label` attribute does not exist. If `FALSE`, these variables are filled with | |
| 28 | #' `NA`s instead. | |
| 29 | #' | |
| 30 | #' @return a named character vector of variable labels from `x`, with names corresponding | |
| 31 | #' to variable names. | |
| 32 | #' | |
| 33 | #' @examples | |
| 34 | #' x <- iris | |
| 35 | #' var_labels(x) | |
| 36 | #' var_labels(x) <- paste("label for", names(iris)) | |
| 37 | #' var_labels(x) | |
| 38 | #' | |
| 39 | #' @export | |
| 40 | var_labels <- function(x, fill = FALSE) { | |
| 41 | 5x | stopifnot(is.data.frame(x)) | 
| 42 | 5x |   if (NCOL(x) == 0) { | 
| 43 | 1x | return(character()) | 
| 44 | } | |
| 45 | ||
| 46 | 4x |   y <- Map(function(col, colname) { | 
| 47 | 38x | label <- attr(col, "label") | 
| 48 | ||
| 49 | 38x |     if (is.null(label)) { | 
| 50 | 11x |       if (fill) { | 
| 51 | ! | colname | 
| 52 |       } else { | |
| 53 | 4x | NA_character_ | 
| 54 | } | |
| 55 |     } else { | |
| 56 | 27x |       if (!is.character(label) && !(length(label) == 1)) { | 
| 57 | ! |         stop("label for variable ", colname, "is not a character string") | 
| 58 | } | |
| 59 | 27x | as.vector(label) | 
| 60 | } | |
| 61 | 4x | }, x, colnames(x)) | 
| 62 | ||
| 63 | 4x | labels <- unlist(y, recursive = FALSE, use.names = TRUE) | 
| 64 | ||
| 65 | 4x |   if (!is.character(labels)) { | 
| 66 | ! |     stop("label extraction failed") | 
| 67 | } | |
| 68 | ||
| 69 | 4x | labels | 
| 70 | } | |
| 71 | ||
| 72 | #' Set label attributes of all variables in a `data.frame` | |
| 73 | #' | |
| 74 | #' Variable labels can be stored as the `label` attribute for each variable. | |
| 75 | #' This functions sets all non-missing (non-`NA`) variable labels in a `data.frame`. | |
| 76 | #' | |
| 77 | #' @inheritParams var_labels | |
| 78 | #' @param value (`character`)\cr a vector of new variable labels. If any values are `NA`, | |
| 79 | #' the label for that variable is removed. | |
| 80 | #' | |
| 81 | #' @return `x` with modified variable labels. | |
| 82 | #' | |
| 83 | #' @examples | |
| 84 | #' x <- iris | |
| 85 | #' var_labels(x) | |
| 86 | #' var_labels(x) <- paste("label for", names(iris)) | |
| 87 | #' var_labels(x) | |
| 88 | #' | |
| 89 | #' if (interactive()) { | |
| 90 | #' View(x) # in RStudio data viewer labels are displayed | |
| 91 | #' } | |
| 92 | #' | |
| 93 | #' @export | |
| 94 | `var_labels<-` <- function(x, value) { | |
| 95 | 3x | stopifnot( | 
| 96 | 3x | is.data.frame(x), | 
| 97 | 3x | is.character(value), | 
| 98 | 3x | ncol(x) == length(value) | 
| 99 | ) | |
| 100 | ||
| 101 | 3x | theseq <- if (!is.null(names(value))) names(value) else seq_along(x) | 
| 102 | # across columns of x | |
| 103 | 3x |   for (j in theseq) { | 
| 104 | 21x |     attr(x[[j]], "label") <- if (!is.na(value[j])) { | 
| 105 | 21x | unname(value[j]) | 
| 106 |     } else { | |
| 107 | ! | NULL | 
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | 3x | x | 
| 112 | } | |
| 113 | ||
| 114 | #' Copy and change variable labels of a `data.frame` | |
| 115 | #' | |
| 116 | #' Relabel a subset of the variables. | |
| 117 | #' | |
| 118 | #' @inheritParams var_labels<- | |
| 119 | #' @param ... name-value pairs, where each name corresponds to a variable name in | |
| 120 | #' `x` and the value to the new variable label. | |
| 121 | #' | |
| 122 | #' @return A copy of `x` with labels modified according to `...` | |
| 123 | #' | |
| 124 | #' @examples | |
| 125 | #' x <- var_relabel(iris, Sepal.Length = "Sepal Length of iris flower") | |
| 126 | #' var_labels(x) | |
| 127 | #' | |
| 128 | #' @export | |
| 129 | var_relabel <- function(x, ...) { | |
| 130 | # todo: make this function more readable / code easier | |
| 131 | 1x | stopifnot(is.data.frame(x)) | 
| 132 | 1x |   if (missing(...)) { | 
| 133 | ! | return(x) | 
| 134 | } | |
| 135 | 1x | dots <- list(...) | 
| 136 | 1x | varnames <- names(dots) | 
| 137 | 1x | stopifnot(!is.null(varnames)) | 
| 138 | ||
| 139 | 1x | map_varnames <- match(varnames, colnames(x)) | 
| 140 | ||
| 141 | 1x |   if (any(is.na(map_varnames))) { | 
| 142 | ! |     stop("variables: ", paste(varnames[is.na(map_varnames)], collapse = ", "), " not found") | 
| 143 | } | |
| 144 | ||
| 145 | 1x |   if (any(vapply(dots, Negate(is.character), logical(1)))) { | 
| 146 | ! |     stop("all variable labels must be of type character") | 
| 147 | } | |
| 148 | ||
| 149 | 1x |   for (i in seq_along(map_varnames)) { | 
| 150 | 1x | attr(x[[map_varnames[[i]]]], "label") <- dots[[i]] | 
| 151 | } | |
| 152 | ||
| 153 | 1x | x | 
| 154 | } | |
| 155 | ||
| 156 | #' Remove variable labels of a `data.frame` | |
| 157 | #' | |
| 158 | #' Remove `label` attribute from all variables in a data frame. | |
| 159 | #' | |
| 160 | #' @param x (`data.frame`)\cr a `data.frame` object. | |
| 161 | #' | |
| 162 | #' @return `x` with its variable labels stripped. | |
| 163 | #' | |
| 164 | #' @examples | |
| 165 | #' x <- var_labels_remove(iris) | |
| 166 | #' | |
| 167 | #' @export | |
| 168 | var_labels_remove <- function(x) { | |
| 169 | 1x | stopifnot(is.data.frame(x)) | 
| 170 | ||
| 171 | 1x |   for (i in seq_len(ncol(x))) { | 
| 172 | 11x | attr(x[[i]], "label") <- NULL | 
| 173 | } | |
| 174 | ||
| 175 | 1x | x | 
| 176 | } | 
| 1 | #' @import grid | |
| 2 | #' @import grDevices | |
| 3 | NULL | |
| 4 | ## https://www.ietf.org/rfc/rfc0678.txt | |
| 5 | ||
| 6 | times_font_name <- function() { | |
| 7 | ## I thought this was going to be OS specific | |
| 8 | ## but it seems like it's not... | |
| 9 | 4x | "Times" | 
| 10 | } | |
| 11 | ||
| 12 | #' Font size specification | |
| 13 | #' | |
| 14 | #' @param font_family (`character(1)`)\cr font family to use during | |
| 15 | #' string width and lines-per-page calculations. You can specify | |
| 16 | #' "Times New Roman" as "Times" or "serif", regardless of OS. | |
| 17 | #' Beyond that, see `family` entry in [graphics::par()] | |
| 18 | #' for details. | |
| 19 | #' @param font_size (`numeric(1)`)\cr font size to use during string width | |
| 20 | #' calculations and lines-per-page calculations. | |
| 21 | #' @param lineheight (`numeric(1)`)\cr line height to use during | |
| 22 | #' lines-per-page calculations. | |
| 23 | #' | |
| 24 | #' @details Passing the output of this constructor | |
| 25 | #' to the rendering or pagination machinery defines | |
| 26 | #' a font for use when calculating word wrapping and pagination. | |
| 27 | #' | |
| 28 | #' @note Specifying font in this way to, e.g., [export_as_txt()] or | |
| 29 | #' [toString()] will not affect the font size of the output, as these | |
| 30 | #' are both raw text formats. [export_as_pdf()] will use the specified font. | |
| 31 | #' | |
| 32 | #' @seealso [nchar_ttype()], [toString()], [`pagination_algo`], [export_as_pdf()] | |
| 33 | #' | |
| 34 | #' @examples | |
| 35 | #' fspec <- font_spec("Courier", 8, 1) | |
| 36 | #' | |
| 37 | #' lets <- paste(letters, collapse = "") | |
| 38 | #' | |
| 39 | #' nchar_ttype(lets, fspec) | |
| 40 | #' | |
| 41 | #' fspec2 <- font_spec("Times", 8, 1) | |
| 42 | #' | |
| 43 | #' nchar_ttype(lets, fspec2) | |
| 44 | #' | |
| 45 | #' @export | |
| 46 | font_spec <- function(font_family = "Courier", | |
| 47 | font_size = 8, | |
| 48 |                       lineheight = 1) { | |
| 49 | 417x |   if (font_family %in% c("Times New Roman", "Times", "serif")) { | 
| 50 | 4x | font_family <- times_font_name() | 
| 51 | } | |
| 52 | 417x | structure( | 
| 53 | 417x | list( | 
| 54 | 417x | family = font_family, | 
| 55 | 417x | size = font_size, | 
| 56 | 417x | lineheight = lineheight | 
| 57 | ), | |
| 58 | 417x |     class = c("font_spec", "list") | 
| 59 | ) | |
| 60 | } | |
| 61 | std_cpi <- 10L | |
| 62 | std_lpi <- 6L | |
| 63 | ||
| 64 | std_full_pg_wd_in <- 8.5 | |
| 65 | ||
| 66 | std_full_pg_ht_in <- 11 | |
| 67 | ||
| 68 | std_log_pg_wd_chars <- 72 | |
| 69 | ||
| 70 | std_log_pg_ht_lines <- 60 | |
| 71 | ||
| 72 | std_marg_ht <- round((std_full_pg_ht_in - std_log_pg_ht_lines / std_lpi) / 2, 2) | |
| 73 | std_marg_wd <- round((std_full_pg_wd_in - std_log_pg_wd_chars / std_cpi) / 2, 2) | |
| 74 | ||
| 75 | std_margins <- list( | |
| 76 | top = std_marg_ht, | |
| 77 | bottom = std_marg_ht, | |
| 78 | left = std_marg_wd, | |
| 79 | right = std_marg_wd | |
| 80 | ) | |
| 81 | ||
| 82 | ## does not appear to be used anywhere | |
| 83 | ## to_inches_num <- function(x) { | |
| 84 | ##   if (is(x, "unit")) { | |
| 85 | ## x <- unclass(convertUnit(x, "inches")) | |
| 86 | ## } | |
| 87 | ## x | |
| 88 | ## } | |
| 89 | ||
| 90 | ## Physical size, does not take margins into account | |
| 91 | pg_dim_names <- list( | |
| 92 | letter = c(8.5, 11), | |
| 93 | a4 = c(8.27, 11.69), | |
| 94 | legal = c(8.5, 14) | |
| 95 | ) | |
| 96 | ||
| 97 | #' Supported named page types | |
| 98 | #' | |
| 99 | #' List supported named page types. | |
| 100 | #' | |
| 101 | #' @return | |
| 102 | #' * `page_types` returns a character vector of supported page types | |
| 103 | #' * `page_dim` returns the dimensions (width, then height) of the selected page type. | |
| 104 | #' | |
| 105 | #' @export | |
| 106 | #' @examples | |
| 107 | #' page_types() | |
| 108 | #' page_dim("a4") | |
| 109 | page_types <- function() { | |
| 110 | 74x | names(pg_dim_names) | 
| 111 | } | |
| 112 | ||
| 113 | #' @param page_type (`string`)\cr the name of a page size specification. Call | |
| 114 | #' [page_types()] for supported values. | |
| 115 | #' | |
| 116 | #' @export | |
| 117 | #' @rdname page_types | |
| 118 | page_dim <- function(page_type) { | |
| 119 | 45x |   if (is.null(page_type)) { | 
| 120 | 28x | return(NULL) | 
| 121 | } | |
| 122 | 17x |   if (!page_type %in% page_types()) { | 
| 123 | 1x |     stop("Unrecognized page-size specification: ", page_type) | 
| 124 | } | |
| 125 | 16x | pg_dim_names[[page_type]] | 
| 126 | } | |
| 127 | ||
| 128 | #' Calculate lines per inch and characters per inch for font | |
| 129 | #' | |
| 130 | #' @inheritParams page_lcpp | |
| 131 | #' | |
| 132 | #' @details | |
| 133 | #' This function opens a PDF graphics device, writes to a temporary file, then | |
| 134 | #' utilizes [grid::convertWidth()] and [grid::convertHeight()] to calculate lines | |
| 135 | #' per inch and characters per inch for the specified font family, size, and | |
| 136 | #' line height. | |
| 137 | #' | |
| 138 | #' An error is thrown if the font is not monospaced (determined by comparing | |
| 139 | #' the effective widths of the `M` and `.` glyphs). | |
| 140 | #' | |
| 141 | #' @return A named list with `cpi` and `lpi`, the characters and lines per | |
| 142 | #' inch, respectively. | |
| 143 | #' | |
| 144 | #' @examples | |
| 145 | #' font_lcpi <- getFromNamespace("font_lcpi", "formatters") | |
| 146 | #' | |
| 147 | #' font_lcpi() | |
| 148 | #' font_lcpi(font_size = 8) | |
| 149 | #' font_lcpi(font_size = 8, lineheight = 1.1) | |
| 150 | #' | |
| 151 | #' @keywords internal | |
| 152 | font_lcpi <- function(font_family = "Courier", | |
| 153 | font_size = 8, | |
| 154 | lineheight = 1, | |
| 155 |                       fontspec = font_spec(font_family, font_size, lineheight)) { | |
| 156 | 58x | new_dev <- open_font_dev(fontspec) | 
| 157 | 58x |   if (new_dev) { | 
| 158 | 10x | on.exit(close_font_dev()) | 
| 159 | } | |
| 160 | 58x | list( | 
| 161 | 58x | cpi = 1 / convertWidth(unit(1, "strwidth", " "), "inches", valueOnly = TRUE), | 
| 162 | 58x | lpi = convertHeight(unit(1, "inches"), "lines", valueOnly = TRUE) | 
| 163 | ) | |
| 164 | } | |
| 165 | ||
| 166 | marg_order <- c("bottom", "left", "top", "right") | |
| 167 | ||
| 168 | #' Determine lines per page (LPP) and characters per page (CPP) based on font and page type | |
| 169 | #' | |
| 170 | #' @inheritParams open_font_dev | |
| 171 | #' @param page_type (`string`)\cr name of a page type. See [`page_types`]. Ignored | |
| 172 | #' when `pg_width` and `pg_height` are set directly. | |
| 173 | #' @param landscape (`flag`)\cr whether the dimensions of `page_type` should be | |
| 174 | #' inverted for landscape orientation. Defaults to `FALSE`, ignored when `pg_width` and | |
| 175 | #' `pg_height` are set directly. | |
| 176 | #' @param font_family (`string`)\cr name of a font family. An error will be thrown | |
| 177 | #' if the family named is not monospaced. Defaults to `"Courier"`. | |
| 178 | #' @param font_size (`numeric(1)`)\cr font size. Defaults to `12`. | |
| 179 | #' @param lineheight (`numeric(1)`)\cr line height. Defaults to `1`. | |
| 180 | #' @param margins (`numeric(4)`)\cr named numeric vector containing `"bottom"`, `"left"`, | |
| 181 | #' `"top"`, and `"right"` margins in inches. Defaults to `.5` inches for both vertical | |
| 182 | #' margins and `.75` for both horizontal margins. | |
| 183 | #' @param pg_width (`numeric(1)`)\cr page width in inches. | |
| 184 | #' @param pg_height (`numeric(1)`)\cr page height in inches. | |
| 185 | #' | |
| 186 | #' @return A named list containing LPP (lines per page) and CPP (characters per page) | |
| 187 | #' elements suitable for use by the pagination machinery. | |
| 188 | #' | |
| 189 | #' @examples | |
| 190 | #' page_lcpp() | |
| 191 | #' page_lcpp(font_size = 10) | |
| 192 | #' page_lcpp("a4", font_size = 10) | |
| 193 | #' | |
| 194 | #' page_lcpp(margins = c(top = 1, bottom = 1, left = 1, right = 1)) | |
| 195 | #' page_lcpp(pg_width = 10, pg_height = 15) | |
| 196 | #' | |
| 197 | #' @export | |
| 198 | page_lcpp <- function(page_type = page_types(), | |
| 199 | landscape = FALSE, | |
| 200 | font_family = "Courier", | |
| 201 | font_size = 8, | |
| 202 | lineheight = 1, | |
| 203 | margins = c(top = .5, bottom = .5, left = .75, right = .75), | |
| 204 | pg_width = NULL, | |
| 205 | pg_height = NULL, | |
| 206 |                       fontspec = font_spec(font_family, font_size, lineheight)) { | |
| 207 | 54x |   if (is.null(page_type)) { | 
| 208 | 19x | page_type <- page_types()[1] | 
| 209 |   } else { | |
| 210 | 35x | page_type <- match.arg(page_type) | 
| 211 | } | |
| 212 | ||
| 213 | 54x |   if (is.null(names(margins))) { | 
| 214 | 12x | names(margins) <- marg_order | 
| 215 |   } else { | |
| 216 | 42x | margins <- margins[marg_order] | 
| 217 | } | |
| 218 | 54x |   if (any(is.na(margins))) { | 
| 219 | ! |     stop("margins argument must have names 'bottom', 'left', 'top' and 'right'.") | 
| 220 | } | |
| 221 | 54x | lcpi <- font_lcpi(fontspec = fontspec) | 
| 222 | ||
| 223 | 54x | wdpos <- ifelse(landscape, 2, 1) | 
| 224 | 54x | pg_width <- pg_width %||% pg_dim_names[[page_type]][wdpos] | 
| 225 | 54x | pg_height <- pg_height %||% pg_dim_names[[page_type]][-wdpos] | 
| 226 | ||
| 227 | 54x |   pg_width <- pg_width - sum(margins[c("left", "right")]) | 
| 228 | 54x |   pg_height <- pg_height - sum(margins[c("top", "bottom")]) | 
| 229 | ||
| 230 | 54x | list( | 
| 231 | 54x | cpp = floor(lcpi[["cpi"]] * pg_width), | 
| 232 | 54x | lpp = floor(lcpi[["lpi"]] * pg_height) | 
| 233 | ) | |
| 234 | } | |
| 235 | ||
| 236 | .open_fdev_is_monospace <- function() { | |
| 237 |   if (!font_dev_state$open) { | |
| 238 | stop( | |
| 239 | ".open_fdev_is_monospace called when font dev state is not open. ", | |
| 240 | "This shouldn't happen, please contact the maintainers." | |
| 241 | ) | |
| 242 | } | |
| 243 | font_dev_state$ismonospace | |
| 244 | } | |
| 245 | ||
| 246 | ## safe wrapper around .open_fdev_is_monospace | |
| 247 | is_monospace <- function(font_family = "Courier", | |
| 248 | font_size = 8, | |
| 249 | lineheight = 1, | |
| 250 | fontspec = font_spec( | |
| 251 | font_family, | |
| 252 | font_size, | |
| 253 | lineheight | |
| 254 |                          )) { | |
| 255 |   if (is.null(fontspec)) { | |
| 256 | return(TRUE) | |
| 257 | } | |
| 258 | new_dev <- open_font_dev(fontspec) | |
| 259 |   if (new_dev) { | |
| 260 | on.exit(close_font_dev()) | |
| 261 | } | |
| 262 | .open_fdev_is_monospace() | |
| 263 | } | |
| 264 | ||
| 265 | ## pg_types <- list( | |
| 266 | ## "fsrp" = c(cpp = 110, lpp = 66), | |
| 267 | ## "fsrp8" = c(cpp = 110, lpp = 66), | |
| 268 | ## "fsrp7" = c(cpp = 110, lpp = 75), | |
| 269 | ## "fsrl" = c(cpp = 149, lpp = 51), | |
| 270 | ## "fsrl8" = c(cpp = 149, lpp = 51), | |
| 271 | ## "fsrl7" = c(cpp = 150, lpp = 59), | |
| 272 | ## "erp" = c(cpp = 96, lpp = 66), | |
| 273 | ## "erp8" = c(cpp = 96, lpp = 66), | |
| 274 | ## "erl" = c(cpp = 149, lpp = 45), | |
| 275 | ## "erl8" = c(cpp = 149, lpp = 45), | |
| 276 | ## "sasp" = c(cpp = 93, lpp = 73), | |
| 277 | ## "sasp8" = c(cpp = 93, lpp = 73), | |
| 278 | ## "sasl" = c(cpp = 134, lpp = 52), | |
| 279 | ## "sasl8" = c(cpp = 134, lpp = 52), | |
| 280 | ## "sasp7" = c(cpp = 107, lpp = 92), | |
| 281 | ## "sasl7" = c(cpp = 154, lpp = 64), | |
| 282 | ## "sasp6" = c(cpp = 125, lpp = 108), | |
| 283 | ## "sasl6" = c(cpp = 180, lpp = 75), | |
| 284 | ## "sasp10" = c(cpp = 78, lpp = 64), | |
| 285 | ## "sasl10" = c(cpp = 108, lpp = 45), | |
| 286 | ## "sasp9" = c(cpp = 87, lpp = 71), | |
| 287 | ## "sasl9" = c(cpp = 120, lpp = 51), | |
| 288 | ## "rapidp10" = c(cpp = 78, lpp = 64), | |
| 289 | ## "rapidl10" = c(cpp = 108, lpp = 45), | |
| 290 | ## "rapidp9" = c(cpp = 87, lpp = 71), | |
| 291 | ## "rapidl9" = c(cpp = 120, lpp = 51), | |
| 292 | ## "rapidp" = c(cpp = 93, lpp = 73), | |
| 293 | ## "rapidp8" = c(cpp = 93, lpp = 73), | |
| 294 | ## "rapidl" = c(cpp = 134, lpp = 52), | |
| 295 | ## "rapidl8" = c(cpp = 134, lpp = 52), | |
| 296 | ## "rapidp7" = c(cpp = 107, lpp = 92), | |
| 297 | ## "rapidl7" = c(cpp = 154, lpp = 64), | |
| 298 | ## "rapidp6" = c(cpp = 125, lpp = 108), | |
| 299 | ## "rapidl6" = c(cpp = 180, lpp = 75), | |
| 300 | ## "shibal" = c(cpp = 170, lpp = 48), | |
| 301 | ## "shibal10" = c(cpp = 137, lpp = 39), | |
| 302 | ## "shibal8" = c(cpp = 170, lpp = 48), | |
| 303 | ## "shibal7" = c(cpp = 194, lpp = 56), | |
| 304 | ## "shibal6" = c(cpp = 225, lpp = 65), | |
| 305 | ## "shibap" = c(cpp = 112, lpp = 78), | |
| 306 | ## "shibap10" = c(cpp = 89, lpp = 64), | |
| 307 | ## "shibap8" = c(cpp = 112, lpp = 78), | |
| 308 | ## "shibap7" = c(cpp = 127, lpp = 92), | |
| 309 | ## "shibap6" = c(cpp = 148, lpp = 108)) | |
| 310 | ||
| 311 | ## ~courier_size, ~cpi, ~lpi, | |
| 312 | ## 6, floor(129 / pg_dim_names[["letter"]][1]), floor(85 / pg_dim_names[["letter"]][2]), | |
| 313 | ## 7, floor(110 / pg_dim_names[["letter"]][1]), floor(76 / pg_dim_names[["letter"]][2]), | |
| 314 | ## 8, floor(95 / pg_dim_names[["letter"]][1]), floor(68 / pg_dim_names[["letter"]][2]), | |
| 315 | ## 9, floor(84 / pg_dim_names[["letter"]][1]), floor(61 / pg_dim_names[["letter"]][2]), | |
| 316 | ## 10, floor(75 / pg_dim_names[["letter"]][1]), floor(56 / pg_dim_names[["letter"]][2]) | |
| 317 | ## ) | |
| 318 | ||
| 319 | ## courier_lcpi <- function(size) { | |
| 320 | ## grid.newpage() | |
| 321 | ## gp <- gpar(fontfamily="Courier New", fontsize = size, lineheight = 1) | |
| 322 | ## pushViewport(plotViewport( gp = gp)) | |
| 323 | ## list(cpi = round(1/convertWidth(unit(1, "strwidth", "h"), "inches", valueOnly = TRUE), 0), | |
| 324 | ## lpi = round(convertHeight(unit(1, "inches"), "lines", valueOnly = TRUE), 0)) | |
| 325 | ## } | 
| 1 | ## credit: rlang, Henry and Wickham. | |
| 2 | ## this one tiny utility function is NOT worth a dependency. | |
| 3 | ## modified it so any length 0 x grabs y | |
| 4 | ||
| 5 | #' `%||%` (if length-0) alternative operator | |
| 6 | #' | |
| 7 | #' @param a (`ANY`)\cr element to select *only* if it is not of length 0. | |
| 8 | #' @param b (`ANY`)\cr element to select if `a` has length 0. | |
| 9 | #' | |
| 10 | #' @return `a` if it is not of length 0, otherwise `b`. | |
| 11 | #' | |
| 12 | #' @examples | |
| 13 | #' 6 %||% 10 | |
| 14 | #' | |
| 15 | #' character() %||% "hi" | |
| 16 | #' | |
| 17 | #' NULL %||% "hi" | |
| 18 | #' | |
| 19 | #' @export | |
| 20 | #' @name ifnotlen0 | |
| 21 | `%||%` <- function(a, b) { | |
| 22 | 429x |   if (length(a) == 0) { | 
| 23 | 131x | b | 
| 24 |   } else { | |
| 25 | 298x | a | 
| 26 | } | |
| 27 | } | 
| 1 | #' Default horizontal separator | |
| 2 | #' | |
| 3 | #' The default horizontal separator character which can be displayed in the current | |
| 4 | #' charset for use in rendering table-like objects. | |
| 5 | #' | |
| 6 | #' @param hsep_char (`string`)\cr character that will be set in the R environment | |
| 7 | #' options as the default horizontal separator. Must be a single character. Use | |
| 8 | #'   `getOption("formatters_default_hsep")` to get its current value (`NULL` if not set). | |
| 9 | #' | |
| 10 | #' @return unicode 2014 (long dash for generating solid horizontal line) if in a | |
| 11 | #' locale that uses a UTF character set, otherwise an ASCII hyphen with a | |
| 12 | #' once-per-session warning. | |
| 13 | #' | |
| 14 | #' @examples | |
| 15 | #' default_hsep() | |
| 16 | #' set_default_hsep("o") | |
| 17 | #' default_hsep() | |
| 18 | #' | |
| 19 | #' @name default_horizontal_sep | |
| 20 | #' @export | |
| 21 | default_hsep <- function() { | |
| 22 | 55x |   system_default_hsep <- getOption("formatters_default_hsep") | 
| 23 | ||
| 24 | 55x |   if (is.null(system_default_hsep)) { | 
| 25 | 54x |     if (any(grepl("^UTF", utils::localeToCharset()))) { | 
| 26 | 54x | hsep <- "\u2014" | 
| 27 |     } else { | |
| 28 | ! |       if (interactive()) { | 
| 29 | ! | warning( | 
| 30 | ! | "Detected non-UTF charset. Falling back to '-' ", | 
| 31 | ! | "as default header/body separator. This warning ", | 
| 32 | ! | "will only be shown once per R session." | 
| 33 | ! | ) # nocov | 
| 34 | } # nocov | |
| 35 | hsep <- "-" # nocov | |
| 36 | } | |
| 37 |   } else { | |
| 38 | 1x | hsep <- system_default_hsep | 
| 39 | } | |
| 40 | 55x | hsep | 
| 41 | } | |
| 42 | ||
| 43 | #' @name default_horizontal_sep | |
| 44 | #' @export | |
| 45 | set_default_hsep <- function(hsep_char) { | |
| 46 | 3x | checkmate::assert_string(hsep_char, n.chars = 1, null.ok = TRUE) | 
| 47 | 2x |   options("formatters_default_hsep" = hsep_char) | 
| 48 | } | |
| 49 | ||
| 50 | #' Default page number format | |
| 51 | #' | |
| 52 | #' If set, the default page number string will appear on the bottom right of | |
| 53 | #' every page of a paginated table. The current `cpp` is used to position the string. | |
| 54 | #' | |
| 55 | #' @param page_number (`string`)\cr single string value to set the page number format. | |
| 56 | #'   It should be formatted similarly to the following format: `"page {i}/{n}"`. | |
| 57 | #'   `{i}` will be replaced with the current page number, and `{n}` will be replaced with the | |
| 58 | #' total page number. Current `cpp` is used to position the string in the bottom right corner. | |
| 59 | #' | |
| 60 | #' @return The page number format string (`NULL` if not set). | |
| 61 | #' | |
| 62 | #' @examples | |
| 63 | #' default_page_number() | |
| 64 | #' set_default_page_number("page {i} of {n}") | |
| 65 | #' default_page_number() | |
| 66 | #' | |
| 67 | #' @name default_page_number | |
| 68 | #' @export | |
| 69 | default_page_number <- function() { | |
| 70 | 35x |   getOption("formatter_default_page_number", default = NULL) | 
| 71 | } | |
| 72 | ||
| 73 | #' @name default_page_number | |
| 74 | #' @export | |
| 75 | set_default_page_number <- function(page_number) { | |
| 76 | 6x | checkmate::assert_string(page_number, null.ok = TRUE) | 
| 77 | 6x |   options("formatter_default_page_number" = page_number) | 
| 78 | } |