| 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 | 1462x |
data.frame( |
| 140 | 1462x |
label = lab, |
| 141 | 1462x |
name = nm, |
| 142 | 1462x |
abs_rownumber = rnum, |
| 143 | 1462x |
path = I(list(pth)), |
| 144 | 1462x |
pos_in_siblings = sibpos, |
| 145 | 1462x |
n_siblings = nsibs, |
| 146 | 1462x |
self_extent = extent, |
| 147 | 1462x |
par_extent = repext, |
| 148 | 1462x |
reprint_inds = I(rep(list(unlist(repind)), length.out = length(nm))), |
| 149 | 1462x |
node_class = rclass, |
| 150 | 1462x |
indent = max(0L, indent), |
| 151 | 1462x |
nrowrefs = nrowrefs, |
| 152 | 1462x |
ncellrefs = ncellrefs, |
| 153 | 1462x |
nreflines = nreflines, |
| 154 |
# ref_info_df = I(list(ref_df)), |
|
| 155 | 1462x |
force_page = force_page, |
| 156 | 1462x |
page_title = page_title, |
| 157 | 1462x |
trailing_sep = trailing_sep, |
| 158 | 1462x |
stringsAsFactors = FALSE, |
| 159 | 1462x |
row.names = NULL, |
| 160 | 1462x |
check.names = FALSE, |
| 161 | 1462x |
fix.empty.names = FALSE |
| 162 |
) |
|
| 163 |
} |
|
| 164 | ||
| 165 |
calc_ref_nlines_df <- function(pagdf) {
|
|
| 166 |
## XXX XXX XXX this is dangerous and wrong!!! |
|
| 167 | 633x |
if (is.null(pagdf$ref_info_df) && sum(pagdf$nreflines) == 0) {
|
| 168 | 225x |
return(ref_df_row()[0, ]) |
| 169 |
} |
|
| 170 | 408x |
refdf <- do.call(rbind.data.frame, pagdf$ref_info_df) |
| 171 | 408x |
if (NROW(refdf) == 0) {
|
| 172 | 376x |
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 | 633x |
rw <- pagdf[guess, ] |
| 258 | ||
| 259 | 633x |
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 | 633x |
if (.is_listing_mf(pagdf) && !is.null(pagdf$self_extent_page_break)) {
|
| 268 | 32x |
pagdf$self_extent[start] <- pagdf$self_extent_page_break[start] |
| 269 |
} |
|
| 270 | ||
| 271 | 633x |
raw_rowlines <- sum(pagdf[start:guess, "self_extent"] - pagdf[start:guess, "nreflines"]) |
| 272 | ||
| 273 | 633x |
refdf_ii <- calc_ref_nlines_df(pagdf[start:guess, ]) |
| 274 | 633x |
reflines <- if (row) sum(refdf_ii$nlines, 0L) else 0L |
| 275 | 633x |
if (reflines > 0 && !have_col_fnotes) {
|
| 276 | 32x |
reflines <- reflines + div_height + 1L |
| 277 |
} |
|
| 278 | ||
| 279 | ||
| 280 | 633x |
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 | 633x |
ncols <- 0L |
| 286 | 633x |
if (row) {
|
| 287 | 261x |
sectlines <- if (start == guess) 0L else sum(!is.na(pagdf[start:(guess - 1), "trailing_sep"])) |
| 288 |
} else { ## columns
|
|
| 289 | 372x |
ncols <- guess - start + 1 + length(pagdf$reprint_inds[[start]]) ## +1 because its inclusive, 5-6 is 2 columns |
| 290 | 372x |
sectlines <- col_gap * (ncols - as.integer(!has_rowlabels)) ## -1 if no row labels |
| 291 |
} |
|
| 292 | 633x |
lines <- rowlines + sectlines |
| 293 | 633x |
rep_ext <- pagdf$par_extent[start] |
| 294 | 633x |
if (lines > rlpp) {
|
| 295 | 384x |
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 | 384x |
return(FALSE) |
| 309 |
} |
|
| 310 | ||
| 311 |
# Special cases: is it a label or content row? |
|
| 312 | 249x |
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 | 249x |
sibpos <- rw[["pos_in_siblings"]] |
| 336 | 249x |
nsib <- rw[["n_siblings"]] |
| 337 |
# okpos <- min(min_sibs + 1, rw[["n_siblings"]]) |
|
| 338 | 249x |
if (sibpos != nsib) {
|
| 339 | 100x |
retfalse <- FALSE |
| 340 | 100x |
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 | 75x |
} 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 | 100x |
if (retfalse) {
|
| 358 | 32x |
return(FALSE) |
| 359 |
} |
|
| 360 |
} |
|
| 361 | 217x |
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 | 201x |
if (verbose) {
|
| 389 | 83x |
message(" OK [", lines + rep_ext, if (row) " lines]" else " chars]")
|
| 390 |
} |
|
| 391 | 201x |
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 | 207x |
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 | 207x |
origuess <- guess |
| 419 | 207x |
while (guess >= start && !valid_pag( |
| 420 | 207x |
pagdf, guess, |
| 421 | 207x |
start = start, |
| 422 | 207x |
rlpp = rlpp, lpp = lpp_or_cpp, context_lpp = context_lpp_or_cpp, # only lpp goes to row pagination |
| 423 | 207x |
min_sibs = min_siblings, |
| 424 | 207x |
nosplit = nosplitin, verbose, row = row, |
| 425 | 207x |
have_col_fnotes = have_col_fnotes, |
| 426 | 207x |
div_height = div_height, |
| 427 | 207x |
col_gap = col_gap, |
| 428 | 207x |
has_rowlabels = has_rowlabels |
| 429 |
)) {
|
|
| 430 | 432x |
guess <- guess - 1 |
| 431 |
} |
|
| 432 | 207x |
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 | 201x |
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 | 97x |
start <- 1 |
| 531 | 97x |
current_page <- 1 |
| 532 | 97x |
nr <- nrow(pagdf) |
| 533 | 97x |
ret <- list() |
| 534 | 97x |
while (start <= nr) {
|
| 535 | 208x |
adjrlpp <- rlpp - pagdf$par_extent[start] |
| 536 | 208x |
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 | 207x |
guess <- min(nr, start + adjrlpp - 1) |
| 544 | 207x |
end <- find_pag( |
| 545 | 207x |
pagdf = pagdf, |
| 546 | 207x |
current_page = current_page, start = start, guess = guess, |
| 547 | 207x |
rlpp = adjrlpp, lpp_or_cpp = lpp_or_cpp, context_lpp_or_cpp = context_lpp_or_cpp, |
| 548 | 207x |
min_siblings = min_siblings, |
| 549 | 207x |
nosplitin = nosplitin, |
| 550 | 207x |
verbose = verbose, |
| 551 | 207x |
row = row, |
| 552 | 207x |
have_col_fnotes = have_col_fnotes, |
| 553 | 207x |
div_height = div_height, |
| 554 | 207x |
col_gap = col_gap, |
| 555 | 207x |
has_rowlabels = has_rowlabels |
| 556 |
) |
|
| 557 | 201x |
ret <- c(ret, list(c( |
| 558 | 201x |
pagdf$reprint_inds[[start]], |
| 559 | 201x |
start:end |
| 560 |
))) |
|
| 561 | 201x |
start <- end + 1 |
| 562 | 201x |
current_page <- current_page + 1 |
| 563 |
} |
|
| 564 | 90x |
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 mf (`MatrixPrintForm`)\cr object to be paginated. |
|
| 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(mf, |
|
| 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 | 46x |
if (is.list(nosplitin)) {
|
| 595 | ! |
nosplitin <- nosplitin[["cols"]] |
| 596 |
} |
|
| 597 |
# mf <- matrix_form(obj, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type) |
|
| 598 | 46x |
clwds <- colwidths %||% propose_column_widths(mf, fontspec = fontspec) |
| 599 | 46x |
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 | 46x |
num_rep_cols(mf) <- rep_cols |
| 604 | ||
| 605 | 46x |
has_rlabs <- mf_has_rlabels(mf) |
| 606 | 46x |
rlabs_flag <- as.integer(has_rlabs) |
| 607 | 46x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
| 608 | ||
| 609 |
# rep_extent <- pdf$par_extent[nrow(pdf)] |
|
| 610 | 46x |
rcpp <- cpp - table_inset(mf) - rlab_extent # rep_extent - table_inset(mf) - rlab_extent |
| 611 | 46x |
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 | 46x |
res <- pag_indices_inner(mf_cinfo(mf), |
| 620 | 46x |
rlpp = rcpp, lpp_or_cpp = cpp, context_lpp_or_cpp = cpp - rcpp, |
| 621 |
# cpp - sum(clwds[seq_len(rep_cols)]), |
|
| 622 | 46x |
verbose = verbose, |
| 623 | 46x |
min_siblings = 1, |
| 624 | 46x |
nosplitin = nosplitin, |
| 625 | 46x |
row = FALSE, |
| 626 | 46x |
col_gap = mf_colgap(mf), |
| 627 | 46x |
has_rowlabels = mf_has_rlabels(mf) |
| 628 |
) |
|
| 629 | 45x |
res |
| 630 |
} |
|
| 631 | ||
| 632 |
mpf_infer_cinfo <- function(mf, colwidths = NULL, rep_cols = num_rep_cols(mf), fontspec, colpaths = NULL) {
|
|
| 633 | 59x |
if (!is.null(mf_cinfo(mf))) {
|
| 634 | 5x |
return(mf_update_cinfo(mf, colwidths = colwidths)) |
| 635 |
} |
|
| 636 | 54x |
new_dev <- open_font_dev(fontspec) |
| 637 | 54x |
if (new_dev) {
|
| 638 | 54x |
on.exit(close_font_dev()) |
| 639 |
} |
|
| 640 | 54x |
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 | 54x |
clwds <- (colwidths %||% mf_col_widths(mf)) %||% propose_column_widths(mf, fontspec = fontspec) |
| 644 | 54x |
has_rlabs <- mf_has_rlabels(mf) |
| 645 | 54x |
rlabs_flag <- as.integer(has_rlabs) |
| 646 | 54x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
| 647 | 54x |
sqstart <- rlabs_flag + 1L # rep_cols + 1L |
| 648 | ||
| 649 | 54x |
pdfrows <- lapply( |
| 650 | 54x |
(sqstart):ncol(mf$strings), |
| 651 | 54x |
function(i) {
|
| 652 | 312x |
rownum <- i - rlabs_flag |
| 653 | 312x |
rep_inds <- seq_len(rep_cols)[seq_len(rep_cols) < rownum] |
| 654 | 312x |
rep_extent_i <- sum( |
| 655 | 312x |
0L, |
| 656 | 312x |
clwds[rlabs_flag + rep_inds] |
| 657 | 312x |
) ## colwidths |
| 658 | 312x |
pagdfrow( |
| 659 | 312x |
row = NA, |
| 660 | 312x |
nm = rownum, |
| 661 | 312x |
lab = rownum, |
| 662 | 312x |
rnum = rownum, |
| 663 | 312x |
pth = NA, |
| 664 | 312x |
extent = clwds[i], |
| 665 | 312x |
repext = rep_extent_i, # sum(clwds[rep_cols]) + mf$col_gap * max(0, (length(rep_cols) - 1)), |
| 666 | 312x |
repind = rep_inds, # rep_cols, |
| 667 | 312x |
rclass = "stuff", |
| 668 | 312x |
sibpos = 1 - 1, |
| 669 | 312x |
nsibs = 1 - 1, |
| 670 | 312x |
fontspec = fontspec |
| 671 |
) |
|
| 672 |
} |
|
| 673 |
) |
|
| 674 | 54x |
pdf <- do.call(rbind, pdfrows) |
| 675 | ||
| 676 | 54x |
refdf <- mf_fnote_df(mf) |
| 677 | 54x |
pdf <- splice_fnote_info_in(pdf, refdf, row = FALSE) |
| 678 | 54x |
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 | 54x |
mf_cinfo(mf) <- pdf |
| 693 | 54x |
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 | 53x |
rws <- mapply(pagdfrow, |
| 724 | 53x |
nm = rnames, lab = labs, extent = extents, |
| 725 | 53x |
rclass = rclass, rnum = rnums, pth = paths, |
| 726 | 53x |
MoreArgs = list(fontspec = fontspec), |
| 727 | 53x |
SIMPLIFY = FALSE, nsibs = 1, sibpos = 1 |
| 728 |
) |
|
| 729 | 53x |
res <- do.call(rbind.data.frame, rws) |
| 730 | 53x |
res$n_siblings <- nrow(res) |
| 731 | 53x |
res$pos_in_siblings <- seq_along(res$n_siblings) |
| 732 | ||
| 733 | 53x |
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 | 53x |
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 | 51x |
structure(list( |
| 757 | 51x |
lpp = lpp, |
| 758 | 51x |
cpp = cpp, |
| 759 | 51x |
max_width = max_width, |
| 760 | 51x |
font_spec = fontspec |
| 761 | 51x |
), class = "page_size_spec") |
| 762 |
} |
|
| 763 | ||
| 764 |
get_font_spec <- function(obj) {
|
|
| 765 | 49x |
if (!is(obj, "page_size_spec")) {
|
| 766 | ! |
stop("get_font_spec is only currently defined for page_size_spec objects")
|
| 767 |
} |
|
| 768 | 49x |
obj$font_spec |
| 769 |
} |
|
| 770 | ||
| 771 | 102x |
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 | 51x |
pg_lcpp <- page_lcpp( |
| 790 | 51x |
page_type = page_type, |
| 791 | 51x |
landscape = landscape, |
| 792 |
## font_family = font_family, |
|
| 793 |
## font_size = font_size, |
|
| 794 |
## lineheight = lineheight, |
|
| 795 | 51x |
fontspec = fontspec, |
| 796 | 51x |
margins = margins, |
| 797 | 51x |
pg_width = pg_width, |
| 798 | 51x |
pg_height = pg_height |
| 799 |
) |
|
| 800 | ||
| 801 | 51x |
if (non_null_na(lpp)) {
|
| 802 | 29x |
lpp <- pg_lcpp$lpp |
| 803 |
} |
|
| 804 | 51x |
if (non_null_na(cpp)) {
|
| 805 | 23x |
cpp <- pg_lcpp$cpp |
| 806 |
} |
|
| 807 | 51x |
stopifnot(!is.na(cpp)) |
| 808 | ||
| 809 | 51x |
max_width <- .handle_max_width(tf_wrap, max_width, cpp, colwidths, col_gap, inset) |
| 810 | ||
| 811 | 51x |
page_size_spec( |
| 812 | 51x |
lpp = lpp, cpp = cpp, max_width = max_width, |
| 813 |
## font_family = font_family, |
|
| 814 |
## font_size = font_size, |
|
| 815 |
## lineheight = lineheight |
|
| 816 | 51x |
fontspec = fontspec |
| 817 |
) |
|
| 818 |
} |
|
| 819 | ||
| 820 |
calc_rlpp <- function(pg_size_spec, mf, colwidths, tf_wrap, verbose) {
|
|
| 821 | 49x |
lpp <- pg_size_spec$lpp |
| 822 | 49x |
max_width <- pg_size_spec$max_width |
| 823 | 49x |
fontspec <- get_font_spec(pg_size_spec) |
| 824 | 49x |
dh <- divider_height(mf) |
| 825 | 49x |
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 | 25x |
if (!tf_wrap) {
|
| 830 | 12x |
tlines <- length(all_titles(mf)) |
| 831 |
} else {
|
|
| 832 | 13x |
tlines <- sum(nlines(all_titles(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec)) |
| 833 |
} |
|
| 834 | 25x |
tlines <- tlines + dh + 1L |
| 835 |
} else {
|
|
| 836 | 24x |
tlines <- 0 |
| 837 |
} |
|
| 838 | ||
| 839 |
## dh for divider line between column labels and table body |
|
| 840 | 49x |
cinfo_lines <- mf_nlheader(mf) + dh |
| 841 | ||
| 842 | 49x |
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 | 49x |
refdf <- mf_fnote_df(mf) |
| 850 | 49x |
cfn_df <- refdf[is.na(refdf$row) & !is.na(refdf$col), ] |
| 851 | ||
| 852 | 49x |
flines <- 0L |
| 853 | 49x |
mnfoot <- main_footer(mf) |
| 854 | 49x |
havemn <- length(mnfoot) && any(nzchar(mnfoot)) |
| 855 | 49x |
if (havemn) {
|
| 856 | 26x |
flines <- nlines( |
| 857 | 26x |
mnfoot, |
| 858 | 26x |
colwidths = colwidths, |
| 859 | 26x |
max_width = max_width - table_inset(mf), |
| 860 | 26x |
fontspec = fontspec |
| 861 |
) |
|
| 862 |
} |
|
| 863 | 49x |
prfoot <- prov_footer(mf) |
| 864 | 49x |
if (length(prfoot) && any(nzchar(prfoot))) {
|
| 865 | 32x |
flines <- flines + nlines(prov_footer(mf), colwidths = colwidths, max_width = max_width, fontspec = fontspec) |
| 866 | 32x |
if (havemn) {
|
| 867 | 25x |
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 | 49x |
if (flines > 0) {
|
| 873 | 33x |
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 | 49x |
if (NROW(cfn_df) > 0) {
|
| 878 | ! |
cinfo_lines <- cinfo_lines + sum(cfn_df$nlines) |
| 879 | ! |
flines <- flines + dh + 1L |
| 880 |
} |
|
| 881 | ||
| 882 | 49x |
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 | 49x |
ret <- lpp - flines - tlines - cinfo_lines |
| 891 | ||
| 892 | 49x |
if (verbose) {
|
| 893 | 17x |
message("Lines per page available for tables rows: ", ret, " (original: ", lpp, ")")
|
| 894 |
} |
|
| 895 | 49x |
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 obj (`ANY`)\cr object to be paginated. Must have a [matrix_form()] method. |
|
| 955 |
#' @param lpp (`numeric(1)` or `NULL`)\cr lines per page. If `NA` (the default), this is calculated automatically |
|
| 956 |
#' based on the specified page size). `NULL` indicates no vertical pagination should occur. |
|
| 957 |
#' @param cpp (`numeric(1)` or `NULL`)\cr width (in characters) per page. If `NA` (the default), this is calculated |
|
| 958 |
#' automatically based on the specified page size). `NULL` indicates no horizontal pagination should occur. |
|
| 959 |
#' @param pg_size_spec (`page_size_spec`)\cr. a pre-calculated page size specification. Typically this is not set by |
|
| 960 |
#' end users. |
|
| 961 |
#' @param col_gap (`numeric(1)`)\cr The number of spaces to be placed between columns |
|
| 962 |
#' in the rendered table (and assumed for horizontal pagination). |
|
| 963 |
#' @param page_num (`string`)\cr placeholder string for page numbers. See [default_page_number] for more |
|
| 964 |
#' information. Defaults to `NULL`. |
|
| 965 |
#' |
|
| 966 |
#' @return |
|
| 967 |
#' * `paginate_indices` returns a `list` with two elements of the same length: `pag_row_indices` and `pag_col_indices`. |
|
| 968 |
#' * `paginate_to_mpfs` returns a `list` of `MatrixPrintForm` objects representing each individual page after |
|
| 969 |
#' pagination (including forced pagination if necessary). |
|
| 970 |
#' |
|
| 971 |
#' @examples |
|
| 972 |
#' mpf <- basic_matrix_form(mtcars) |
|
| 973 |
#' |
|
| 974 |
#' paginate_indices(mpf, pg_width = 5, pg_height = 3) |
|
| 975 |
#' |
|
| 976 |
#' paginate_to_mpfs(mpf, pg_width = 5, pg_height = 3) |
|
| 977 |
#' |
|
| 978 |
#' @aliases paginate pagination |
|
| 979 |
#' @export |
|
| 980 |
paginate_indices <- function(obj, |
|
| 981 |
page_type = "letter", |
|
| 982 |
font_family = "Courier", |
|
| 983 |
font_size = 8, |
|
| 984 |
lineheight = 1, |
|
| 985 |
landscape = FALSE, |
|
| 986 |
pg_width = NULL, |
|
| 987 |
pg_height = NULL, |
|
| 988 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
| 989 |
lpp = NA_integer_, |
|
| 990 |
cpp = NA_integer_, |
|
| 991 |
min_siblings = 2, |
|
| 992 |
nosplitin = list( |
|
| 993 |
rows = character(), |
|
| 994 |
cols = character() |
|
| 995 |
), |
|
| 996 |
colwidths = NULL, |
|
| 997 |
tf_wrap = FALSE, |
|
| 998 |
max_width = NULL, |
|
| 999 |
indent_size = 2, |
|
| 1000 |
pg_size_spec = NULL, |
|
| 1001 |
rep_cols = num_rep_cols(obj), |
|
| 1002 |
col_gap = 3, |
|
| 1003 |
fontspec = font_spec(font_family, font_size, lineheight), |
|
| 1004 |
round_type = c("iec", "sas"),
|
|
| 1005 |
verbose = FALSE) {
|
|
| 1006 |
## this preserves backwards compatibility |
|
| 1007 |
## could start deprecation cycle of char input |
|
| 1008 | 51x |
if (is.character(nosplitin)) {
|
| 1009 | 48x |
nosplitin <- list( |
| 1010 | 48x |
rows = nosplitin, |
| 1011 | 48x |
cols = character() |
| 1012 |
) |
|
| 1013 |
} |
|
| 1014 | 51x |
newdev <- open_font_dev(fontspec) |
| 1015 | 51x |
if (newdev) {
|
| 1016 | 3x |
on.exit(close_font_dev()) |
| 1017 |
} |
|
| 1018 | ||
| 1019 | ||
| 1020 |
## this MUST alsways return a list, inluding list(obj) when |
|
| 1021 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
| 1022 |
## based on s3 classes that are lists underneath!!! |
|
| 1023 | 51x |
fpags <- do_forced_paginate(obj) |
| 1024 |
## if we have more than one forced "page", |
|
| 1025 |
## paginate each of them individually and return the result. |
|
| 1026 |
## forced pagination is ***currently*** only vertical, so |
|
| 1027 |
## we don't have to worry about divying up colwidths here, |
|
| 1028 |
## but we will if we ever allow force_paginate to do horiz |
|
| 1029 |
## pagination. |
|
| 1030 | 51x |
if (length(fpags) > 1) {
|
| 1031 | 1x |
stop( |
| 1032 | 1x |
"forced pagination is required for this object (class: ", class(obj)[1], |
| 1033 | 1x |
") this is not supported in paginate_indices. Use paginate_to_mpfs or call ", |
| 1034 | 1x |
"do_forced_paginate on your object and paginate each returned section separately." |
| 1035 |
) |
|
| 1036 |
} |
|
| 1037 | ||
| 1038 |
## order is annoying here, since we won't actually need the mpf if |
|
| 1039 |
## we run into forced pagination, but life is short and this should work fine. |
|
| 1040 |
# this step can be very slow. No need to do it internally |
|
| 1041 | 50x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) |
| 1042 | ||
| 1043 | 50x |
if (is.null(colwidths)) {
|
| 1044 | 2x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) |
| 1045 |
} else {
|
|
| 1046 | 48x |
mf_col_widths(mpf) <- colwidths |
| 1047 |
} |
|
| 1048 | ||
| 1049 | 50x |
mf_colgap(mpf) <- col_gap |
| 1050 | 50x |
if (!is.null(rep_cols) && rep_cols != num_rep_cols(mpf)) {
|
| 1051 | ! |
num_rep_cols(mpf) <- rep_cols |
| 1052 |
} |
|
| 1053 | 50x |
if (NROW(mf_cinfo(mpf)) == 0) {
|
| 1054 | ! |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) |
| 1055 |
} |
|
| 1056 | ||
| 1057 | 50x |
if (is.null(pg_size_spec)) {
|
| 1058 | 2x |
pg_size_spec <- calc_lcpp( |
| 1059 | 2x |
page_type = page_type, |
| 1060 |
## font_family = font_family, |
|
| 1061 |
## font_size = font_size, |
|
| 1062 |
## lineheight = lineheight, |
|
| 1063 | 2x |
fontspec = fontspec, |
| 1064 | 2x |
landscape = landscape, |
| 1065 | 2x |
pg_width = pg_width, |
| 1066 | 2x |
pg_height = pg_height, |
| 1067 | 2x |
margins = margins, |
| 1068 | 2x |
lpp = lpp, |
| 1069 | 2x |
cpp = cpp, |
| 1070 | 2x |
tf_wrap = tf_wrap, |
| 1071 | 2x |
max_width = max_width, |
| 1072 | 2x |
colwidths = colwidths, |
| 1073 | 2x |
inset = table_inset(mpf), |
| 1074 | 2x |
col_gap = col_gap |
| 1075 |
) |
|
| 1076 |
} |
|
| 1077 | ||
| 1078 |
## we can't support forced pagination in paginate_indices because |
|
| 1079 |
## forced pagination is generally going to set page titles, which |
|
| 1080 |
## we can't preserve when just returning lists of indices. |
|
| 1081 |
## Instead we make a hard assumption here that any forced pagination |
|
| 1082 |
## has already occurred. |
|
| 1083 | ||
| 1084 |
## this wraps the cell contents AND shoves referential footnote |
|
| 1085 |
## info into mf_rinfo(mpf) |
|
| 1086 | 50x |
ori_mpf <- mpf # used for pulling right split labels |
| 1087 | 50x |
mpf <- do_cell_fnotes_wrap(ori_mpf, colwidths, max_width, tf_wrap = tf_wrap, fontspec = fontspec) |
| 1088 | ||
| 1089 |
# rlistings note: if there is a wrapping in a keycol, it is not calculated correctly |
|
| 1090 |
# in the above call, so we need to keep this information in mf_rinfo |
|
| 1091 |
# and use it here. |
|
| 1092 | 50x |
mfri <- mf_rinfo(mpf) |
| 1093 | 50x |
ori_mfri <- mf_rinfo(ori_mpf) # no wrapping but yes wrapping of \n (see later) |
| 1094 | 50x |
keycols <- .get_keycols_from_listing(mpf) |
| 1095 | 50x |
seq_nrh <- seq_len(mf_nlheader(ori_mpf)) # for Removal of header lines |
| 1096 | 50x |
if (NROW(mfri) > 1 && .is_listing_mf(mpf) && length(keycols) > 0) {
|
| 1097 |
# Lets determine the groupings created by keycols |
|
| 1098 | 13x |
keycols_grouping_df <- NULL |
| 1099 | 13x |
for (i in seq_along(keycols)) {
|
| 1100 | 26x |
kcol <- keycols[i] |
| 1101 |
# This makes the function work also in the case we have only matrix form (mainly for testing purposes) |
|
| 1102 | 26x |
ori_mpf_colnames <- names(mf_strings(ori_mpf)[1, , drop = TRUE]) |
| 1103 | 26x |
kcolvec <- mf_strings(ori_mpf)[, ori_mpf_colnames == kcol][-seq_nrh] |
| 1104 | ||
| 1105 |
# if the keycol is not present, we just use the previous one |
|
| 1106 | 26x |
for (j in seq_along(kcolvec)) {
|
| 1107 | 608x |
if (j > 1 && kcolvec[j] == "") {
|
| 1108 | 513x |
kcolvec[j] <- kcolvec[j - 1] |
| 1109 |
} |
|
| 1110 |
} |
|
| 1111 | ||
| 1112 |
# # if there is already a self_extent (e.g. wrapping from \n), we take it into account |
|
| 1113 | 26x |
if (any(ori_mfri$self_extent > 1)) {
|
| 1114 | 2x |
init_groupings <- lapply(seq(1, nrow(ori_mfri)), function(ii) rep(ii, ori_mfri$self_extent[ii])) |> |
| 1115 | 2x |
unlist() |
| 1116 | 2x |
new_kcolvec <- rep("", nrow(ori_mfri))
|
| 1117 | ||
| 1118 |
# collapse the keycolvec by the initial groupings (here "" on keycols derives from \n in other cols) |
|
| 1119 | 2x |
for (uj in unique(init_groupings)) {
|
| 1120 | 8x |
new_kcolvec[uj] <- paste0(kcolvec[init_groupings == uj], collapse = "\n") |
| 1121 |
} |
|
| 1122 | 2x |
kcolvec <- new_kcolvec |
| 1123 |
} |
|
| 1124 | ||
| 1125 | 26x |
groupings <- as.numeric(factor(kcolvec, levels = unique(kcolvec))) |
| 1126 | 26x |
where_they_start <- which(c(1, diff(groupings)) > 0) |
| 1127 | 26x |
keycols_grouping_df <- cbind( |
| 1128 | 26x |
keycols_grouping_df, |
| 1129 | 26x |
where_they_start[groupings] |
| 1130 | 26x |
) # take the groupings |
| 1131 |
} |
|
| 1132 | ||
| 1133 |
# Creating the real self_extend for mf_rinfo (if the line is chosen for pagination start) |
|
| 1134 | 13x |
self_extent_df <- apply(keycols_grouping_df, 2, function(x) mfri$self_extent[x]) |
| 1135 | 13x |
mf_rinfo(mpf) <- cbind(mfri, "self_extent_page_break" = apply(self_extent_df, 1, max)) |
| 1136 |
} |
|
| 1137 | ||
| 1138 | 50x |
if (is.null(pg_size_spec$lpp)) {
|
| 1139 | 1x |
pag_row_indices <- list(seq_len(mf_nrow(mpf))) |
| 1140 |
} else {
|
|
| 1141 | 49x |
rlpp <- calc_rlpp( |
| 1142 | 49x |
pg_size_spec, mpf, |
| 1143 | 49x |
colwidths = colwidths, |
| 1144 | 49x |
tf_wrap = tf_wrap, verbose = verbose |
| 1145 |
) |
|
| 1146 | 49x |
pag_row_indices <- pag_indices_inner( |
| 1147 | 49x |
pagdf = mf_rinfo(mpf), |
| 1148 | 49x |
rlpp = rlpp, |
| 1149 | 49x |
lpp_or_cpp = pg_size_spec$lpp, |
| 1150 | 49x |
context_lpp_or_cpp = pg_size_spec$lpp - rlpp, |
| 1151 | 49x |
verbose = verbose, |
| 1152 | 49x |
min_siblings = min_siblings, |
| 1153 | 49x |
nosplitin = nosplitin[["rows"]], |
| 1154 | 49x |
col_gap = col_gap, |
| 1155 | 49x |
has_rowlabels = mf_has_rlabels(mpf) |
| 1156 |
) |
|
| 1157 |
} |
|
| 1158 | ||
| 1159 | 45x |
if (is.null(pg_size_spec$cpp)) {
|
| 1160 | 1x |
pag_col_indices <- list(seq_len(mf_ncol(mpf))) |
| 1161 |
} else {
|
|
| 1162 | 44x |
pag_col_indices <- vert_pag_indices( |
| 1163 | 44x |
mpf, |
| 1164 | 44x |
cpp = pg_size_spec$cpp, colwidths = colwidths, |
| 1165 | 44x |
rep_cols = rep_cols, fontspec = fontspec, |
| 1166 | 44x |
nosplitin = nosplitin[["cols"]], |
| 1167 | 44x |
round_type = round_type, |
| 1168 | 44x |
verbose = verbose |
| 1169 |
) |
|
| 1170 |
} |
|
| 1171 | ||
| 1172 | 44x |
list(pag_row_indices = pag_row_indices, pag_col_indices = pag_col_indices) |
| 1173 |
} |
|
| 1174 | ||
| 1175 | 48x |
setGeneric("has_page_title", function(obj) standardGeneric("has_page_title"))
|
| 1176 | ||
| 1177 | 48x |
setMethod("has_page_title", "ANY", function(obj) length(page_titles(obj)) > 0)
|
| 1178 | ||
| 1179 |
#' @rdname paginate_indices |
|
| 1180 |
#' @export |
|
| 1181 |
paginate_to_mpfs <- function(obj, |
|
| 1182 |
page_type = "letter", |
|
| 1183 |
font_family = "Courier", |
|
| 1184 |
font_size = 8, |
|
| 1185 |
lineheight = 1, |
|
| 1186 |
landscape = FALSE, |
|
| 1187 |
pg_width = NULL, |
|
| 1188 |
pg_height = NULL, |
|
| 1189 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
| 1190 |
lpp = NA_integer_, |
|
| 1191 |
cpp = NA_integer_, |
|
| 1192 |
min_siblings = 2, |
|
| 1193 |
nosplitin = character(), |
|
| 1194 |
colwidths = NULL, |
|
| 1195 |
tf_wrap = FALSE, |
|
| 1196 |
max_width = NULL, |
|
| 1197 |
indent_size = 2, |
|
| 1198 |
pg_size_spec = NULL, |
|
| 1199 |
page_num = default_page_number(), |
|
| 1200 |
rep_cols = NULL, |
|
| 1201 |
# rep_cols = num_rep_cols(obj), |
|
| 1202 |
# col_gap = 3, # this could be change in default - breaking change |
|
| 1203 |
col_gap = 3, |
|
| 1204 |
fontspec = font_spec(font_family, font_size, lineheight), |
|
| 1205 |
round_type = c("iec", "sas"),
|
|
| 1206 |
verbose = FALSE) {
|
|
| 1207 | 58x |
newdev <- open_font_dev(fontspec) |
| 1208 | 58x |
if (newdev) {
|
| 1209 | 33x |
on.exit(close_font_dev()) |
| 1210 |
} |
|
| 1211 | ||
| 1212 | 58x |
if (isTRUE(page_num)) {
|
| 1213 | 1x |
page_num <- "page {i}/{n}"
|
| 1214 |
} |
|
| 1215 | 58x |
checkmate::assert_string(page_num, null.ok = TRUE, min.chars = 1) |
| 1216 | ||
| 1217 |
# We can return a list of paginated tables and listings |
|
| 1218 | 58x |
if (.is_list_of_tables_or_listings(obj)) {
|
| 1219 | 8x |
cur_call <- match.call(expand.dots = FALSE) |
| 1220 | 8x |
mpfs <- unlist( |
| 1221 | 8x |
lapply(obj, function(obj_i) {
|
| 1222 | 15x |
cur_call[["obj"]] <- obj_i |
| 1223 | 15x |
eval(cur_call, envir = parent.frame(3L)) |
| 1224 |
}), |
|
| 1225 | 8x |
recursive = FALSE |
| 1226 |
) |
|
| 1227 | ||
| 1228 | 7x |
if (!is.null(page_num)) {
|
| 1229 | 3x |
extracted_cpp <- max( |
| 1230 | 3x |
sapply(mpfs, function(mpf) {
|
| 1231 | 12x |
pf <- prov_footer(mpf) |
| 1232 | 12x |
nchar(pf[length(pf)]) |
| 1233 |
}) |
|
| 1234 |
) |
|
| 1235 | 3x |
mpfs <- .modify_footer_for_page_nums(mpfs, page_num, extracted_cpp) |
| 1236 |
} |
|
| 1237 | ||
| 1238 | 7x |
return(mpfs) |
| 1239 |
} |
|
| 1240 | ||
| 1241 | 50x |
if (!is.null(page_num)) {
|
| 1242 |
# Only adding a line for pagination -> lpp - 1 would have worked too |
|
| 1243 | 14x |
prov_footer(obj) <- c(prov_footer(obj), page_num) |
| 1244 |
} |
|
| 1245 | ||
| 1246 | 50x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) |
| 1247 |
# For listings, keycols are mandatory rep_num_cols |
|
| 1248 | 50x |
if (is.null(rep_cols)) {
|
| 1249 | 45x |
rep_cols <- num_rep_cols(obj) |
| 1250 |
} |
|
| 1251 | 50x |
num_rep_cols(mpf) <- rep_cols |
| 1252 | ||
| 1253 |
# Turning off min_siblings for listings |
|
| 1254 | 50x |
if (.is_listing_mf(mpf)) {
|
| 1255 | 14x |
min_siblings <- 0 |
| 1256 |
} |
|
| 1257 | ||
| 1258 |
# Checking colwidths |
|
| 1259 | 50x |
if (is.null(colwidths)) {
|
| 1260 | 34x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) |
| 1261 |
} else {
|
|
| 1262 | 16x |
cur_ncol <- ncol(mpf) |
| 1263 | 16x |
if (!.is_listing_mf(mpf)) {
|
| 1264 | 10x |
cur_ncol <- cur_ncol + as.numeric(mf_has_rlabels(mpf)) |
| 1265 |
} |
|
| 1266 | 16x |
if (length(colwidths) != cur_ncol) {
|
| 1267 | 1x |
stop( |
| 1268 | 1x |
"non-null colwidths argument must have length ncol(x) (+ 1 if row labels are present and if it is a table) [", |
| 1269 | 1x |
cur_ncol, "], got length ", length(colwidths) |
| 1270 |
) |
|
| 1271 |
} |
|
| 1272 | 15x |
mf_col_widths(mpf) <- colwidths |
| 1273 |
} |
|
| 1274 | ||
| 1275 | 49x |
if (NROW(mf_cinfo(mpf)) == 0) {
|
| 1276 | ! |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols, fontspec = fontspec) |
| 1277 |
} |
|
| 1278 | ||
| 1279 | 49x |
if (is.null(pg_size_spec)) {
|
| 1280 | 47x |
pg_size_spec <- calc_lcpp( |
| 1281 | 47x |
page_type = page_type, |
| 1282 |
## font_family = font_family, |
|
| 1283 |
## font_size = font_size, |
|
| 1284 |
## lineheight = lineheight, |
|
| 1285 | 47x |
fontspec = fontspec, |
| 1286 | 47x |
landscape = landscape, |
| 1287 | 47x |
pg_width = pg_width, |
| 1288 | 47x |
pg_height = pg_height, |
| 1289 | 47x |
margins = margins, |
| 1290 | 47x |
lpp = lpp, |
| 1291 | 47x |
cpp = cpp, |
| 1292 | 47x |
tf_wrap = tf_wrap, |
| 1293 | 47x |
max_width = max_width, |
| 1294 | 47x |
colwidths = colwidths, |
| 1295 | 47x |
inset = table_inset(mpf), |
| 1296 | 47x |
col_gap = col_gap |
| 1297 |
) |
|
| 1298 |
} |
|
| 1299 |
## this MUST always return a list, including list(obj) when |
|
| 1300 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
| 1301 |
## based on s3 classes that are lists underneath!!! |
|
| 1302 | 49x |
fpags <- do_forced_paginate(obj) |
| 1303 | ||
| 1304 |
## if we have more than one forced "page", |
|
| 1305 |
## paginate each of them individually and return the result. |
|
| 1306 |
## forced pagination is ***currently*** only vertical, so |
|
| 1307 |
## we don't have to worry about divying up colwidths here, |
|
| 1308 |
## but we will if we ever allow force_paginate to do horiz |
|
| 1309 |
## pagination. |
|
| 1310 | 49x |
if (length(fpags) > 1) {
|
| 1311 |
# Correction for case we are entering here (page_by) |
|
| 1312 | 1x |
if (!is.null(page_num)) {
|
| 1313 | ! |
prov_footer(obj) <- head(prov_footer(obj), -1) |
| 1314 | ! |
fpags <- lapply(fpags, function(x) {
|
| 1315 | ! |
prov_footer(x) <- head(prov_footer(x), -1) |
| 1316 | ! |
x |
| 1317 |
}) |
|
| 1318 |
} |
|
| 1319 |
# XXX to merge with listings and avoid recursive (after PR #296) |
|
| 1320 | 1x |
deep_pag <- paginate_to_mpfs( # what about the other parameters? |
| 1321 | 1x |
fpags, |
| 1322 | 1x |
pg_size_spec = pg_size_spec, |
| 1323 | 1x |
colwidths = colwidths, |
| 1324 | 1x |
min_siblings = min_siblings, |
| 1325 | 1x |
nosplitin = nosplitin, |
| 1326 | 1x |
fontspec = fontspec, |
| 1327 | 1x |
verbose = verbose, |
| 1328 | 1x |
rep_cols = rep_cols, |
| 1329 | 1x |
page_num = page_num, |
| 1330 | 1x |
round_type = round_type |
| 1331 |
) |
|
| 1332 | 1x |
return(deep_pag) |
| 1333 | 48x |
} else if (has_page_title(fpags[[1]])) {
|
| 1334 | ! |
obj <- fpags[[1]] |
| 1335 |
## we run into forced pagination, but life is short and this should work fine. |
|
| 1336 | ! |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) |
| 1337 | ! |
num_rep_cols(mpf) <- rep_cols |
| 1338 | ! |
if (is.null(colwidths)) {
|
| 1339 | ! |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf, fontspec = fontspec, round_type = round_type) |
| 1340 |
} |
|
| 1341 | ! |
mf_col_widths(mpf) <- colwidths |
| 1342 | ! |
mf_colgap(mpf) <- col_gap |
| 1343 |
} |
|
| 1344 | ||
| 1345 | 48x |
page_indices <- paginate_indices( |
| 1346 | 48x |
obj = mpf, |
| 1347 |
## page_type = page_type, |
|
| 1348 |
## font_family = font_family, |
|
| 1349 |
## font_size = font_size, |
|
| 1350 |
## lineheight = lineheight, |
|
| 1351 |
## landscape = landscape, |
|
| 1352 |
## pg_width = pg_width, |
|
| 1353 |
## pg_height = pg_height, |
|
| 1354 |
## margins = margins, |
|
| 1355 | 48x |
pg_size_spec = pg_size_spec, |
| 1356 |
## lpp = lpp, |
|
| 1357 |
## cpp = cpp, |
|
| 1358 | 48x |
min_siblings = min_siblings, |
| 1359 | 48x |
nosplitin = nosplitin, |
| 1360 | 48x |
colwidths = colwidths, |
| 1361 | 48x |
tf_wrap = tf_wrap, |
| 1362 |
## max_width = max_width, |
|
| 1363 | 48x |
rep_cols = rep_cols, |
| 1364 | 48x |
verbose = verbose, |
| 1365 | 48x |
col_gap = col_gap, |
| 1366 | 48x |
fontspec = fontspec, |
| 1367 | 48x |
round_type = round_type |
| 1368 |
) |
|
| 1369 | ||
| 1370 | 44x |
pagmats <- lapply(page_indices$pag_row_indices, function(ii) {
|
| 1371 | 91x |
mpf_subset_rows(mpf, ii, keycols = .get_keycols_from_listing(obj)) |
| 1372 |
}) |
|
| 1373 |
## these chunks now carry around their (correctly subset) col widths... |
|
| 1374 | 44x |
res <- lapply(pagmats, function(matii) {
|
| 1375 | 91x |
lapply(page_indices$pag_col_indices, function(jj) {
|
| 1376 | 222x |
mpf_subset_cols(matii, jj, keycols = .get_keycols_from_listing(obj)) |
| 1377 |
}) |
|
| 1378 |
}) |
|
| 1379 | ||
| 1380 | 44x |
res <- unlist(res, recursive = FALSE) |
| 1381 | ||
| 1382 |
# Adding page numbers if needed |
|
| 1383 | 44x |
if (!is.null(page_num)) {
|
| 1384 | 14x |
res <- .modify_footer_for_page_nums( |
| 1385 | 14x |
mf_list = res, |
| 1386 | 14x |
page_num_format = page_num, |
| 1387 | 14x |
current_cpp = pg_size_spec$cpp |
| 1388 |
) |
|
| 1389 |
} |
|
| 1390 | ||
| 1391 | 43x |
res |
| 1392 |
} |
|
| 1393 | ||
| 1394 |
.modify_footer_for_page_nums <- function(mf_list, page_num_format, current_cpp) {
|
|
| 1395 | 17x |
total_pages <- length(mf_list) |
| 1396 | 17x |
page_str <- gsub("\\{n\\}", total_pages, page_num_format)
|
| 1397 | 17x |
page_nums <- vapply( |
| 1398 | 17x |
seq_len(total_pages), |
| 1399 | 17x |
function(x) {
|
| 1400 | 135x |
gsub("\\{i\\}", x, page_str)
|
| 1401 |
}, |
|
| 1402 | 17x |
FUN.VALUE = character(1) |
| 1403 |
) |
|
| 1404 | 17x |
page_footer <- sprintf(paste0("%", current_cpp, "s"), page_nums)
|
| 1405 | 17x |
if (any(nchar(page_footer) > current_cpp)) {
|
| 1406 | 1x |
stop("Page numbering string (page_num) is too wide to fit the desired page size width (cpp).")
|
| 1407 |
} |
|
| 1408 | ||
| 1409 | 16x |
lapply(seq_along(mf_list), function(pg_i) {
|
| 1410 | 69x |
prov_footer(mf_list[[pg_i]]) <- c(head(prov_footer(mf_list[[pg_i]]), -1), page_footer[pg_i]) |
| 1411 | 69x |
mf_list[[pg_i]] |
| 1412 |
}) |
|
| 1413 |
} |
|
| 1414 | ||
| 1415 |
# This works only with matrix_form objects |
|
| 1416 |
.is_listing_mf <- function(mf) {
|
|
| 1417 | 1159x |
all(mf_rinfo(mf)$node_class == "listing_df") |
| 1418 |
} |
|
| 1419 | ||
| 1420 |
# Extended copy of get_keycols |
|
| 1421 |
.get_keycols_from_listing <- function(obj) {
|
|
| 1422 | 93x |
if (is(obj, "listing_df")) {
|
| 1423 | ! |
names(which(sapply(obj, is, class2 = "listing_keycol"))) |
| 1424 | 93x |
} else if (is(obj, "MatrixPrintForm") && .is_listing_mf(obj)) {
|
| 1425 | 57x |
obj$listing_keycols |
| 1426 |
} else {
|
|
| 1427 | 36x |
NULL # table case |
| 1428 |
} |
|
| 1429 |
} |
|
| 1430 | ||
| 1431 |
#' @importFrom utils capture.output |
|
| 1432 |
#' @details |
|
| 1433 |
#' `diagnose_pagination` attempts pagination and then, regardless of success or failure, returns diagnostic |
|
| 1434 |
#' information about pagination attempts (if any) after each row and column. |
|
| 1435 |
#' |
|
| 1436 |
#' The diagnostics data reflects the final time the pagination algorithm evaluated a page break at the |
|
| 1437 |
#' specified location, regardless of how many times the position was assessed in total. |
|
| 1438 |
#' |
|
| 1439 |
#' To get information about intermediate attempts, perform pagination with `verbose = TRUE` and inspect |
|
| 1440 |
#' the messages in order. |
|
| 1441 |
#' |
|
| 1442 |
#' @importFrom utils capture.output |
|
| 1443 |
#' |
|
| 1444 |
#' @return |
|
| 1445 |
#' * `diagnose_pagination` returns a `list` containing: |
|
| 1446 |
#' |
|
| 1447 |
#' \describe{
|
|
| 1448 |
#' \item{`lpp_diagnostics`}{Diagnostic information regarding lines per page.}
|
|
| 1449 |
#' \item{`row_diagnostics`}{Basic information about rows, whether pagination was attempted
|
|
| 1450 |
#' after each row, and the final result of such an attempt, if made.} |
|
| 1451 |
#' \item{`cpp_diagnostics`}{Diagnostic information regarding columns per page.}
|
|
| 1452 |
#' \item{`col_diagnostics`}{Very basic information about leaf columns, whether pagination
|
|
| 1453 |
#' was attempted after each leaf column, ad the final result of such attempts, if made.} |
|
| 1454 |
#' } |
|
| 1455 |
#' |
|
| 1456 |
#' @note |
|
| 1457 |
#' For `diagnose_pagination`, the column labels are not displayed in the `col_diagnostics` element |
|
| 1458 |
#' due to certain internal implementation details; rather the diagnostics are reported in terms of |
|
| 1459 |
#' absolute (leaf) column position. This is a known limitation, and may eventually be changed, but the |
|
| 1460 |
#' information remains useful as it is currently reported. |
|
| 1461 |
#' |
|
| 1462 |
#' `diagnose_pagination` is intended for interactive debugging use and *should not be programmed against*, |
|
| 1463 |
#' as the exact content and form of the verbose messages it captures and returns is subject to change. |
|
| 1464 |
#' |
|
| 1465 |
#' Because `diagnose_pagination` relies on `capture.output(type = "message")`, it cannot be used within the |
|
| 1466 |
#' `testthat` (and likely other) testing frameworks, and likely cannot be used within `knitr`/`rmarkdown` |
|
| 1467 |
#' contexts either, as this clashes with those systems' capture of messages. |
|
| 1468 |
#' |
|
| 1469 |
#' @examples |
|
| 1470 |
#' diagnose_pagination(mpf, pg_width = 5, pg_height = 3) |
|
| 1471 |
#' clws <- propose_column_widths(mpf) |
|
| 1472 |
#' clws[1] <- floor(clws[1] / 3) |
|
| 1473 |
#' dgnost <- diagnose_pagination(mpf, pg_width = 5, pg_height = 3, colwidths = clws) |
|
| 1474 |
#' try(diagnose_pagination(mpf, pg_width = 1)) # fails |
|
| 1475 |
#' |
|
| 1476 |
#' @rdname paginate_indices |
|
| 1477 |
#' @export |
|
| 1478 |
diagnose_pagination <- function(obj, |
|
| 1479 |
page_type = "letter", |
|
| 1480 |
font_family = "Courier", |
|
| 1481 |
font_size = 8, |
|
| 1482 |
lineheight = 1, |
|
| 1483 |
landscape = FALSE, |
|
| 1484 |
pg_width = NULL, |
|
| 1485 |
pg_height = NULL, |
|
| 1486 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
| 1487 |
lpp = NA_integer_, |
|
| 1488 |
cpp = NA_integer_, |
|
| 1489 |
min_siblings = 2, |
|
| 1490 |
nosplitin = character(), |
|
| 1491 |
colwidths = propose_column_widths(matrix_form(obj, TRUE, round_type = round_type), |
|
| 1492 |
fontspec = fontspec, round_type = round_type |
|
| 1493 |
), |
|
| 1494 |
tf_wrap = FALSE, |
|
| 1495 |
max_width = NULL, |
|
| 1496 |
indent_size = 2, |
|
| 1497 |
pg_size_spec = NULL, |
|
| 1498 |
rep_cols = num_rep_cols(obj), |
|
| 1499 |
col_gap = 3, |
|
| 1500 |
verbose = FALSE, |
|
| 1501 |
fontspec = font_spec( |
|
| 1502 |
font_family, |
|
| 1503 |
font_size, |
|
| 1504 |
lineheight |
|
| 1505 |
), |
|
| 1506 |
round_type = c("iec", "sas"),
|
|
| 1507 |
...) {
|
|
| 1508 | 6x |
new_dev <- open_font_dev(fontspec) |
| 1509 | 6x |
if (new_dev) {
|
| 1510 | 4x |
on.exit(close_font_dev()) |
| 1511 |
} |
|
| 1512 | 6x |
fpag <- do_forced_paginate(obj) |
| 1513 | 6x |
if (length(fpag) > 1) {
|
| 1514 | 1x |
return(lapply( |
| 1515 | 1x |
fpag, |
| 1516 | 1x |
diagnose_pagination, |
| 1517 | 1x |
page_type = page_type, |
| 1518 | 1x |
landscape = landscape, |
| 1519 | 1x |
pg_width = pg_width, |
| 1520 | 1x |
pg_height = pg_height, |
| 1521 | 1x |
margins = margins, |
| 1522 | 1x |
lpp = lpp, |
| 1523 | 1x |
cpp = cpp, |
| 1524 | 1x |
tf_wrap = tf_wrap, |
| 1525 | 1x |
max_width = max_width, |
| 1526 | 1x |
colwidths = colwidths, |
| 1527 | 1x |
col_gap = col_gap, |
| 1528 | 1x |
min_siblings = min_siblings, |
| 1529 | 1x |
nosplitin = nosplitin, |
| 1530 | 1x |
fontspec = fontspec, |
| 1531 | 1x |
round_type = round_type |
| 1532 |
)) |
|
| 1533 |
} |
|
| 1534 | ||
| 1535 | 5x |
mpf <- matrix_form(obj, TRUE, fontspec = fontspec) |
| 1536 | 5x |
msgres <- capture.output( |
| 1537 |
{
|
|
| 1538 | 5x |
tmp <- try( |
| 1539 | 5x |
paginate_to_mpfs( |
| 1540 | 5x |
obj, |
| 1541 | 5x |
page_type = page_type, |
| 1542 | 5x |
landscape = landscape, |
| 1543 | 5x |
pg_width = pg_width, |
| 1544 | 5x |
pg_height = pg_height, |
| 1545 | 5x |
margins = margins, |
| 1546 | 5x |
lpp = lpp, |
| 1547 | 5x |
cpp = cpp, |
| 1548 | 5x |
tf_wrap = tf_wrap, |
| 1549 | 5x |
max_width = max_width, |
| 1550 | 5x |
colwidths = colwidths, |
| 1551 | 5x |
col_gap = col_gap, |
| 1552 | 5x |
min_siblings = min_siblings, |
| 1553 | 5x |
nosplitin = nosplitin, |
| 1554 | 5x |
fontspec = fontspec, |
| 1555 | 5x |
round_type = round_type, |
| 1556 | 5x |
verbose = TRUE |
| 1557 |
) |
|
| 1558 |
) |
|
| 1559 |
}, |
|
| 1560 | 5x |
type = "message" |
| 1561 |
) |
|
| 1562 | 5x |
if (is(tmp, "try-error") && grepl("Width of row labels equal to or larger", tmp)) {
|
| 1563 | ! |
cond <- attr(tmp, "condition") |
| 1564 | ! |
stop(conditionMessage(cond), call. = conditionCall(cond)) |
| 1565 |
} |
|
| 1566 | ||
| 1567 | 5x |
lpp_diagnostic <- grep("^(Determining lines|Lines per page available).*$", msgres, value = TRUE)
|
| 1568 | 5x |
cpp_diagnostic <- unique(grep("^Adjusted characters per page.*$", msgres, value = TRUE))
|
| 1569 | ||
| 1570 | 5x |
mpf <- do_cell_fnotes_wrap( |
| 1571 | 5x |
mpf, |
| 1572 | 5x |
widths = colwidths, max_width = max_width, tf_wrap = tf_wrap, |
| 1573 | 5x |
fontspec = font_spec(font_family, font_size, lineheight) |
| 1574 |
) |
|
| 1575 | 5x |
mpf <- mpf_infer_cinfo(mpf, colwidths = colwidths, fontspec = fontspec) |
| 1576 | ||
| 1577 | 5x |
rownls <- grep("Checking pagination after row", msgres, fixed = TRUE)
|
| 1578 | 5x |
rownum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[rownls]))
|
| 1579 | 5x |
rowmsgs <- vapply(unique(rownum), function(ii) {
|
| 1580 | ! |
idx <- max(which(rownum == ii)) |
| 1581 | ! |
gsub("\\t[.]*", "", msgres[rownls[idx] + 1])
|
| 1582 |
}, "") |
|
| 1583 | ||
| 1584 | 5x |
msgdf <- data.frame( |
| 1585 | 5x |
abs_rownumber = unique(rownum), |
| 1586 | 5x |
final_pag_result = rowmsgs, stringsAsFactors = FALSE |
| 1587 |
) |
|
| 1588 | 5x |
rdf <- mf_rinfo(mpf)[, c("abs_rownumber", "label", "self_extent", "par_extent", "node_class")]
|
| 1589 | 5x |
rdf$pag_attempted <- rdf$abs_rownumber %in% rownum |
| 1590 | 5x |
row_diagnose <- merge(rdf, msgdf, by = "abs_rownumber", all.x = TRUE) |
| 1591 | ||
| 1592 | 5x |
colnls <- grep("Checking pagination after column", msgres, fixed = TRUE)
|
| 1593 | 5x |
colnum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[colnls]))
|
| 1594 | 5x |
colmsgs <- vapply(unique(colnum), function(ii) {
|
| 1595 | ! |
idx <- max(which(colnum == ii)) |
| 1596 | ! |
gsub("\\t[.]*", "", msgres[colnls[idx] + 1])
|
| 1597 |
}, "") |
|
| 1598 | ||
| 1599 | 5x |
colmsgdf <- data.frame( |
| 1600 | 5x |
abs_rownumber = unique(colnum), |
| 1601 | 5x |
final_pag_result = colmsgs, |
| 1602 | 5x |
stringsAsFactors = FALSE |
| 1603 |
) |
|
| 1604 | 5x |
cdf <- mf_cinfo(mpf)[, c("abs_rownumber", "self_extent")]
|
| 1605 | 5x |
cdf$pag_attempted <- cdf$abs_rownumber %in% colnum |
| 1606 | 5x |
col_diagnose <- merge(cdf, colmsgdf, by = "abs_rownumber", all.x = TRUE) |
| 1607 | 5x |
names(col_diagnose) <- gsub("^abs_rownumber$", "abs_colnumber", names(col_diagnose))
|
| 1608 | 5x |
list( |
| 1609 | 5x |
lpp_diagnostics = lpp_diagnostic, |
| 1610 | 5x |
row_diagnostics = row_diagnose, |
| 1611 | 5x |
cpp_diagnostics = cpp_diagnostic, |
| 1612 | 5x |
col_diagnostics = col_diagnose |
| 1613 |
) |
|
| 1614 |
} |
| 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 | 269x |
has_topleft <- mf_has_topleft(matform) |
| 15 | 269x |
strmat <- mf_strings(matform) |
| 16 | 269x |
frmmat <- mf_formats(matform) |
| 17 | 269x |
spamat <- mf_spans(matform) |
| 18 | 269x |
alimat <- mf_aligns(matform) |
| 19 | 269x |
nr_header <- mf_nrheader(matform) |
| 20 | 269x |
nl_inds_header <- seq(mf_nlheader(matform)) |
| 21 | 269x |
hdr_inds <- seq(nr_header) |
| 22 | ||
| 23 |
# hack that is necessary only if top-left is bottom aligned (default) |
|
| 24 | 269x |
topleft_has_nl_char <- FALSE |
| 25 | ||
| 26 |
# Exract top-left information |
|
| 27 | 269x |
tl <- strmat[nl_inds_header, 1, drop = TRUE] |
| 28 | 269x |
has_topleft <- has_topleft && any(nzchar(tl)) # update topleft info if there is any |
| 29 | ||
| 30 | 269x |
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 | 269x |
line_grouping <- mf_lgrouping(matform) |
| 47 | 269x |
if (any(duplicated(line_grouping))) {
|
| 48 | 12x |
strmat <- .compress_mat(strmat, line_grouping, "nl") |
| 49 | 12x |
frmmat <- .compress_mat(frmmat, line_grouping, "unique") # never not unique |
| 50 | 12x |
spamat <- .compress_mat(spamat, line_grouping, "unique") |
| 51 | 12x |
alimat <- .compress_mat(alimat, line_grouping, "unique") |
| 52 | 12x |
line_grouping <- unique(line_grouping) |
| 53 |
} |
|
| 54 | ||
| 55 |
# nlines detects if there is a newline character |
|
| 56 |
# colwidths = NULL, max_width = NULL, fontspec = NULL |
|
| 57 |
# because we don't care about wrapping here we're counting lines |
|
| 58 |
# TODO probably better if we had a nlines_nowrap fun to be more explicit |
|
| 59 | ||
| 60 | 269x |
row_nlines <- apply( # tells how many nlines for each row |
| 61 | 269x |
strmat, |
| 62 | 269x |
1, |
| 63 | 269x |
function(x) {
|
| 64 | 5142x |
max( |
| 65 | 5142x |
vapply(x, |
| 66 | 5142x |
nlines, |
| 67 | 5142x |
colwidths = NULL, |
| 68 | 5142x |
max_width = NULL, |
| 69 | 5142x |
fontspec = NULL, 1L |
| 70 |
), |
|
| 71 | 5142x |
1L |
| 72 |
) |
|
| 73 |
} |
|
| 74 |
) |
|
| 75 | ||
| 76 |
# Correction for the case where there are more lines for topleft material than for cols |
|
| 77 | 269x |
if (has_topleft && (sum(row_nlines[hdr_inds]) < tl_how_many_nl)) {
|
| 78 | 1x |
row_nlines[1] <- row_nlines[1] + tl_how_many_nl - sum(row_nlines[hdr_inds]) |
| 79 |
} |
|
| 80 | ||
| 81 |
# There is something to change |
|
| 82 | 269x |
if (any(row_nlines > 1) || topleft_has_nl_char) {
|
| 83 |
# False: Padder should be bottom aligned if no topleft (case of rlistings) |
|
| 84 |
# It is always bottom: tl_padder <- ifelse(has_topleft, pad_vert_top, pad_vert_bottom) |
|
| 85 | ||
| 86 | 44x |
newstrmat <- rbind( |
| 87 | 44x |
cbind( |
| 88 | 44x |
expand_mat_rows(strmat[hdr_inds, 1, drop = FALSE], |
| 89 | 44x |
row_nlines[hdr_inds], |
| 90 | 44x |
cpadder = pad_vert_bottom # topleft info is NOT top aligned |
| 91 |
), |
|
| 92 | 44x |
expand_mat_rows(strmat[hdr_inds, -1, drop = FALSE], |
| 93 | 44x |
row_nlines[hdr_inds], |
| 94 | 44x |
cpadder = pad_vert_bottom # colnames are bottom aligned |
| 95 |
) |
|
| 96 |
), |
|
| 97 | 44x |
expand_mat_rows(strmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) |
| 98 |
) |
|
| 99 | 44x |
colnames(newstrmat) <- colnames(strmat) |
| 100 | ||
| 101 | 44x |
newfrmmat <- rbind( |
| 102 | 44x |
expand_mat_rows( |
| 103 | 44x |
frmmat[hdr_inds, , drop = FALSE], |
| 104 | 44x |
row_nlines[hdr_inds], |
| 105 | 44x |
cpadder = pad_vert_bottom |
| 106 |
), |
|
| 107 | 44x |
expand_mat_rows(frmmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) |
| 108 |
) |
|
| 109 | ||
| 110 | 44x |
if (has_topleft) {
|
| 111 | 6x |
starts_from_ind <- if (sum(row_nlines[hdr_inds]) - tl_how_many_nl > 0) {
|
| 112 | 4x |
sum(row_nlines[hdr_inds]) - tl_how_many_nl |
| 113 |
} else {
|
|
| 114 | 2x |
0 |
| 115 |
} |
|
| 116 | 6x |
newstrmat[starts_from_ind + seq_along(tl_to_add_back), 1] <- tl_to_add_back |
| 117 |
} |
|
| 118 | ||
| 119 | 44x |
mf_strings(matform) <- newstrmat |
| 120 | 44x |
mf_formats(matform) <- newfrmmat |
| 121 | 44x |
mf_spans(matform) <- expand_mat_rows(spamat, row_nlines, rep_vec_to_len) |
| 122 | 44x |
mf_aligns(matform) <- expand_mat_rows(alimat, row_nlines, rep_vec_to_len) |
| 123 |
## mf_display(matform) <- expand_mat_rows(mf_display(matform), row_nlines, rep_vec_to_len) |
|
| 124 | 44x |
mf_lgrouping(matform) <- rep(line_grouping, times = row_nlines) |
| 125 |
} |
|
| 126 | ||
| 127 |
# Solve \n in titles |
|
| 128 | 269x |
if (any(grepl("\n", all_titles(matform)))) {
|
| 129 | 2x |
if (any(grepl("\n", main_title(matform)))) {
|
| 130 | 2x |
tmp_title_vec <- .quick_handle_nl(main_title(matform)) |
| 131 | 2x |
main_title(matform) <- tmp_title_vec[1] |
| 132 | 2x |
subtitles(matform) <- c(tmp_title_vec[-1], .quick_handle_nl(subtitles(matform))) |
| 133 |
} else {
|
|
| 134 | ! |
subtitles(matform) <- .quick_handle_nl(subtitles(matform)) |
| 135 |
} |
|
| 136 |
} |
|
| 137 | ||
| 138 |
# Solve \n in footers |
|
| 139 | 269x |
main_footer(matform) <- .quick_handle_nl(main_footer(matform)) |
| 140 | 269x |
prov_footer(matform) <- .quick_handle_nl(prov_footer(matform)) |
| 141 | ||
| 142 |
# xxx \n in page titles are not working atm (I think) |
|
| 143 | 269x |
matform |
| 144 |
} |
|
| 145 | ||
| 146 |
.quick_handle_nl <- function(str_v) {
|
|
| 147 | 542x |
if (any(grepl("\n", str_v))) {
|
| 148 | 4x |
unlist(strsplit(str_v, "\n", fixed = TRUE)) |
| 149 |
} else {
|
|
| 150 | 538x |
str_v |
| 151 |
} |
|
| 152 |
} |
|
| 153 | ||
| 154 |
#### Helper function to recompact the lines following line groupings to then have them expanded again |
|
| 155 |
# This version now ensures its output has the exact same row order as the original function. |
|
| 156 |
# -> see file in `dev/benchmark_compress_mat.R` for a benchmark of the two versions(+). |
|
| 157 |
.compress_mat <- function(mat, line_grouping, collapse_method = c("nl", "unique")) {
|
|
| 158 | 48x |
df <- as.data.frame(mat, stringsAsFactors = FALSE) |
| 159 | ||
| 160 |
# The original function processes groups in the order they appear in `unique(line_grouping)`. |
|
| 161 |
# We create a factor with levels set to that specific order to force `split` to maintain it. |
|
| 162 | 48x |
factor_grouping <- factor(line_grouping, levels = unique(line_grouping)) |
| 163 | 48x |
list_of_dfs <- split(df, factor_grouping) |
| 164 | ||
| 165 | 48x |
if (collapse_method[1] == "nl") {
|
| 166 | 12x |
result_list <- lapply(list_of_dfs, function(sub_df) {
|
| 167 | 127x |
sapply(sub_df, paste, collapse = "\n") |
| 168 |
}) |
|
| 169 |
} else { # "unique" method
|
|
| 170 | 36x |
result_list <- lapply(list_of_dfs, function(sub_df) {
|
| 171 | 381x |
sapply(sub_df, function(col) {
|
| 172 | 4344x |
val <- unique(col[nzchar(col)]) |
| 173 | 4344x |
if (length(val) > 1) {
|
| 174 | ! |
stop("Problem in linegroupings! Some do not have the same values.")
|
| 175 | 4344x |
} else if (length(val) == 0) {
|
| 176 |
"" |
|
| 177 |
} else {
|
|
| 178 | 4200x |
val |
| 179 |
} |
|
| 180 |
}) |
|
| 181 |
}) |
|
| 182 |
} |
|
| 183 | 48x |
do.call("rbind", result_list)
|
| 184 |
} |
|
| 185 | ||
| 186 |
disp_from_spans <- function(spans) {
|
|
| 187 | 419x |
display <- matrix(rep(TRUE, length(spans)), ncol = ncol(spans)) |
| 188 | ||
| 189 | 419x |
print_cells_mat <- spans == 1L |
| 190 | 419x |
if (!all(print_cells_mat)) {
|
| 191 | 1x |
display_rws <- lapply( |
| 192 | 1x |
seq_len(nrow(spans)), |
| 193 | 1x |
function(i) {
|
| 194 | 2x |
print_cells <- print_cells_mat[i, ] |
| 195 | 2x |
row <- spans[i, ] |
| 196 |
## display <- t(apply(spans, 1, function(row) {
|
|
| 197 |
## print_cells <- row == 1 |
|
| 198 | ||
| 199 | 2x |
if (!all(print_cells)) {
|
| 200 |
## need to calculate which cell need to be printed |
|
| 201 | 1x |
print_cells <- spans_to_viscell(row) |
| 202 |
} |
|
| 203 | 2x |
print_cells |
| 204 |
} |
|
| 205 |
) |
|
| 206 | 1x |
display <- do.call(rbind, display_rws) |
| 207 |
} |
|
| 208 | 419x |
display |
| 209 |
} |
|
| 210 | ||
| 211 |
#' Constructor for Matrix Print Form |
|
| 212 |
#' |
|
| 213 |
#' Constructor for `MatrixPrintForm`, an intermediate representation for ASCII table printing. |
|
| 214 |
#' |
|
| 215 |
#' @inheritParams open_font_dev |
|
| 216 |
#' @param strings (`character matrix`)\cr matrix of formatted, ready-to-display strings |
|
| 217 |
#' organized as they will be positioned when rendered. Elements that span more than one |
|
| 218 |
#' column must be followed by the correct number of placeholders (typically either empty |
|
| 219 |
#' strings or repeats of the value). |
|
| 220 |
#' @param spans (`numeric matrix`)\cr matrix of same dimension as `strings` giving the |
|
| 221 |
#' spanning information for each element. Must be repeated to match placeholders in `strings`. |
|
| 222 |
#' @param aligns (`character matrix`)\cr matrix of same dimension as `strings` giving the text |
|
| 223 |
#' alignment information for each element. Must be repeated to match placeholders in `strings`. |
|
| 224 |
#' Must be a supported text alignment. See [decimal_align] for allowed values. |
|
| 225 |
#' @param formats (`matrix`)\cr matrix of same dimension as `strings` giving the text format |
|
| 226 |
#' information for each element. Must be repeated to match placeholders in `strings`. |
|
| 227 |
#' @param row_info (`data.frame`)\cr data frame with row-information necessary for pagination (see |
|
| 228 |
#' [basic_pagdf()] for more details). |
|
| 229 |
#' @param colpaths (`list` or `NULL`)\cr `NULL`, or a list of paths to each leaf column, |
|
| 230 |
#' for use during horizontal pagination. |
|
| 231 |
#' @param line_grouping (`integer`)\cr sequence of integers indicating how print lines correspond |
|
| 232 |
#' to semantic rows in the object. Typically this should not be set manually unless |
|
| 233 |
#' `expand_newlines` is set to `FALSE`. |
|
| 234 |
#' @param ref_fnotes (`list`)\cr referential footnote information, if applicable. |
|
| 235 |
#' @param nlines_header (`numeric(1)`)\cr number of lines taken up by the values of the header |
|
| 236 |
#' (i.e. not including the divider). |
|
| 237 |
#' @param nrow_header (`numeric(1)`)\cr number of *rows* corresponding to the header. |
|
| 238 |
#' @param has_topleft (`flag`)\cr does the corresponding table have "top left information" |
|
| 239 |
#' which should be treated differently when expanding newlines. Ignored if `expand_newlines` |
|
| 240 |
#' is `FALSE`. |
|
| 241 |
#' @param has_rowlabs (`flag`)\cr do the matrices (`strings`, `spans`, `aligns`) each contain a |
|
| 242 |
#' column that corresponds with row labels (rather than with table cell values). Defaults to `TRUE`. |
|
| 243 |
#' @param main_title (`string`)\cr main title as a string. |
|
| 244 |
#' @param subtitles (`character`)\cr subtitles, as a character vector. |
|
| 245 |
#' @param page_titles (`character`)\cr page-specific titles, as a character vector. |
|
| 246 |
#' @param main_footer (`character`)\cr main footer, as a character vector. |
|
| 247 |
#' @param prov_footer (`character`)\cr provenance footer information, as a character vector. |
|
| 248 |
#' @param listing_keycols (`character`)\cr. if matrix form of a listing, this contains |
|
| 249 |
#' the key columns as a character vector. |
|
| 250 |
#' @param header_section_div (`string`)\cr divider to be used between header and body sections. |
|
| 251 |
#' @param horizontal_sep (`string`)\cr horizontal separator to be used for printing divisors |
|
| 252 |
#' between header and table body and between different footers. |
|
| 253 |
#' @param expand_newlines (`flag`)\cr whether the matrix form generated should expand rows whose |
|
| 254 |
#' values contain newlines into multiple 'physical' rows (as they will appear when rendered into |
|
| 255 |
#' ASCII). Defaults to `TRUE`. |
|
| 256 |
#' @param col_gap (`numeric(1)`)\cr space (in characters) between columns. |
|
| 257 |
#' @param table_inset (`numeric(1)`)\cr table inset. See [table_inset()]. |
|
| 258 |
#' @param colwidths (`numeric` or `NULL`)\cr column rendering widths. If non-`NULL`, must have length |
|
| 259 |
#' equal to `ncol(strings)`. |
|
| 260 |
#' @param indent_size (`numeric(1)`)\cr number of spaces to be used per level of indent (if supported by |
|
| 261 |
#' the relevant method). Defaults to 2. |
|
| 262 |
#' @param rep_cols (`numeric(1)`)\cr number of columns to be repeated as context during horizontal pagination. |
|
| 263 |
#' |
|
| 264 |
#' @return An object of class `MatrixPrintForm`. Currently this is implemented as an S3 class inheriting |
|
| 265 |
#' from list with the following elements: |
|
| 266 |
#' \describe{
|
|
| 267 |
#' \item{`strings`}{see argument.}
|
|
| 268 |
#' \item{`spans`}{see argument.}
|
|
| 269 |
#' \item{`aligns`}{see argument.}
|
|
| 270 |
#' \item{`display`}{logical matrix of same dimension as `strings` that specifies whether an element
|
|
| 271 |
#' in `strings` will be displayed when the table is rendered.} |
|
| 272 |
#' \item{`formats`}{see argument.}
|
|
| 273 |
#' \item{`row_info`}{see argument.}
|
|
| 274 |
#' \item{`line_grouping`}{see argument.}
|
|
| 275 |
#' \item{`ref_footnotes`}{see argument.}
|
|
| 276 |
#' \item{`main_title`}{see argument.}
|
|
| 277 |
#' \item{`subtitles`}{see argument.}
|
|
| 278 |
#' \item{`page_titles`}{see argument.}
|
|
| 279 |
#' \item{`main_footer`}{see argument.}
|
|
| 280 |
#' \item{`prov_footer`}{see argument.}
|
|
| 281 |
#' \item{`header_section_div`}{see argument.}
|
|
| 282 |
#' \item{`horizontal_sep`}{see argument.}
|
|
| 283 |
#' \item{`col_gap`}{see argument.}
|
|
| 284 |
#' \item{`table_inset`}{see argument.}
|
|
| 285 |
#' } |
|
| 286 |
#' |
|
| 287 |
#' as well as the following attributes: |
|
| 288 |
#' |
|
| 289 |
#' \describe{
|
|
| 290 |
#' \item{`nlines_header`}{see argument.}
|
|
| 291 |
#' \item{`nrow_header`}{see argument.}
|
|
| 292 |
#' \item{`ncols`}{number of columns *of the table*, not including any row names/row labels}
|
|
| 293 |
#' } |
|
| 294 |
#' |
|
| 295 |
#' @note The bare constructor for the `MatrixPrintForm` should generally |
|
| 296 |
#' only be called by `matrix_form` custom methods, and almost never from other code. |
|
| 297 |
#' |
|
| 298 |
#' @examples |
|
| 299 |
#' basic_matrix_form(iris) # calls matrix_form which calls this constructor |
|
| 300 |
#' |
|
| 301 |
#' @export |
|
| 302 |
MatrixPrintForm <- function(strings = NULL, |
|
| 303 |
spans, |
|
| 304 |
aligns, |
|
| 305 |
formats, |
|
| 306 |
row_info, |
|
| 307 |
colpaths = NULL, |
|
| 308 |
line_grouping = seq_len(NROW(strings)), |
|
| 309 |
ref_fnotes = list(), |
|
| 310 |
nlines_header, |
|
| 311 |
nrow_header, |
|
| 312 |
has_topleft = TRUE, |
|
| 313 |
has_rowlabs = has_topleft, |
|
| 314 |
expand_newlines = TRUE, |
|
| 315 |
main_title = "", |
|
| 316 |
subtitles = character(), |
|
| 317 |
page_titles = character(), |
|
| 318 |
listing_keycols = NULL, |
|
| 319 |
main_footer = "", |
|
| 320 |
prov_footer = character(), |
|
| 321 |
header_section_div = NA_character_, |
|
| 322 |
horizontal_sep = default_hsep(), |
|
| 323 |
col_gap = 3, |
|
| 324 |
table_inset = 0L, |
|
| 325 |
colwidths = NULL, |
|
| 326 |
indent_size = 2, |
|
| 327 |
fontspec = font_spec(), |
|
| 328 |
rep_cols = 0L) {
|
|
| 329 | 54x |
display <- disp_from_spans(spans) |
| 330 | ||
| 331 | 54x |
ncs <- if (has_rowlabs) ncol(strings) - 1 else ncol(strings) |
| 332 | 54x |
ret <- structure( |
| 333 | 54x |
list( |
| 334 | 54x |
strings = strings, |
| 335 | 54x |
spans = spans, |
| 336 | 54x |
aligns = aligns, |
| 337 | 54x |
display = display, |
| 338 | 54x |
formats = formats, |
| 339 | 54x |
row_info = row_info, |
| 340 | 54x |
line_grouping = line_grouping, |
| 341 | 54x |
ref_footnotes = ref_fnotes, |
| 342 | 54x |
main_title = main_title, |
| 343 | 54x |
subtitles = subtitles, |
| 344 | 54x |
page_titles = page_titles, |
| 345 | 54x |
main_footer = main_footer, |
| 346 | 54x |
prov_footer = prov_footer, |
| 347 | 54x |
header_section_div = header_section_div, |
| 348 | 54x |
horizontal_sep = horizontal_sep, |
| 349 | 54x |
col_gap = col_gap, |
| 350 | 54x |
listing_keycols = listing_keycols, |
| 351 | 54x |
table_inset = as.integer(table_inset), |
| 352 | 54x |
has_topleft = has_topleft, |
| 353 | 54x |
indent_size = indent_size, |
| 354 | 54x |
col_widths = colwidths, |
| 355 | 54x |
fontspec = fontspec, |
| 356 | 54x |
num_rep_cols = rep_cols |
| 357 |
), |
|
| 358 | 54x |
nrow_header = nrow_header, |
| 359 | 54x |
ncols = ncs, |
| 360 | 54x |
class = c("MatrixPrintForm", "list")
|
| 361 |
) |
|
| 362 | ||
| 363 |
## .do_mat_expand(ret) |
|
| 364 | 54x |
if (expand_newlines) {
|
| 365 | 54x |
ret <- mform_handle_newlines(ret) |
| 366 |
} |
|
| 367 | ||
| 368 |
## ret <- shove_refdf_into_rowinfo(ret) |
|
| 369 | 54x |
if (is.null(colwidths)) {
|
| 370 | 54x |
colwidths <- propose_column_widths(ret, fontspec = fontspec) |
| 371 |
} |
|
| 372 | 54x |
mf_col_widths(ret) <- colwidths |
| 373 | 54x |
ret <- mform_build_refdf(ret) |
| 374 | 54x |
ret <- mpf_infer_cinfo(ret, colpaths = colpaths, fontspec = fontspec) |
| 375 | ||
| 376 | 54x |
ret |
| 377 |
} |
|
| 378 | ||
| 379 |
mf_update_cinfo <- function(mf, colwidths = NULL, rep_cols = NULL) {
|
|
| 380 | 472x |
need_update <- FALSE |
| 381 | 472x |
if (!is.null(colwidths)) {
|
| 382 | 376x |
mf$col_widths <- colwidths |
| 383 | 376x |
need_update <- TRUE |
| 384 |
} |
|
| 385 | ||
| 386 | 472x |
if (!is.null(rep_cols)) {
|
| 387 | 96x |
mf$num_rep_cols <- rep_cols |
| 388 | 96x |
need_update <- TRUE |
| 389 |
} |
|
| 390 | ||
| 391 | 472x |
if (need_update && !is.null(mf_cinfo(mf))) {
|
| 392 | 418x |
cinfo <- mf_cinfo(mf) |
| 393 | 418x |
r_colwidths <- mf_col_widths(mf) |
| 394 | 418x |
has_rlabs <- mf_has_rlabels(mf) |
| 395 | 418x |
if (has_rlabs) {
|
| 396 | 332x |
r_colwidths <- r_colwidths[-1] ## row label widths |
| 397 |
} |
|
| 398 | 418x |
cinfo$self_extent <- r_colwidths |
| 399 | 418x |
nrepcols <- num_rep_cols(mf) |
| 400 | 418x |
rep_seq <- seq_len(nrepcols) |
| 401 | 418x |
is_listing <- !is.null(mf$listing_keycols) |
| 402 | ||
| 403 |
# empty listing |
|
| 404 | 418x |
if (is_listing && length(mf$listing_keycols) == 1 && length(r_colwidths) - nrepcols < 1) {
|
| 405 | ! |
cinfo$par_extent <- 0 |
| 406 |
# listing with all key columns |
|
| 407 | 418x |
} else if (is_listing && mf_ncol(mf) == length(mf$listing_keycols)) {
|
| 408 | 1x |
cinfo$par_extent <- cumsum(c(0, cinfo$self_extent[seq_len(nrepcols - 1)])) |
| 409 |
} else {
|
|
| 410 | 417x |
cinfo$par_extent <- cumsum(c(0, cinfo$self_extent[seq_len(nrepcols)], rep(0, length(r_colwidths) - nrepcols - 1))) |
| 411 |
} |
|
| 412 | ||
| 413 | 418x |
cinfo$reprint_inds <- I(lapply(seq_len(NROW(cinfo)), function(i) rep_seq[rep_seq < i])) |
| 414 | 418x |
mf_cinfo(mf) <- cinfo |
| 415 |
} |
|
| 416 | 472x |
mf |
| 417 |
} |
|
| 418 | ||
| 419 |
#' Create a row for a referential footnote information data frame |
|
| 420 |
#' |
|
| 421 |
#' @inheritParams nlines |
|
| 422 |
#' @param row_path (`character`)\cr row path (or `NA_character_` for none). |
|
| 423 |
#' @param col_path (`character`)\cr column path (or `NA_character_` for none). |
|
| 424 |
#' @param row (`integer(1)`)\cr integer position of the row. |
|
| 425 |
#' @param col (`integer(1)`)\cr integer position of the column. |
|
| 426 |
#' @param symbol (`string`)\cr symbol for the reference. `NA_character_` to use the |
|
| 427 |
#' `ref_index` automatically. |
|
| 428 |
#' @param ref_index (`integer(1)`)\cr index of the footnote, used for ordering even when |
|
| 429 |
#' symbol is not `NA`. |
|
| 430 |
#' @param msg (`string`)\cr the string message, not including the symbol portion (`{symbol} - `)
|
|
| 431 |
#' |
|
| 432 |
#' @return A single row data frame with the appropriate columns. |
|
| 433 |
#' |
|
| 434 |
#' @export |
|
| 435 |
ref_df_row <- function(row_path = NA_character_, |
|
| 436 |
col_path = NA_character_, |
|
| 437 |
row = NA_integer_, |
|
| 438 |
col = NA_integer_, |
|
| 439 |
symbol = NA_character_, |
|
| 440 |
ref_index = NA_integer_, |
|
| 441 |
msg = NA_character_, |
|
| 442 |
max_width = NULL) {
|
|
| 443 | 6390x |
nlines <- nlines(msg, max_width = max_width) |
| 444 | 6390x |
data.frame( |
| 445 | 6390x |
row_path = I(list(row_path)), |
| 446 | 6390x |
col_path = I(list(col_path)), |
| 447 | 6390x |
row = row, |
| 448 | 6390x |
col = col, |
| 449 | 6390x |
symbol = symbol, |
| 450 | 6390x |
ref_index = ref_index, |
| 451 | 6390x |
msg = msg, |
| 452 | 6390x |
nlines = nlines, |
| 453 | 6390x |
stringsAsFactors = FALSE |
| 454 |
) |
|
| 455 |
} |
|
| 456 | ||
| 457 |
## this entire thing is a hatchetjob of a hack which should not be necessary. |
|
| 458 |
## mf_rinfo(mform) should have the relevant info in it and |
|
| 459 |
## mf_cinfo(mform) should be non-null (!!!) and have the info in it |
|
| 460 |
## in which case this becomes silly and dumb, but here we are, so here we go. |
|
| 461 |
infer_ref_info <- function(mform, colspace_only) {
|
|
| 462 | 216x |
if (colspace_only) {
|
| 463 | 108x |
idx <- seq_len(mf_nlheader(mform)) |
| 464 |
} else {
|
|
| 465 | 108x |
idx <- seq_len(nrow(mf_strings(mform))) |
| 466 |
} |
|
| 467 | ||
| 468 | 216x |
hasrlbs <- mf_has_rlabels(mform) |
| 469 | ||
| 470 | 216x |
strs <- mf_strings(mform)[idx, , drop = FALSE] |
| 471 | ||
| 472 |
## they're nested so \\2 is the inner one, without the brackets |
|
| 473 |
## include space in front of { so we don't catch \{ when
|
|
| 474 |
## rtfs want to pass markup through |
|
| 475 | 216x |
refs <- gsub("^[^{]*([{]([^}]+)[}]){0,1}$", "\\2", strs)
|
| 476 |
## handle spanned values |
|
| 477 | 216x |
refs[!mf_display(mform)[idx, ]] <- "" |
| 478 | ||
| 479 |
## we want to count across rows first, not down columns, cause |
|
| 480 |
## thats how footnote numbering works |
|
| 481 | 216x |
refs_inorder <- as.vector(t(refs)) |
| 482 | 216x |
keepem <- nzchar(refs_inorder) |
| 483 | 216x |
if (sum(keepem) == 0) {
|
| 484 | 214x |
return(ref_df_row()[0, ]) |
| 485 |
} |
|
| 486 | ||
| 487 | 2x |
refs_spl <- strsplit(refs_inorder[keepem], ", ", fixed = TRUE) |
| 488 | 2x |
runvec <- vapply(refs_spl, length, 1L) |
| 489 | ||
| 490 | 2x |
row_index <- as.vector( |
| 491 | 2x |
t(do.call(cbind, replicate(ncol(strs), list(mf_lgrouping(mform)[idx] - mf_nlheader(mform))))) |
| 492 | 2x |
)[keepem] |
| 493 | 2x |
row_index[row_index < 1] <- NA_integer_ |
| 494 | 2x |
c_torep <- if (hasrlbs) c(NA_integer_, seq(1, ncol(strs) - 1)) else seq_len(ncol(strs)) |
| 495 | 2x |
col_index <- rep(c_torep, nrow(strs))[keepem] |
| 496 | ||
| 497 | 2x |
ret <- data.frame( |
| 498 | 2x |
symbol = unlist(refs_spl), |
| 499 | 2x |
row_path = I(mf_rinfo(mform)$path[rep(row_index, times = runvec)]), |
| 500 | 2x |
row = rep(row_index, times = runvec), |
| 501 | 2x |
col = rep(col_index, times = runvec) |
| 502 |
) |
|
| 503 | 2x |
ret$msg <- vapply(ret$symbol, function(sym) {
|
| 504 | 16x |
fullmsg <- unique(grep(paste0("{", sym, "}"), fixed = TRUE, mf_rfnotes(mform), value = TRUE))
|
| 505 | 16x |
gsub("^[{][^}]+[}] - ", "", fullmsg)
|
| 506 |
}, "") |
|
| 507 | ||
| 508 | 2x |
col_pths <- mf_col_paths(mform) |
| 509 | 2x |
ret$col_path <- replicate(nrow(ret), list(NA_character_)) |
| 510 | 2x |
non_na_col <- !is.na(ret$col) |
| 511 | 2x |
ret$col_path[non_na_col] <- col_pths[ret$col[non_na_col]] |
| 512 | 2x |
ret$ref_index <- match(ret$symbol, unique(ret$symbol)) |
| 513 |
## |
|
| 514 | 2x |
ret$nlines <- vapply(paste0("{", ret$symbol, "} - ", ret$msg), nlines, 1L)
|
| 515 | 2x |
ret <- ret[, names(ref_df_row())] |
| 516 | 2x |
ret |
| 517 |
} |
|
| 518 | ||
| 519 |
mform_build_refdf <- function(mform) {
|
|
| 520 | 108x |
rdf <- mf_rinfo(mform) |
| 521 | 108x |
cref_rows <- infer_ref_info(mform, colspace_only = TRUE) |
| 522 |
## this will recheck sometimes but its safer and shouldn't |
|
| 523 |
## be too prohibitively costly |
|
| 524 | 108x |
if (NROW(rdf$ref_info_df) > 0 && sum(sapply(rdf$ref_info_df, NROW)) > 0) {
|
| 525 | ! |
cref_rows <- infer_ref_info(mform, colspace_only = TRUE) |
| 526 | ! |
rref_rows <- rdf$ref_info_df |
| 527 |
} else {
|
|
| 528 | 108x |
cref_rows <- infer_ref_info(mform, colspace_only = FALSE) |
| 529 | 108x |
rref_rows <- list() |
| 530 |
} |
|
| 531 | 108x |
mf_fnote_df(mform) <- do.call(rbind.data.frame, c(list(cref_rows), rref_rows)) |
| 532 | 108x |
update_mf_nlines(mform, colwidths = mf_col_widths(mform), max_width = NULL) |
| 533 |
} |
|
| 534 | ||
| 535 |
## hide the implementation behind abstraction in case we decide we want a real class someday |
|
| 536 |
#' Getters and setters for aspects of `MatrixPrintForm` objects |
|
| 537 |
#' |
|
| 538 |
#' Most of these functions, particularly the setters, are intended almost exclusively for |
|
| 539 |
#' internal use in, e.g., [`matrix_form`] methods, and should generally not be called by end users. |
|
| 540 |
#' |
|
| 541 |
#' @param mf (`MatrixPrintForm`)\cr a `MatrixPrintForm` object. |
|
| 542 |
#' @param value (`ANY`)\cr the new value for the component in question. |
|
| 543 |
#' |
|
| 544 |
#' @return |
|
| 545 |
#' * Getters return the associated element of `mf`. |
|
| 546 |
#' * Setters return the modified `mf` object. |
|
| 547 |
#' |
|
| 548 |
#' @export |
|
| 549 |
#' @rdname mpf_accessors |
|
| 550 | 4923x |
mf_strings <- function(mf) mf$strings |
| 551 | ||
| 552 |
#' @export |
|
| 553 |
#' @rdname mpf_accessors |
|
| 554 | ||
| 555 | 687x |
mf_spans <- function(mf) mf$spans |
| 556 |
#' @export |
|
| 557 |
#' @rdname mpf_accessors |
|
| 558 | ||
| 559 | 1024x |
mf_aligns <- function(mf) mf$aligns |
| 560 | ||
| 561 |
#' @export |
|
| 562 |
#' @rdname mpf_accessors |
|
| 563 | 470x |
mf_display <- function(mf) mf$display |
| 564 | ||
| 565 |
#' @export |
|
| 566 |
#' @rdname mpf_accessors |
|
| 567 | 591x |
mf_formats <- function(mf) mf$formats |
| 568 | ||
| 569 |
#' @export |
|
| 570 |
#' @rdname mpf_accessors |
|
| 571 | 4834x |
mf_rinfo <- function(mf) mf$row_info |
| 572 | ||
| 573 |
#' @export |
|
| 574 |
#' @rdname mpf_accessors |
|
| 575 | 1593x |
mf_cinfo <- function(mf) mf$col_info |
| 576 | ||
| 577 | ||
| 578 |
#' @export |
|
| 579 |
#' @rdname mpf_accessors |
|
| 580 | 269x |
mf_has_topleft <- function(mf) mf$has_topleft |
| 581 | ||
| 582 |
#' @export |
|
| 583 |
#' @rdname mpf_accessors |
|
| 584 | 5957x |
mf_lgrouping <- function(mf) mf$line_grouping |
| 585 | ||
| 586 |
#' @export |
|
| 587 |
#' @rdname mpf_accessors |
|
| 588 | 175x |
mf_rfnotes <- function(mf) mf$ref_footnotes |
| 589 | ||
| 590 |
#' @export |
|
| 591 |
#' @rdname mpf_accessors |
|
| 592 | 2825x |
mf_nlheader <- function(mf) sum(mf_lgrouping(mf) <= mf_nrheader(mf)) |
| 593 | ||
| 594 |
#' @export |
|
| 595 |
#' @rdname mpf_accessors |
|
| 596 | 4779x |
mf_nrheader <- function(mf) attr(mf, "nrow_header", exact = TRUE) |
| 597 | ||
| 598 |
#' @export |
|
| 599 |
#' @rdname mpf_accessors |
|
| 600 | 369x |
mf_colgap <- function(mf) mf$col_gap |
| 601 | ||
| 602 |
#' @export |
|
| 603 |
#' @rdname mpf_accessors |
|
| 604 | 7x |
mf_fontspec <- function(mf) mf$fontspec |
| 605 | ||
| 606 |
#' @export |
|
| 607 |
#' @rdname mpf_accessors |
|
| 608 |
`mf_fontspec<-` <- function(mf, value) {
|
|
| 609 | 280x |
mf$fontspec <- value |
| 610 | 280x |
mf |
| 611 |
} |
|
| 612 | ||
| 613 |
## XXX should this be exported? not sure if there's a point |
|
| 614 |
mf_col_paths <- function(mf) {
|
|
| 615 | 2x |
if (!is.null(mf_cinfo(mf))) {
|
| 616 | 2x |
mf_cinfo(mf)$path |
| 617 |
} else {
|
|
| 618 | ! |
as.list(paste0("col", seq_len(nrow(mf_strings(mf)) - mf_has_topleft(mf))))
|
| 619 |
} |
|
| 620 |
} |
|
| 621 | ||
| 622 |
mf_col_widths <- function(mf) {
|
|
| 623 | 1091x |
mf$col_widths |
| 624 |
} |
|
| 625 | ||
| 626 |
`mf_col_widths<-` <- function(mf, value) {
|
|
| 627 | 371x |
if (!is.null(value) && length(value) != NCOL(mf_strings(mf))) {
|
| 628 | ! |
stop( |
| 629 | ! |
"Number of column widths (", length(value), ") does not match ",
|
| 630 | ! |
"number of columns in strings matrix (", NCOL(mf_strings(mf)), ")."
|
| 631 |
) |
|
| 632 |
} |
|
| 633 | 371x |
mf <- mf_update_cinfo(mf, colwidths = value, rep_cols = NULL) |
| 634 | 371x |
mf |
| 635 |
} |
|
| 636 | ||
| 637 |
mf_fnote_df <- function(mf) {
|
|
| 638 | 1734x |
mf$ref_fnote_df |
| 639 |
} |
|
| 640 | ||
| 641 |
`mf_fnote_df<-` <- function(mf, value) {
|
|
| 642 | 455x |
stopifnot(is.null(value) || (is.data.frame(value) && identical(names(value), names(ref_df_row())))) |
| 643 | 455x |
mf$ref_fnote_df <- value |
| 644 | 455x |
mf |
| 645 |
} |
|
| 646 | ||
| 647 |
splice_fnote_info_in <- function(df, refdf, row = TRUE) {
|
|
| 648 | 401x |
if (NROW(df) == 0) {
|
| 649 | ! |
return(df) |
| 650 |
} |
|
| 651 | ||
| 652 | 401x |
colnm <- ifelse(row, "row", "col") |
| 653 | 401x |
refdf <- refdf[!is.na(refdf[[colnm]]), ] |
| 654 | ||
| 655 | 401x |
refdf_spl <- split(refdf, refdf[[colnm]]) |
| 656 | 401x |
df$ref_info_df <- replicate(nrow(df), list(ref_df_row()[0, ])) |
| 657 | 401x |
df$ref_info_df[as.integer(names(refdf_spl))] <- refdf_spl |
| 658 | 401x |
df |
| 659 |
} |
|
| 660 | ||
| 661 |
shove_refdf_into_rowinfo <- function(mform) {
|
|
| 662 | 347x |
refdf <- mf_fnote_df(mform) |
| 663 | 347x |
rowinfo <- mf_rinfo(mform) |
| 664 | 347x |
mf_rinfo(mform) <- splice_fnote_info_in(rowinfo, refdf) |
| 665 | 347x |
mform |
| 666 |
} |
|
| 667 | ||
| 668 |
update_mf_nlines <- function(mform, colwidths, max_width) {
|
|
| 669 | 321x |
mform <- update_mf_ref_nlines(mform, max_width = max_width) |
| 670 | 321x |
mform <- update_mf_rinfo_extents(mform) |
| 671 | ||
| 672 | 321x |
mform |
| 673 |
} |
|
| 674 | ||
| 675 |
update_mf_rinfo_extents <- function(mform) {
|
|
| 676 | 321x |
rinfo <- mf_rinfo(mform) |
| 677 | 321x |
refdf_all <- mf_fnote_df(mform) |
| 678 | 321x |
refdf_rows <- refdf_all[!is.na(refdf_all$row), ] |
| 679 | 321x |
if (NROW(rinfo) == 0) {
|
| 680 | ! |
return(mform) |
| 681 |
} |
|
| 682 | 321x |
lgrp <- mf_lgrouping(mform) - mf_nrheader(mform) |
| 683 | 321x |
lgrp <- lgrp[lgrp > 0] |
| 684 | 321x |
rf_nlines <- vapply(seq_len(max(lgrp)), function(ii) {
|
| 685 | 5985x |
refdfii <- refdf_rows[refdf_rows$row == ii, ] |
| 686 | 5985x |
refdfii <- refdfii[!duplicated(refdfii$symbol), ] |
| 687 | 5985x |
if (NROW(refdfii) == 0L) {
|
| 688 | 5889x |
return(0L) |
| 689 |
} |
|
| 690 | 96x |
sum(refdfii$nlines) |
| 691 | 321x |
}, 1L) |
| 692 | ||
| 693 | 321x |
raw_self_exts <- vapply(split(lgrp, lgrp), length, 0L) |
| 694 | 321x |
stopifnot(length(raw_self_exts) == length(rf_nlines)) |
| 695 | 321x |
new_exts <- raw_self_exts + rf_nlines |
| 696 | ||
| 697 | 321x |
mapdf <- data.frame( |
| 698 | 321x |
row_num = as.integer(names(new_exts)), |
| 699 | 321x |
raw_extent = raw_self_exts |
| 700 |
) |
|
| 701 | 321x |
stopifnot(all(mapdf$row_num == rinfo$abs_rownumber)) |
| 702 | ||
| 703 | 321x |
new_par_exts <- vapply(rinfo$reprint_inds, function(idx) {
|
| 704 | 5985x |
sum(0L, mapdf$raw_extent[mapdf$row_num %in% idx]) |
| 705 | 321x |
}, 1L) |
| 706 | ||
| 707 | 321x |
rinfo$self_extent <- new_exts |
| 708 | 321x |
rinfo$par_extent <- new_par_exts |
| 709 | 321x |
rinfo$nreflines <- rf_nlines |
| 710 | 321x |
mf_rinfo(mform) <- rinfo |
| 711 | 321x |
mform |
| 712 |
} |
|
| 713 | ||
| 714 |
update_mf_ref_nlines <- function(mform, max_width) {
|
|
| 715 | 321x |
refdf <- mf_fnote_df(mform) |
| 716 | 321x |
if (NROW(refdf) == 0) {
|
| 717 | 294x |
return(mform) |
| 718 |
} |
|
| 719 | ||
| 720 | 27x |
refdf$nlines <- vapply( |
| 721 | 27x |
paste0("{", refdf$symbol, "} - ", refdf$msg),
|
| 722 | 27x |
nlines, |
| 723 | 27x |
max_width = max_width, |
| 724 | 27x |
fontspec = mf_fontspec(mform), |
| 725 | 27x |
1L |
| 726 |
) |
|
| 727 | 27x |
mf_fnote_df(mform) <- refdf |
| 728 | 27x |
shove_refdf_into_rowinfo(mform) |
| 729 |
} |
|
| 730 | ||
| 731 |
#' @export |
|
| 732 |
#' @rdname mpf_accessors |
|
| 733 |
`mf_strings<-` <- function(mf, value) {
|
|
| 734 | 822x |
mf$strings <- value |
| 735 | 822x |
mf |
| 736 |
} |
|
| 737 | ||
| 738 |
.chkdim_and_replace <- function(mf, value, component) {
|
|
| 739 | 1110x |
strdim <- dim(mf_strings(mf)) |
| 740 | 1110x |
vdim <- dim(value) |
| 741 | 1110x |
if (!is.null(strdim) && !identical(strdim, vdim)) {
|
| 742 | 1x |
stop( |
| 743 | 1x |
"Dimensions of new '", component, "' value (",
|
| 744 | 1x |
vdim[1], ", ", vdim[2], # nocov |
| 745 | 1x |
") do not match dimensions of existing 'strings' component (", # nocov
|
| 746 | 1x |
strdim[1], ", ", strdim[2], ")." # nocov |
| 747 |
) |
|
| 748 |
} |
|
| 749 | 1109x |
mf[[component]] <- value |
| 750 | 1109x |
mf |
| 751 |
} |
|
| 752 | ||
| 753 |
#' @export |
|
| 754 |
#' @rdname mpf_accessors |
|
| 755 |
`mf_spans<-` <- function(mf, value) {
|
|
| 756 | 366x |
mf <- .chkdim_and_replace(mf, value, component = "spans") |
| 757 | 365x |
mf$display <- disp_from_spans(value) |
| 758 | 365x |
mf |
| 759 |
} |
|
| 760 | ||
| 761 |
#' @export |
|
| 762 |
#' @rdname mpf_accessors |
|
| 763 |
`mf_aligns<-` <- function(mf, value) {
|
|
| 764 | 379x |
.chkdim_and_replace(mf, value, component = "aligns") |
| 765 |
} |
|
| 766 | ||
| 767 |
#' @export |
|
| 768 |
#' @rdname mpf_accessors |
|
| 769 |
`mf_display<-` <- function(mf, value) {
|
|
| 770 | ! |
stop("display is now a derived element of the matrix print form, modify it via `mf_spans<-`")
|
| 771 | ! |
.chkdim_and_replace(mf, value, component = "display") |
| 772 |
} |
|
| 773 | ||
| 774 |
#' @export |
|
| 775 |
#' @rdname mpf_accessors |
|
| 776 |
`mf_formats<-` <- function(mf, value) {
|
|
| 777 | 365x |
.chkdim_and_replace(mf, value, component = "formats") |
| 778 |
} |
|
| 779 | ||
| 780 |
## NB NROW(v) == length(v) for atomic vectors so this is ok for lgrouping as wellas rinfo |
|
| 781 |
.chknrow_and_replace <- function(mf, value, component, noheader = FALSE) {
|
|
| 782 | 365x |
strdim <- NROW(mf_strings(mf)) - if (noheader) mf_nlheader(mf) else 0L |
| 783 | 365x |
vdim <- NROW(value) |
| 784 | 365x |
if (!is.null(strdim) && !identical(strdim, vdim)) {
|
| 785 | ! |
stop( |
| 786 | ! |
"Number of rows/length of new '", component, "' value (",
|
| 787 | ! |
vdim[1], |
| 788 | ! |
") does not match existing 'strings' component (",
|
| 789 | ! |
strdim[1], ")." |
| 790 |
) |
|
| 791 |
} |
|
| 792 | 365x |
mf[[component]] <- value |
| 793 | 365x |
mf |
| 794 |
} |
|
| 795 | ||
| 796 |
#' @export |
|
| 797 |
#' @rdname mpf_accessors |
|
| 798 |
`mf_rinfo<-` <- function(mf, value) {
|
|
| 799 |
## this can someijtmes be called after expanding newlines so in general |
|
| 800 |
## we should not expect it to match the number of rows in the strings matrix |
|
| 801 |
## .chknrow_and_replace(mf, value, component = "row_info", noheader = TRUE) |
|
| 802 | 787x |
lgrps <- mf_lgrouping(mf) |
| 803 | 787x |
nrs <- length(unique(lgrps[-seq_len(mf_nlheader(mf))])) |
| 804 | 787x |
if (NROW(value) != nrs) {
|
| 805 | 1x |
stop( |
| 806 | 1x |
"Rows in new row_info component (",
|
| 807 | 1x |
NROW(value), |
| 808 | 1x |
") does not match number of rows reflected in line_grouping component (",
|
| 809 | 1x |
nrs, ")" |
| 810 |
) |
|
| 811 |
} |
|
| 812 | 786x |
mf$row_info <- value |
| 813 | 786x |
mf |
| 814 |
} |
|
| 815 | ||
| 816 |
#' @export |
|
| 817 |
#' @rdname mpf_accessors |
|
| 818 |
`mf_cinfo<-` <- function(mf, value) {
|
|
| 819 | 694x |
if (NROW(value) > 0 && NROW(value) != mf_ncol(mf)) {
|
| 820 | ! |
stop( |
| 821 | ! |
"Number of rows in new cinfo (", NROW(value), ") does not match ",
|
| 822 | ! |
"number of columns (", mf_ncol(mf), ")"
|
| 823 |
) |
|
| 824 |
} |
|
| 825 | 694x |
mf$col_info <- value |
| 826 | 694x |
mf |
| 827 |
} |
|
| 828 | ||
| 829 |
#' @export |
|
| 830 |
#' @rdname mpf_accessors |
|
| 831 |
`mf_lgrouping<-` <- function(mf, value) {
|
|
| 832 | 365x |
.chknrow_and_replace(mf, value, component = "line_grouping") |
| 833 |
} |
|
| 834 | ||
| 835 |
#' @export |
|
| 836 |
#' @rdname mpf_accessors |
|
| 837 |
`mf_rfnotes<-` <- function(mf, value) {
|
|
| 838 | 325x |
mf$ref_footnotes <- value |
| 839 | 325x |
mf |
| 840 |
} |
|
| 841 | ||
| 842 |
#' @export |
|
| 843 |
#' @rdname mpf_accessors |
|
| 844 |
`mf_nrheader<-` <- function(mf, value) {
|
|
| 845 | 2x |
attr(mf, "nrow_header") <- value |
| 846 | 2x |
mf |
| 847 |
} |
|
| 848 | ||
| 849 |
#' @export |
|
| 850 |
#' @rdname mpf_accessors |
|
| 851 |
`mf_colgap<-` <- function(mf, value) {
|
|
| 852 | 50x |
mf$col_gap <- value |
| 853 | 50x |
mf |
| 854 |
} |
|
| 855 | ||
| 856 |
#' @export |
|
| 857 |
#' @rdname mpf_accessors |
|
| 858 | 2617x |
mf_ncol <- function(mf) attr(mf, "ncols", exact = TRUE) |
| 859 | ||
| 860 |
#' @export |
|
| 861 |
#' @rdname mpf_accessors |
|
| 862 | 10x |
mf_nrow <- function(mf) max(mf_lgrouping(mf)) - mf_nrheader(mf) |
| 863 | ||
| 864 |
#' @export |
|
| 865 |
#' @rdname mpf_accessors |
|
| 866 |
`mf_ncol<-` <- function(mf, value) {
|
|
| 867 | 444x |
stopifnot(is.numeric(value)) |
| 868 | 444x |
attr(mf, "ncols") <- value |
| 869 | 444x |
mf |
| 870 |
} |
|
| 871 | ||
| 872 |
#' @param x `MatrixPrintForm`. The object. |
|
| 873 |
#' @export |
|
| 874 |
#' @rdname mpf_accessors |
|
| 875 |
setMethod( |
|
| 876 |
"ncol", "MatrixPrintForm", |
|
| 877 | 27x |
function(x) mf_ncol(x) |
| 878 |
) |
|
| 879 | ||
| 880 |
#' @export |
|
| 881 |
#' @rdname mpf_accessors |
|
| 882 |
mpf_has_rlabels <- function(mf) {
|
|
| 883 | ! |
.Deprecated("mf_has_rlabels")
|
| 884 | ! |
mf_has_rlabels(mf) |
| 885 |
} |
|
| 886 | ||
| 887 |
#' @export |
|
| 888 |
#' @rdname mpf_accessors |
|
| 889 | 1280x |
mf_has_rlabels <- function(mf) ncol(mf$strings) > mf_ncol(mf) |
| 890 | ||
| 891 |
#' Create spoof matrix form from a data frame |
|
| 892 |
#' |
|
| 893 |
#' Useful functions for writing tests and examples, and a starting point for |
|
| 894 |
#' more sophisticated custom `matrix_form` methods. |
|
| 895 |
#' |
|
| 896 |
#' @inheritParams open_font_dev |
|
| 897 |
#' @param df (`data.frame`)\cr a data frame. |
|
| 898 |
#' @param indent_rownames (`flag`)\cr whether row names should be indented. Being this |
|
| 899 |
#' used for testing purposes, it defaults to `FALSE`. If `TRUE`, it assigns label rows |
|
| 900 |
#' on even lines (also format is `"-"` and value strings are `""`). Indentation works |
|
| 901 |
#' only if split labels are used (see parameters `split_labels` and `data_labels`). |
|
| 902 |
#' @param parent_path (`string`)\cr parent path that all rows should be "children of". |
|
| 903 |
#' Defaults to `NULL`, as usually this is not needed. It may be necessary to use `"root"`, |
|
| 904 |
#' for some specific scenarios. |
|
| 905 |
#' @param ignore_rownames (`flag`)\cr whether row names should be ignored. |
|
| 906 |
#' @param add_decoration (`flag`)\cr whether adds title and footer decorations should |
|
| 907 |
#' be added to the matrix form. |
|
| 908 |
#' @param split_labels (`string`)\cr indicates which column to use as split labels. If |
|
| 909 |
#' `NULL`, no split labels are used. |
|
| 910 |
#' @param data_labels (`string`)\cr indicates which column to use as data labels. It is |
|
| 911 |
#' ignored if no `split_labels` is present and is automatically assigned to |
|
| 912 |
#' `"Analysis method"` when `split_labels` is present, but `data_labels` is `NULL`. |
|
| 913 |
#' Its direct column name is used as node name in `"DataRow"` pathing. See [mf_rinfo()] |
|
| 914 |
#' for more information. |
|
| 915 |
#' @param num_rep_cols (`numeric(1)`)\cr Number of columns to be treated as repeating columns. |
|
| 916 |
#' Defaults to `0` for `basic_matrix_form` and `length(keycols)` for |
|
| 917 |
#' `basic_listing_mf`. Note repeating columns are separate from row labels if present. |
|
| 918 |
#' |
|
| 919 |
#' @return A valid `MatrixPrintForm` object representing `df` that is ready for |
|
| 920 |
#' ASCII rendering. |
|
| 921 |
#' |
|
| 922 |
#' @details |
|
| 923 |
#' If some of the column has a [obj_format] assigned, it will be respected for all column |
|
| 924 |
#' values except for label rows, if present (see parameter `split_labels`). |
|
| 925 |
#' |
|
| 926 |
#' @examples |
|
| 927 |
#' mform <- basic_matrix_form(mtcars) |
|
| 928 |
#' cat(toString(mform)) |
|
| 929 |
#' |
|
| 930 |
#' @examplesIf require("dplyr")
|
|
| 931 |
#' # Advanced test case with label rows |
|
| 932 |
#' library(dplyr) |
|
| 933 |
#' iris_output <- iris %>% |
|
| 934 |
#' group_by(Species) %>% |
|
| 935 |
#' summarize("all obs" = round(mean(Petal.Length), 2)) %>%
|
|
| 936 |
#' mutate("DataRow_label" = "Mean")
|
|
| 937 |
#' mf <- basic_matrix_form(iris_output, |
|
| 938 |
#' indent_rownames = TRUE, |
|
| 939 |
#' split_labels = "Species", data_labels = "DataRow_label" |
|
| 940 |
#' ) |
|
| 941 |
#' cat(toString(mf)) |
|
| 942 |
#' |
|
| 943 |
#' @name test_matrix_form |
|
| 944 |
#' @export |
|
| 945 |
basic_matrix_form <- function(df, |
|
| 946 |
indent_rownames = FALSE, |
|
| 947 |
parent_path = NULL, |
|
| 948 |
ignore_rownames = FALSE, |
|
| 949 |
add_decoration = FALSE, |
|
| 950 |
fontspec = font_spec(), |
|
| 951 |
split_labels = NULL, |
|
| 952 |
data_labels = NULL, |
|
| 953 |
num_rep_cols = 0L) {
|
|
| 954 | 52x |
checkmate::assert_data_frame(df) |
| 955 | 52x |
checkmate::assert_flag(indent_rownames) |
| 956 | 52x |
checkmate::assert_character(parent_path, null.ok = TRUE) |
| 957 | 52x |
checkmate::assert_flag(ignore_rownames) |
| 958 | 52x |
checkmate::assert_flag(add_decoration) |
| 959 | 52x |
checkmate::assert_character(split_labels, null.ok = TRUE) |
| 960 | 52x |
checkmate::assert_character(data_labels, null.ok = TRUE) |
| 961 | ||
| 962 |
# Some defaults |
|
| 963 | 52x |
row_classes <- "DataRow" # Default for all rows |
| 964 | 52x |
data_row_format <- "xx" # Default if no labels are used |
| 965 | 52x |
indent_size <- 2 |
| 966 | 52x |
indent_space <- paste0(rep(" ", indent_size), collapse = "")
|
| 967 | ||
| 968 |
# Pre-processing the fake split |
|
| 969 | 52x |
if (!is.null(split_labels)) {
|
| 970 | 4x |
checkmate::assert_choice(split_labels, colnames(df)) |
| 971 | 4x |
label_rows <- as.character(df[[split_labels]]) |
| 972 | 4x |
if (is.null(data_labels)) {
|
| 973 | ! |
data_rows <- rep("Analysis Method", nrow(df))
|
| 974 | ! |
data_labels <- "Analyzed Variable" |
| 975 |
} else {
|
|
| 976 | 4x |
checkmate::assert_choice(data_labels, colnames(df)) |
| 977 | 4x |
data_rows <- as.character(df[[data_labels]]) |
| 978 |
} |
|
| 979 | 4x |
rnms_special <- c(rbind(label_rows, data_rows)) |
| 980 | 4x |
row_classes <- c(rbind( |
| 981 | 4x |
rep("LabelRow", length(label_rows)),
|
| 982 | 4x |
rep("DataRow", length(data_rows))
|
| 983 |
)) |
|
| 984 | 4x |
data_colnm <- setdiff(colnames(df), c(split_labels, data_labels)) |
| 985 | 4x |
tmp_df <- NULL |
| 986 | 4x |
for (col_i in seq_along(data_colnm)) {
|
| 987 | 8x |
lbl_and_dt <- c(rbind(rep("", length(label_rows)), df[[data_colnm[col_i]]]))
|
| 988 | 8x |
tmp_df <- cbind(tmp_df, lbl_and_dt) |
| 989 |
} |
|
| 990 | 4x |
colnames(tmp_df) <- data_colnm |
| 991 | 4x |
rownames(tmp_df) <- NULL |
| 992 | 4x |
df <- as.data.frame(tmp_df) |
| 993 | 4x |
ignore_rownames <- FALSE |
| 994 |
} |
|
| 995 | ||
| 996 |
# Formats |
|
| 997 | 52x |
fmts <- lapply(df, function(x) {
|
| 998 | 308x |
if (is.null(obj_format(x))) {
|
| 999 | 308x |
fmt_tmp <- data_row_format |
| 1000 |
} else {
|
|
| 1001 | ! |
fmt_tmp <- obj_format(x) # Can be assigned for each column |
| 1002 |
} |
|
| 1003 | 308x |
out <- rep(fmt_tmp, NROW(df)) |
| 1004 | 308x |
if (!is.null(split_labels)) {
|
| 1005 | 8x |
out[row_classes == "LabelRow"] <- "-" |
| 1006 |
} |
|
| 1007 | 308x |
out |
| 1008 |
}) |
|
| 1009 | ||
| 1010 | 52x |
formats <- rbind("", data.frame(fmts))
|
| 1011 | 52x |
if (!ignore_rownames) {
|
| 1012 | 38x |
formats <- cbind("rnms" = "", formats)
|
| 1013 |
} |
|
| 1014 | ||
| 1015 |
# Strings |
|
| 1016 | 52x |
bodystrs <- mapply(function(x, coli_fmt) {
|
| 1017 | 308x |
coli_fmt[coli_fmt == "-"] <- "xx" |
| 1018 | 308x |
sapply(seq_along(x), function(y) {
|
| 1019 | 8748x |
format_value(x[y], format = coli_fmt[y]) |
| 1020 |
}) |
|
| 1021 | 52x |
}, x = df, coli_fmt = fmts) |
| 1022 | ||
| 1023 | 52x |
if (!ignore_rownames) {
|
| 1024 | 38x |
rnms <- row.names(df) |
| 1025 | 38x |
if (!is.null(split_labels)) {
|
| 1026 |
# This overload is done because identical rownames not allowed (e.g. Mean.1 Mean.2) |
|
| 1027 | 4x |
rnms <- rnms_special |
| 1028 | 34x |
} else if (is.null(rnms)) {
|
| 1029 | ! |
rnms <- as.character(seq_len(NROW(df))) |
| 1030 |
} |
|
| 1031 |
} |
|
| 1032 | ||
| 1033 | 52x |
strings <- rbind(colnames(df), bodystrs) |
| 1034 | ||
| 1035 | 52x |
rownames(strings) <- NULL |
| 1036 | 52x |
if (!ignore_rownames) {
|
| 1037 | 38x |
strings <- cbind("rnms" = c("", rnms), strings)
|
| 1038 |
} |
|
| 1039 |
# colnames(strings) <- NULL # to add after fixing basic_mf for listings |
|
| 1040 | ||
| 1041 |
# Spans |
|
| 1042 | 52x |
spans <- matrix(1, nrow = nrow(strings), ncol = ncol(strings)) |
| 1043 | ||
| 1044 |
# Aligns |
|
| 1045 |
# Default alignment is left for rownames column and center for the rest |
|
| 1046 | 52x |
aligns <- matrix("center",
|
| 1047 | 52x |
nrow = NROW(strings), |
| 1048 | 52x |
ncol = NCOL(strings) - as.numeric(!ignore_rownames) |
| 1049 |
) |
|
| 1050 | 52x |
if (!ignore_rownames) {
|
| 1051 | 38x |
aligns <- cbind("left", aligns)
|
| 1052 |
} |
|
| 1053 | ||
| 1054 |
# Row Info: build up fake pagination df |
|
| 1055 | 52x |
charcols <- which(sapply(df, is.character)) |
| 1056 | 52x |
if (length(charcols) > 0) {
|
| 1057 | 15x |
exts <- apply(df[, charcols, drop = FALSE], 1, function(x) max(vapply(x, nlines, fontspec = fontspec, 1L))) |
| 1058 |
} else {
|
|
| 1059 | 37x |
exts <- rep(1L, NROW(df)) |
| 1060 |
} |
|
| 1061 |
# Constructing path roughly |
|
| 1062 | 52x |
if (!is.null(split_labels)) {
|
| 1063 | 4x |
paths <- lapply( |
| 1064 | 4x |
seq_along(rnms), |
| 1065 | 4x |
function(row_path_i) {
|
| 1066 | 24x |
if (row_classes[row_path_i] == "DataRow") {
|
| 1067 | 12x |
c( |
| 1068 | 12x |
split_labels, |
| 1069 | 12x |
rnms[row_path_i - 1], # LabelRow before |
| 1070 | 12x |
data_labels, |
| 1071 | 12x |
rnms[row_path_i] |
| 1072 |
) |
|
| 1073 |
} else {
|
|
| 1074 | 12x |
c(split_labels, rnms[row_path_i]) |
| 1075 |
} |
|
| 1076 |
} |
|
| 1077 |
) |
|
| 1078 |
} else {
|
|
| 1079 | 48x |
rnms <- row.names(df) |
| 1080 | 48x |
if (is.null(rnms)) {
|
| 1081 | ! |
rnms <- as.character(seq_len(NROW(df))) |
| 1082 |
} |
|
| 1083 | 48x |
paths <- lapply(rnms, function(x) c(parent_path, x)) |
| 1084 |
} |
|
| 1085 | 52x |
rowdf <- basic_pagdf( |
| 1086 | 52x |
rnames = rnms, |
| 1087 | 52x |
extents = exts, |
| 1088 | 52x |
rclass = row_classes, |
| 1089 | 52x |
parent_path = NULL, # Overloaded by above parent_path lapply |
| 1090 | 52x |
paths = paths |
| 1091 |
) |
|
| 1092 | ||
| 1093 |
# Indentation happens last so to be sure we have all ready (only strings and formats change) |
|
| 1094 | 52x |
if (indent_rownames && !is.null(split_labels)) {
|
| 1095 | 2x |
where_to_indent <- which(row_classes == "DataRow") + 1 # +1 because of colnames |
| 1096 | 2x |
strings[where_to_indent, 1] <- paste0(indent_space, strings[where_to_indent, 1]) |
| 1097 | 2x |
formats[where_to_indent, 1] <- paste0(indent_space, formats[where_to_indent, 1]) # Needs fixing |
| 1098 | 2x |
rowdf$indent[where_to_indent - 1] <- 1 # -1 because only rows |
| 1099 |
} |
|
| 1100 | ||
| 1101 | 52x |
ret <- MatrixPrintForm( |
| 1102 | 52x |
strings = strings, |
| 1103 | 52x |
aligns = aligns, |
| 1104 | 52x |
spans = spans, |
| 1105 | 52x |
formats = formats, ## matrix("xx", nrow = fnr, ncol = fnc),
|
| 1106 | 52x |
row_info = rowdf, |
| 1107 | 52x |
has_topleft = FALSE, |
| 1108 | 52x |
nlines_header = 1, |
| 1109 | 52x |
nrow_header = 1, |
| 1110 | 52x |
has_rowlabs = isFALSE(ignore_rownames), |
| 1111 | 52x |
fontspec = fontspec, |
| 1112 | 52x |
col_gap = 3, |
| 1113 | 52x |
indent_size = indent_size, |
| 1114 | 52x |
rep_cols = num_rep_cols |
| 1115 |
) |
|
| 1116 | ||
| 1117 |
# Check for ncols |
|
| 1118 | 52x |
stopifnot(mf_has_rlabels(ret) == isFALSE(ignore_rownames)) |
| 1119 | ||
| 1120 | 52x |
ret <- mform_build_refdf(ret) |
| 1121 | ||
| 1122 | 52x |
if (add_decoration) {
|
| 1123 | 8x |
main_title(ret) <- "main title" |
| 1124 | 8x |
main_footer(ret) <- c("main", " footer")
|
| 1125 | 8x |
prov_footer(ret) <- "prov footer" |
| 1126 | 8x |
subtitles(ret) <- c("sub", "titles")
|
| 1127 |
} |
|
| 1128 | ||
| 1129 | 52x |
ret |
| 1130 |
} |
|
| 1131 | ||
| 1132 |
#' @describeIn test_matrix_form Create a `MatrixPrintForm` object from data frame `df` that |
|
| 1133 |
#' respects the default formats for a listing object. |
|
| 1134 |
#' |
|
| 1135 |
#' @param keycols (`character`)\cr a vector of `df` column names that are printed first and for which |
|
| 1136 |
#' repeated values are assigned `""`. This format is characteristic of a listing matrix form. |
|
| 1137 |
#' |
|
| 1138 |
#' @return A valid `MatrixPrintForm` object representing `df` as a listing that is ready for ASCII |
|
| 1139 |
#' rendering. |
|
| 1140 |
#' |
|
| 1141 |
#' @examples |
|
| 1142 |
#' mform <- basic_listing_mf(mtcars) |
|
| 1143 |
#' cat(toString(mform)) |
|
| 1144 |
#' |
|
| 1145 |
#' @export |
|
| 1146 |
basic_listing_mf <- function(df, |
|
| 1147 |
keycols = names(df)[1], |
|
| 1148 |
add_decoration = TRUE, |
|
| 1149 |
fontspec = font_spec()) {
|
|
| 1150 | 9x |
checkmate::assert_data_frame(df) |
| 1151 | 9x |
checkmate::assert_subset(keycols, colnames(df)) |
| 1152 | ||
| 1153 | 9x |
dfmf <- basic_matrix_form( |
| 1154 | 9x |
df = df, |
| 1155 | 9x |
indent_rownames = FALSE, |
| 1156 | 9x |
ignore_rownames = TRUE, |
| 1157 | 9x |
add_decoration = add_decoration, |
| 1158 | 9x |
num_rep_cols = length(keycols), |
| 1159 | 9x |
fontspec = fontspec |
| 1160 |
) |
|
| 1161 | ||
| 1162 |
# keycols addition to MatrixPrintForm (should happen in the constructor) |
|
| 1163 | 9x |
dfmf$listing_keycols <- keycols |
| 1164 | ||
| 1165 |
# Modifications needed for making it a listings |
|
| 1166 | 9x |
mf_strings(dfmf)[1, ] <- colnames(mf_strings(dfmf)) # set colnames |
| 1167 | ||
| 1168 | 9x |
if (!is.null(keycols)) {
|
| 1169 | 9x |
str_dfmf <- mf_strings(dfmf)[-1, ] |
| 1170 |
# Ordering |
|
| 1171 | 9x |
ord <- do.call( |
| 1172 | 9x |
order, |
| 1173 | 9x |
as.list( |
| 1174 | 9x |
data.frame( |
| 1175 | 9x |
str_dfmf[, keycols] |
| 1176 |
) |
|
| 1177 |
) |
|
| 1178 |
) |
|
| 1179 | 9x |
str_dfmf <- str_dfmf[ord, ] |
| 1180 |
# Making keycols with empties |
|
| 1181 | 9x |
curkey <- "" |
| 1182 | 9x |
for (i in seq_along(keycols)) {
|
| 1183 | 17x |
kcol <- keycols[i] |
| 1184 | 17x |
kcolvec <- str_dfmf[, kcol] # -1 is col label row |
| 1185 | 17x |
str_dfmf[, kcol] <- "" |
| 1186 | 17x |
kcolvec <- vapply(kcolvec, format_value, "", format = NULL, na_str = "NA") |
| 1187 | 17x |
curkey <- paste0(curkey, kcolvec) |
| 1188 | 17x |
disp <- c(TRUE, tail(curkey, -1) != head(curkey, -1)) |
| 1189 | 17x |
str_dfmf[disp, kcol] <- kcolvec[disp] |
| 1190 |
} |
|
| 1191 | 9x |
mf_strings(dfmf)[-1, ] <- str_dfmf |
| 1192 |
# keycols as first |
|
| 1193 | 9x |
mf_strings(dfmf) <- cbind( |
| 1194 | 9x |
mf_strings(dfmf)[, keycols, drop = FALSE], |
| 1195 | 9x |
mf_strings(dfmf)[, !colnames(mf_strings(dfmf)) %in% keycols, drop = FALSE] |
| 1196 |
) |
|
| 1197 |
} |
|
| 1198 | ||
| 1199 | 9x |
dfmf$aligns[seq(2, nrow(dfmf$aligns)), ] <- "center" # the default for listings |
| 1200 | ||
| 1201 |
# the default for listings is a 1 double?? |
|
| 1202 | 9x |
dfmf$formats <- matrix(1, nrow = nrow(dfmf$formats), ncol = ncol(dfmf$formats)) |
| 1203 | ||
| 1204 |
# row info |
|
| 1205 | 9x |
ri <- dfmf$row_info |
| 1206 | 9x |
rownames(ri) <- ri$abs_rownumber |
| 1207 | 9x |
ri$label <- ri$name <- "" |
| 1208 | 9x |
ri$path <- as.list(NA_character_) # same format of listings |
| 1209 | 9x |
ri$node_class <- "listing_df" |
| 1210 |
# l_ri$pos_in_siblings # why is it like this in rlistings?? also n_siblings |
|
| 1211 | 9x |
class(ri$path) <- "AsIs" # Artifact from I() |
| 1212 | 9x |
dfmf$row_info <- ri |
| 1213 | ||
| 1214 |
# colwidths need to be sorted too!! |
|
| 1215 | 9x |
dfmf$col_widths <- dfmf$col_widths[colnames(mf_strings(dfmf))] |
| 1216 | ||
| 1217 | 9x |
if (!add_decoration) {
|
| 1218 |
# This is probably a forced behavior in the original matrix_form in rlistings |
|
| 1219 | 2x |
main_title(dfmf) <- character() |
| 1220 | 2x |
main_footer(dfmf) <- character() |
| 1221 |
} |
|
| 1222 | ||
| 1223 | 9x |
dfmf |
| 1224 |
} |
|
| 1225 | ||
| 1226 |
map_to_new <- function(old, map) {
|
|
| 1227 | 418x |
inds <- match(old, map$old_idx) |
| 1228 | 418x |
map$new_idx[inds] |
| 1229 |
} |
|
| 1230 | ||
| 1231 |
reconstruct_basic_fnote_list <- function(mf) {
|
|
| 1232 | 322x |
refdf <- mf_fnote_df(mf) |
| 1233 | 322x |
if (NROW(refdf) == 0) {
|
| 1234 | 282x |
return(NULL) |
| 1235 |
} |
|
| 1236 | 40x |
refdf <- refdf[!duplicated(refdf$symbol), ] |
| 1237 | 40x |
paste0("{", refdf$symbol, "} - ", refdf$msg)
|
| 1238 |
} |
|
| 1239 | ||
| 1240 |
.mf_subset_core_mats <- function(mf, i, keycols = NULL, row = TRUE) {
|
|
| 1241 | 320x |
fillnum <- if (row) nrow(mf_strings(mf)) - mf_nlheader(mf) else mf_ncol(mf) |
| 1242 | 320x |
if (is.logical(i) || all(i < 0)) {
|
| 1243 | ! |
i <- seq_len(fillnum)[i] |
| 1244 |
} |
|
| 1245 | 320x |
nlh <- mf_nlheader(mf) |
| 1246 | ||
| 1247 | 320x |
if (row) {
|
| 1248 | 98x |
ncolrows <- mf_nrheader(mf) |
| 1249 | 98x |
i_mat <- c(seq_len(nlh), which(mf_lgrouping(mf) %in% (i + ncolrows))) |
| 1250 | 98x |
j_mat <- seq_len(ncol(mf_strings(mf))) |
| 1251 |
} else {
|
|
| 1252 | 222x |
nlabcol <- as.integer(mf_has_rlabels(mf)) |
| 1253 | 222x |
i_mat <- seq_len(nrow(mf_strings(mf))) |
| 1254 | 222x |
j_mat <- c(seq_len(nlabcol), i + nlabcol) |
| 1255 |
} |
|
| 1256 | ||
| 1257 | 320x |
tmp_strmat <- mf_strings(mf)[i_mat, j_mat, drop = FALSE] |
| 1258 | ||
| 1259 |
# Only for listings - Fix pagination with empty values in key columns |
|
| 1260 | 320x |
if (nrow(tmp_strmat) > 0 && .is_listing_mf(mf)) { # safe check for empty listings
|
| 1261 | 43x |
ind_keycols <- which(colnames(tmp_strmat) %in% keycols) |
| 1262 | ||
| 1263 |
# Fix for missing labels in key columns (only for rlistings) |
|
| 1264 | 43x |
empty_keycols <- !nzchar(tmp_strmat[-seq_len(nlh), ind_keycols, drop = FALSE][1, ]) |
| 1265 | ||
| 1266 | 43x |
if (any(empty_keycols)) { # only if there are missing keycol labels
|
| 1267 |
# find the first non-empty label in the key columns |
|
| 1268 | 9x |
keycols_needed <- mf_strings(mf)[, empty_keycols, drop = FALSE] |
| 1269 | 9x |
first_nonempty <- apply(keycols_needed, 2, function(x) {
|
| 1270 | 44x |
section_ind <- i_mat[-seq_len(nlh)][1] |
| 1271 | 44x |
sec_ind_no_header <- seq_len(section_ind)[-seq_len(nlh)] |
| 1272 | 44x |
tail(x[sec_ind_no_header][nzchar(x[sec_ind_no_header])], 1) |
| 1273 |
}) |
|
| 1274 | ||
| 1275 |
# if there are only "" the previous returns character() |
|
| 1276 | 9x |
any_chr_empty <- if (length(first_nonempty) > 1) {
|
| 1277 | 9x |
vapply(first_nonempty, length, numeric(1)) |
| 1278 |
} else {
|
|
| 1279 | ! |
length(first_nonempty) |
| 1280 |
} |
|
| 1281 | 9x |
if (any(any_chr_empty == 0L)) {
|
| 1282 | 2x |
warning( |
| 1283 | 2x |
"There are empty key columns in the listing. ", |
| 1284 | 2x |
"We keep empty strings for each page." |
| 1285 |
) |
|
| 1286 | 2x |
first_nonempty[any_chr_empty == 0L] <- "" |
| 1287 |
} |
|
| 1288 | ||
| 1289 |
# replace the empty labels with the first non-empty label |
|
| 1290 | 9x |
tmp_strmat[nlh + 1, empty_keycols] <- unlist(first_nonempty) |
| 1291 |
} |
|
| 1292 |
} |
|
| 1293 | ||
| 1294 | 320x |
mf_strings(mf) <- tmp_strmat |
| 1295 | ||
| 1296 | 320x |
mf_lgrouping(mf) <- as.integer(as.factor(mf_lgrouping(mf)[i_mat])) |
| 1297 | ||
| 1298 | 320x |
if (!row) {
|
| 1299 | 222x |
newspans <- truncate_spans(mf_spans(mf), j_mat) # 'i' is the columns here, bc row is FALSE |
| 1300 |
} else {
|
|
| 1301 | 98x |
newspans <- mf_spans(mf)[i_mat, j_mat, drop = FALSE] |
| 1302 |
} |
|
| 1303 | ||
| 1304 | 320x |
mf_spans(mf) <- newspans |
| 1305 | 320x |
mf_formats(mf) <- mf_formats(mf)[i_mat, j_mat, drop = FALSE] |
| 1306 | ||
| 1307 | 320x |
mf_aligns(mf) <- mf_aligns(mf)[i_mat, j_mat, drop = FALSE] |
| 1308 | 320x |
if (!row) {
|
| 1309 | 222x |
mf_ncol(mf) <- length(i) |
| 1310 | 222x |
if (!is.null(mf_cinfo(mf))) {
|
| 1311 | 222x |
mf_cinfo(mf) <- mf_cinfo(mf)[i, ] |
| 1312 |
} |
|
| 1313 | 222x |
if (!is.null(mf_col_widths(mf))) {
|
| 1314 | 222x |
mf_col_widths(mf) <- mf_col_widths(mf)[j_mat] |
| 1315 |
} |
|
| 1316 |
} |
|
| 1317 | 320x |
mf |
| 1318 |
} |
|
| 1319 | ||
| 1320 |
## ugh. spans are **way** more of a pain than I expected x.x |
|
| 1321 |
truncate_one_span <- function(spanrow, j) {
|
|
| 1322 | 3803x |
i <- 1 |
| 1323 | 3803x |
len <- length(spanrow) |
| 1324 | 3803x |
while (i < len) {
|
| 1325 | 41857x |
spnlen <- spanrow[i] |
| 1326 | 41857x |
inds <- seq(i, i + spnlen - 1) |
| 1327 | 41857x |
newspnlen <- sum(inds %in% j) |
| 1328 | 41857x |
spanrow[inds] <- newspnlen |
| 1329 | 41857x |
i <- i + spnlen |
| 1330 |
} |
|
| 1331 | 3803x |
spanrow[j] |
| 1332 |
} |
|
| 1333 | ||
| 1334 |
truncate_spans <- function(spans, j) {
|
|
| 1335 | 222x |
if (length(spans[1, ]) == 1 || length(j) == 1) {
|
| 1336 | ! |
as.matrix(apply(spans, 1, truncate_one_span, j = j)) |
| 1337 |
} else {
|
|
| 1338 | 222x |
t(apply(spans, 1, truncate_one_span, j = j)) |
| 1339 |
} |
|
| 1340 |
} |
|
| 1341 | ||
| 1342 |
mpf_subset_rows <- function(mf, i, keycols = NULL) {
|
|
| 1343 | 98x |
nlh <- mf_nlheader(mf) |
| 1344 | 98x |
lgrps <- mf_lgrouping(mf) |
| 1345 | 98x |
row_lgrps <- tail(lgrps, -1 * nlh) |
| 1346 | 98x |
nrs <- length(unique(row_lgrps)) |
| 1347 | 98x |
ncolrows <- length(unique(lgrps[seq_len(nlh)])) |
| 1348 | ||
| 1349 | 98x |
ncs <- mf_ncol(mf) |
| 1350 | 98x |
mf <- .mf_subset_core_mats(mf, i, keycols = keycols, row = TRUE) |
| 1351 | 98x |
map <- data.frame( |
| 1352 | 98x |
old_idx = c(seq_len(ncolrows), i + ncolrows), |
| 1353 | 98x |
new_idx = c(seq_len(ncolrows), ncolrows + order(i)) |
| 1354 |
) |
|
| 1355 | ||
| 1356 | 98x |
row_map <- data.frame(old_idx = i, new_idx = order(i)) |
| 1357 | ||
| 1358 | 98x |
refdf <- mf_fnote_df(mf) |
| 1359 | ||
| 1360 | 98x |
old_nas <- is.na(refdf$row) |
| 1361 | 98x |
refdf$row <- map_to_new(refdf$row, row_map) |
| 1362 | 98x |
refdf <- refdf[old_nas | !is.na(refdf$row), ] |
| 1363 | 98x |
mf_fnote_df(mf) <- refdf |
| 1364 | ||
| 1365 | 98x |
rinfo <- mf_rinfo(mf) |
| 1366 | ||
| 1367 | 98x |
rinfo <- rinfo[rinfo$abs_rownumber %in% i, ] |
| 1368 | ||
| 1369 | 98x |
rinfo$abs_rownumber <- map_to_new(rinfo$abs_rownumber, row_map) |
| 1370 | 98x |
mf_rinfo(mf) <- rinfo |
| 1371 | ||
| 1372 | 98x |
mf <- shove_refdf_into_rowinfo(mf) |
| 1373 | 98x |
mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) |
| 1374 | 98x |
mf |
| 1375 |
} |
|
| 1376 | ||
| 1377 |
## we only care about referential footnotes, cause |
|
| 1378 |
## they are currently the only place we're tracking |
|
| 1379 |
## column information that will need to be touched up |
|
| 1380 |
## but lets be careful and do a bit more anyway |
|
| 1381 |
mpf_subset_cols <- function(mf, j, keycols = NULL) {
|
|
| 1382 | 222x |
nc <- mf_ncol(mf) |
| 1383 | 222x |
if (is.logical(j) || all(j < 0)) {
|
| 1384 | ! |
j <- seq_len(nc)[j] |
| 1385 |
} |
|
| 1386 | 222x |
if (any(j < 0)) {
|
| 1387 | ! |
stop("cannot mix negative and positive indices")
|
| 1388 |
} |
|
| 1389 | ||
| 1390 | 222x |
if (length(unique(j)) != length(j)) {
|
| 1391 | ! |
stop("duplicated columns are not allowed when subsetting a matrix print form objects")
|
| 1392 |
} |
|
| 1393 | ||
| 1394 |
# j_mat <- c(if(mf_has_topleft(mf)) seq_len(nlabcol), j + nlabcol) |
|
| 1395 | 222x |
map <- data.frame(old_idx = j, new_idx = order(j)) |
| 1396 | ||
| 1397 |
## this has to happen before the remap inher |
|
| 1398 | 222x |
refdf <- mf_fnote_df(mf) |
| 1399 | ||
| 1400 | 222x |
mf <- .mf_subset_core_mats(mf, j, keycols = keycols, row = FALSE) |
| 1401 | ||
| 1402 |
## future proofing (pipe dreams) |
|
| 1403 |
## uncomment if we ever manage to have col info information on MPFs |
|
| 1404 |
## if(!is.null(mf_cinfo(mf))) {
|
|
| 1405 |
## cinfo <- mf_cinfo(mf) |
|
| 1406 |
## cinfo <- cinfo[j, , drop = FALSE] |
|
| 1407 |
## cinfo$abs_pos <- map_to_new(cinfo$abs_pos, map) |
|
| 1408 |
## mf_cinfo(mf) <- mf |
|
| 1409 |
## } |
|
| 1410 | ||
| 1411 | 222x |
keep <- is.na(refdf$col) | refdf$col %in% j |
| 1412 | 222x |
refdf <- refdf[keep, , drop = FALSE] |
| 1413 | ||
| 1414 | 222x |
refdf$col <- map_to_new(refdf$col, map) |
| 1415 | 222x |
mf_fnote_df(mf) <- refdf |
| 1416 | 222x |
mf <- shove_refdf_into_rowinfo(mf) |
| 1417 | 222x |
mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) |
| 1418 | 222x |
mf_ncol(mf) <- length(j) |
| 1419 | 222x |
mf |
| 1420 |
} |
| 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 | 1146x |
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 | 82728x |
if (is.null(fontspec)) {
|
| 62 | 1x |
return(invisible(FALSE)) |
| 63 | 82726x |
} else if (font_dev_is_open()) {
|
| 64 | 82211x |
if (identical(font_dev_state$fontspec, fontspec)) {
|
| 65 | 82210x |
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 | 82210x |
return(invisible(FALSE)) |
| 75 |
} else {
|
|
| 76 | 1x |
close_font_dev() |
| 77 |
} |
|
| 78 |
} |
|
| 79 | 516x |
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 | 516x |
tmppdf <- tempfile(fileext = ".pdf") |
| 100 | 516x |
pdf(tmppdf) |
| 101 | 516x |
grid.newpage() |
| 102 | 516x |
gp <- gpar_from_fspec(fontspec) |
| 103 | 516x |
pushViewport(plotViewport(gp = gp)) |
| 104 | 516x |
spcwidth <- cwidth_inches_unsafe(" ")
|
| 105 | 516x |
assign("open", TRUE, envir = font_dev_state)
|
| 106 | 516x |
assign("fontspec", fontspec, envir = font_dev_state)
|
| 107 | 516x |
assign("spacewidth", spcwidth, envir = font_dev_state)
|
| 108 | 516x |
assign("ismonospace", spcwidth == cwidth_inches_unsafe("W"),
|
| 109 | 516x |
envir = font_dev_state |
| 110 |
) |
|
| 111 | 516x |
assign("dev_num", dev.cur(),
|
| 112 | 516x |
envir = font_dev_state |
| 113 |
) |
|
| 114 | 516x |
invisible(TRUE) |
| 115 |
} |
|
| 116 | ||
| 117 |
#' @rdname open_font_dev |
|
| 118 |
#' @export |
|
| 119 |
close_font_dev <- function() {
|
|
| 120 | 516x |
if (font_dev_state$open) {
|
| 121 | 516x |
dev.off(font_dev_state$dev_num) |
| 122 | 516x |
assign("open", FALSE, envir = font_dev_state)
|
| 123 | 516x |
assign("fontspec", list(), envir = font_dev_state)
|
| 124 | 516x |
assign("spacewidth", NA_real_, envir = font_dev_state)
|
| 125 | 516x |
assign("ismonospace", NA, envir = font_dev_state)
|
| 126 | 516x |
assign("dev_num", NA_integer_, envir = font_dev_state)
|
| 127 |
} |
|
| 128 | 516x |
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 | 32819x |
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 | 32817x |
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 | 32818x |
if (is.null(fontspec)) {
|
| 175 | 1x |
return(TRUE) |
| 176 |
} |
|
| 177 | 32817x |
new_dev <- open_font_dev(fontspec) |
| 178 | 32817x |
if (new_dev) {
|
| 179 | 31x |
on.exit(close_font_dev()) |
| 180 |
} |
|
| 181 | 32817x |
.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 | 522x |
gpar( |
| 200 | 522x |
fontfamily = fontspec$family, |
| 201 | 522x |
fontsize = fontspec$size, |
| 202 | 522x |
lineheight = fontspec$lineheight |
| 203 |
) |
|
| 204 |
} |
|
| 205 | ||
| 206 | 115571x |
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 | 374x |
spans <- mat$spans |
| 259 | 374x |
keep_mat <- mat$display |
| 260 | 374x |
body <- mat$strings |
| 261 | ||
| 262 | 374x |
nr <- nrow(body) |
| 263 | ||
| 264 | 374x |
cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE) |
| 265 | 374x |
nc <- ncol(cell_widths_mat) |
| 266 | ||
| 267 | 374x |
for (i in seq_len(nrow(body))) {
|
| 268 | 6574x |
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 | 374x |
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 | 216x |
col_gap <- mf_colgap(mat) |
| 288 | 216x |
ncchar <- sum(widths) + (length(widths) - as.integer(mf_has_rlabels(mat))) * col_gap |
| 289 | 216x |
inset <- table_inset(mat) |
| 290 | ||
| 291 |
## Text wrapping checks |
|
| 292 | 216x |
if (tf_wrap) {
|
| 293 | 95x |
if (is.null(max_width)) {
|
| 294 | 25x |
max_width <- getOption("width", 80L)
|
| 295 | 70x |
} else if (is.character(max_width) && identical(max_width, "auto")) {
|
| 296 | ! |
max_width <- ncchar + inset |
| 297 |
} |
|
| 298 | 95x |
assert_number(max_width, lower = 0) |
| 299 |
} |
|
| 300 | ||
| 301 |
## Check for having the right number of widths |
|
| 302 | 216x |
stopifnot(length(widths) == ncol(mat$strings)) |
| 303 | ||
| 304 |
## format the to ASCII |
|
| 305 | 216x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
| 306 | ||
| 307 |
# Check that indentation is correct (it works only for body) |
|
| 308 | 216x |
.check_indentation(mat, row_col_width = cell_widths_mat[, 1, drop = TRUE]) |
| 309 | 213x |
mod_ind_list <- .modify_indentation(mat, cell_widths_mat, do_what = "remove") |
| 310 | 213x |
mfs <- mod_ind_list[["mfs"]] |
| 311 | 213x |
cell_widths_mat <- mod_ind_list[["cell_widths_mat"]] |
| 312 | ||
| 313 |
# Main wrapper |
|
| 314 | 213x |
mf_strings(mat) <- matrix( |
| 315 | 213x |
unlist(mapply(wrap_string, |
| 316 | 213x |
str = mfs, |
| 317 | 213x |
width = cell_widths_mat, |
| 318 | 213x |
collapse = "\n", |
| 319 | 213x |
MoreArgs = list(fontspec = fontspec) |
| 320 |
)), |
|
| 321 | 213x |
ncol = ncol(mfs) |
| 322 |
) |
|
| 323 | ||
| 324 | 213x |
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 | 213x |
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 | 213x |
mat <- update_mf_nlines(mat, max_width = max_width) |
| 332 | ||
| 333 |
# Re-indenting |
|
| 334 | 213x |
mf_strings(mat) <- .modify_indentation(mat, cell_widths_mat, do_what = "add")[["mfs"]] |
| 335 | 213x |
.check_indentation(mat) # all went well |
| 336 |
} |
|
| 337 | 213x |
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 | 430x |
mf_nlh <- mf_nlheader(mat) |
| 345 | 430x |
mf_lgrp <- mf_lgrouping(mat) |
| 346 | 430x |
mf_str <- mf_strings(mat) |
| 347 |
# we base everything on the groupings -> unique indentation identifiers |
|
| 348 | 430x |
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()
|
| 349 | 430x |
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 | 430x |
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "")
|
| 354 | ||
| 355 |
# Expected indent (-x negative numbers should not appear at this stage) |
|
| 356 | 430x |
stopifnot(all(mf_ind >= 0)) |
| 357 | 430x |
real_indent <- vapply(mf_ind, function(ii) {
|
| 358 | 7855x |
paste0(rep(ind_std, ii), collapse = "") |
| 359 | 430x |
}, character(1)) |
| 360 | ||
| 361 | 430x |
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 | 216x |
consistency_check <- vapply(unique(mf_lgrp), function(ii) {
|
| 365 | 3946x |
width_per_grp <- row_col_width[which(mf_lgrp == ii)] |
| 366 | 3946x |
all(width_per_grp == width_per_grp[1]) |
| 367 | 216x |
}, logical(1)) |
| 368 | 216x |
stopifnot(all(consistency_check)) |
| 369 | ||
| 370 |
# Taking only one width for each indentation grouping |
|
| 371 | 216x |
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 | 216x |
nchar_real_indent <- nchar(real_indent) |
| 375 | 216x |
body_rows <- seq(mf_nrheader(mat) + 1, length(nchar_real_indent)) |
| 376 | 216x |
nchar_real_indent[body_rows] <- nchar_real_indent[body_rows] + |
| 377 | 216x |
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 | 216x |
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 | 428x |
correct_indentation <- vapply(seq_along(mf_lgrp), function(xx) {
|
| 392 | 8315x |
grouping <- mf_lgrp[xx] |
| 393 | 8315x |
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 | 8282x |
TRUE |
| 402 | 428x |
}, logical(1)) |
| 403 | ||
| 404 | 428x |
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 | 426x |
mfs <- mf_strings(mat) # we work on mfs |
| 416 | 426x |
mf_nlh <- mf_nlheader(mat) |
| 417 | 426x |
mf_l <- mf_lgrouping(mat) |
| 418 | 426x |
if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()
|
| 419 | 426x |
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 | 426x |
stopifnot(length(mf_ind) == length(unique(mf_l))) # Check for indentation and grouping |
| 424 | 426x |
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") # standard size of indent 1
|
| 425 | ||
| 426 |
# Create real indentation |
|
| 427 | 426x |
real_indent <- sapply(mf_ind, function(ii) paste0(rep(ind_std, ii), collapse = "")) |
| 428 | ||
| 429 |
# Use groupings to add or remove proper indentation |
|
| 430 | 426x |
lbl_row <- mfs[, 1, drop = TRUE] |
| 431 | 426x |
for (ii in seq_along(lbl_row)) {
|
| 432 | 8306x |
grp <- mf_l[ii] |
| 433 | 8306x |
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 | 8277x |
mfs[ii, 1] <- lbl_row[ii] |
| 445 |
} |
|
| 446 |
} |
|
| 447 |
# Final return |
|
| 448 | 426x |
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 | 609x |
sdiff <- setdiff(vec, c(list_valid_aligns(), "c")) |
| 461 | 609x |
if (length(sdiff) > 0) {
|
| 462 | ! |
stop("Invalid text-alignment(s): ", paste(sdiff, collapse = ", "))
|
| 463 |
} |
|
| 464 | 609x |
grepl("dec", vec)
|
| 465 |
} |
|
| 466 | ||
| 467 | 464x |
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 | 162x |
if (nchar(str) == 0) {
|
| 594 | ! |
return(0) |
| 595 |
} |
|
| 596 | 162x |
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 | 165x |
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 | 165x |
new_dev <- open_font_dev(fontspec) |
| 655 | 165x |
if (new_dev) {
|
| 656 | 155x |
on.exit(close_font_dev()) |
| 657 |
} |
|
| 658 | ||
| 659 | 165x |
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 | 165x |
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 | 165x |
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 | 165x |
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 | 165x |
if (!is.null(mf_rinfo(mat)) && # rare case of rtables::rtable() |
| 684 | 165x |
(length(mf_lgrouping(mat)) != nrow(mf_strings(mat)) || # non-unique grouping test # nolint |
| 685 | 165x |
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 | 165x |
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 | 165x |
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 | 161x |
if (is.null(widths)) {
|
| 720 |
# if mf does not have widths -> propose them |
|
| 721 | 135x |
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 | 161x |
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 | 161x |
max_width <- .handle_max_width( |
| 733 | 161x |
tf_wrap = tf_wrap, |
| 734 | 161x |
max_width = max_width, |
| 735 | 161x |
colwidths = widths, |
| 736 | 161x |
col_gap = col_gap, |
| 737 | 161x |
inset = inset |
| 738 |
) |
|
| 739 | ||
| 740 |
# Main wrapper function for table core |
|
| 741 | 161x |
mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap, fontspec = fontspec) |
| 742 | ||
| 743 | 158x |
body <- mf_strings(mat) |
| 744 | 158x |
aligns <- mf_aligns(mat) |
| 745 | 158x |
keep_mat <- mf_display(mat) |
| 746 |
## spans <- mat$spans |
|
| 747 | 158x |
mf_ri <- mf_rinfo(mat) |
| 748 | 158x |
ref_fnotes <- mf_rfnotes(mat) |
| 749 | 158x |
nl_header <- mf_nlheader(mat) |
| 750 | ||
| 751 | 158x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
| 752 | ||
| 753 |
# decimal alignment |
|
| 754 | 158x |
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 | 158x |
content <- matrix( |
| 760 | 158x |
mapply(padstr, body, cell_widths_mat, aligns, MoreArgs = list(fontspec = fontspec)), |
| 761 | 158x |
ncol = ncol(body) |
| 762 |
) |
|
| 763 | 158x |
content[!keep_mat] <- NA |
| 764 | ||
| 765 |
# Define gap string and divisor string |
|
| 766 | 158x |
gap_str <- strrep(" ", col_gap)
|
| 767 | 158x |
if (is.null(hsep)) {
|
| 768 | 123x |
hsep <- horizontal_sep(mat) |
| 769 |
} |
|
| 770 | 158x |
adj_hsep <- calc_str_adj(hsep, fontspec) |
| 771 | 158x |
div <- substr(strrep(hsep, ceiling(ncchar * adj_hsep)), 1, ceiling(ncchar * adj_hsep)) |
| 772 | 158x |
hsd <- header_section_div(mat) |
| 773 | 158x |
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 | 158x |
hsd <- NULL # no divisor |
| 778 |
} |
|
| 779 | ||
| 780 |
# text head (paste w/o NA content header and gap string) |
|
| 781 | 158x |
txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str) |
| 782 | ||
| 783 |
# txt body |
|
| 784 | 158x |
sec_seps_df <- mf_ri[, c("abs_rownumber", "trailing_sep"), drop = FALSE]
|
| 785 | 158x |
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 | 156x |
txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str) |
| 828 |
} |
|
| 829 | ||
| 830 |
# retrieving titles and footers |
|
| 831 | 158x |
allts <- all_titles(mat) |
| 832 | ||
| 833 | 158x |
ref_fnotes <- reorder_ref_fnotes(ref_fnotes) |
| 834 |
# Fix for ref_fnotes with \n characters XXX this does not count in the pagination |
|
| 835 | 158x |
if (any(grepl("\\n", ref_fnotes))) {
|
| 836 | 2x |
ref_fnotes <- unlist(strsplit(ref_fnotes, "\n", fixed = TRUE)) |
| 837 |
} |
|
| 838 | ||
| 839 | 158x |
allfoots <- list( |
| 840 | 158x |
"main_footer" = main_footer(mat), |
| 841 | 158x |
"prov_footer" = prov_footer(mat), |
| 842 | 158x |
"ref_footnotes" = ref_fnotes |
| 843 |
) |
|
| 844 | 158x |
allfoots <- allfoots[!sapply(allfoots, is.null)] |
| 845 | ||
| 846 |
## Wrapping titles if they go beyond the horizontally allowed space |
|
| 847 | 158x |
if (tf_wrap) {
|
| 848 | 70x |
new_line_warning(allts) |
| 849 | 70x |
allts <- wrap_txt(allts, max_width, fontspec = fontspec) |
| 850 |
} |
|
| 851 | 158x |
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 | 158x |
if (tf_wrap) {
|
| 855 | 70x |
new_line_warning(allfoots) |
| 856 | 70x |
allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset, fontspec = fontspec) |
| 857 | 70x |
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 | 70x |
allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width, fontspec = fontspec) |
| 860 |
} |
|
| 861 | ||
| 862 |
# Final return |
|
| 863 | 158x |
paste0( |
| 864 | 158x |
paste(c( |
| 865 | 158x |
titles_txt, # .do_inset(div, inset) happens if there are any titles |
| 866 | 158x |
.do_inset(txt_head, inset), |
| 867 | 158x |
.do_inset(div, inset), |
| 868 | 158x |
.do_inset(hsd, inset), # header_section_div if present |
| 869 | 158x |
.do_inset(txt_body, inset), |
| 870 | 158x |
.footer_inset_helper(allfoots, div, inset) |
| 871 | 158x |
), collapse = "\n"), |
| 872 | 158x |
"\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 | 241x |
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 | 241x |
} else if (tf_wrap) {
|
| 887 | 124x |
if (is.null(max_width)) {
|
| 888 | 38x |
if (is.null(cpp) || is.na(cpp)) {
|
| 889 | 8x |
getOption("width", 80L)
|
| 890 |
} else {
|
|
| 891 | 30x |
cpp |
| 892 |
} |
|
| 893 | 86x |
} else if (is.numeric(max_width)) {
|
| 894 | 81x |
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 | 239x |
return(max_width) |
| 906 |
} |
|
| 907 | ||
| 908 |
.do_inset <- function(x, inset) {
|
|
| 909 | 1069x |
if (inset == 0 || !any(nzchar(x))) {
|
| 910 | 1050x |
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 | 107x |
c(.do_inset(div, inset), "", txt) |
| 923 |
} |
|
| 924 | ||
| 925 |
.footer_inset_helper <- function(footers_v, div, inset) {
|
|
| 926 | 158x |
div_done <- FALSE # nolint |
| 927 | 158x |
fter <- footers_v$main_footer |
| 928 | 158x |
prvf <- footers_v$prov_footer |
| 929 | 158x |
rfn <- footers_v$ref_footnotes |
| 930 | 158x |
footer_txt <- .do_inset(rfn, inset) |
| 931 | 158x |
if (any(nzchar(footer_txt))) {
|
| 932 | 14x |
footer_txt <- .inset_div(footer_txt, div, inset) |
| 933 |
} |
|
| 934 | 158x |
if (any(vapply( |
| 935 | 158x |
footers_v, function(x) any(nzchar(x)), |
| 936 | 158x |
TRUE |
| 937 |
))) {
|
|
| 938 | 93x |
if (any(nzchar(prvf))) {
|
| 939 | 91x |
provtxt <- c( |
| 940 | 91x |
if (any(nzchar(fter))) "", |
| 941 | 91x |
prvf |
| 942 |
) |
|
| 943 |
} else {
|
|
| 944 | 2x |
provtxt <- character() |
| 945 |
} |
|
| 946 | 93x |
footer_txt <- c( |
| 947 | 93x |
footer_txt, |
| 948 | 93x |
.inset_div( |
| 949 | 93x |
c( |
| 950 | 93x |
.do_inset(fter, inset), |
| 951 | 93x |
provtxt |
| 952 |
), |
|
| 953 | 93x |
div, |
| 954 | 93x |
inset |
| 955 |
) |
|
| 956 |
) |
|
| 957 |
} |
|
| 958 | 158x |
footer_txt |
| 959 |
} |
|
| 960 | ||
| 961 |
reorder_ref_fnotes <- function(fns) {
|
|
| 962 | 161x |
ind <- gsub("\\{(.*)\\}.*", "\\1", fns)
|
| 963 | 161x |
ind_num <- suppressWarnings(as.numeric(ind)) |
| 964 | 161x |
is_num <- !is.na(ind_num) |
| 965 | 161x |
is_asis <- ind == fns |
| 966 | ||
| 967 | 161x |
if (all(is_num)) {
|
| 968 | 145x |
ord_num <- order(ind_num) |
| 969 | 145x |
ord_char <- NULL |
| 970 | 145x |
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 | 161x |
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 | 140x |
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 | 36721x |
if (length(str) > 1) {
|
| 1023 | 118x |
return( |
| 1024 | 118x |
unlist( |
| 1025 | 118x |
lapply(str, wrap_string, width = width, collapse = collapse, fontspec = fontspec), |
| 1026 | 118x |
use.names = FALSE |
| 1027 |
) |
|
| 1028 |
) |
|
| 1029 |
} |
|
| 1030 | 36603x |
str <- unlist(str, use.names = FALSE) # it happens is one list element |
| 1031 | 36603x |
if (!length(str) || !nzchar(str) || is.na(str)) {
|
| 1032 | 3955x |
return(str) |
| 1033 |
} |
|
| 1034 | 32648x |
checkmate::assert_character(str) |
| 1035 | 32648x |
checkmate::assert_int(width, lower = 1) |
| 1036 | ||
| 1037 | 32648x |
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 | 32648x |
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 | 32645x |
ret <- .go_stri_wrap(str, width) |
| 1051 | ||
| 1052 |
# Check if it went fine |
|
| 1053 | 32645x |
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 | 32584x |
if (!is.null(collapse)) {
|
| 1103 | 32115x |
return(paste0(ret, collapse = collapse)) |
| 1104 |
} |
|
| 1105 | ||
| 1106 | 469x |
return(ret) |
| 1107 |
} |
|
| 1108 | ||
| 1109 |
.go_stri_wrap <- function(str, w) {
|
|
| 1110 | 32795x |
if (w < 1) {
|
| 1111 | ! |
return(str) |
| 1112 |
} |
|
| 1113 | 32795x |
stringi::stri_wrap(str, |
| 1114 | 32795x |
width = w, |
| 1115 | 32795x |
normalize = FALSE, # keeps spaces |
| 1116 | 32795x |
simplify = TRUE, # If FALSE makes it a list with str elements |
| 1117 | 32795x |
indent = 0, |
| 1118 | 32795x |
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 | 410x |
new_dev <- open_font_dev(fontspec) |
| 1266 | 410x |
if (new_dev) {
|
| 1267 | 2x |
on.exit(close_font_dev()) |
| 1268 |
} |
|
| 1269 | ||
| 1270 | 410x |
unlist(wrap_string(str, width, collapse, fontspec = fontspec), use.names = FALSE) |
| 1271 |
} |
|
| 1272 | ||
| 1273 |
pad_vert_top <- function(x, len, default = "") {
|
|
| 1274 | 5810x |
c(x, rep(default, len - length(x))) |
| 1275 |
} |
|
| 1276 | ||
| 1277 |
pad_vert_bottom <- function(x, len, default = "") {
|
|
| 1278 | 450x |
c(rep(default, len - length(x)), x) |
| 1279 |
} |
|
| 1280 | ||
| 1281 |
pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) {
|
|
| 1282 | 777x |
dat <- unlist(lapply(vec[-1], cpadder, len = len)) |
| 1283 | 777x |
dat <- c(rlpadder(vec[[1]], len = len), dat) |
| 1284 | 777x |
matrix(dat, nrow = len) |
| 1285 |
} |
|
| 1286 | ||
| 1287 |
rep_vec_to_len <- function(vec, len, ...) {
|
|
| 1288 | 730x |
matrix(unlist(lapply(vec, rep, times = len)), |
| 1289 | 730x |
nrow = len |
| 1290 |
) |
|
| 1291 |
} |
|
| 1292 | ||
| 1293 |
safe_strsplit <- function(x, split, ...) {
|
|
| 1294 | 893x |
ret <- strsplit(x, split, ...) |
| 1295 | 893x |
lapply(ret, function(reti) if (length(reti) == 0) "" else reti) |
| 1296 |
} |
|
| 1297 | ||
| 1298 |
.expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) {
|
|
| 1299 | 1507x |
leni <- row_nlines[i] |
| 1300 | 1507x |
rw <- mat[i, ] |
| 1301 | 1507x |
if (is.character(rw)) {
|
| 1302 | 893x |
rw <- safe_strsplit(rw, "\n", fixed = TRUE) |
| 1303 |
} |
|
| 1304 | 1507x |
expfun(rw, len = leni, ...) |
| 1305 |
} |
|
| 1306 | ||
| 1307 |
expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) {
|
|
| 1308 | 308x |
rinds <- seq_len(nrow(mat)) |
| 1309 | 308x |
exprows <- lapply(rinds, .expand_mat_rows_inner, |
| 1310 | 308x |
mat = mat, |
| 1311 | 308x |
row_nlines = row_nlines, |
| 1312 | 308x |
expfun = expfun, |
| 1313 |
... |
|
| 1314 |
) |
|
| 1315 | 308x |
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 | 96x |
new_dev <- open_font_dev(fontspec) |
| 1384 | 96x |
if (new_dev) {
|
| 1385 | 66x |
on.exit(close_font_dev()) |
| 1386 |
} |
|
| 1387 | ||
| 1388 | 96x |
if (!is(x, "MatrixPrintForm")) {
|
| 1389 | ! |
x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type) |
| 1390 |
} |
|
| 1391 | 96x |
body <- mf_strings(x) |
| 1392 | 96x |
spans <- mf_spans(x) |
| 1393 | 96x |
aligns <- mf_aligns(x) |
| 1394 | 96x |
display <- mf_display(x) |
| 1395 | ||
| 1396 |
# compute decimal alignment if asked in alignment matrix |
|
| 1397 | 96x |
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 | 93x |
chars <- nchar_ttype(body, fontspec) |
| 1406 | ||
| 1407 |
# first check column widths without colspan |
|
| 1408 | 93x |
has_spans <- spans != 1 |
| 1409 | 93x |
chars_ns <- chars |
| 1410 | 93x |
chars_ns[has_spans] <- 0 |
| 1411 | 93x |
widths <- apply(chars_ns, 2, max) |
| 1412 | ||
| 1413 |
# now check if the colspans require extra width |
|
| 1414 | 93x |
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 | 93x |
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 | 48989x |
if (is.null(fontspec)) {
|
| 1493 | 1x |
return(nchar(x)) |
| 1494 |
} |
|
| 1495 | 48988x |
new_dev <- open_font_dev(fontspec) |
| 1496 | 48988x |
if (new_dev) {
|
| 1497 | 149x |
on.exit(close_font_dev()) |
| 1498 |
} |
|
| 1499 | 48988x |
if (font_dev_state$ismonospace) { ## WAY faster if we can do it
|
| 1500 | 48964x |
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 | 15759x |
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 | 15757x |
nc <- nchar_ttype(x, fontspec) |
| 1559 | ! |
if (n < nc) stop("\"", x, "\" has more than ", n, " characters")
|
| 1560 | ||
| 1561 | 15757x |
switch(just, |
| 1562 |
center = {
|
|
| 1563 | 13852x |
pad <- (n - nc) / 2 |
| 1564 | 13852x |
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 | 29828x |
strrep(" ", n)
|
| 1579 |
} |
|
| 1580 | ||
| 1581 |
.paste_no_na <- function(x, ...) {
|
|
| 1582 | 2418x |
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 |
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 | 16374x |
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 | 9802x |
if (length(x) == 0) {
|
| 313 | 1x |
return("")
|
| 314 |
} |
|
| 315 | ||
| 316 | 9801x |
output <- match.arg(output) |
| 317 | 9801x |
round_type <- match.arg(round_type) |
| 318 | ||
| 319 |
# Checks for NAs in the input |
|
| 320 | 9801x |
if (length(na_str) == 0) {
|
| 321 | 1x |
na_str <- "NA" |
| 322 |
} |
|
| 323 | 9801x |
if (any(is.na(na_str))) {
|
| 324 | 1x |
na_str[is.na(na_str)] <- "NA" |
| 325 |
} |
|
| 326 | 9801x |
if (length(na_str) == 1) {
|
| 327 | 9797x |
if (!all(is.na(x))) {
|
| 328 |
## array adds an unneeded dim attribute which causes problems |
|
| 329 | 9767x |
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 | 9801x |
txt <- if (all(is.na(x)) && length(na_str) == 1L) {
|
| 341 | 30x |
na_str |
| 342 | 9801x |
} else if (is.null(format)) {
|
| 343 | 318x |
toString(x) |
| 344 | 9801x |
} 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 | 9801x |
} else if (is.character(format)) {
|
| 347 | 9448x |
l <- if (format %in% formats_1d) {
|
| 348 | 8963x |
1 |
| 349 | 9448x |
} else if (format %in% formats_2d) {
|
| 350 | 403x |
2 |
| 351 | 9448x |
} 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 | 9447x |
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 | 9445x |
switch(format, |
| 366 | 8752x |
"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 | 9801x |
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 | 9798x |
txt[is.na(txt)] <- na_str[1] |
| 514 | ||
| 515 | ||
| 516 | 9798x |
if (output == "ascii") {
|
| 517 | 9797x |
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 | 16x |
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 | 16x |
if (paginate) {
|
| 76 | 16x |
pages <- paginate_to_mpfs( |
| 77 | 16x |
x, |
| 78 | 16x |
page_type = page_type, |
| 79 |
## font_family = font_family, |
|
| 80 |
## font_size = font_size, |
|
| 81 |
## lineheight = lineheight, |
|
| 82 | 16x |
landscape = landscape, |
| 83 | 16x |
pg_width = pg_width, |
| 84 | 16x |
pg_height = pg_height, |
| 85 | 16x |
margins = margins, |
| 86 | 16x |
lpp = lpp, |
| 87 | 16x |
cpp = cpp, |
| 88 | 16x |
min_siblings = min_siblings, |
| 89 | 16x |
nosplitin = nosplitin, |
| 90 | 16x |
colwidths = colwidths, |
| 91 | 16x |
tf_wrap = tf_wrap, |
| 92 | 16x |
max_width = max_width, |
| 93 | 16x |
indent_size = indent_size, |
| 94 | 16x |
verbose = verbose, |
| 95 | 16x |
rep_cols = rep_cols, |
| 96 | 16x |
page_num = page_num, |
| 97 | 16x |
fontspec = fontspec, |
| 98 | 16x |
col_gap = col_gap, |
| 99 | 16x |
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 | 15x |
if (!is.character(max_width)) {
|
| 117 | 15x |
max_width <- .handle_max_width( |
| 118 | 15x |
tf_wrap = tf_wrap, |
| 119 | 15x |
max_width = max_width, |
| 120 | 15x |
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 | 15x |
strings <- vapply( |
| 127 | 15x |
pages, toString, "", |
| 128 | 15x |
widths = NULL, |
| 129 | 15x |
hsep = hsep, tf_wrap = tf_wrap, max_width = max_width, col_gap = col_gap |
| 130 |
) |
|
| 131 | ||
| 132 | 15x |
res <- paste(strings, collapse = page_break) |
| 133 | ||
| 134 | 15x |
if (is.null(file)) {
|
| 135 | 13x |
res |
| 136 |
} else {
|
|
| 137 | 2x |
cat(res, file = file) |
| 138 |
} |
|
| 139 |
} |
|
| 140 | ||
| 141 |
.is_list_of_tables_or_listings <- function(a_list) {
|
|
| 142 | 82x |
if (is(a_list, "list")) {
|
| 143 | 82x |
all_matrix_forms <- FALSE |
| 144 | 82x |
obj_are_tables_or_listings <- FALSE |
| 145 | ||
| 146 | 82x |
if (is(a_list[[1]], "MatrixPrintForm")) {
|
| 147 | 15x |
all_matrix_forms <- all(sapply(a_list, is, class2 = "MatrixPrintForm")) |
| 148 |
} else {
|
|
| 149 | 67x |
obj_are_tables_or_listings <- all( |
| 150 | 67x |
sapply(a_list, function(list_i) {
|
| 151 | 1675x |
is(list_i, "listing_df") || is(list_i, "VTableTree") |
| 152 |
}) |
|
| 153 |
) |
|
| 154 |
} |
|
| 155 | 82x |
out <- obj_are_tables_or_listings || all_matrix_forms |
| 156 |
} else {
|
|
| 157 | ! |
out <- FALSE |
| 158 |
} |
|
| 159 | ||
| 160 | 82x |
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 |
#' @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 | 422x |
if (font_family %in% c("Times New Roman", "Times", "serif")) {
|
| 50 | 4x |
font_family <- times_font_name() |
| 51 |
} |
|
| 52 | 422x |
structure( |
| 53 | 422x |
list( |
| 54 | 422x |
family = font_family, |
| 55 | 422x |
size = font_size, |
| 56 | 422x |
lineheight = lineheight |
| 57 |
), |
|
| 58 | 422x |
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 | 75x |
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 | 47x |
if (is.null(page_type)) {
|
| 120 | 30x |
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 | 59x |
new_dev <- open_font_dev(fontspec) |
| 157 | 59x |
if (new_dev) {
|
| 158 | 10x |
on.exit(close_font_dev()) |
| 159 |
} |
|
| 160 | 59x |
list( |
| 161 | 59x |
cpi = 1 / convertWidth(unit(1, "strwidth", " "), "inches", valueOnly = TRUE), |
| 162 | 59x |
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 | 55x |
if (is.null(page_type)) {
|
| 208 | 20x |
page_type <- page_types()[1] |
| 209 |
} else {
|
|
| 210 | 35x |
page_type <- match.arg(page_type) |
| 211 |
} |
|
| 212 | ||
| 213 | 55x |
if (is.null(names(margins))) {
|
| 214 | 12x |
names(margins) <- marg_order |
| 215 |
} else {
|
|
| 216 | 43x |
margins <- margins[marg_order] |
| 217 |
} |
|
| 218 | 55x |
if (any(is.na(margins))) {
|
| 219 | ! |
stop("margins argument must have names 'bottom', 'left', 'top' and 'right'.")
|
| 220 |
} |
|
| 221 | 55x |
lcpi <- font_lcpi(fontspec = fontspec) |
| 222 | ||
| 223 | 55x |
wdpos <- ifelse(landscape, 2, 1) |
| 224 | 55x |
pg_width <- pg_width %||% pg_dim_names[[page_type]][wdpos] |
| 225 | 55x |
pg_height <- pg_height %||% pg_dim_names[[page_type]][-wdpos] |
| 226 | ||
| 227 | 55x |
pg_width <- pg_width - sum(margins[c("left", "right")])
|
| 228 | 55x |
pg_height <- pg_height - sum(margins[c("top", "bottom")])
|
| 229 | ||
| 230 | 55x |
list( |
| 231 | 55x |
cpp = floor(lcpi[["cpi"]] * pg_width), |
| 232 | 55x |
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 | 437x |
if (length(a) == 0) {
|
| 23 | 134x |
b |
| 24 |
} else {
|
|
| 25 | 303x |
a |
| 26 |
} |
|
| 27 |
} |
| 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 | 285x |
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 | 285x |
if (!is.null(fontspec)) {
|
| 145 | 280x |
mf_fontspec(obj) <- fontspec |
| 146 |
} |
|
| 147 | 285x |
if (!is.null(col_gap) && !isTRUE(all.equal(col_gap, mf_colgap(obj)))) {
|
| 148 | ! |
mf_colgap(obj) <- col_gap |
| 149 |
} |
|
| 150 | 285x |
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 | 50x |
setGeneric("divider_height", function(obj) standardGeneric("divider_height"))
|
| 174 | ||
| 175 |
#' @rdname divider_height |
|
| 176 |
#' @export |
|
| 177 |
setMethod( |
|
| 178 |
"divider_height", "ANY", |
|
| 179 | 50x |
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 | 50958x |
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 | 50955x |
splstr <- strsplit(x, "\n", fixed = TRUE) |
| 227 | 50955x |
if (length(x) == 0) {
|
| 228 | 1x |
return(0L) |
| 229 |
} |
|
| 230 | ||
| 231 | 50954x |
sum(vapply(splstr, |
| 232 | 50954x |
function(xi, max_width) {
|
| 233 | 51006x |
if (length(xi) == 0) {
|
| 234 | 1543x |
1L |
| 235 | 49463x |
} else if (length(max_width) == 0) { ## this happens with strsplit("", "\n")
|
| 236 | 49335x |
length(xi) |
| 237 |
} else {
|
|
| 238 | 128x |
length(wrap_txt(xi, max_width, fontspec = fontspec)) |
| 239 |
} |
|
| 240 | 50954x |
}, 1L, |
| 241 | 50954x |
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 | 312x |
setGeneric("obj_format", function(obj) standardGeneric("obj_format"))
|
| 323 | ||
| 324 |
## this covers rcell, etc |
|
| 325 |
#' @rdname lab_name |
|
| 326 |
#' @exportMethod obj_format |
|
| 327 | 310x |
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 | 509x |
setGeneric("main_title", function(obj) standardGeneric("main_title"))
|
| 429 | ||
| 430 |
#' @export |
|
| 431 |
#' @rdname title_footer |
|
| 432 |
setMethod( |
|
| 433 |
"main_title", "MatrixPrintForm", |
|
| 434 | 509x |
function(obj) obj$main_title |
| 435 |
) |
|
| 436 | ||
| 437 |
##' @rdname title_footer |
|
| 438 |
##' @export |
|
| 439 | 18x |
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 | 18x |
obj$main_title <- value |
| 447 | 18x |
obj |
| 448 |
} |
|
| 449 |
) |
|
| 450 | ||
| 451 |
# subtitles --------------------------------------------------------------- |
|
| 452 | ||
| 453 |
#' @export |
|
| 454 |
#' @rdname title_footer |
|
| 455 | 508x |
setGeneric("subtitles", function(obj) standardGeneric("subtitles"))
|
| 456 | ||
| 457 |
#' @export |
|
| 458 |
#' @rdname title_footer |
|
| 459 |
setMethod( |
|
| 460 |
"subtitles", "MatrixPrintForm", |
|
| 461 | 508x |
function(obj) obj$subtitles |
| 462 |
) |
|
| 463 | ||
| 464 |
##' @rdname title_footer |
|
| 465 |
##' @export |
|
| 466 | 15x |
setGeneric("subtitles<-", function(obj, value) standardGeneric("subtitles<-"))
|
| 467 | ||
| 468 |
##' @rdname title_footer |
|
| 469 |
##' @export |
|
| 470 |
setMethod( |
|
| 471 |
"subtitles<-", "MatrixPrintForm", |
|
| 472 |
function(obj, value) {
|
|
| 473 | 15x |
obj$subtitles <- value |
| 474 | 15x |
obj |
| 475 |
} |
|
| 476 |
) |
|
| 477 | ||
| 478 |
# page_titles --------------------------------------------------------------- |
|
| 479 | ||
| 480 |
#' @export |
|
| 481 |
#' @rdname title_footer |
|
| 482 | 550x |
setGeneric("page_titles", function(obj) standardGeneric("page_titles"))
|
| 483 | ||
| 484 |
#' @export |
|
| 485 |
#' @rdname title_footer |
|
| 486 |
setMethod( |
|
| 487 |
"page_titles", "MatrixPrintForm", |
|
| 488 | 550x |
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 | 488x |
setGeneric("main_footer", function(obj) standardGeneric("main_footer"))
|
| 516 | ||
| 517 |
#' @export |
|
| 518 |
#' @rdname title_footer |
|
| 519 |
setMethod( |
|
| 520 |
"main_footer", "MatrixPrintForm", |
|
| 521 | 488x |
function(obj) obj$main_footer |
| 522 |
) |
|
| 523 | ||
| 524 |
#' @rdname title_footer |
|
| 525 |
#' @param value character. New value. |
|
| 526 |
#' @export |
|
| 527 | 285x |
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 | 285x |
if (!is.character(value)) {
|
| 535 | ! |
stop("main footer must be a character vector. Got object of class ", class(value))
|
| 536 |
} |
|
| 537 | 285x |
obj$main_footer <- value |
| 538 | 285x |
obj |
| 539 |
} |
|
| 540 |
) |
|
| 541 | ||
| 542 |
# prov_footer --------------------------------------------------------------- |
|
| 543 | ||
| 544 |
#' @export |
|
| 545 |
#' @rdname title_footer |
|
| 546 | 614x |
setGeneric("prov_footer", function(obj) standardGeneric("prov_footer"))
|
| 547 | ||
| 548 |
#' @export |
|
| 549 |
#' @rdname title_footer |
|
| 550 |
setMethod( |
|
| 551 |
"prov_footer", "MatrixPrintForm", |
|
| 552 | 614x |
function(obj) obj$prov_footer |
| 553 |
) |
|
| 554 | ||
| 555 |
#' @rdname title_footer |
|
| 556 |
#' @export |
|
| 557 | 368x |
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 | 368x |
if (!is.character(value)) {
|
| 565 | ! |
stop("provenance footer must be a character vector. Got object of class ", class(value))
|
| 566 |
} |
|
| 567 | 368x |
obj$prov_footer <- value |
| 568 | 368x |
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 | 501x |
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 | 467x |
setGeneric("table_inset", function(obj) standardGeneric("table_inset"))
|
| 600 | ||
| 601 |
#' @rdname table_inset |
|
| 602 |
#' @export |
|
| 603 |
setMethod( |
|
| 604 |
"table_inset", "MatrixPrintForm", |
|
| 605 | 467x |
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 | 106x |
setGeneric("do_forced_paginate", function(obj) standardGeneric("do_forced_paginate"))
|
| 642 | ||
| 643 |
#' @export |
|
| 644 |
#' @rdname do_forced_paginate |
|
| 645 | 103x |
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 | 573x |
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 | 573x |
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 | 96x |
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 | 96x |
obj <- mf_update_cinfo(obj, colwidths = NULL, rep_cols = value) |
| 692 | 96x |
obj |
| 693 |
}) |
|
| 694 | ||
| 695 |
# header_section_div ----------------------------------------------------------- |
|
| 696 | ||
| 697 |
#' @keywords internal |
|
| 698 | 158x |
setGeneric("header_section_div", function(obj) standardGeneric("header_section_div"))
|
| 699 | ||
| 700 |
#' @keywords internal |
|
| 701 |
setMethod( |
|
| 702 |
"header_section_div", "MatrixPrintForm", |
|
| 703 | 158x |
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 | 123x |
setGeneric("horizontal_sep", function(obj) standardGeneric("horizontal_sep"))
|
| 722 | ||
| 723 |
#' @keywords internal |
|
| 724 |
setMethod( |
|
| 725 |
"horizontal_sep", "MatrixPrintForm", |
|
| 726 | 123x |
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 |
#' 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 |
#' 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 | 56x |
system_default_hsep <- getOption("formatters_default_hsep")
|
| 23 | ||
| 24 | 56x |
if (is.null(system_default_hsep)) {
|
| 25 | 55x |
if (any(grepl("^UTF", utils::localeToCharset()))) {
|
| 26 | 55x |
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 | 56x |
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 | 36x |
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 |
} |