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 |
#' @name pagination_algo |
|
41 |
#' @rdname pagination_algo |
|
42 |
#' @title Pagination |
|
43 |
#' @section Pagination Algorithm: |
|
44 |
#' |
|
45 |
#' Pagination is performed independently in the vertical and horizontal |
|
46 |
#' directions based solely on a *pagination data.frame*, which includes the |
|
47 |
#' following information for each row/column: |
|
48 |
#' |
|
49 |
#' - number of lines/characters rendering the row will take **after |
|
50 |
#' word-wrapping** (`self_extent`) |
|
51 |
#' - the indices (`reprint_inds`) and number of lines (`par_extent`) |
|
52 |
#' of the rows which act as **context** for the row |
|
53 |
#' - the row's number of siblings and position within its siblings |
|
54 |
#' |
|
55 |
#' Given `lpp` (`cpp`) already adjusted for rendered elements which |
|
56 |
#' are not rows/columns and a dataframe of pagination information, |
|
57 |
#' pagination is performed via the following algorithm, and with a |
|
58 |
#' `start = 1`: |
|
59 |
#' |
|
60 |
#' Core Pagination Algorithm: |
|
61 |
#' 1. Initial guess for pagination point is `start + lpp` (`start + cpp`) |
|
62 |
#' |
|
63 |
#' 2. While the guess is not a valid pagination position, and `guess > |
|
64 |
#' start`, decrement guess and repeat |
|
65 |
#' - an error is thrown if all possible pagination positions between |
|
66 |
#' `start` and `start + lpp` (`start + cpp`) would ever be `< start` |
|
67 |
#' after decrementing |
|
68 |
#' 3. Retain pagination index |
|
69 |
#' 4. if pagination point was less than `NROW(tt)` (`ncol(tt)`), set |
|
70 |
#' `start` to `pos + 1`, and repeat steps (1) - (4). |
|
71 |
#' |
|
72 |
#' Validating pagination position: |
|
73 |
#' |
|
74 |
#' Given an (already adjusted) `lpp` or `cpp` value, a pagination is invalid if: |
|
75 |
#' |
|
76 |
#' - The rows/columns on the page would take more than (adjusted) `lpp` lines/`cpp` |
|
77 |
#' characters to render **including** |
|
78 |
#' - word-wrapping |
|
79 |
#' - (vertical only) context repetition |
|
80 |
#' - (vertical only) footnote messages and or section divider lines |
|
81 |
#' take up too many lines after rendering rows |
|
82 |
#' - (vertical only) row is a label or content (row-group summary) row |
|
83 |
#' - (vertical only) row at the pagination point has siblings, and |
|
84 |
#' it has less than `min_siblings` preceding or following siblings |
|
85 |
#' - pagination would occur within a sub-table listed in `nosplitin` |
|
86 |
#' |
|
87 |
NULL |
|
88 | ||
89 |
#' Create row of pagination data frame |
|
90 |
#' @param nm character(1). Name |
|
91 |
#' @param lab character(1). Label |
|
92 |
#' @param rnum numeric(1). Absolute row number |
|
93 |
#' @param pth character or NULL. Path within larger table |
|
94 |
#' @param sibpos integer(1). Position among sibling rows |
|
95 |
#' @param nsibs integer(1). Number of siblings (including self). |
|
96 |
#' @param extent numeric(1). Number of lines required to print the row |
|
97 |
#' @param colwidths numeric. Column widths |
|
98 |
#' @param repext integer(1). Number of lines required to reprint all context for this row if it appears directly |
|
99 |
#' after pagination. |
|
100 |
#' @param repind integer. Vector of row numbers to be reprinted if this row appears directly after pagination. |
|
101 |
#' @param indent integer. Indent |
|
102 |
#' @param rclass character(1). Class of row object. |
|
103 |
#' @param nrowrefs integer(1). Number of row referential footnotes for this row |
|
104 |
#' @param ncellrefs integer(1). Number of cell referential footnotes for the cells in this row |
|
105 |
#' @param nreflines integer(1). Total number of lines required by all referential footnotes |
|
106 |
#' @param force_page logical(1). Currently Ignored. |
|
107 |
#' @param page_title logical(1). Currently Ignored. |
|
108 |
#' @param trailing_sep character(1). The string to used as a separator below this row during printing (or |
|
109 |
#' `NA_character_` for no separator). |
|
110 |
#' @param row ANY. Object representing the row, which is used for default values of \code{nm}, \code{lab}, |
|
111 |
#' \code{extent} and \code{rclass} if provided. Must have methods for \code{obj_name}, \code{obj_label}, |
|
112 |
#' and \code{nlines}, respectively, for default values of \code{nm}, \code{lab} and \code{extent} to |
|
113 |
#' be retrieved, respectively. |
|
114 |
#' |
|
115 |
#' @return a single row data.frame with the columns appropriate for a pagination info data frame. |
|
116 |
#' @export |
|
117 |
pagdfrow <- function(row, |
|
118 |
nm = obj_name(row), |
|
119 |
lab = obj_label(row), |
|
120 |
rnum, |
|
121 |
pth, |
|
122 |
sibpos = NA_integer_, |
|
123 |
nsibs = NA_integer_, |
|
124 |
extent = nlines(row, colwidths), |
|
125 |
colwidths = NULL, |
|
126 |
repext = 0L, |
|
127 |
repind = integer(), |
|
128 |
indent = 0L, |
|
129 |
rclass = class(row), |
|
130 |
nrowrefs = 0L, |
|
131 |
ncellrefs = 0L, |
|
132 |
nreflines = 0L, |
|
133 |
# ref_df = .make_ref_df(NULL, NULL), |
|
134 |
force_page = FALSE, |
|
135 |
page_title = NA_character_, |
|
136 |
trailing_sep = NA_character_) { |
|
137 | 859x |
data.frame( |
138 | 859x |
label = lab, |
139 | 859x |
name = nm, |
140 | 859x |
abs_rownumber = rnum, |
141 | 859x |
path = I(list(pth)), |
142 | 859x |
pos_in_siblings = sibpos, |
143 | 859x |
n_siblings = nsibs, |
144 | 859x |
self_extent = extent, |
145 | 859x |
par_extent = repext, |
146 | 859x |
reprint_inds = I(rep(list(unlist(repind)), length.out = length(nm))), |
147 | 859x |
node_class = rclass, |
148 | 859x |
indent = max(0L, indent), |
149 | 859x |
nrowrefs = nrowrefs, |
150 | 859x |
ncellrefs = ncellrefs, |
151 | 859x |
nreflines = nreflines, |
152 |
# ref_info_df = I(list(ref_df)), |
|
153 | 859x |
force_page = force_page, |
154 | 859x |
page_title = page_title, |
155 | 859x |
trailing_sep = trailing_sep, |
156 | 859x |
stringsAsFactors = FALSE, |
157 | 859x |
row.names = NULL, |
158 | 859x |
check.names = FALSE, |
159 | 859x |
fix.empty.names = FALSE |
160 |
) |
|
161 |
} |
|
162 | ||
163 | ||
164 |
calc_ref_nlines_df <- function(pagdf) { |
|
165 |
## XXX XXX XXX this is dangerous and wrong!!! |
|
166 | 395x |
if(is.null(pagdf$ref_info_df) && sum(pagdf$nreflines) == 0) |
167 | 154x |
return(ref_df_row()[0, ]) |
168 | 241x |
refdf <- do.call(rbind.data.frame, pagdf$ref_info_df) |
169 | 241x |
if(NROW(refdf) == 0) |
170 | 146x |
return(ref_df_row()[0, ]) |
171 | 95x |
unqsyms <- !duplicated(refdf$symbol) |
172 | 95x |
refdf[unqsyms, ,drop = FALSE] |
173 |
} |
|
174 | ||
175 | ||
176 |
build_fail_msg <- function(row, lines, raw_rowlines, |
|
177 |
start, guess, rep_ext, n_reprint, |
|
178 |
reflines, n_refs, sectlines) { |
|
179 | 249x |
if(row) { |
180 | 102x |
spacetype <- "lines" |
181 | 102x |
spacetype_abr <- "lns" |
182 | 102x |
structtype_abr <- "rws" |
183 | 102x |
sprintf("\t....................... FAIL: requires %d %s [raw: %d %s (%d %s), rep. context: %d %s (%d %s), refs: %d %s (%d) sect. divs: %d %s].", |
184 | 102x |
lines, |
185 | 102x |
spacetype, |
186 | 102x |
raw_rowlines, |
187 | 102x |
spacetype_abr, |
188 | 102x |
guess - start + 1, # because it includes both start and guess |
189 | 102x |
structtype_abr, |
190 | 102x |
rep_ext, |
191 | 102x |
spacetype_abr, |
192 | 102x |
n_reprint, |
193 | 102x |
structtype_abr, |
194 | 102x |
reflines, |
195 | 102x |
spacetype_abr, |
196 | 102x |
n_refs, |
197 | 102x |
sectlines, |
198 | 102x |
spacetype_abr) |
199 |
} else { ## !row |
|
200 | 147x |
spacetype <- "chars" |
201 | 147x |
spacetype_abr <- "chars" |
202 | 147x |
structtype_abr <- "cols" |
203 | 147x |
sprintf("\t....................... FAIL: requires %d %s (%d %s).", |
204 | 147x |
lines, |
205 | 147x |
spacetype, |
206 | 147x |
guess - start + 1, # because it includes both start and guess |
207 | 147x |
structtype_abr) |
208 |
} |
|
209 |
} |
|
210 | ||
211 |
valid_pag <- function(pagdf, |
|
212 |
guess, |
|
213 |
start, |
|
214 |
rlpp, |
|
215 |
min_sibs, |
|
216 |
nosplit = NULL, |
|
217 |
div_height = 1L, |
|
218 |
verbose = FALSE, |
|
219 |
row = TRUE, |
|
220 |
have_col_fnotes = FALSE) { |
|
221 | 395x |
rw <- pagdf[guess, ] |
222 | ||
223 | ||
224 | 395x |
if (verbose) { |
225 | 372x |
message( |
226 | 372x |
"Checking pagination after ", |
227 | 372x |
paste(ifelse(row, "row", "column"), guess) |
228 |
) |
|
229 |
} |
|
230 | 395x |
raw_rowlines <- sum(pagdf[start:guess, "self_extent"] - pagdf[start:guess, "nreflines"]) |
231 | ||
232 | 395x |
refdf_ii <- calc_ref_nlines_df(pagdf[start:guess,]) |
233 | 395x |
reflines <- if(row) sum(refdf_ii$nlines, 0L) else 0L |
234 | 395x |
if (reflines > 0 && !have_col_fnotes) |
235 | 32x |
reflines <- reflines + div_height + 1L |
236 | ||
237 |
## reflines <- sum(pagdf[start:guess, "nreflines"]) |
|
238 | 395x |
rowlines <- raw_rowlines + reflines ##sum(pagdf[start:guess, "self_extent"]) - reflines ## self extent includes reflines |
239 |
## self extent does ***not*** currently include trailing sep |
|
240 |
## we don't include the trailing_sep for guess because if we paginate here it won't be printed |
|
241 | 395x |
sectlines <- if (start == guess) 0L else sum(!is.na(pagdf[start:(guess - 1), "trailing_sep"])) |
242 | 395x |
lines <- rowlines + sectlines # guess - start + 1 because inclusive of start |
243 | 395x |
rep_ext <- pagdf$par_extent[start] |
244 | 395x |
if(lines > rlpp) { |
245 | 264x |
if(verbose) { |
246 | 249x |
structtype <- ifelse(row, "rows", "columns") |
247 | 249x |
structtype_abr <- ifelse(row, "rows", "cols") |
248 | 249x |
spacetype <- ifelse(row, "lines", "chars") |
249 | 249x |
spacetype_abr <- ifelse(row, "lns", "chrs") |
250 | 249x |
msg <- build_fail_msg(row, lines, raw_rowlines, start, guess, rep_ext, length(pagdf$reprint_inds[[start]]),reflines, NROW(refdf_ii), sectlines) |
251 | 249x |
message(msg) |
252 |
} |
|
253 | 264x |
return(FALSE) |
254 |
} |
|
255 | 131x |
if (rw[["node_class"]] %in% c("LabelRow", "ContentRow")) { |
256 | 5x |
if (verbose) { |
257 | 5x |
message("\t....................... FAIL: last row is a label or content row") |
258 |
} |
|
259 | 5x |
return(FALSE) |
260 |
} |
|
261 | ||
262 | 126x |
sibpos <- rw[["pos_in_siblings"]] |
263 | 126x |
nsib <- rw[["n_siblings"]] |
264 |
# okpos <- min(min_sibs + 1, rw[["n_siblings"]]) |
|
265 | 126x |
if (sibpos != nsib) { |
266 | 67x |
retfalse <- FALSE |
267 | 67x |
if (sibpos < min_sibs + 1) { |
268 | 25x |
retfalse <- TRUE |
269 | 25x |
if (verbose) { |
270 | 25x |
message( |
271 | 25x |
"\t....................... FAIL: last row had only ", sibpos - 1, |
272 | 25x |
" preceding siblings, needed ", min_sibs |
273 |
) |
|
274 |
} |
|
275 | 42x |
} else if (nsib - sibpos < min_sibs + 1) { |
276 | 4x |
retfalse <- TRUE |
277 | 4x |
if (verbose) { |
278 | 4x |
message( |
279 | 4x |
"\t....................... FAIL: last row had only ", nsib - sibpos - 1, |
280 | 4x |
" following siblings, needed ", min_sibs |
281 |
) |
|
282 |
} |
|
283 |
} |
|
284 | 67x |
if (retfalse) { |
285 | 29x |
return(FALSE) |
286 |
} |
|
287 |
} |
|
288 | 97x |
if (guess < nrow(pagdf) && length(nosplit > 0)) { |
289 |
## paths end at the leaf name which is *always* different |
|
290 | 16x |
curpth <- head(unlist(rw$path), -1) |
291 | 16x |
nxtpth <- head(unlist(pagdf$path[[guess + 1]]), -1) |
292 | ||
293 | 16x |
inplay <- nosplit[(nosplit %in% intersect(curpth, nxtpth))] |
294 | 16x |
if (length(inplay) > 0) { |
295 | 16x |
ok_split <- vapply(inplay, |
296 | 16x |
function(var) { |
297 | 16x |
!identical(curpth[match(var, curpth) + 1], |
298 | 16x |
nxtpth[match(var, nxtpth) +1]) |
299 |
}, |
|
300 | 16x |
TRUE) |
301 | ||
302 | 16x |
curvals <- curpth[match(inplay, curpth) + 1] |
303 | 16x |
nxtvals <- nxtpth[match(inplay, nxtpth) + 1] |
304 | 16x |
if (!all(ok_split)) { |
305 | 16x |
if (verbose) { |
306 | 16x |
message( |
307 | 16x |
"\t....................... FAIL: nosplit variable [", |
308 | 16x |
inplay[min(which(!ok_split))], "] would be constant [", |
309 | 16x |
curvals, "] across this pagebreak.") |
310 |
} |
|
311 | 16x |
return(FALSE) |
312 |
} |
|
313 |
} |
|
314 |
} |
|
315 | 81x |
if (verbose) { |
316 | 73x |
message("\t....................... OK [", lines + rep_ext, if (row) " lines]" else " chars]") |
317 |
} |
|
318 | 81x |
TRUE |
319 |
} |
|
320 | ||
321 | ||
322 |
find_pag <- function(pagdf, |
|
323 |
start, |
|
324 |
guess, |
|
325 |
rlpp, |
|
326 |
min_siblings, |
|
327 |
nosplitin = character(), |
|
328 |
verbose = FALSE, |
|
329 |
row = TRUE, |
|
330 |
have_col_fnotes = FALSE, |
|
331 |
div_height = 1L) { |
|
332 | 86x |
origuess <- guess |
333 | 86x |
while (guess >= start && !valid_pag(pagdf, guess, |
334 | 86x |
start = start, rlpp = rlpp, min_sibs = min_siblings, |
335 | 86x |
nosplit = nosplitin, verbose, row = row, have_col_fnotes = have_col_fnotes, |
336 | 86x |
div_height = div_height |
337 |
)) { |
|
338 | 314x |
guess <- guess - 1 |
339 |
} |
|
340 | 86x |
if (guess < start) { |
341 | 5x |
stop("Unable to find any valid pagination between ", start, " and ", origuess) |
342 |
} |
|
343 | 81x |
guess |
344 |
} |
|
345 | ||
346 | ||
347 |
#' Find Pagination Indices From Pagination Info Dataframe |
|
348 |
#' |
|
349 |
#' Pagination methods should typically call the `make_row_df` method |
|
350 |
#' for their object and then call this function on the resulting |
|
351 |
#' pagination info data.frame. |
|
352 |
#' |
|
353 |
#' @details `pab_indices_inner` implements the Core Pagination Algorithm |
|
354 |
#' for a single direction (vertical if `row = TRUE`, the default, horizontal otherwise) |
|
355 |
#' based on the pagination dataframe and (already adjusted for non-body rows/columns) |
|
356 |
#' lines (or characters) per page. |
|
357 |
#' |
|
358 |
#' @inheritSection pagination_algo Pagination Algorithm |
|
359 |
#' @param pagdf data.frame. A pagination info data.frame as created by |
|
360 |
#' either `make_rows_df` or `make_cols_df`. |
|
361 |
#' @param rlpp numeric. Maximum number of \emph{row} lines per page (not including header materials), including |
|
362 |
#' (re)printed header and context rows |
|
363 |
#' @param min_siblings numeric. Minimum sibling rows which must appear on either side of pagination row for a |
|
364 |
#' mid-subtable split to be valid. Defaults to 2. |
|
365 |
#' @param nosplitin character. List of names of sub-tables where page-breaks are not allowed, regardless of other |
|
366 |
#' considerations. Defaults to none. |
|
367 |
#' @param verbose logical(1). Should additional informative messages about the search for |
|
368 |
#' pagination breaks be shown. Defaults to \code{FALSE}. |
|
369 |
#' @param row logical(1). Is pagination happening in row |
|
370 |
#' space (`TRUE`, the default) or column space (`FALSE`) |
|
371 |
#' @param have_col_fnotes logical(1). Does the table-like object being rendered have |
|
372 |
#' column-associated referential footnotes. |
|
373 |
#' @param div_height numeric(1). The height of the divider line when the |
|
374 |
#' associated object is rendered. Defaults to `1`. |
|
375 |
#' @return A list containing the vector of row numbers, broken up by page |
|
376 |
#' |
|
377 |
#' @examples |
|
378 |
#' mypgdf <- basic_pagdf(row.names(mtcars)) |
|
379 |
#' |
|
380 |
#' paginds <- pag_indices_inner(mypgdf, rlpp = 15, min_siblings = 0) |
|
381 |
#' lapply(paginds, function(x) mtcars[x, ]) |
|
382 |
#' |
|
383 |
#' @export |
|
384 |
pag_indices_inner <- function(pagdf, rlpp, |
|
385 |
min_siblings, |
|
386 |
nosplitin = character(), |
|
387 |
verbose = FALSE, |
|
388 |
row = TRUE, |
|
389 |
have_col_fnotes = FALSE, |
|
390 |
div_height = 1L) { |
|
391 | 33x |
start <- 1 |
392 | 33x |
nr <- nrow(pagdf) |
393 | 33x |
ret <- list() |
394 | 33x |
while (start <= nr) { |
395 | 87x |
adjrlpp <- rlpp - pagdf$par_extent[start] |
396 | 87x |
if (adjrlpp <= 0) { |
397 | 1x |
if (row) { |
398 | 1x |
stop("Lines of repeated context (plus header materials) larger than specified lines per page") |
399 |
} else { |
|
400 | ! |
stop("Width of row labels equal to or larger than specified characters per page.") |
401 |
} |
|
402 |
} |
|
403 | 86x |
guess <- min(nr, start + adjrlpp - 1) |
404 | 86x |
end <- find_pag(pagdf, start, guess, |
405 | 86x |
rlpp = adjrlpp, |
406 | 86x |
min_siblings = min_siblings, |
407 | 86x |
nosplitin = nosplitin, |
408 | 86x |
verbose = verbose, |
409 | 86x |
row = row, |
410 | 86x |
have_col_fnotes = have_col_fnotes, |
411 | 86x |
div_height = div_height |
412 |
) |
|
413 | 81x |
ret <- c(ret, list(c( |
414 | 81x |
pagdf$reprint_inds[[start]], |
415 | 81x |
start:end |
416 |
))) |
|
417 | 81x |
start <- end + 1 |
418 |
} |
|
419 | 27x |
ret |
420 |
} |
|
421 | ||
422 |
#' Find Column Indices for Vertical Pagination |
|
423 |
#' @param obj ANY. object to be paginated. Must have a |
|
424 |
#' \code{\link{matrix_form}} method. |
|
425 |
#' @param cpp numeric(1). Number of characters per page (width) |
|
426 |
#' @param colwidths numeric vector. Column widths (in characters) for |
|
427 |
#' use with vertical pagination. |
|
428 |
#' @param rep_cols numeric(1). Number of \emph{columns} (not including |
|
429 |
#' row labels) to be repeated on every page. Defaults to 0 |
|
430 |
#' @inheritParams pag_indices_inner |
|
431 |
#' |
|
432 |
#' @return A list partitioning the vector of column indices |
|
433 |
#' into subsets for 1 or more horizontally paginated pages. |
|
434 |
#' |
|
435 |
#' @examples |
|
436 |
#' mf <- basic_matrix_form(df = mtcars) |
|
437 |
#' colpaginds <- vert_pag_indices(mf) |
|
438 |
#' lapply(colpaginds, function(j) mtcars[, j, drop = FALSE]) |
|
439 |
#' @export |
|
440 |
vert_pag_indices <- function(obj, cpp = 40, colwidths = NULL, verbose = FALSE, rep_cols = 0L) { |
|
441 | 14x |
mf <- matrix_form(obj, TRUE) |
442 | 14x |
clwds <- colwidths %||% propose_column_widths(mf) |
443 | 14x |
if(is.null(mf_cinfo(mf))) ## like always, ugh. |
444 | 2x |
mf <- mpf_infer_cinfo(mf, colwidths = clwds, rep_cols = rep_cols) |
445 | ||
446 | 14x |
has_rlabs <- mf_has_rlabels(mf) |
447 | 14x |
rlabs_flag <- as.integer(has_rlabs) |
448 | 14x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
449 | ||
450 |
# rep_extent <- pdf$par_extent[nrow(pdf)] |
|
451 | 14x |
rcpp <- cpp - table_inset(mf) - rlab_extent # rep_extent - table_inset(mf) - rlab_extent |
452 | 14x |
if (verbose) { |
453 | 12x |
message( |
454 | 12x |
"Adjusted characters per page: ", rcpp, |
455 | 12x |
" [original: ", cpp, |
456 | 12x |
", table inset: ", table_inset(mf), if (has_rlabs) paste0(", row labels: ", clwds[1]), |
457 |
"]" |
|
458 |
) |
|
459 |
} |
|
460 | 14x |
res <- pag_indices_inner(mf_cinfo(mf), |
461 | 14x |
rlpp = rcpp, # cpp - sum(clwds[seq_len(rep_cols)]), |
462 | 14x |
verbose = verbose, |
463 | 14x |
min_siblings = 1, |
464 | 14x |
row = FALSE |
465 |
) |
|
466 | 14x |
res |
467 |
} |
|
468 | ||
469 |
mpf_infer_cinfo <- function(mf, colwidths = NULL, rep_cols = num_rep_cols(mf)) { |
|
470 | ||
471 | 42x |
if (!is(rep_cols, "numeric") || is.na(rep_cols) || rep_cols < 0) { |
472 | ! |
stop("got invalid number of columns to be repeated: ", rep_cols) |
473 |
} |
|
474 | 42x |
clwds <- (colwidths %||% mf_col_widths(mf)) %||% propose_column_widths(mf) |
475 | 42x |
has_rlabs <- mf_has_rlabels(mf) |
476 | 42x |
rlabs_flag <- as.integer(has_rlabs) |
477 | 42x |
rlab_extent <- if (has_rlabs) clwds[1] else 0L |
478 | 42x |
sqstart <- rlabs_flag + 1L # rep_cols + 1L |
479 | ||
480 | 42x |
pdfrows <- lapply( |
481 | 42x |
(sqstart):ncol(mf$strings), |
482 | 42x |
function(i) { |
483 | 470x |
rownum <- i - rlabs_flag |
484 | 470x |
rep_inds <- seq_len(rep_cols)[seq_len(rep_cols) < rownum] |
485 | 470x |
rep_extent_i <- sum(0L, clwds[rlabs_flag + rep_inds]) + mf$col_gap * length(rep_inds) |
486 | 470x |
pagdfrow( |
487 | 470x |
row = NA, |
488 | 470x |
nm = rownum, |
489 | 470x |
lab = rownum, |
490 | 470x |
rnum = rownum, |
491 | 470x |
pth = NA, |
492 | 470x |
extent = clwds[i] + mf$col_gap, |
493 | 470x |
repext = rep_extent_i, # sum(clwds[rep_cols]) + mf$col_gap * max(0, (length(rep_cols) - 1)), |
494 | 470x |
repind = rep_inds, # rep_cols, |
495 | 470x |
rclass = "stuff", |
496 | 470x |
sibpos = 1 - 1, |
497 | 470x |
nsibs = 1 - 1 |
498 |
) |
|
499 |
} |
|
500 |
) |
|
501 | 42x |
pdf <- do.call(rbind, pdfrows) |
502 | ||
503 | 42x |
refdf <- mf_fnote_df(mf) |
504 | 42x |
pdf <- splice_fnote_info_in(pdf, refdf, row = FALSE) |
505 | 42x |
mf_cinfo(mf) <- pdf |
506 | 42x |
mf |
507 |
} |
|
508 | ||
509 | ||
510 |
#' Basic/spoof pagination info dataframe |
|
511 |
#' |
|
512 |
#' Returns a minimal pagination info data.frame (with no sibling/footnote/etc info). |
|
513 |
#' @inheritParams basic_matrix_form |
|
514 |
#' @param rnames character. Vector of row names |
|
515 |
#' @param labs character. Vector of row labels (defaults to names) |
|
516 |
#' @param rnums integer. Vector of row numbers. Defaults to `seq_along(rnames)`. |
|
517 |
#' @param extents integer. Number of lines each row will take to print, defaults to 1 for all rows |
|
518 |
#' @param rclass character. Class(es) for the rows. Defaults to "NA" |
|
519 |
#' |
|
520 |
#' @return A data.frame suitable for use in both the `matrix_print_form` constructor and the pagination machinery |
|
521 |
#' |
|
522 |
#' @examples |
|
523 |
#' |
|
524 |
#' basic_pagdf(c("hi", "there")) |
|
525 |
#' @export |
|
526 |
basic_pagdf <- function(rnames, labs = rnames, rnums = seq_along(rnames), |
|
527 |
extents = 1L, |
|
528 |
rclass = "NA", |
|
529 |
parent_path = "root") { |
|
530 | 13x |
rws <- mapply(pagdfrow, |
531 | 13x |
nm = rnames, lab = labs, extent = extents, |
532 | 13x |
rclass = rclass, rnum = rnums, pth = lapply(rnames, function(x) c(parent_path, x)), |
533 | 13x |
SIMPLIFY = FALSE, nsibs = 1, sibpos = 1 |
534 |
) |
|
535 | 13x |
res <- do.call(rbind.data.frame, rws) |
536 | 13x |
res$n_siblings <- nrow(res) |
537 | 13x |
res$pos_in_siblings <- seq_along(res$n_siblings) |
538 | 13x |
res |
539 |
} |
|
540 | ||
541 | ||
542 | ||
543 | ||
544 |
## write paginate() which operates **solely** on a MatrixPrintForm obj |
|
545 | ||
546 | ||
547 |
page_size_spec <- function(lpp, cpp, max_width) { |
|
548 | 19x |
structure(list(lpp = lpp, |
549 | 19x |
cpp = cpp, |
550 | 19x |
max_width = max_width), |
551 | 19x |
class = "page_size_spec") |
552 |
} |
|
553 | ||
554 | ||
555 | 38x |
non_null_na <- function(x) !is.null(x) && is.na(x) |
556 | ||
557 | ||
558 |
calc_lcpp <- function(page_type = NULL, |
|
559 |
landscape = FALSE, |
|
560 |
pg_width = page_dim(page_type)[if(landscape) 2 else 1], |
|
561 |
pg_height = page_dim(page_type)[if(landscape) 1 else 2], |
|
562 |
font_family = "Courier", |
|
563 |
font_size = 8, # grid parameters |
|
564 |
cpp = NA_integer_, |
|
565 |
lpp = NA_integer_, |
|
566 |
tf_wrap = TRUE, |
|
567 |
max_width = NULL, |
|
568 |
lineheight = 1, |
|
569 |
margins = c(bottom = .5, left = .75, top = .5, right = .75), |
|
570 |
colwidths, |
|
571 |
col_gap, |
|
572 |
inset |
|
573 |
) { |
|
574 | ||
575 | 19x |
pg_lcpp <- page_lcpp(page_type = page_type, |
576 | 19x |
landscape = landscape, |
577 | 19x |
font_family = font_family, |
578 | 19x |
font_size = font_size, |
579 | 19x |
lineheight = lineheight, |
580 | 19x |
margins = margins, |
581 | 19x |
pg_width = pg_width, |
582 | 19x |
pg_height = pg_height) |
583 | ||
584 | 19x |
if (non_null_na(lpp)) { |
585 | 11x |
lpp <- pg_lcpp$lpp |
586 |
} |
|
587 | 19x |
if(non_null_na(cpp)) { |
588 | 13x |
cpp <- pg_lcpp$cpp |
589 |
} |
|
590 | 19x |
stopifnot(!is.na(cpp)) |
591 | 19x |
if(!tf_wrap && !is.null(max_width)) { |
592 | ! |
warning("tf_wrap is FALSE - ignoring non-null max_width value.") |
593 | ! |
max_width <- NULL |
594 | 19x |
} else if(tf_wrap && is.null(max_width)) |
595 | 4x |
max_width <- cpp |
596 | ||
597 | 19x |
if(is.character(max_width) && identical(max_width, "auto")) { |
598 | ! |
max_width <- inset + sum(colwidths) + (length(colwidths) - 1) * col_gap |
599 |
} |
|
600 | 19x |
page_size_spec(lpp = lpp, cpp = cpp, max_width = max_width) |
601 |
} |
|
602 | ||
603 | ||
604 |
calc_rlpp <- function(pg_size_spec, mf, colwidths, tf_wrap, verbose) { |
|
605 | 17x |
lpp <- pg_size_spec$lpp |
606 | 17x |
max_width = pg_size_spec$max_width |
607 | ||
608 | 17x |
dh <- divider_height(mf) |
609 | 17x |
if (any(nzchar(all_titles(mf)))) { |
610 |
## +1 is for blank line between subtitles and divider |
|
611 |
## dh is for divider line **between subtitles and column labels** |
|
612 |
## other divider line is accounted for in cinfo_lines |
|
613 | 11x |
if(!tf_wrap) |
614 | 9x |
tlines <- length(all_titles(mf)) |
615 |
else |
|
616 | 2x |
tlines <- sum(nlines(all_titles(mf), colwidths = colwidths, |
617 | 2x |
max_width = max_width)) |
618 | 11x |
tlines <- tlines + dh + 1L |
619 |
} else { |
|
620 | 6x |
tlines <- 0 |
621 |
} |
|
622 | ||
623 |
## dh for divider line between column labels and table body |
|
624 | 17x |
cinfo_lines <- mf_nlheader(mf) + dh |
625 | ||
626 | 17x |
if(verbose) |
627 | 15x |
message("Determining lines required for header content: ", |
628 | 15x |
tlines, " title and ", cinfo_lines, " table header lines") |
629 | ||
630 | 17x |
refdf <- mf_fnote_df(mf) |
631 | 17x |
cfn_df <- refdf[is.na(refdf$row) & !is.na(refdf$col),] |
632 | ||
633 | 17x |
flines <- 0L |
634 | 17x |
mnfoot <- main_footer(mf) |
635 | 17x |
havemn <- length(mnfoot) && any(nzchar(mnfoot)) |
636 | 17x |
if(havemn) |
637 | 12x |
flines <- nlines(mnfoot, colwidths = colwidths, |
638 | 12x |
max_width = max_width - table_inset(mf)) |
639 | 17x |
prfoot <- prov_footer(mf) |
640 | 17x |
if(length(prfoot) && nzchar(prfoot)) { |
641 | 11x |
flines <- flines + nlines(prov_footer(mf), colwidths = colwidths, max_width = max_width) |
642 | 11x |
if(havemn) |
643 | 11x |
flines <- flines + 1L ## space between main and prov footer. |
644 |
} |
|
645 |
## this time its for the divider between the footers and whatever is above them |
|
646 |
## (either table body or referential footnotes) |
|
647 | 17x |
if(flines > 0) |
648 | 12x |
flines <- flines + dh + 1L |
649 |
## this time its for the divider between the referential footnotes and |
|
650 |
## the table body IFF we have any, otherwise that divider+blanks pace doesn't get drawn |
|
651 | 17x |
if(NROW(cfn_df) > 0) { |
652 | ! |
cinfo_lines <- cinfo_lines + sum(cfn_df$nlines) |
653 | ! |
flines <- flines + dh + 1L |
654 |
} |
|
655 | ||
656 | 17x |
if(verbose) |
657 | 15x |
message("Determining lines required for footer content", |
658 | 15x |
if(NROW(cfn_df) > 0) " [column fnotes present]", |
659 | 15x |
": ", flines, " lines") |
660 | ||
661 | 17x |
ret <- lpp - flines - tlines - cinfo_lines |
662 | ||
663 | 17x |
if(verbose) |
664 | 15x |
message("Lines per page available for tables rows: ", ret, " (original: ", lpp, ")") |
665 | 17x |
ret |
666 |
} |
|
667 | ||
668 | ||
669 |
calc_rcpp <- function(pg_size_spec, mf, colwidths) { |
|
670 | ||
671 | ! |
cpp <- pg_size_spec$cpp |
672 | ||
673 | ! |
cpp - table_inset(mf) - colwidths[1] - mf_colgap(mf) |
674 |
} |
|
675 | ||
676 | ||
677 |
splice_idx_lists <- function(lsts) { |
|
678 | ! |
list(pag_row_indices = do.call(c, lapply(lsts, function(xi) xi$pag_row_indices)), |
679 | ! |
pag_col_indices = do.call(c, lapply(lsts, function(yi) yi$pag_col_indices))) |
680 | ||
681 |
} |
|
682 | ||
683 | ||
684 | ||
685 |
#' @title Paginate a table-like object for rendering |
|
686 |
#' |
|
687 |
#' @description |
|
688 |
#' These functions perform or diagnose bi-directional pagination on |
|
689 |
#' an object. |
|
690 |
#' |
|
691 |
#' `paginate_to_mpfs` renders `obj` into the `MatrixPrintForm` (`MPF`) |
|
692 |
#' intermediate representation, and then paginates that `MPF` into |
|
693 |
#' component `MPF`s each corresponding to an individual page and |
|
694 |
#' returns those in a list. |
|
695 |
#' |
|
696 |
#' `paginate_indices` renders `obj` into an `MPF`, then uses |
|
697 |
#' that representation to calculate the rows and columns of |
|
698 |
#' `obj` corresponding to each page of the pagination of `obj`, |
|
699 |
#' but simply returns these indices rather than paginating |
|
700 |
#' \code{obj} itself (see details for an important caveat). |
|
701 |
#' |
|
702 |
#' `diagnose_pagination` attempts pagination via `paginate_to_mpfs` |
|
703 |
#' and then returns diagnostic information which explains why page |
|
704 |
#' breaks were positioned where they were, or alternatively why |
|
705 |
#' no valid paginations could be found. |
|
706 |
#' |
|
707 |
#' @details |
|
708 |
#' |
|
709 |
#' All three of these functions generally support all classes which have |
|
710 |
#' a corresponding `matrix_form` method which returns a valid `MatrixPrintForm` |
|
711 |
#' object (including `MatrixPrintForm` objects themselves). |
|
712 |
#' |
|
713 |
#' `paginate_indices` is directly called by `paginate_to_mpfs` (and thus |
|
714 |
#' `diagnose_pagination`). For most classes, and most tables represented |
|
715 |
#' by supported classes, calling `paginate_to_mpfs` is equivalent to a |
|
716 |
#' manual `paginate_indices -> subset obj into pages -> matrix_form` |
|
717 |
#' workflow. |
|
718 |
#' |
|
719 |
#' The exception to this equivalence is objects which support 'forced pagination', |
|
720 |
#' or pagination logic which built into the object itself rather than being a |
|
721 |
#' function of space on a page. Forced pagination generally involves the creation |
|
722 |
#' of, e.g., page-specific titles which apply to these forced paginations. |
|
723 |
#' `paginate_to_mpfs` and `diagnose_pagination` support forced pagination by |
|
724 |
#' automatically calling the `do_forced_pagination` generic on the object |
|
725 |
#' and then paginating each object returned by that generic separately. The |
|
726 |
#' assumption here, then, is that page-specific titles and such are |
|
727 |
#' handled by the class' `do_forced_pagination` method. |
|
728 |
#' |
|
729 |
#' `paginate_indices`, on the other hand, \emph{does not support forced pagination}, |
|
730 |
#' because it returns only a set of indices for row and column subsetting for each page, |
|
731 |
#' and thus cannot retain any changes, e.g., to titles, done within `do_forced_paginate`. |
|
732 |
#' `paginate_indices` does call `do_forced_paginate`, but instead of continuing, it |
|
733 |
#' throws an error in the case that the result is more than a single "page". |
|
734 |
#' |
|
735 |
#' @inheritParams vert_pag_indices |
|
736 |
#' @inheritParams pag_indices_inner |
|
737 |
#' @inheritParams page_lcpp |
|
738 |
#' @inheritParams toString |
|
739 |
#' @inheritParams propose_column_widths |
|
740 |
#' @param lpp numeric(1) or NULL. Lines per page. if NA (the default, |
|
741 |
#' this is calculated automatically based on the specified page |
|
742 |
#' size). `NULL` indicates no vertical pagination should occur. |
|
743 |
#' @param cpp numeric(1) or NULL. Width in characters per page. if NA (the default, |
|
744 |
#' this is calculated automatically based on the specified page |
|
745 |
#' size). `NULL` indicates no horizontal pagination should occur. |
|
746 | ||
747 |
#' @param pg_size_spec page_size_spec. A pre-calculated page |
|
748 |
#' size specification. Typically this is not set in end user code. |
|
749 |
#' @param col_gap numeric(1). Currently unused. |
|
750 |
#' @return for `paginate_indices` a list with two elements of the same |
|
751 |
#' length: `pag_row_indices`, and `pag_col_indices`. For |
|
752 |
#' `paginate_to_mpfs`, a list of `MatrixPrintForm` objects |
|
753 |
#' representing each individual page after pagination (including |
|
754 |
#' forced pagination if necessary). |
|
755 |
#' @export |
|
756 |
#' @aliases paginate pagination |
|
757 |
#' @examples |
|
758 |
#' mpf <- basic_matrix_form(mtcars) |
|
759 |
#' |
|
760 |
#' paginate_indices(mpf, pg_width = 5, pg_height = 3) |
|
761 |
#' |
|
762 |
#' paginate_to_mpfs(mpf, pg_width = 5, pg_height = 3) |
|
763 |
paginate_indices <- function(obj, |
|
764 |
page_type = "letter", |
|
765 |
font_family = "Courier", |
|
766 |
font_size = 8, |
|
767 |
lineheight = 1, |
|
768 |
landscape = FALSE, |
|
769 |
pg_width = NULL, |
|
770 |
pg_height = NULL, |
|
771 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
772 |
lpp = NA_integer_, |
|
773 |
cpp = NA_integer_, |
|
774 |
min_siblings = 2, |
|
775 |
nosplitin = character(), |
|
776 |
colwidths = NULL, |
|
777 |
tf_wrap = FALSE, |
|
778 |
max_width = NULL, |
|
779 |
indent_size = 2, |
|
780 |
pg_size_spec = NULL, |
|
781 |
rep_cols = num_rep_cols(obj), |
|
782 |
col_gap = 3, |
|
783 |
verbose = FALSE) { |
|
784 | ||
785 | ||
786 |
## this MUST alsways return a list, inluding list(obj) when |
|
787 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
788 |
## based on s3 classes that are lists underneath!!! |
|
789 | 19x |
fpags <- do_forced_paginate(obj) |
790 | ||
791 |
## if we have more than one forced "page", |
|
792 |
## paginate each of them individually and return the result. |
|
793 |
## forced pagination is ***currently*** only vertical, so |
|
794 |
## we don't have to worry about divying up colwidths here, |
|
795 |
## but we will if we ever allow force_paginate to do horiz |
|
796 |
## pagination. |
|
797 | 19x |
if(length(fpags) > 1) { |
798 | 1x |
stop("forced pagination is required for this object (class: ", class(obj)[1], |
799 | 1x |
") this is not supported in paginate_indices. Use paginate_to_mpfs or call ", |
800 | 1x |
"do_forced_paginate on your object and paginate each returned section separately.") |
801 |
} |
|
802 | ||
803 | ||
804 |
## I'm not sure this is worth doing. |
|
805 |
## ## We can't support forced pagination here, but we can support calls to, |
|
806 |
## ## e.g., paginate_indices(do_forced_pag(tt)) |
|
807 |
## if(is.list(obj) && !is.object(obj)) { |
|
808 |
## res <- lapply(obj, paginate_indices, |
|
809 |
## page_type = page_type, |
|
810 |
## font_family = font_family, |
|
811 |
## font_size = font_size, |
|
812 |
## lineheight = lineheight, |
|
813 |
## landscape = landscape, |
|
814 |
## pg_width = pg_width, |
|
815 |
## pg_height = pg_height, |
|
816 |
## margins = margins, |
|
817 |
## lpp = lpp, |
|
818 |
## cpp = cpp, |
|
819 |
## tf_wrap = tf_wrap, |
|
820 |
## max_width = max_width, |
|
821 |
## colwidths = colwidths, |
|
822 |
## min_siblings = min_siblings, |
|
823 |
## nosplitin = nosplitin, |
|
824 |
## col_gap = col_gap, |
|
825 |
## ## not setting num_rep_cols here cause it wont' get it right |
|
826 |
## verbose = verbose) |
|
827 |
## return(splice_idx_lists(res)) |
|
828 |
## } |
|
829 |
## order is annoying here, since we won't actually need the mpf if |
|
830 |
## we run into forced pagination, but life is short and this should work fine. |
|
831 | 18x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size) |
832 | 18x |
if(is.null(colwidths)) |
833 | 2x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf) |
834 |
else |
|
835 | 16x |
mf_col_widths(mpf) <- colwidths |
836 | 18x |
if(NROW(mf_cinfo(mpf)) == 0) |
837 | 18x |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols) |
838 | ||
839 | ||
840 | 18x |
if(is.null(pg_size_spec)) { |
841 | 2x |
pg_size_spec <- calc_lcpp(page_type = page_type, |
842 | 2x |
font_family = font_family, |
843 | 2x |
font_size = font_size, |
844 | 2x |
lineheight = lineheight, |
845 | 2x |
landscape = landscape, |
846 | 2x |
pg_width = pg_width, |
847 | 2x |
pg_height = pg_height, |
848 | 2x |
margins = margins, |
849 | 2x |
lpp = lpp, |
850 | 2x |
cpp = cpp, |
851 | 2x |
tf_wrap = tf_wrap, |
852 | 2x |
max_width = max_width, |
853 | 2x |
colwidths = colwidths, |
854 | 2x |
inset = table_inset(mpf), |
855 | 2x |
col_gap = col_gap) |
856 |
} |
|
857 | ||
858 |
## we can't support forced pagination in paginate_indices because |
|
859 |
## forced pagination is generally going to set page titles, which |
|
860 |
## we can't preserve when just returning lists of indices. |
|
861 |
## Instead we make a hard assumption here that any forced pagination |
|
862 |
## has already occured. |
|
863 | ||
864 | ||
865 | ||
866 | ||
867 | ||
868 |
## this wraps the cell contents AND shoves referential footnote |
|
869 |
## info into mf_rinfo(mpf) |
|
870 | 18x |
mpf <- do_cell_fnotes_wrap(mpf, colwidths, max_width, tf_wrap = tf_wrap) |
871 | ||
872 | 18x |
if(is.null(pg_size_spec$lpp)) |
873 | 1x |
pag_row_indices <- list(seq_len(mf_nrow(mpf))) |
874 |
else |
|
875 | 17x |
pag_row_indices <- pag_indices_inner(pagdf= mf_rinfo(mpf), |
876 | 17x |
rlpp = calc_rlpp(pg_size_spec, mpf, colwidths = colwidths, tf_wrap = tf_wrap, |
877 | 17x |
verbose = verbose), |
878 | 17x |
verbose = verbose, |
879 | 17x |
min_siblings = min_siblings, |
880 | 17x |
nosplitin = nosplitin) |
881 | ||
882 | 13x |
if(is.null(pg_size_spec$cpp)) |
883 | 1x |
pag_col_indices <- list(seq_len(mf_ncol(mpf))) |
884 |
else |
|
885 | 12x |
pag_col_indices <- vert_pag_indices(mpf, cpp = pg_size_spec$cpp, colwidths = colwidths, |
886 | 12x |
rep_cols = rep_cols, verbose = verbose) |
887 | ||
888 | 13x |
list(pag_row_indices = pag_row_indices, |
889 | 13x |
pag_col_indices = pag_col_indices) |
890 |
} |
|
891 | ||
892 | 16x |
setGeneric("has_page_title", function(obj) standardGeneric("has_page_title")) |
893 | ||
894 | 16x |
setMethod("has_page_title", "ANY", function(obj) length(page_titles(obj)) > 0) |
895 | ||
896 |
#' @rdname paginate_indices |
|
897 |
#' @export |
|
898 |
paginate_to_mpfs <- function(obj, |
|
899 |
page_type = "letter", |
|
900 |
font_family = "Courier", |
|
901 |
font_size = 8, |
|
902 |
lineheight = 1, |
|
903 |
landscape = FALSE, |
|
904 |
pg_width = NULL, |
|
905 |
pg_height = NULL, |
|
906 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
907 |
lpp = NA_integer_, |
|
908 |
cpp = NA_integer_, |
|
909 |
min_siblings = 2, |
|
910 |
nosplitin = character(), |
|
911 |
colwidths = NULL, |
|
912 |
tf_wrap = FALSE, |
|
913 |
max_width = NULL, |
|
914 |
indent_size = 2, |
|
915 |
pg_size_spec = NULL, |
|
916 |
rep_cols = num_rep_cols(obj), |
|
917 |
col_gap = 2, |
|
918 |
verbose = FALSE) { |
|
919 | ||
920 | 17x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size) |
921 | 17x |
if(is.null(colwidths)) |
922 | 9x |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf) |
923 |
else |
|
924 | 8x |
mf_col_widths(mpf) <- colwidths |
925 | 17x |
if(NROW(mf_cinfo(mpf)) == 0) |
926 | 17x |
mpf <- mpf_infer_cinfo(mpf, colwidths, rep_cols) |
927 | ||
928 | ||
929 | 17x |
if(is.null(pg_size_spec)) { |
930 | 15x |
pg_size_spec <- calc_lcpp(page_type = page_type, |
931 | 15x |
font_family = font_family, |
932 | 15x |
font_size = font_size, |
933 | 15x |
lineheight = lineheight, |
934 | 15x |
landscape = landscape, |
935 | 15x |
pg_width = pg_width, |
936 | 15x |
pg_height = pg_height, |
937 | 15x |
margins = margins, |
938 | 15x |
lpp = lpp, |
939 | 15x |
cpp = cpp, |
940 | 15x |
tf_wrap = tf_wrap, |
941 | 15x |
max_width = max_width, |
942 | 15x |
colwidths = colwidths, |
943 | 15x |
inset = table_inset(mpf), |
944 | 15x |
col_gap = col_gap) |
945 |
} |
|
946 |
## this MUST alsways return a list, inluding list(obj) when |
|
947 |
## no forced pagination is needed! otherwise stuff breaks for things |
|
948 |
## based on s3 classes that are lists underneath!!! |
|
949 | 17x |
fpags <- do_forced_paginate(obj) |
950 | ||
951 |
## if we have more than one forced "page", |
|
952 |
## paginate each of them individually and return the result. |
|
953 |
## forced pagination is ***currently*** only vertical, so |
|
954 |
## we don't have to worry about divying up colwidths here, |
|
955 |
## but we will if we ever allow force_paginate to do horiz |
|
956 |
## pagination. |
|
957 | 17x |
if(length(fpags) > 1) { |
958 | 1x |
deep_pag <- lapply(fpags, paginate_to_mpfs, |
959 | 1x |
pg_size_spec = pg_size_spec, |
960 | 1x |
colwidths = colwidths, |
961 | 1x |
min_siblings = min_siblings, |
962 | 1x |
nosplitin = nosplitin, |
963 | 1x |
verbose = verbose) |
964 | 1x |
return(unlist(deep_pag, recursive = FALSE)) |
965 | 16x |
} else if (has_page_title(fpags[[1]])) { |
966 | ! |
obj <- fpags[[1]] |
967 |
} |
|
968 | ||
969 | ||
970 |
## we run into forced pagination, but life is short and this should work fine. |
|
971 | 16x |
mpf <- matrix_form(obj, TRUE, TRUE, indent_size = indent_size) |
972 | 16x |
if(is.null(colwidths)) |
973 | ! |
colwidths <- mf_col_widths(mpf) %||% propose_column_widths(mpf) |
974 | 16x |
mf_col_widths(mpf) <- colwidths |
975 | ||
976 | 16x |
page_indices <- paginate_indices(obj = obj, |
977 |
## page_type = page_type, |
|
978 |
## font_family = font_family, |
|
979 |
## font_size = font_size, |
|
980 |
## lineheight = lineheight, |
|
981 |
## landscape = landscape, |
|
982 |
## pg_width = pg_width, |
|
983 |
## pg_height = pg_height, |
|
984 |
## margins = margins, |
|
985 | 16x |
pg_size_spec = pg_size_spec, |
986 |
## lpp = lpp, |
|
987 |
## cpp = cpp, |
|
988 | 16x |
min_siblings = min_siblings, |
989 | 16x |
nosplitin = nosplitin, |
990 | 16x |
colwidths = colwidths, |
991 | 16x |
tf_wrap = tf_wrap, |
992 |
## max_width = max_width, |
|
993 | 16x |
rep_cols = rep_cols, |
994 | 16x |
verbose = verbose) |
995 | ||
996 | 13x |
pagmats <- lapply(page_indices$pag_row_indices, function(ii) { |
997 | 32x |
mpf_subset_rows(mpf, ii) |
998 |
}) |
|
999 | ||
1000 |
## these chunks now carry around their (correctly subset) col widths... |
|
1001 | 13x |
res <- lapply(pagmats, function(matii) { |
1002 | 32x |
lapply(page_indices$pag_col_indices, function(jj) { |
1003 | 65x |
mpf_subset_cols(matii, jj) |
1004 |
}) |
|
1005 |
}) |
|
1006 | 13x |
unlist(res, recursive = FALSE) |
1007 |
} |
|
1008 | ||
1009 | ||
1010 |
#' @importFrom utils capture.output |
|
1011 |
#' @details |
|
1012 |
#' |
|
1013 |
#' `diagnose_pagination` attempts pagination and then, regardless of success |
|
1014 |
#' or failure, returns diagnostic information about pagination |
|
1015 |
#' attempts (if any) after each row and column. |
|
1016 |
#' |
|
1017 |
#' The diagnostics data reflects the final time the pagination algorithm |
|
1018 |
#' evaluated a page break at the specified location, regardless of how |
|
1019 |
#' many times the position was assessed total. |
|
1020 |
#' |
|
1021 |
#' To get information about intermediate attempts, perform pagination |
|
1022 |
#' with `verbose = TRUE` and inspect the messages in order. |
|
1023 |
#' |
|
1024 |
#' @return For `diagnose_pagination` a list containing: |
|
1025 |
#' |
|
1026 |
#' \describe{ |
|
1027 |
#' \item{`lpp_diagnostics`}{diagnostic information regarding lines per page} |
|
1028 |
#' \item{`row_diagnostics`}{basic information about rows, whether pagination was attempted after each row, and the final result of such an attempt, if made} |
|
1029 |
#' \item{`cpp_diagnostics}{diagnostic information regarding columns per page} |
|
1030 |
#' \item{`col_diagnostics`}{(very) basic information about leaf columns, whether pagination was attempted after each leaf column, ad the final result of such attempts, if made} |
|
1031 |
#' } |
|
1032 |
#' |
|
1033 |
#' @note For `diagnose_pagination`, the column labels are not |
|
1034 |
#' displayed in the `col_diagnostics` element due to certain |
|
1035 |
#' internal implementation details; rather the diagnostics are |
|
1036 |
#' reported in terms of absolute (leaf) column position. This is a |
|
1037 |
#' known limitation, and may eventually be changed, but the |
|
1038 |
#' information remains useful as it is currently reported. |
|
1039 |
#' |
|
1040 |
#' @note `diagnose_pagination` is intended for interactive debugging |
|
1041 |
#' use and \emph{should not be programmed against}, as the exact |
|
1042 |
#' content and form of the verbose messages it captures and |
|
1043 |
#' returns is subject to change. |
|
1044 |
#' |
|
1045 |
#' @note because `diagnose_pagination` relies on `capture.output(type = "message")`, |
|
1046 |
#' it cannot be used within the `testthat` (and likely other) testing frameworks, |
|
1047 |
#' and likely cannot be used within `knitr`/`rmarkdown` contexts either, |
|
1048 |
#' as this clashes with those systems' capture of messages. |
|
1049 |
#' |
|
1050 |
#' @export |
|
1051 |
#' |
|
1052 |
#' @rdname paginate_indices |
|
1053 |
#' @examples |
|
1054 |
#' |
|
1055 |
#' diagnose_pagination(mpf, pg_width = 5, pg_height = 3) |
|
1056 |
#' clws <- propose_column_widths(mpf) |
|
1057 |
#' clws[1] <- floor(clws[1]/3) |
|
1058 |
#' dgnost <- diagnose_pagination(mpf, pg_width = 5, pg_height = 3, colwidths = clws) |
|
1059 |
#' try(diagnose_pagination(mpf, pg_width = 1)) #fails |
|
1060 |
#' |
|
1061 |
diagnose_pagination <- function(obj, |
|
1062 |
page_type = "letter", |
|
1063 |
font_family = "Courier", |
|
1064 |
font_size = 8, |
|
1065 |
lineheight = 1, |
|
1066 |
landscape = FALSE, |
|
1067 |
pg_width = NULL, |
|
1068 |
pg_height = NULL, |
|
1069 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
1070 |
lpp = NA_integer_, |
|
1071 |
cpp = NA_integer_, |
|
1072 |
min_siblings = 2, |
|
1073 |
nosplitin = character(), |
|
1074 |
colwidths = propose_column_widths(matrix_form(obj, TRUE)), |
|
1075 |
tf_wrap = FALSE, |
|
1076 |
max_width = NULL, |
|
1077 |
indent_size = 2, |
|
1078 |
pg_size_spec = NULL, |
|
1079 |
rep_cols = num_rep_cols(obj), |
|
1080 |
col_gap = 2, |
|
1081 |
verbose = FALSE, |
|
1082 |
...) { |
|
1083 | ||
1084 | ||
1085 | 6x |
fpag <- do_forced_paginate(obj) |
1086 | 6x |
if(length(fpag) > 1) { |
1087 | 1x |
return(lapply(fpag, |
1088 | 1x |
diagnose_pagination, |
1089 | 1x |
page_type = page_type, |
1090 | 1x |
font_family = font_family, |
1091 | 1x |
font_size = font_size, |
1092 | 1x |
lineheight = lineheight, |
1093 | 1x |
landscape = landscape, |
1094 | 1x |
pg_width = pg_width, |
1095 | 1x |
pg_height = pg_height, |
1096 | 1x |
margins = margins, |
1097 | 1x |
lpp = lpp, |
1098 | 1x |
cpp = cpp, |
1099 | 1x |
tf_wrap = tf_wrap, |
1100 | 1x |
max_width = max_width, |
1101 | 1x |
colwidths = colwidths, |
1102 | 1x |
col_gap = col_gap, |
1103 | 1x |
min_siblings = min_siblings, |
1104 | 1x |
nosplitin = nosplitin)) |
1105 |
} |
|
1106 | ||
1107 | 5x |
mpf <- matrix_form(obj, TRUE) |
1108 | 5x |
msgres <- capture.output({tmp <- try(paginate_to_mpfs(obj, page_type = page_type, |
1109 | 5x |
font_family = font_family, |
1110 | 5x |
font_size = font_size, |
1111 | 5x |
lineheight = lineheight, |
1112 | 5x |
landscape = landscape, |
1113 | 5x |
pg_width = pg_width, |
1114 | 5x |
pg_height = pg_height, |
1115 | 5x |
margins = margins, |
1116 | 5x |
lpp = lpp, |
1117 | 5x |
cpp = cpp, |
1118 | 5x |
tf_wrap = tf_wrap, |
1119 | 5x |
max_width = max_width, |
1120 | 5x |
colwidths = colwidths, |
1121 | 5x |
col_gap = col_gap, |
1122 | 5x |
min_siblings = min_siblings, |
1123 | 5x |
nosplitin = nosplitin, |
1124 | 5x |
verbose = TRUE))}, |
1125 | 5x |
type = "message") |
1126 | 5x |
if(is(tmp, "try-error") && grepl("Width of row labels equal to or larger", tmp)) { |
1127 | ! |
cond <- attr(tmp, "condition") |
1128 | ! |
stop(conditionMessage(cond), call. = conditionCall(cond)) |
1129 |
} |
|
1130 | ||
1131 | 5x |
lpp_diagnostic <- grep("^(Determining lines|Lines per page available).*$", msgres, value = TRUE) |
1132 | 5x |
cpp_diagnostic <- unique(grep("^Adjusted characters per page.*$", msgres, value = TRUE)) |
1133 | ||
1134 | 5x |
mpf <- do_cell_fnotes_wrap(mpf, widths = colwidths, max_width = max_width, tf_wrap = tf_wrap) |
1135 | 5x |
mpf <- mpf_infer_cinfo(mpf, colwidths = colwidths) |
1136 | ||
1137 | 5x |
rownls <- grep("Checking pagination after row", msgres, fixed = TRUE) |
1138 | 5x |
rownum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[rownls])) |
1139 | 5x |
rowmsgs <- vapply(unique(rownum), function(ii) { |
1140 | ! |
idx <- max(which(rownum == ii)) |
1141 | ! |
gsub("\\t[.]*", "", msgres[rownls[idx] + 1]) |
1142 |
}, "") |
|
1143 | ||
1144 | 5x |
msgdf <- data.frame(abs_rownumber = unique(rownum), |
1145 | 5x |
final_pag_result = rowmsgs, stringsAsFactors = FALSE) |
1146 | 5x |
rdf <-mf_rinfo(mpf)[, c("abs_rownumber", "label", "self_extent", "par_extent", "node_class")] |
1147 | 5x |
rdf$pag_attempted <- rdf$abs_rownumber %in% rownum |
1148 | 5x |
row_diagnose <- merge(rdf, msgdf, by = "abs_rownumber", all.x = TRUE) |
1149 | ||
1150 | 5x |
colnls <- grep("Checking pagination after column", msgres, fixed = TRUE) |
1151 | 5x |
colnum <- as.integer(gsub("[^[:digit:]]*(.*)$", "\\1", msgres[colnls])) |
1152 | 5x |
colmsgs <- vapply(unique(colnum), function(ii) { |
1153 | ! |
idx <- max(which(colnum == ii)) |
1154 | ! |
gsub("\\t[.]*", "", msgres[colnls[idx] + 1]) |
1155 |
}, "") |
|
1156 | ||
1157 | 5x |
colmsgdf <- data.frame(abs_rownumber = unique(colnum), |
1158 | 5x |
final_pag_result = colmsgs, |
1159 | 5x |
stringsAsFactors = FALSE) |
1160 | 5x |
cdf <- mf_cinfo(mpf)[, c("abs_rownumber", "self_extent")] |
1161 | 5x |
cdf$pag_attempted <- cdf$abs_rownumber %in% colnum |
1162 | 5x |
col_diagnose <- merge(cdf, colmsgdf, by = "abs_rownumber", all.x = TRUE) |
1163 | 5x |
names(col_diagnose) <- gsub("^abs_rownumber$", "abs_colnumber", names(col_diagnose)) |
1164 | 5x |
list(lpp_diagnostics = lpp_diagnostic, |
1165 | 5x |
row_diagnostics = row_diagnose, |
1166 | 5x |
cpp_diagnostics = cpp_diagnostic, |
1167 | 5x |
col_diagnostics = col_diagnose) |
1168 |
} |
1 |
## until we do it for real |
|
2 | ||
3 |
#' @title Matrix Print Form - Intermediate Representation for ASCII Table Printing |
|
4 |
#' |
|
5 |
#' @name MatrixPrintForm-class |
|
6 |
#' |
|
7 |
#' @rdname MatrixPrintForm_class |
|
8 |
#' @aliases MatrixPrintForm-class |
|
9 |
#' @exportClass MatrixPrintForm |
|
10 |
setOldClass(c("MatrixPrintForm", "list")) |
|
11 | ||
12 | ||
13 |
mform_handle_newlines <- function(matform) { |
|
14 |
# Retrieving relevant information |
|
15 | 70x |
has_topleft <- mf_has_topleft(matform) |
16 | 70x |
strmat <- mf_strings(matform) |
17 | 70x |
frmmat <- mf_formats(matform) |
18 |
# nlines detects if there is a newline character |
|
19 | 70x |
row_nlines <- apply(strmat, 1, function(x) max(vapply(x, nlines, 1L), 1L)) |
20 | 70x |
nr_header <- mf_nrheader(matform) |
21 | ||
22 |
# There is something to change |
|
23 | 70x |
if (any(row_nlines > 1)) { |
24 |
# Header indices |
|
25 | 4x |
hdr_inds <- 1:nr_header |
26 |
## groundwork for sad haxx to get tl to not be messed up |
|
27 | 4x |
if (has_topleft) { |
28 | 2x |
tl <- strmat[hdr_inds, 1] |
29 | 2x |
strmat[hdr_inds, 1] <- "" |
30 |
## recalc them without topleft cause thats handled separately |
|
31 | 2x |
row_nlines <- apply(strmat, 1, function(x) max(vapply(x, nlines, 1L), 1L)) |
32 |
} else { |
|
33 | 2x |
tl <- character() |
34 |
} |
|
35 |
## used below even though we don't store it on the resulting object |
|
36 | 4x |
new_nlines_hdr <- sum(row_nlines[hdr_inds]) |
37 | 4x |
newstrmat <- rbind( |
38 | 4x |
expand_mat_rows(strmat[hdr_inds, , drop = FALSE], |
39 | 4x |
row_nlines[hdr_inds], |
40 | 4x |
cpadder = pad_vert_bottom |
41 |
), |
|
42 | 4x |
expand_mat_rows(strmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) |
43 |
) |
|
44 | 4x |
newfrmmat <- rbind( |
45 | 4x |
expand_mat_rows(frmmat[hdr_inds, , drop = FALSE], |
46 | 4x |
row_nlines[hdr_inds], |
47 | 4x |
cpadder = pad_vert_bottom |
48 |
), |
|
49 | 4x |
expand_mat_rows(frmmat[-1 * hdr_inds, , drop = FALSE], row_nlines[-hdr_inds]) |
50 |
) |
|
51 |
## sad haxx :( |
|
52 | 4x |
if (has_topleft) { |
53 | 2x |
newtl <- unlist(strsplit(tl, "\n")) |
54 | 2x |
if (length(newtl) > new_nlines_hdr) { |
55 |
stop("Expanding top-left material resulted in more lines (", length(newtl), # nocov |
|
56 |
"than fit in the header.") # nocov |
|
57 |
} |
|
58 | 1x |
newstrmat[1:new_nlines_hdr, 1] <- c(newtl, rep("", new_nlines_hdr - length(newtl))) |
59 | 1x |
newfrmmat[1:new_nlines_hdr, 1] <- "xx" |
60 |
} |
|
61 | 3x |
mf_strings(matform) <- newstrmat |
62 | 3x |
mf_formats(matform) <- newfrmmat |
63 | 3x |
mf_spans(matform) <- expand_mat_rows(mf_spans(matform), row_nlines, rep_vec_to_len) |
64 | 3x |
mf_aligns(matform) <- expand_mat_rows(mf_aligns(matform), row_nlines, rep_vec_to_len) |
65 |
## mf_display(matform) <- expand_mat_rows(mf_display(matform), row_nlines, rep_vec_to_len) |
|
66 | 3x |
mf_lgrouping(matform) <- rep(mf_lgrouping(matform), times = row_nlines) |
67 |
} |
|
68 | ||
69 | 69x |
matform |
70 |
} |
|
71 | ||
72 |
disp_from_spans <- function(spans) { |
|
73 | ||
74 | 121x |
display <- matrix(rep(TRUE, length(spans)), ncol = ncol(spans)) |
75 | ||
76 | 121x |
print_cells_mat <- spans == 1L |
77 | 121x |
if (!all(print_cells_mat)) { |
78 | 1x |
display_rws <- lapply( |
79 | 1x |
seq_len(nrow(spans)), |
80 | 1x |
function(i) { |
81 | 2x |
print_cells <- print_cells_mat[i, ] |
82 | 2x |
row <- spans[i, ] |
83 |
## display <- t(apply(spans, 1, function(row) { |
|
84 |
## print_cells <- row == 1 |
|
85 | ||
86 | 2x |
if (!all(print_cells)) { |
87 |
## need to calculate which cell need to be printed |
|
88 | 1x |
print_cells <- spans_to_viscell(row) |
89 |
} |
|
90 | 2x |
print_cells |
91 |
} |
|
92 |
) |
|
93 | 1x |
display <- do.call(rbind, display_rws) |
94 |
} |
|
95 | 121x |
display |
96 |
} |
|
97 | ||
98 |
## constructor |
|
99 |
#' @title Matrix Print Form - Intermediate Representation for ASCII Table Printing |
|
100 |
#' |
|
101 |
#' @note The bare constructor for the `MatrixPrintForm` should generally |
|
102 |
#' only be called by `matrix_form` custom methods, and almost never from other code. |
|
103 |
#' |
|
104 |
#' @param strings character matrix. Matrix of formatted, ready to |
|
105 |
#' display strings organized as they will be positioned when |
|
106 |
#' rendered. Elements that span more than one column must be |
|
107 |
#' followed by the correct number of placeholders (typically |
|
108 |
#' either empty strings or repeats of the value). |
|
109 |
#' @param spans numeric matrix. Matrix of same dimension as |
|
110 |
#' \code{strings} giving the spanning information for each |
|
111 |
#' element. Must be repeated to match placeholders in |
|
112 |
#' \code{strings}. |
|
113 |
#' @param aligns character matrix. Matrix of same dimension as |
|
114 |
#' \code{strings} giving the text alignment information for each |
|
115 |
#' element. Must be repeated to match placeholders in |
|
116 |
#' \code{strings}. |
|
117 |
#' @param formats matrix. Matrix of same dimension |
|
118 |
#' as \code{strings} giving the text format information for |
|
119 |
#' each element. Must be repeated to match placeholders in |
|
120 |
#' \code{strings}. |
|
121 |
#' @param row_info data.frame. Data.frame with row-information |
|
122 |
#' necessary for pagination (XXX document exactly what that is). |
|
123 |
#' @param line_grouping integer. Sequence of integers indicating how |
|
124 |
#' print lines correspond to semantic rows in the object. |
|
125 |
#' Typically this should not be set manually unless |
|
126 |
#' `expact_newlines` is set to \code{FALSE}. |
|
127 |
#' @param ref_fnotes list. Referential footnote information if |
|
128 |
#' applicable. |
|
129 |
#' @param nlines_header numeric(1). Number of lines taken up by the |
|
130 |
#' values of the header (i.e. not including the divider). |
|
131 |
#' @param nrow_header numeric(1). Number of \emph{rows} corresponding |
|
132 |
#' to the header. |
|
133 |
#' @param has_topleft logical(1). Does the corresponding table have |
|
134 |
#' 'top left information' which should be treated differently when |
|
135 |
#' expanding newlines. Ignored if \code{expand_newlines} is |
|
136 |
#' \code{FALSE}. |
|
137 |
#' @param has_rowlabs logical(1). Do the matrices (\code{strings}, |
|
138 |
#' \code{spans}, \code{aligns}) each contain a column that |
|
139 |
#' corresponds with row labels (Rather than with table cell |
|
140 |
#' values). Defaults to \code{TRUE}. |
|
141 |
#' @param main_title character(1). Main title as a string. |
|
142 |
#' @param subtitles character. Subtitles, as a character vector. |
|
143 |
#' @param page_titles character. Page-specific titles, as a character |
|
144 |
#' vector. |
|
145 |
#' @param main_footer character(1). Main footer as a string. |
|
146 |
#' @param prov_footer character. Provenance footer information as a |
|
147 |
#' character vector. |
|
148 |
#' @param expand_newlines logical(1). Should the matrix form generated |
|
149 |
#' expand rows whose values contain newlines into multiple |
|
150 |
#' 'physical' rows (as they will appear when rendered into |
|
151 |
#' ASCII). Defaults to \code{TRUE} |
|
152 |
#' @param col_gap numeric(1). Space (in characters) between columns |
|
153 |
#' @param table_inset numeric(1). Table inset. See |
|
154 |
#' \code{\link{table_inset}} |
|
155 |
#' @param colwidths numeric. NULL, or a vector of column rendering widths. |
|
156 |
#' if non-NULL, must have length equal to `ncol(strings)` |
|
157 |
#' @param indent_size numeric(1). Number of spaces to be used per level of indent (if supported by |
|
158 |
#' the relevant method). Defaults to 2. |
|
159 |
#' @export |
|
160 |
#' @return An object of class `MatrixPrintForm`. Currently this is |
|
161 |
#' implemented as an S3 class inheriting from list with the following |
|
162 |
#' elements: |
|
163 |
#' \describe{ |
|
164 |
#' \item{\code{strings}}{see argument} |
|
165 |
#' \item{\code{spans}}{see argument} |
|
166 |
#' \item{\code{aligns}}{see argument} |
|
167 |
#' \item{\code{display}}{logical matrix of same dimension as `strings` |
|
168 |
#' that specifies whether an element in `strings` will be displayed |
|
169 |
#' when the table is rendered} |
|
170 |
#' \item{\code{formats}}{see argument} |
|
171 |
#' \item{\code{row_info}}{see argument} |
|
172 |
#' \item{\code{line_grouping}}{see argument} |
|
173 |
#' \item{\code{ref_footnotes}}{see argument} |
|
174 |
#' \item{\code{main_title}}{see argument} |
|
175 |
#' \item{\code{subtitles}}{see argument} |
|
176 |
#' \item{\code{page_titles}}{see argument} |
|
177 |
#' \item{\code{main_footer}}{see argument} |
|
178 |
#' \item{\code{prov_footer}}{see argument} |
|
179 |
#' \item{\code{col_gap}}{see argument} |
|
180 |
#' \item{\code{table_inset}}{see argument} |
|
181 |
#' } |
|
182 |
#' |
|
183 |
#' as well as the following attributes: |
|
184 |
#' \describe{ |
|
185 |
#' \item{\code{nlines_header}}{see argument} |
|
186 |
#' \item{\code{nrow_header}}{see argument} |
|
187 |
#' \item{\code{ncols}}{number of columns \emph{of the table}, not including |
|
188 |
#' any row names/row labels} |
|
189 |
#' } |
|
190 |
MatrixPrintForm <- function(strings = NULL, |
|
191 |
spans, |
|
192 |
aligns, |
|
193 |
formats, |
|
194 |
row_info, |
|
195 |
line_grouping = seq_len(NROW(strings)), |
|
196 |
ref_fnotes = list(), |
|
197 |
nlines_header, |
|
198 |
nrow_header, |
|
199 |
has_topleft = TRUE, |
|
200 |
has_rowlabs = has_topleft, |
|
201 |
expand_newlines = TRUE, |
|
202 |
main_title = "", |
|
203 |
subtitles = character(), |
|
204 |
page_titles = character(), |
|
205 |
main_footer = "", |
|
206 |
prov_footer = character(), |
|
207 |
col_gap = 3, |
|
208 |
table_inset = 0L, |
|
209 |
colwidths = NULL, |
|
210 |
indent_size = 2) { |
|
211 | 14x |
display <- disp_from_spans(spans) |
212 | ||
213 | ||
214 | 14x |
ncs <- if (has_rowlabs) ncol(strings) - 1 else ncol(strings) |
215 | 14x |
ret <- structure( |
216 | 14x |
list( |
217 | 14x |
strings = strings, |
218 | 14x |
spans = spans, |
219 | 14x |
aligns = aligns, |
220 | 14x |
display = display, |
221 | 14x |
formats = formats, |
222 | 14x |
row_info = row_info, |
223 | 14x |
line_grouping = line_grouping, |
224 | 14x |
ref_footnotes = ref_fnotes, |
225 | 14x |
main_title = main_title, |
226 | 14x |
subtitles = subtitles, |
227 | 14x |
page_titles = page_titles, |
228 | 14x |
main_footer = main_footer, |
229 | 14x |
prov_footer = prov_footer, |
230 | 14x |
col_gap = col_gap, |
231 | 14x |
table_inset = as.integer(table_inset), |
232 | 14x |
has_topleft = has_topleft, |
233 | 14x |
indent_size = indent_size, |
234 | 14x |
col_widths = colwidths |
235 |
), |
|
236 | 14x |
nrow_header = nrow_header, |
237 | 14x |
ncols = ncs, |
238 | 14x |
class = c("MatrixPrintForm", "list") |
239 |
) |
|
240 | ||
241 | ||
242 |
## .do_mat_expand(ret) |
|
243 | 14x |
if (expand_newlines) { |
244 | 14x |
ret <- mform_handle_newlines(ret) |
245 |
} |
|
246 | ||
247 | ||
248 |
## ret <- shove_refdf_into_rowinfo(ret) |
|
249 | 14x |
if(is.null(colwidths)) |
250 | 14x |
colwidths <- propose_column_widths(ret) |
251 | 14x |
mf_col_widths(ret) <- colwidths |
252 | 14x |
ret <- mform_build_refdf(ret) |
253 | 14x |
ret |
254 |
} |
|
255 | ||
256 | ||
257 |
#'Create a row for a referential footnote information dataframe |
|
258 |
#' |
|
259 |
#' @inheritParams nlines |
|
260 |
#' @param row_path character. row path (`NA_character_` for none) |
|
261 |
#' @param col_path character. column path (`NA_character_` for none) |
|
262 |
#' @param row integer(1). Integer position of the row. |
|
263 |
#' @param col integer(1). Integer position of the column. |
|
264 |
#' @param symbol character(1). Symbol for the reference. `NA_character_` to use the `ref_index` automatically. |
|
265 |
#' @param ref_index integer(1). The index of the footnote, used for ordering even when symbol is not NA |
|
266 |
#' @param msg character(1). The string message, not including the symbol portion (`{symbol} - `) |
|
267 |
#' |
|
268 |
#' @return a single row data.frame with the appropriate columns. |
|
269 |
#' |
|
270 |
#' @export |
|
271 |
#' |
|
272 |
ref_df_row <- function(row_path = NA_character_, col_path = NA_character_, row = NA_integer_, col = NA_integer_, symbol = NA_character_, ref_index = NA_integer_, msg = NA_character_, max_width = NULL) { |
|
273 | 3117x |
nlines <- nlines(msg, max_width = max_width) |
274 | 3117x |
data.frame(row_path = I(list(row_path)), |
275 | 3117x |
col_path = I(list(col_path)), |
276 | 3117x |
row = row, |
277 | 3117x |
col = col, |
278 | 3117x |
symbol = symbol, |
279 | 3117x |
ref_index = ref_index, |
280 | 3117x |
msg = msg, |
281 | 3117x |
nlines = nlines, |
282 | 3117x |
stringsAsFactors = FALSE) |
283 |
} |
|
284 | ||
285 | ||
286 |
## this entire thing is a hatchetjob of a hack which should not be necessary. |
|
287 |
## mf_rinfo(mform) should have the relevant info in it and |
|
288 |
## mf_cinfo(mform) should be non-null (!!!) and have the info in it |
|
289 |
## in which case this becomes silly and dumb, but here we are, so here we go. |
|
290 |
infer_ref_info <- function(mform, colspace_only) { |
|
291 | 56x |
if(colspace_only) |
292 | 28x |
idx <- seq_len(mf_nlheader(mform)) |
293 |
else |
|
294 | 28x |
idx <- seq_len(nrow(mf_strings(mform))) |
295 | ||
296 | ||
297 | 56x |
hasrlbs <- mf_has_rlabels(mform) |
298 | ||
299 | 56x |
strs <- mf_strings(mform)[idx, , drop = FALSE] |
300 | ||
301 |
## they're nested so \\2 is the inner one, without the brackets |
|
302 | 56x |
refs <- gsub("^[^{]*([{]([^}]+)[}]){0,1}$", "\\2", strs) |
303 |
## handle spanned values |
|
304 | 56x |
refs[!mf_display(mform)[idx,]] <- "" |
305 | ||
306 |
## we want to count across rows first, not down columns, cause |
|
307 |
## thats how footnote numbering works |
|
308 | 56x |
refs_inorder <- as.vector(t(refs)) |
309 | 56x |
keepem <- nzchar(refs_inorder) |
310 | 56x |
if(sum(keepem) == 0) |
311 | 54x |
return(ref_df_row()[0,]) |
312 | ||
313 | 2x |
refs_spl <- strsplit(refs_inorder[keepem], ", ", fixed = TRUE) |
314 | 2x |
runvec <- vapply(refs_spl, length, 1L) |
315 | ||
316 | ||
317 | ||
318 | 2x |
row_index <- as.vector(t(do.call(cbind, replicate(ncol(strs), |
319 | 2x |
list(mf_lgrouping(mform)[idx] - mf_nlheader(mform))))))[keepem] |
320 | 2x |
row_index[row_index < 1] <- NA_integer_ |
321 | 2x |
c_torep <- if(hasrlbs) c(NA_integer_, seq(1, ncol(strs) - 1)) else seq_len(ncol(strs)) |
322 | 2x |
col_index <- rep(c_torep, nrow(strs))[keepem] |
323 | ||
324 | ||
325 | ||
326 | ||
327 | 2x |
ret <- data.frame(symbol = unlist(refs_spl), |
328 | 2x |
row_path = I(mf_rinfo(mform)$path[rep(row_index, times = runvec)]), |
329 | 2x |
row = rep(row_index, times = runvec), |
330 | 2x |
col = rep(col_index, times = runvec)) |
331 | 2x |
ret$msg <- vapply(ret$symbol, function(sym) { |
332 | 16x |
fullmsg <- unique(grep(paste0("{",sym, "}"), fixed = TRUE, mf_rfnotes(mform), value = TRUE)) |
333 | 16x |
gsub("^[{][^}]+[}] - ", "", fullmsg) |
334 |
}, "") |
|
335 | ||
336 | ||
337 | 2x |
col_pths <- mf_col_paths(mform) |
338 | 2x |
ret$col_path <- replicate(nrow(ret), list(NA_character_)) |
339 | 2x |
non_na_col <- !is.na(ret$col) |
340 | 2x |
ret$col_path[non_na_col] <- col_pths[ret$col[non_na_col]] |
341 | 2x |
ret$ref_index <- seq_len(nrow(ret)) |
342 |
## |
|
343 | 2x |
ret$nlines <- vapply(paste0("{", ret$symbol, "} - ", ret$msg), nlines, 1L) |
344 | 2x |
ret <- ret[,names(ref_df_row())] |
345 | 2x |
ret |
346 |
} |
|
347 | ||
348 |
mform_build_refdf <- function(mform) { |
|
349 | 28x |
rdf <- mf_rinfo(mform) |
350 | 28x |
cref_rows <- infer_ref_info(mform, colspace_only = TRUE) |
351 |
## this will recheck sometimes but its safer and shouldn't |
|
352 |
## be too prohibitively costly |
|
353 | 28x |
if(NROW(rdf$ref_info_df) > 0 && sum(sapply(rdf$ref_info_df, NROW)) > 0) { |
354 | ! |
cref_rows <- infer_ref_info(mform, colspace_only = TRUE) |
355 | ! |
rref_rows <- rdf$ref_info_df |
356 |
} else { |
|
357 | 28x |
cref_rows <- infer_ref_info(mform, colspace_only = FALSE) |
358 | 28x |
rref_rows <- list() |
359 |
} |
|
360 | 28x |
mf_fnote_df(mform) <- do.call(rbind.data.frame, c(list(cref_rows), rref_rows)) |
361 | 28x |
update_mf_nlines(mform, colwidths = mf_col_widths(mform), max_width = NULL) |
362 |
} |
|
363 | ||
364 | ||
365 | ||
366 | ||
367 | ||
368 | ||
369 | ||
370 | ||
371 | ||
372 |
## constructor with snake_case naming convention |
|
373 |
#' @rdname MatrixPrintForm |
|
374 |
#' @export |
|
375 |
matrix_print_form <- MatrixPrintForm |
|
376 | ||
377 | ||
378 |
## hide the implementation behind abstraction in case we decide we want a real class someday |
|
379 |
#' `Setters` and `getters` for aspects of `MatrixPrintForm` Objects |
|
380 |
#' |
|
381 |
#' Most of these functions, particularly the `settters`, are intended |
|
382 |
#' almost exclusively for internal use in, e.g., `matrix_form` methods, |
|
383 |
#' and should generally not be called by end users. |
|
384 |
#' |
|
385 |
#' @param mf `MatrixPrintForm(1)`. A `MatrixPrintForm` object |
|
386 |
#' @param value ANY. The new value for the component in question. |
|
387 |
#' @return The element of the `MatrixPrintForm` associated with the `getter`, or |
|
388 |
#' the modified `MatrixPrintForm` object in the case of a `setter`. |
|
389 |
#' @export |
|
390 |
#' @rdname mpf_accessors |
|
391 | 1010x |
mf_strings <- function(mf) mf$strings |
392 | ||
393 |
#' @export |
|
394 |
#' @rdname mpf_accessors |
|
395 | ||
396 | 108x |
mf_spans <- function(mf) mf$spans |
397 |
#' @export |
|
398 |
#' @rdname mpf_accessors |
|
399 | ||
400 | 107x |
mf_aligns <- function(mf) mf$aligns |
401 | ||
402 |
#' @export |
|
403 |
#' @rdname mpf_accessors |
|
404 | 56x |
mf_display <- function(mf) mf$display |
405 | ||
406 |
#' @export |
|
407 |
#' @rdname mpf_accessors |
|
408 | 174x |
mf_formats <- function(mf) mf$formats |
409 | ||
410 |
#' @export |
|
411 |
#' @rdname mpf_accessors |
|
412 | 421x |
mf_rinfo <- function(mf) mf$row_info |
413 | ||
414 |
#' @export |
|
415 |
#' @rdname mpf_accessors |
|
416 | 70x |
mf_cinfo <- function(mf) mf$col_info |
417 | ||
418 | ||
419 |
#' @export |
|
420 |
#' @rdname mpf_accessors |
|
421 | 126x |
mf_has_topleft <- function(mf) mf$has_topleft |
422 | ||
423 |
#' @export |
|
424 |
#' @rdname mpf_accessors |
|
425 | 1270x |
mf_lgrouping <- function(mf) mf$line_grouping |
426 | ||
427 |
#' @export |
|
428 |
#' @rdname mpf_accessors |
|
429 | 17x |
mf_rfnotes <- function(mf) mf$ref_footnotes |
430 | ||
431 |
#' @export |
|
432 |
#' @rdname mpf_accessors |
|
433 | 533x |
mf_nlheader <- function(mf) sum(mf_lgrouping(mf) <= mf_nrheader(mf)) |
434 | ||
435 |
#' @export |
|
436 |
#' @rdname mpf_accessors |
|
437 | 752x |
mf_nrheader <- function(mf) attr(mf, "nrow_header", exact = TRUE) |
438 | ||
439 | ||
440 |
#' @export |
|
441 |
#' @rdname mpf_accessors |
|
442 | 87x |
mf_colgap <- function(mf) mf$col_gap |
443 | ||
444 | ||
445 | ||
446 | ||
447 |
## XXX should this be exported? not sure if there's a point |
|
448 |
mf_col_paths <- function(mf) { |
|
449 | 2x |
if(!is.null(mf_cinfo(mf))) |
450 | ! |
mf_cinfo(mf)$path |
451 |
else |
|
452 | 2x |
as.list(paste0("col", seq_len(nrow(mf_strings(mf)) - mf_has_topleft(mf)))) |
453 |
} |
|
454 | ||
455 | ||
456 |
mf_col_widths <- function(mf) { |
|
457 | 173x |
mf$col_widths |
458 |
} |
|
459 | ||
460 |
`mf_col_widths<-` <- function(mf, value) { |
|
461 | 123x |
if(!is.null(value) && length(value) != NCOL(mf_strings(mf))) |
462 | ! |
stop("Number of column widths (", length(value), ") does not match ", |
463 | ! |
"number of columns in strings matrix (", NCOL(mf_strings(mf)), ").") |
464 | 123x |
mf$col_widths <- value |
465 | 123x |
mf |
466 |
} |
|
467 | ||
468 |
mf_fnote_df <- function(mf) { |
|
469 | 564x |
mf$ref_fnote_df |
470 |
} |
|
471 | ||
472 |
`mf_fnote_df<-` <- function(mf, value) { |
|
473 | 159x |
stopifnot(is.null(value) || ( |
474 | 159x |
is.data.frame(value) && identical(names(value), names(ref_df_row())))) |
475 | 159x |
mf$ref_fnote_df <- value |
476 | 159x |
mf |
477 |
} |
|
478 | ||
479 | ||
480 |
splice_fnote_info_in <- function(df, refdf, row = TRUE) { |
|
481 | 173x |
if(NROW(df) == 0) |
482 | ! |
return(df) |
483 | ||
484 | 173x |
colnm <- ifelse(row, "row", "col") |
485 | 173x |
refdf <- refdf[!is.na(refdf[[colnm]]),] |
486 | ||
487 | 173x |
refdf_spl <- split(refdf, refdf[[colnm]]) |
488 | 173x |
df$ref_info_df <- replicate(nrow(df), list(ref_df_row()[0,])) |
489 | 173x |
df$ref_info_df[as.integer(names(refdf_spl))] <- refdf_spl |
490 | 173x |
df |
491 |
} |
|
492 | ||
493 | ||
494 |
shove_refdf_into_rowinfo <- function(mform) { |
|
495 | 131x |
refdf <- mf_fnote_df(mform) |
496 | 131x |
rowinfo <- mf_rinfo(mform) |
497 | 131x |
mf_rinfo(mform) <- splice_fnote_info_in(rowinfo, refdf) |
498 | 131x |
mform |
499 |
} |
|
500 | ||
501 |
update_mf_nlines <- function(mform, colwidths, max_width) { |
|
502 | 82x |
mform <- update_mf_ref_nlines(mform, max_width = max_width) |
503 | 82x |
mform <- update_mf_rinfo_extents(mform) |
504 | ||
505 | 82x |
mform |
506 |
} |
|
507 | ||
508 |
update_mf_rinfo_extents <- function(mform) { |
|
509 | 82x |
rinfo <- mf_rinfo(mform) |
510 | 82x |
refdf_all <- mf_fnote_df(mform) |
511 | 82x |
refdf_rows <- refdf_all[!is.na(refdf_all$row),] |
512 | 82x |
if(NROW(rinfo) == 0) |
513 | ! |
return(mform) |
514 | 82x |
lgrp <- mf_lgrouping(mform) - mf_nrheader(mform) |
515 | 82x |
lgrp <- lgrp[lgrp > 0] |
516 | 82x |
rf_nlines <- vapply(seq_len(max(lgrp)), function(ii) { |
517 | ||
518 | 2053x |
refdfii <- refdf_rows[refdf_rows$row == ii,] |
519 | 2053x |
refdfii <- refdfii[!duplicated(refdfii$symbol), ] |
520 | 2053x |
if(NROW(refdfii) == 0L) |
521 | 1957x |
return(0L) |
522 | 96x |
sum(refdfii$nlines) |
523 | 82x |
}, 1L) |
524 | ||
525 | 82x |
raw_self_exts <- vapply(split(lgrp, lgrp), length, 0L) |
526 | 82x |
stopifnot(length(raw_self_exts) == length(rf_nlines)) |
527 | 82x |
new_exts <- raw_self_exts + rf_nlines |
528 | ||
529 | 82x |
mapdf <- data.frame(row_num = as.integer(names(new_exts)), |
530 | 82x |
raw_extent = raw_self_exts) |
531 | 82x |
stopifnot(all(mapdf$row_num == rinfo$abs_rownumber)) |
532 | ||
533 | ||
534 | 82x |
new_par_exts <- vapply(rinfo$reprint_inds, |
535 | 82x |
function(idx) { |
536 | 2053x |
sum(0L, mapdf$raw_extent[mapdf$row_num %in% idx]) |
537 | 82x |
}, 1L) |
538 | ||
539 | 82x |
rinfo$self_extent <- new_exts |
540 | 82x |
rinfo$par_extent <- new_par_exts |
541 | 82x |
rinfo$nreflines <- rf_nlines |
542 | 82x |
mf_rinfo(mform) <- rinfo |
543 | 82x |
mform |
544 |
} |
|
545 | ||
546 |
update_mf_ref_nlines <- function(mform, max_width) { |
|
547 | 82x |
refdf <- mf_fnote_df(mform) |
548 | 82x |
if(NROW(refdf) == 0) |
549 | 55x |
return(mform) |
550 | ||
551 | 27x |
refdf$nlines <- vapply(paste0("{", refdf$symbol, "} - ", refdf$msg), |
552 | 27x |
nlines, |
553 | 27x |
max_width = max_width, |
554 | 27x |
1L) |
555 | 27x |
mf_fnote_df(mform) <- refdf |
556 | 27x |
shove_refdf_into_rowinfo(mform) |
557 |
} |
|
558 | ||
559 | ||
560 | ||
561 |
#' @export |
|
562 |
#' @rdname mpf_accessors |
|
563 |
`mf_strings<-` <- function(mf, value) { |
|
564 | 162x |
mf$strings <- value |
565 | 162x |
mf |
566 |
} |
|
567 | ||
568 |
.chkdim_and_replace <- function(mf, value, component) { |
|
569 | 322x |
strdim <- dim(mf_strings(mf)) |
570 | 322x |
vdim <- dim(value) |
571 | 322x |
if (!is.null(strdim) && !identical(strdim, vdim)) { |
572 | 1x |
stop( |
573 | 1x |
"Dimensions of new '", component, "' value (", |
574 | 1x |
vdim[1], ", ", vdim[2], # nocov |
575 | 1x |
") do not match dimensions of existing 'strings' component (", # nocov |
576 | 1x |
strdim[1], ", ", strdim[2], ")." # nocov |
577 |
) |
|
578 |
} |
|
579 | 321x |
mf[[component]] <- value |
580 | 321x |
mf |
581 |
} |
|
582 | ||
583 | ||
584 |
#' @export |
|
585 |
#' @rdname mpf_accessors |
|
586 |
`mf_spans<-` <- function(mf, value) { |
|
587 | 108x |
mf <- .chkdim_and_replace(mf, value, component = "spans") |
588 | 107x |
mf$display <- disp_from_spans(value) |
589 | 107x |
mf |
590 |
} |
|
591 | ||
592 |
#' @export |
|
593 |
#' @rdname mpf_accessors |
|
594 |
`mf_aligns<-` <- function(mf, value) { |
|
595 | 107x |
.chkdim_and_replace(mf, value, component = "aligns") |
596 |
} |
|
597 | ||
598 | ||
599 |
#' @export |
|
600 |
#' @rdname mpf_accessors |
|
601 |
`mf_display<-` <- function(mf, value) { |
|
602 | ! |
stop("display is now a derived element of the matrix print form, modify it via `mf_spans<-`") |
603 | ! |
.chkdim_and_replace(mf, value, component = "display") |
604 |
} |
|
605 | ||
606 |
#' @export |
|
607 |
#' @rdname mpf_accessors |
|
608 |
`mf_formats<-` <- function(mf, value) { |
|
609 | 107x |
.chkdim_and_replace(mf, value, component = "formats") |
610 |
} |
|
611 | ||
612 | ||
613 |
## NB NROW(v) == length(v) for atomic vectors so this is ok for lgrouping as wellas rinfo |
|
614 |
.chknrow_and_replace <- function(mf, value, component, noheader = FALSE) { |
|
615 | 107x |
strdim <- NROW(mf_strings(mf)) - if (noheader) mf_nlheader(mf) else 0L |
616 | 107x |
vdim <- NROW(value) |
617 | 107x |
if (!is.null(strdim) && !identical(strdim, vdim)) { |
618 | ! |
stop( |
619 | ! |
"Number of rows/length of new '", component, "' value (", |
620 | ! |
vdim[1], |
621 | ! |
") does not match existing 'strings' component (", |
622 | ! |
strdim[1], ")." |
623 |
) |
|
624 |
} |
|
625 | 107x |
mf[[component]] <- value |
626 | 107x |
mf |
627 |
} |
|
628 | ||
629 |
#' @export |
|
630 |
#' @rdname mpf_accessors |
|
631 |
`mf_rinfo<-` <- function(mf, value) { |
|
632 |
## this can someijtmes be called after expanding newlines so in general |
|
633 |
## we should not expect it to match the number of rows in the strings matrix |
|
634 |
##.chknrow_and_replace(mf, value, component = "row_info", noheader = TRUE) |
|
635 | 257x |
lgrps <- mf_lgrouping(mf) |
636 | 257x |
nrs <- length(unique(lgrps[-seq_len(mf_nlheader(mf))])) |
637 | 257x |
if(NROW(value) != nrs) |
638 | 1x |
stop("Rows in new row_info component (", |
639 | 1x |
NROW(value), |
640 | 1x |
") does not match number of rows reflected in line_grouping component (", |
641 | 1x |
nrs, ")") |
642 | 256x |
mf$row_info <- value |
643 | 256x |
mf |
644 |
} |
|
645 | ||
646 |
#' @export |
|
647 |
#' @rdname mpf_accessors |
|
648 |
`mf_cinfo<-` <- function(mf, value) { |
|
649 | 42x |
if(NROW(value) > 0 && NROW(value) != ncol(mf)) |
650 | ! |
stop("Number of rows in new cinfo (", NROW(value), ") does not match ", |
651 | ! |
"number of columns (", ncol(mf), ")") |
652 | 42x |
mf$col_info <- value |
653 | 42x |
mf |
654 |
} |
|
655 | ||
656 | ||
657 |
#' @export |
|
658 |
#' @rdname mpf_accessors |
|
659 |
`mf_lgrouping<-` <- function(mf, value) { |
|
660 | 107x |
.chknrow_and_replace(mf, value, component = "line_grouping") |
661 |
} |
|
662 | ||
663 | ||
664 |
#' @export |
|
665 |
#' @rdname mpf_accessors |
|
666 |
`mf_rfnotes<-` <- function(mf, value) { |
|
667 | 109x |
mf$ref_footnotes <- value |
668 | 109x |
mf |
669 |
} |
|
670 | ||
671 | ||
672 | ||
673 |
#' @export |
|
674 |
#' @rdname mpf_accessors |
|
675 |
`mf_nrheader<-` <- function(mf, value) { |
|
676 | 2x |
attr(mf, "nrow_header") <- value |
677 | 2x |
mf |
678 |
} |
|
679 | ||
680 |
#' @export |
|
681 |
#' @rdname mpf_accessors |
|
682 |
`mf_colgap<-` <- function(mf, value) { |
|
683 | ! |
mf$col_gap <- value |
684 | ! |
mf |
685 |
} |
|
686 | ||
687 |
#' @export |
|
688 |
#' @rdname mpf_accessors |
|
689 | 402x |
mf_ncol <- function(mf) attr(mf, "ncols", exact = TRUE) |
690 | ||
691 |
#' @export |
|
692 |
#' @rdname mpf_accessors |
|
693 | 10x |
mf_nrow <- function(mf) max(mf_lgrouping(mf)) - mf_nrheader(mf) |
694 | ||
695 | ||
696 | ||
697 |
#' @export |
|
698 |
#' @rdname mpf_accessors |
|
699 |
`mf_ncol<-` <- function(mf, value) { |
|
700 | 130x |
stopifnot(is.numeric(value)) |
701 | 130x |
attr(mf, "ncols") <- value |
702 | 130x |
mf |
703 |
} |
|
704 | ||
705 |
#' @param x `MatrixPrintForm`. The object. |
|
706 |
#' @export |
|
707 |
#' @rdname mpf_accessors |
|
708 |
setMethod( |
|
709 |
"ncol", "MatrixPrintForm", |
|
710 | 401x |
function(x) mf_ncol(x) |
711 |
) |
|
712 | ||
713 |
#' @export |
|
714 |
#' @rdname mpf_accessors |
|
715 |
mpf_has_rlabels <- function(mf) { |
|
716 | ! |
.Deprecated("mf_has_rlabels") |
717 | ! |
mf_has_rlabels(mf) |
718 |
} |
|
719 | ||
720 |
#' @export |
|
721 |
#' @rdname mpf_accessors |
|
722 | 178x |
mf_has_rlabels <- function(mf) ncol(mf$strings) > ncol(mf) |
723 | ||
724 |
#' Create spoof matrix form from a data.frame |
|
725 |
#' |
|
726 |
#' This is useful primarily for writing testing/examples, and as a |
|
727 |
#' starting point for more sophisticated custom `matrix_form` methods |
|
728 |
#' |
|
729 |
#' @param df data.frame |
|
730 |
#' @param parent_path character. parent path that all rows should be "children of", |
|
731 |
#' defaults to `"root"`, and generally should not matter to end users. |
|
732 |
#' |
|
733 |
#' @return A valid `MatrixPrintForm` object representing `df`, |
|
734 |
#' ready for ASCII rendering |
|
735 |
#' |
|
736 |
#' @examples |
|
737 |
#' mform <- basic_matrix_form(mtcars) |
|
738 |
#' cat(toString(mform)) |
|
739 |
#' @export |
|
740 |
basic_matrix_form <- function(df, parent_path = "root") { |
|
741 | 12x |
fmts <- lapply(df, function(x) if (is.null(obj_format(x))) "xx" else obj_format(x)) |
742 | ||
743 | 12x |
bodystrs <- mapply(function(x, fmt) { |
744 | 80x |
sapply(x, format_value, format = fmt) |
745 | 12x |
}, x = df, fmt = fmts) |
746 | ||
747 | 12x |
rnms <- row.names(df) |
748 | 12x |
if (is.null(rnms)) { |
749 | ! |
rnms <- as.character(seq_len(NROW(df))) |
750 |
} |
|
751 | ||
752 | 12x |
cnms <- names(df) |
753 | ||
754 | 12x |
strings <- rbind( |
755 | 12x |
c("", cnms), |
756 | 12x |
cbind(rnms, bodystrs) |
757 |
) |
|
758 | ||
759 | 12x |
fnr <- nrow(strings) |
760 | 12x |
fnc <- ncol(strings) |
761 | ||
762 |
## center alignment for column labels, left alignment for everything else |
|
763 | 12x |
aligns <- rbind( |
764 | 12x |
"center", |
765 | 12x |
matrix("left", nrow = NROW(df), ncol = fnc) |
766 |
) |
|
767 | ||
768 | ||
769 |
## build up fake pagination df |
|
770 | 12x |
charcols <- which(sapply(df, is.character)) |
771 | 12x |
if (length(charcols) > 0) { |
772 | 1x |
exts <- apply(df[, charcols, drop = FALSE], 1, function(x) max(vapply(x, nlines, 1L))) |
773 |
} else { |
|
774 | 11x |
exts <- rep(1L, NROW(df)) |
775 |
} |
|
776 | 12x |
rowdf <- basic_pagdf(row.names(df), |
777 | 12x |
extents = exts, |
778 | 12x |
parent_path = parent_path |
779 |
) |
|
780 | 12x |
formats <- cbind( |
781 |
"", |
|
782 | 12x |
rbind( |
783 |
"", |
|
784 | 12x |
matrix("xx", nrow = nrow(df), ncol = ncol(df)) |
785 |
) |
|
786 |
) |
|
787 | ||
788 | 12x |
ret <- matrix_print_form( |
789 | 12x |
strings = strings, |
790 | 12x |
aligns = aligns, |
791 | 12x |
spans = matrix(1, nrow = fnr, ncol = fnc), |
792 | 12x |
formats = formats, ## matrix("xx", nrow = fnr, ncol = fnc), |
793 | 12x |
row_info = rowdf, |
794 | 12x |
has_topleft = FALSE, |
795 | 12x |
nlines_header = 1, |
796 | 12x |
nrow_header = 1, |
797 | 12x |
has_rowlabs = TRUE |
798 |
) |
|
799 | 12x |
mform_build_refdf(ret) |
800 |
} |
|
801 | ||
802 | ||
803 |
map_to_new <- function(old, map) { |
|
804 | 143x |
inds <- match(old, map$old_idx) |
805 | 143x |
map$new_idx[inds] |
806 | ||
807 |
} |
|
808 | ||
809 | ||
810 |
reconstruct_basic_fnote_list <- function(mf) { |
|
811 | 106x |
refdf <- mf_fnote_df(mf) |
812 | 106x |
if(NROW(refdf) == 0) |
813 | 66x |
return(NULL) |
814 | 40x |
refdf <- refdf[!duplicated(refdf$symbol),] |
815 | 40x |
paste0("{", refdf$symbol, "} - ", refdf$msg) |
816 |
} |
|
817 | ||
818 | ||
819 |
fix_fnote_df <- function(df) { |
|
820 | 39x |
ind_symb <- df$symbol == as.character(df$ref_index) |
821 | 39x |
df$ref_index <- seq_len(nrow(df)) |
822 | 39x |
df$symbol[ind_symb] <- as.character(df$ref_index[ind_symb]) |
823 | 39x |
df |
824 |
} |
|
825 | ||
826 | ||
827 | ||
828 | ||
829 | ||
830 | ||
831 | ||
832 |
.mf_subset_core_mats <- function(mf, i, row = TRUE) { |
|
833 | 104x |
fillnum <- if(row) nrow(mf_strings(mf)) - mf_nlheader(mf) else ncol(mf) |
834 | 104x |
if(is.logical(i) || all(i < 0)) |
835 | ! |
i <- seq_len(fillnum)[i] |
836 | ||
837 | 104x |
if(row) { |
838 | 39x |
nlh <- mf_nlheader(mf) |
839 | 39x |
ncolrows <- mf_nrheader(mf) |
840 | 39x |
i_mat <- c(seq_len(nlh), which(mf_lgrouping(mf) %in% (i + ncolrows))) |
841 | 39x |
j_mat <- seq_len(ncol(mf_strings(mf))) |
842 |
} else { |
|
843 | 65x |
nlabcol <- as.integer(mf_has_rlabels(mf)) |
844 | 65x |
i_mat <- seq_len(nrow(mf_strings(mf))) |
845 | 65x |
j_mat <- c(seq_len(nlabcol), i + nlabcol) |
846 |
} |
|
847 | ||
848 | ||
849 | 104x |
mf_strings(mf) <- mf_strings(mf)[i_mat, j_mat, drop = FALSE] |
850 | 104x |
mf_lgrouping(mf) <- as.integer(as.factor(mf_lgrouping(mf)[i_mat])) |
851 | 104x |
if(!row) |
852 | 65x |
newspans <- truncate_spans(mf_spans(mf), j_mat) #'i' is the columns here, b/c row is FALSE |
853 |
else |
|
854 | 39x |
newspans <- mf_spans(mf)[i_mat, j_mat, drop = FALSE] |
855 | 104x |
mf_spans(mf) <- newspans |
856 | 104x |
mf_formats(mf) <- mf_formats(mf)[i_mat, j_mat, drop = FALSE] |
857 | ||
858 | 104x |
mf_aligns(mf) <- mf_aligns(mf)[i_mat, j_mat, drop = FALSE] |
859 | 104x |
if(!row) { |
860 | 65x |
mf_ncol(mf) <- length(i) |
861 | 65x |
if(!is.null(mf_col_widths(mf))) |
862 | 65x |
mf_col_widths(mf) <- mf_col_widths(mf)[j_mat] |
863 |
} |
|
864 | 104x |
mf |
865 |
} |
|
866 | ||
867 |
## ugh. spans are **way** more of a pain than I expected x.x |
|
868 |
truncate_one_span <- function(spanrow, j) { |
|
869 | 1857x |
i <- 1 |
870 | 1857x |
len <- length(spanrow) |
871 | 1857x |
while(i < len) { |
872 | 21606x |
spnlen <- spanrow[i] |
873 | 21606x |
inds <- seq(i, i + spnlen - 1) |
874 | 21606x |
newspnlen <- sum(inds %in% j) |
875 | 21606x |
spanrow[inds] <- newspnlen |
876 | 21606x |
i <- i + spnlen |
877 |
} |
|
878 | 1857x |
spanrow[j] |
879 |
} |
|
880 | ||
881 |
truncate_spans <- function(spans, j) { |
|
882 | 65x |
t(apply(spans, 1, truncate_one_span, j = j)) |
883 |
} |
|
884 | ||
885 | ||
886 |
mpf_subset_rows <- function(mf, i) { |
|
887 | 39x |
nlh <- mf_nlheader(mf) |
888 | 39x |
lgrps <- mf_lgrouping(mf) |
889 | 39x |
row_lgrps <- tail(lgrps, -1*nlh) |
890 | 39x |
nrs <- length(unique(row_lgrps)) |
891 | 39x |
ncolrows <- length(unique(lgrps[seq_len(nlh)])) |
892 | ||
893 | 39x |
ncs <- ncol(mf) |
894 | 39x |
mf <- .mf_subset_core_mats(mf, i, row = TRUE) |
895 | 39x |
map <- data.frame(old_idx = c(seq_len(ncolrows), i + ncolrows), |
896 | 39x |
new_idx = c(seq_len(ncolrows), ncolrows + order(i))) |
897 | ||
898 | 39x |
row_map <- data.frame(old_idx = i, new_idx = order(i)) |
899 | ||
900 | 39x |
refdf <- mf_fnote_df(mf) |
901 | ||
902 | 39x |
old_nas <- is.na(refdf$row) |
903 | 39x |
refdf$row <- map_to_new(refdf$row, row_map) |
904 | 39x |
refdf <- refdf[old_nas | !is.na(refdf$row),] |
905 | 39x |
refdf <- fix_fnote_df(refdf) |
906 | 39x |
mf_fnote_df(mf) <- refdf |
907 | ||
908 | 39x |
rinfo <- mf_rinfo(mf) |
909 | ||
910 | 39x |
rinfo <- rinfo[rinfo$abs_rownumber %in% i,] |
911 | ||
912 | 39x |
rinfo$abs_rownumber <- map_to_new(rinfo$abs_rownumber, row_map) |
913 | 39x |
mf_rinfo(mf) <- rinfo |
914 | ||
915 | 39x |
mf <- shove_refdf_into_rowinfo(mf) |
916 | 39x |
mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) |
917 | 39x |
mf |
918 | ||
919 |
} |
|
920 | ||
921 | ||
922 | ||
923 |
## we only care about referential footnotes, cause |
|
924 |
## they are currently the only place we're tracking |
|
925 |
## column information that will need to be touched up |
|
926 |
## but lets be careful and do a bit more anyway |
|
927 |
mpf_subset_cols <- function(mf, j) { |
|
928 | ||
929 | 65x |
nc <- ncol(mf) |
930 | 65x |
if(is.logical(j) || all(j < 0)) |
931 | ! |
j <- seq_len(nc)[j] |
932 | 65x |
if(any(j < 0)) |
933 | ! |
stop("cannot mix negative and positive indices") |
934 | ||
935 | 65x |
if(length(unique(j)) != length(j)) |
936 | ! |
stop("duplicated columns are not allowed when subsetting a matrix print form objects") |
937 | ||
938 | ||
939 |
# j_mat <- c(if(mf_has_topleft(mf)) seq_len(nlabcol), j + nlabcol) |
|
940 | 65x |
map <- data.frame(old_idx = j, new_idx = order(j)) |
941 | ||
942 |
## this has to happen before the remap inher |
|
943 | 65x |
refdf <- mf_fnote_df(mf) |
944 | ||
945 | 65x |
mf <- .mf_subset_core_mats(mf, j, row = FALSE) |
946 | ||
947 | ||
948 |
## future proofing (pipe dreams) |
|
949 |
## uncomment if we ever manage to have col info information on MPFs |
|
950 |
## if(!is.null(mf_cinfo(mf))) { |
|
951 |
## cinfo <- mf_cinfo(mf) |
|
952 |
## cinfo <- cinfo[j, , drop = FALSE] |
|
953 |
## cinfo$abs_pos <- map_to_new(cinfo$abs_pos, map) |
|
954 |
## mf_cinfo(mf) <- mf |
|
955 |
## } |
|
956 | ||
957 | ||
958 | ||
959 | 65x |
keep <- is.na(refdf$col) | refdf$col %in% j |
960 | 65x |
refdf <- refdf[keep, , drop = FALSE] |
961 | ||
962 | 65x |
refdf$col <- map_to_new(refdf$col, map) |
963 | 65x |
mf_fnote_df(mf) <- refdf |
964 | 65x |
mf <- shove_refdf_into_rowinfo(mf) |
965 | 65x |
mf_rfnotes(mf) <- reconstruct_basic_fnote_list(mf) |
966 | 65x |
mf_ncol(mf) <- length(j) |
967 | 65x |
mf |
968 |
} |
1 |
# `toString` ---- |
|
2 | ||
3 |
## this can't be tested from within R |
|
4 |
# nocov start |
|
5 |
#' @importFrom stats na.omit |
|
6 |
#' @importFrom utils head tail localeToCharset |
|
7 |
#' @import checkmate |
|
8 | ||
9 |
d_hsep_factory <- function() { |
|
10 |
warn_sent <- FALSE |
|
11 |
function() { |
|
12 |
if (any(grepl("^UTF", localeToCharset()))) { |
|
13 |
"\u2014" |
|
14 |
} else { |
|
15 |
if (!warn_sent && interactive()) { |
|
16 |
message( |
|
17 |
"Detected non-UTF charset. Falling back to '-' ", |
|
18 |
"as default header/body separator. This warning ", |
|
19 |
"will only be shown once per R session." |
|
20 |
) |
|
21 |
warn_sent <<- TRUE |
|
22 |
} |
|
23 |
"-" |
|
24 |
} |
|
25 |
} |
|
26 |
} |
|
27 | ||
28 |
#' Default horizontal Separator |
|
29 |
#' |
|
30 |
#' The default horizontal separator character which can be |
|
31 |
#' displayed in the current `charset` for use in rendering table-likes. |
|
32 |
#' |
|
33 |
#' @return `unicode` 2014 (long dash for generating solid horizontal line) |
|
34 |
#' if in a locale that uses a UTF character set, otherwise an ASCII hyphen |
|
35 |
#' with a once-per-session warning. |
|
36 |
#' |
|
37 |
#' @export |
|
38 |
#' @examples |
|
39 |
#' default_hsep() |
|
40 |
default_hsep <- d_hsep_factory() |
|
41 | ||
42 |
# nocov end |
|
43 | ||
44 |
.calc_cell_widths <- function(mat, colwidths, col_gap) { |
|
45 | 86x |
spans <- mat$spans |
46 | 86x |
keep_mat <- mat$display |
47 | 86x |
body <- mat$strings |
48 | ||
49 | 86x |
nr <- nrow(body) |
50 | ||
51 | 86x |
cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE) |
52 | 86x |
nc <- ncol(cell_widths_mat) |
53 | ||
54 | 86x |
for (i in seq_len(nrow(body))) { |
55 | 2237x |
if (any(!keep_mat[i, ])) { # any spans? |
56 | 6x |
j <- 1 |
57 | 6x |
while (j <= nc) { |
58 | 10x |
nj <- spans[i, j] |
59 | 10x |
j <- if (nj > 1) { |
60 | 6x |
js <- seq(j, j + nj - 1) |
61 | 6x |
cell_widths_mat[i, js] <- sum(cell_widths_mat[i, js]) + col_gap * (nj - 1) |
62 | 6x |
j + nj |
63 |
} else { |
|
64 | 4x |
j + 1 |
65 |
} |
|
66 |
} |
|
67 |
} |
|
68 |
} |
|
69 | 86x |
cell_widths_mat |
70 |
} |
|
71 | ||
72 | ||
73 | ||
74 |
do_cell_fnotes_wrap <- function(mat, widths, max_width, tf_wrap) { |
|
75 | ||
76 | 55x |
col_gap <- mf_colgap(mat) |
77 | 55x |
ncchar <- sum(widths) + (length(widths) - 1) * col_gap |
78 | 55x |
inset <- table_inset(mat) |
79 | ||
80 |
## Text wrapping checks |
|
81 | 55x |
if (tf_wrap) { |
82 | 16x |
if (is.null(max_width)) { |
83 | 2x |
max_width <- getOption("width", 80L) |
84 | 14x |
} else if (is.character(max_width) && identical(max_width, "auto")) { |
85 | ! |
max_width <- ncchar + inset |
86 |
} |
|
87 | 16x |
assert_number(max_width, lower = 0) |
88 |
} |
|
89 | ||
90 |
## Check for having the right number of widths |
|
91 | 55x |
stopifnot(length(widths) == ncol(mat$strings)) |
92 | ||
93 |
## format the to ASCII |
|
94 | 55x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
95 |
## wrap_string calls strwrap, which destroys whitespace so we need to make |
|
96 |
## sure to put the indents back in |
|
97 | ||
98 |
## See if indentation is properly set |
|
99 | 55x |
ind_from_mf <- mf_rinfo(mat)$indent > 0 |
100 | 55x |
nlh <- mf_nlheader(mat) |
101 | 55x |
ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") |
102 |
## Body indentation |
|
103 | 55x |
old_indent <- sapply(mf_rinfo(mat)$indent, function(i) paste0(rep(ind_std, i), collapse = "")) |
104 |
## Header indentation (it happens with toplefts, not \n in titles, dealt afterwards) |
|
105 |
## NB: what about \n in topleft? -> not supported |
|
106 | 55x |
header_indent <- gsub("^([[:space:]]*).*", "\\1", mat$strings[1:nlh, 1]) # Supposedly never with empty strings " " |
107 | 55x |
old_indent <- c(header_indent, old_indent) |
108 | 55x |
need_reindent <- nzchar(old_indent) |
109 |
## Check for which row has indent |
|
110 | 55x |
ind_from_strings <- nchar(old_indent)[-seq_len(nlh)] > 0 |
111 | 55x |
if (!all(ind_from_strings == ind_from_mf)) { |
112 |
stop("Row-info and string indentations are different.", # nocov |
|
113 |
" Please contact the maintainer, this should not happen.") # nocov |
|
114 |
} |
|
115 | 55x |
ori_mflg <- mf_lgrouping(mat) # Original groups |
116 | 55x |
reindent_old_idx <- ori_mflg[need_reindent] # Indent groups bf wrap |
117 | ||
118 |
## Taking care in advance of indented word wrappings |
|
119 | 55x |
cell_widths_mat[need_reindent, 1] <- cell_widths_mat[need_reindent, 1] - nchar(old_indent)[need_reindent] |
120 | ||
121 |
## Case in which the indentation is taking too much space vs desired wrapping |
|
122 | 55x |
if (any(cell_widths_mat < 0)) { |
123 | 1x |
col_culprits <- apply(cell_widths_mat, 2, function(i) any(i < 0)) |
124 | 1x |
stop( |
125 | 1x |
"Inserted width(s) for column(s) ", which(col_culprits), |
126 | 1x |
" is(are) not wide enough for the desired indentation." |
127 |
) |
|
128 |
} |
|
129 | ||
130 | 54x |
new_strings <- matrix( |
131 | 54x |
unlist(mapply(wrap_string, |
132 | 54x |
str = mat$strings, |
133 | 54x |
max_width = cell_widths_mat, |
134 | 54x |
hard = TRUE |
135 |
)), |
|
136 | 54x |
ncol = ncol(mat$strings) |
137 |
) |
|
138 | 54x |
mat$strings <- new_strings |
139 | ||
140 |
## XXXXX this is wrong and will break for listings cause we don't know when |
|
141 |
## we need has_topleft to be FALSE!!!!!!!!!! |
|
142 | 54x |
mat <- mform_handle_newlines(mat) |
143 | ||
144 |
## Indent groups after newline |
|
145 | 54x |
reindent_new_idx <- mf_lgrouping(mat) %in% reindent_old_idx |
146 | 54x |
if (anyNA(reindent_new_idx)) { |
147 |
stop("Unable to remap indenting after cell content text wrapping. ", # nocov |
|
148 |
"Please contact the maintainer, this should not happen.") # nocov |
|
149 |
} |
|
150 | ||
151 |
## Adding the indentation back in |
|
152 | 54x |
ind_v <- NULL |
153 | 54x |
for (i in mf_lgrouping(mat)[reindent_new_idx]) { |
154 | 4x |
ind_v <- c(ind_v, which(i == ori_mflg)[1]) |
155 |
} |
|
156 | 54x |
new_indent <- old_indent[ind_v] |
157 | ||
158 |
## Additional safety check |
|
159 | 54x |
if (length(new_indent) > 0 && !all(nzchar(new_indent))) { |
160 |
stop("Recovered indentation contains empty strings. This is an", # nocov |
|
161 |
" indexing problem, please contact the maintainer, this should not happen.") # nocov |
|
162 |
} |
|
163 | ||
164 |
## Indentation is different for topleft material |
|
165 | 54x |
if (isTRUE(mf_has_topleft(mat))) { |
166 |
## mf_nlheader counts actual header lines while mf_nrheader is 'virtual' |
|
167 |
## A bit of an hack, but unforeseen behavior, related to \n in topleft is not supported |
|
168 |
## Therefore, this still suppose that we dealt with \n in the cols before |
|
169 | 2x |
indx_topleft <- which(reindent_new_idx[1:nlh]) |
170 | 2x |
new_indent[seq_along(indx_topleft)] <- old_indent[indx_topleft] |
171 |
} |
|
172 | ||
173 |
## Main addition of the 'saved' indentation to strings |
|
174 | 54x |
mf_strings(mat)[reindent_new_idx, 1] <- paste0( |
175 | 54x |
new_indent, |
176 | 54x |
mat$strings[reindent_new_idx, 1] |
177 |
) |
|
178 |
## this updates extents in rinfo AND nlines in ref_fnotes_df |
|
179 | 54x |
mat <- update_mf_nlines(mat, max_width = max_width) |
180 | 54x |
mat |
181 |
} |
|
182 | ||
183 |
#' @rdname tostring |
|
184 |
#' |
|
185 |
#' @inheritParams MatrixPrintForm |
|
186 |
#' @param widths numeric (or NULL). (proposed) widths for the columns |
|
187 |
#' of \code{x}. The expected length of this numeric vector can be |
|
188 |
#' retrieved with `ncol() + 1` as the column of row names must |
|
189 |
#' also be considered. |
|
190 |
#' @param hsep character(1). Characters to repeat to create |
|
191 |
#' header/body separator line. |
|
192 |
#' @param tf_wrap logical(1). Should the texts for title, subtitle, |
|
193 |
#' and footnotes be wrapped? |
|
194 |
#' @param max_width integer(1), character(1) or NULL. Width that title |
|
195 |
#' and footer (including footnotes) materials should be |
|
196 |
#' word-wrapped to. If NULL, it is set to the current print width |
|
197 |
#' of the session (`getOption("width")`). If set to `"auto"`, |
|
198 |
#' the width of the table (plus any table inset) is used. Ignored |
|
199 |
#' completely if `tf_wrap` is `FALSE`. |
|
200 |
#' |
|
201 |
#' @details |
|
202 |
#' |
|
203 |
#' Manual insertion of newlines is not supported when `tf_wrap` is on |
|
204 |
#' and will result in a warning and undefined wrapping behavior. Passing |
|
205 |
#' vectors of already split strings remains supported, however in this |
|
206 |
#' case each string is word-wrapped separately with the behavior |
|
207 |
#' described above. |
|
208 |
#' |
|
209 |
#' @examples |
|
210 |
#' mform <- basic_matrix_form(mtcars) |
|
211 |
#' cat(toString(mform)) |
|
212 |
#' |
|
213 |
#' @return A character string containing the ASCII rendering |
|
214 |
#' of the table-like object represented by `x` |
|
215 |
#' |
|
216 |
#' @exportMethod toString |
|
217 |
setMethod("toString", "MatrixPrintForm", function(x, |
|
218 |
widths = NULL, |
|
219 |
tf_wrap = FALSE, |
|
220 |
max_width = NULL, |
|
221 |
col_gap = mf_colgap(x), |
|
222 |
hsep = default_hsep()) { |
|
223 | 32x |
assert_flag(tf_wrap) |
224 | ||
225 | 32x |
mat <- matrix_form(x, indent_rownames = TRUE) |
226 | 32x |
inset <- table_inset(mat) |
227 | ||
228 | 32x |
if (is.null(widths)) { |
229 | 30x |
widths <- mf_col_widths(x) %||% propose_column_widths(x) |
230 |
} else { |
|
231 | 2x |
mf_col_widths(x) <- widths |
232 |
} |
|
233 | 32x |
ncchar <- sum(widths) + (length(widths) - 1) * col_gap |
234 |
## Text wrapping checks |
|
235 | 32x |
if (tf_wrap) { |
236 | 14x |
if (is.null(max_width)) { |
237 | 9x |
max_width <- getOption("width", 80L) |
238 | 5x |
} else if (is.character(max_width) && identical(max_width, "auto")) { |
239 | 2x |
max_width <- ncchar + inset |
240 |
} |
|
241 | 14x |
assert_number(max_width, lower = 0) |
242 |
} |
|
243 | ||
244 | 32x |
mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap) |
245 | ||
246 | 31x |
body <- mat$strings |
247 | 31x |
aligns <- mat$aligns |
248 | 31x |
keep_mat <- mat$display |
249 |
## spans <- mat$spans |
|
250 |
## ri <- mat$row_info |
|
251 | 31x |
ref_fnotes <- mat$ref_footnotes |
252 | 31x |
nl_header <- mf_nlheader(mat) |
253 | ||
254 | 31x |
cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap) |
255 | ||
256 | 31x |
content <- matrix(mapply(padstr, body, cell_widths_mat, aligns), ncol = ncol(body)) |
257 | 31x |
content[!keep_mat] <- NA |
258 |
# apply(content, 1, function(x) sum(nchar(x), na.rm = TRUE)) |
|
259 | ||
260 | 31x |
gap_str <- strrep(" ", col_gap) |
261 | ||
262 | 31x |
div <- substr(strrep(hsep, ncchar), 1, ncchar) |
263 | 31x |
txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str) |
264 | 31x |
sec_seps_df <- x$row_info[, c("abs_rownumber", "trailing_sep"), drop = FALSE] |
265 | 31x |
if (!is.null(sec_seps_df) && any(!is.na(sec_seps_df$trailing_sep))) { |
266 | 1x |
bdy_cont <- tail(content, -nl_header) |
267 |
## unfortunately we count "header rows" wrt lihnegrouping so it |
|
268 |
## doesn't match the real (i.e. body) rows as is |
|
269 | 1x |
row_grouping <- tail(x$line_grouping, -nl_header) - mf_nrheader(x) |
270 | 1x |
nrbody <- NROW(bdy_cont) |
271 | 1x |
stopifnot(length(row_grouping) == nrbody) |
272 |
## all rows with non-NA section divs and the final row (regardless of NA status) |
|
273 |
## fixes #77 |
|
274 | 1x |
sec_seps_df <- sec_seps_df[unique(c( |
275 | 1x |
which(!is.na(sec_seps_df$trailing_sep)), |
276 | 1x |
NROW(sec_seps_df) |
277 |
)), ] |
|
278 | 1x |
txt_body <- character() |
279 | 1x |
sec_strt <- 1 |
280 | 1x |
section_rws <- sec_seps_df$abs_rownumber |
281 | 1x |
for (i in seq_len(NROW(section_rws))) { |
282 | 2x |
cur_rownum <- section_rws[i] |
283 | 2x |
sec_end <- max(which(row_grouping == cur_rownum)) |
284 | 2x |
txt_body <- c( |
285 | 2x |
txt_body, |
286 | 2x |
apply(bdy_cont[seq(sec_strt, sec_end), , drop = FALSE], |
287 | 2x |
1, |
288 | 2x |
.paste_no_na, |
289 | 2x |
collapse = gap_str |
290 |
), |
|
291 |
## don't print section dividers if they would be the last thing before the |
|
292 |
## footer divider |
|
293 |
## this also ensures an extraneous sec div won't be printed if we have non-sec-div |
|
294 |
## rows after the last sec div row (#77) |
|
295 | 2x |
if (sec_end < nrbody) { |
296 | 1x |
substr( |
297 | 1x |
strrep(sec_seps_df$trailing_sep[i], ncchar), 1, |
298 | 1x |
ncchar - inset |
299 |
) |
|
300 |
} |
|
301 |
) |
|
302 | 2x |
sec_strt <- sec_end + 1 |
303 |
} |
|
304 |
} else { |
|
305 | 30x |
txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str) |
306 |
} |
|
307 | ||
308 | ||
309 | 31x |
allts <- all_titles(x) |
310 | ||
311 | 31x |
allfoots <- list( |
312 | 31x |
"main_footer" = main_footer(x), |
313 | 31x |
"prov_footer" = prov_footer(x), |
314 | 31x |
"ref_footnotes" = ref_fnotes |
315 |
) |
|
316 | 31x |
allfoots <- allfoots[!sapply(allfoots, is.null)] |
317 | ||
318 | ||
319 |
## Wrapping titles if they go beyond the horizontally allowed space |
|
320 | 31x |
if (tf_wrap) { |
321 | 14x |
new_line_warning(allts) |
322 | 14x |
allts <- wrap_txt(allts, max_width = max_width) |
323 |
} |
|
324 | ||
325 | 31x |
titles_txt <- if (any(nzchar(allts))) c(allts, "", .do_inset(div, inset)) else NULL |
326 | ||
327 |
# Wrapping footers if they go beyond the horizontally allowed space |
|
328 | 31x |
if (tf_wrap) { |
329 | 14x |
new_line_warning(allfoots) |
330 | 14x |
allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset) |
331 | 14x |
allfoots$ref_footnotes <- wrap_txt(allfoots$ref_footnotes, max_width - inset) |
332 |
## no - inset here because the prov_footer is not inset |
|
333 | 14x |
allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width) |
334 |
} |
|
335 | ||
336 | 31x |
paste0(paste( |
337 | 31x |
c( |
338 | 31x |
titles_txt, |
339 | 31x |
.do_inset(txt_head, inset), |
340 | 31x |
.do_inset(div, inset), |
341 | 31x |
.do_inset(txt_body, inset), |
342 | 31x |
.footer_inset_helper(allfoots, div, inset) |
343 |
), |
|
344 | 31x |
collapse = "\n" |
345 | 31x |
), "\n") |
346 |
}) |
|
347 | ||
348 |
.do_inset <- function(x, inset) { |
|
349 | 214x |
if (inset == 0 || !any(nzchar(x))) { |
350 | 195x |
return(x) |
351 |
} |
|
352 | 19x |
padding <- strrep(" ", inset) |
353 | 19x |
if (is.character(x)) { |
354 | 19x |
x <- paste0(padding, x) |
355 | ! |
} else if (is(x, "matrix")) { |
356 | ! |
x[, 1] <- .do_inset(x[, 1, drop = TRUE], inset) |
357 |
} |
|
358 | 19x |
x |
359 |
} |
|
360 | ||
361 | ||
362 |
.inset_div <- function(txt, div, inset) { |
|
363 | 40x |
c(.do_inset(div, inset), "", txt) |
364 |
} |
|
365 | ||
366 |
.footer_inset_helper <- function(footers_v, div, inset) { |
|
367 | 31x |
div_done <- FALSE # nolint |
368 | 31x |
fter <- footers_v$main_footer |
369 | 31x |
prvf <- footers_v$prov_footer |
370 | 31x |
rfn <- footers_v$ref_footnotes |
371 | 31x |
footer_txt <- .do_inset(rfn, inset) |
372 | 31x |
if (any(nzchar(footer_txt))) { |
373 | 14x |
footer_txt <- .inset_div(footer_txt, div, inset) |
374 |
} |
|
375 | 31x |
if (any(vapply( |
376 | 31x |
footers_v, function(x) any(nzchar(x)), |
377 | 31x |
TRUE |
378 |
))) { |
|
379 | 26x |
if (any(nzchar(prvf))) { |
380 | 24x |
provtxt <- c( |
381 | 24x |
if (any(nzchar(fter))) "", |
382 | 24x |
prvf |
383 |
) |
|
384 |
} else { |
|
385 | 2x |
provtxt <- character() |
386 |
} |
|
387 | 26x |
footer_txt <- c( |
388 | 26x |
footer_txt, |
389 | 26x |
.inset_div( |
390 | 26x |
c( |
391 | 26x |
.do_inset(fter, inset), |
392 | 26x |
provtxt |
393 |
), |
|
394 | 26x |
div, |
395 | 26x |
inset |
396 |
) |
|
397 |
) |
|
398 |
} |
|
399 | 31x |
footer_txt |
400 |
} |
|
401 | ||
402 |
new_line_warning <- function(str_v) { |
|
403 | 28x |
if (any(unlist(sapply(str_v, grepl, pattern = "\n")))) { |
404 | 2x |
msg <- c( |
405 | 2x |
"Detected manual newlines when automatic title/footer word-wrapping is on.", |
406 | 2x |
"This is unsupported and will result in undefined behavior. Please either ", |
407 | 2x |
"utilize automatic word-wrapping with newline characters inserted, or ", |
408 | 2x |
"turn off automatic wrapping and wordwrap all contents manually by inserting ", |
409 | 2x |
"newlines." |
410 |
) |
|
411 | 2x |
warning(paste0(msg, collapse = "")) |
412 |
} |
|
413 |
} |
|
414 | ||
415 |
#' Wrap a string to within a maximum width |
|
416 |
#' @param str character(1). String to be wrapped |
|
417 |
#' @param max_width numeric(1). Maximum width, in characters, that the |
|
418 |
#' text should be wrapped at. |
|
419 |
#' @param hard logical(1). Should hard wrapping (embedding newlines in |
|
420 |
#' the incoming strings) or soft (breaking wrapped strings into vectors |
|
421 |
#' of length >1) be used. Defaults to `FALSE` (i.e. soft wrapping). |
|
422 |
#' |
|
423 |
#' @details Word wrapping happens as with \link[base:strwrap]{base::strwrap} |
|
424 |
#' with the following exception: individual words which are longer |
|
425 |
#' than `max_width` are broken up in a way that fits with the rest of the |
|
426 |
#' word wrapping. |
|
427 |
#' |
|
428 |
#' @return A string (`wrap_string` or character vector (`wrap_txt`) containing |
|
429 |
#' the hard or soft word-wrapped content. |
|
430 |
#' |
|
431 |
#' @export |
|
432 |
wrap_string <- function(str, max_width, hard = FALSE) { |
|
433 | 14755x |
stopifnot(is.character(str) && length(str) == 1) |
434 | 14755x |
naive <- strwrap(str, max_width + 1) |
435 | 14755x |
while (any(nchar(naive) > max_width)) { |
436 | 14x |
good <- character() |
437 | 14x |
bwi <- which(nchar(naive) > max_width)[1] |
438 | 14x |
curbw <- naive[bwi] |
439 | 14x |
if (bwi > 2) { |
440 | ! |
good <- c(good, naive[1:(bwi - 2)]) |
441 |
} |
|
442 | 14x |
if (bwi > 1) { |
443 | 4x |
str_before <- naive[bwi - 1] |
444 |
} else { |
|
445 | 10x |
str_before <- "" |
446 |
} |
|
447 | 14x |
room <- max_width - nchar(str_before) - (bwi > 1) |
448 | 14x |
if (room <= 0) { |
449 | 4x |
toadd <- c(str_before, substr(curbw, 1, max_width)) |
450 | 4x |
room <- 0 |
451 | 4x |
leftover <- substr(curbw, max_width + 1, nchar(curbw)) |
452 |
} else { |
|
453 | 10x |
goodpart <- substr(curbw, 1, room) |
454 | 10x |
if (nzchar(str_before)) { |
455 | ! |
toadd <- paste(str_before, goodpart) |
456 |
} else { |
|
457 | 10x |
toadd <- goodpart |
458 |
} |
|
459 | 10x |
leftover <- substr(curbw, room + 1, nchar(curbw)) |
460 |
} |
|
461 | 14x |
good <- c(good, toadd) |
462 | 14x |
if (bwi == length(naive)) { |
463 | 13x |
good <- c(good, leftover) |
464 |
} else { |
|
465 | 1x |
good <- c( |
466 | 1x |
good, |
467 | 1x |
paste(leftover, naive[bwi + 1]), |
468 | 1x |
if (bwi < length(naive) - 1) naive[seq(bwi + 2, length(naive))] |
469 |
) |
|
470 |
} |
|
471 | 14x |
str <- paste(good, collapse = " ") |
472 | 14x |
naive <- strwrap(str, max_width + 1) |
473 |
} |
|
474 | 14755x |
if (hard) { |
475 | 14626x |
naive <- paste(naive, collapse = "\n") |
476 |
} |
|
477 | 14755x |
naive |
478 |
} |
|
479 | ||
480 |
#' @param txt character. A vector of strings that should be (independently) |
|
481 |
#' text-wrapped. |
|
482 |
#' @rdname wrap_string |
|
483 |
#' @export |
|
484 |
wrap_txt <- function(txt, max_width, hard = FALSE) { |
|
485 | 105x |
unlist(lapply(txt, wrap_string, max_width = max_width, hard = hard), use.names = FALSE) |
486 |
} |
|
487 | ||
488 |
pad_vert_top <- function(x, len) { |
|
489 | 2376x |
c(x, rep("", len - length(x))) |
490 |
} |
|
491 | ||
492 |
pad_vert_bottom <- function(x, len) { |
|
493 | 78x |
c(rep("", len - length(x)), x) |
494 |
} |
|
495 | ||
496 |
pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) { |
|
497 | 204x |
dat <- unlist(lapply(vec[-1], cpadder, len = len)) |
498 | 204x |
dat <- c(rlpadder(vec[[1]], len = len), dat) |
499 | 204x |
matrix(dat, nrow = len) |
500 |
} |
|
501 | ||
502 |
rep_vec_to_len <- function(vec, len, ...) { |
|
503 | 138x |
matrix(unlist(lapply(vec, rep, times = len)), |
504 | 138x |
nrow = len |
505 |
) |
|
506 |
} |
|
507 | ||
508 | ||
509 |
safe_strsplit <- function(x, split, ...) { |
|
510 | 273x |
ret <- strsplit(x, split, ...) |
511 | 273x |
lapply(ret, function(reti) if (length(reti) == 0) "" else reti) |
512 |
} |
|
513 | ||
514 |
.expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) { |
|
515 | 342x |
leni <- row_nlines[i] |
516 | 342x |
rw <- mat[i, ] |
517 | 342x |
if (is.character(rw)) { |
518 | 273x |
rw <- safe_strsplit(rw, "\n", fixed = TRUE) |
519 |
} |
|
520 | 342x |
expfun(rw, len = leni, ...) |
521 |
} |
|
522 | ||
523 |
expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) { |
|
524 | 22x |
rinds <- seq_len(nrow(mat)) |
525 | 22x |
exprows <- lapply(rinds, .expand_mat_rows_inner, |
526 | 22x |
mat = mat, |
527 | 22x |
row_nlines = row_nlines, |
528 | 22x |
expfun = expfun, |
529 |
... |
|
530 |
) |
|
531 | 22x |
do.call(rbind, exprows) |
532 |
} |
|
533 | ||
534 | ||
535 |
#' Transform vectors of spans (with duplication) to Visibility vector |
|
536 |
#' |
|
537 |
#' @param spans numeric. A vector of spans, with each span value repeated |
|
538 |
#' for the cells it covers. |
|
539 |
#' |
|
540 |
#' @details |
|
541 |
#' |
|
542 |
#' The values of \code{spans} are assumed to be repeated to such that |
|
543 |
#' each individual position covered by the span has the repeated value. |
|
544 |
#' |
|
545 |
#' This means that each block of values in \code{span} must be of a length |
|
546 |
#' at least equal to its value (i.e. two 2s, three 3s, etc). |
|
547 |
#' |
|
548 |
#' This function correctly handles cases where two spans of the same size |
|
549 |
#' are next to each other; i.e., a block of four 2s represents two large |
|
550 |
#' cells each of which span two individual cells. |
|
551 |
#' @export |
|
552 |
#' @note |
|
553 |
#' |
|
554 |
#' Currently no checking or enforcement is done that the vector of |
|
555 |
#' spans is valid in the sense described in the Details section above. |
|
556 |
#' @examples |
|
557 |
#' |
|
558 |
#' spans_to_viscell(c(2, 2, 2, 2, 1, 3, 3, 3)) |
|
559 |
#' @return a logical vector the same length as `spans` indicating |
|
560 |
#' whether the contents of a string vector with those spans |
|
561 |
spans_to_viscell <- function(spans) { |
|
562 | 2x |
if (!is.vector(spans)) { |
563 | ! |
spans <- as.vector(spans) |
564 |
} |
|
565 | 2x |
myrle <- rle(spans) |
566 | 2x |
unlist( |
567 | 2x |
mapply( |
568 | 2x |
function(vl, ln) { |
569 | 4x |
rep(c(TRUE, rep(FALSE, vl - 1L)), times = ln / vl) |
570 |
}, |
|
571 | 2x |
SIMPLIFY = FALSE, |
572 | 2x |
vl = myrle$values, |
573 | 2x |
ln = myrle$lengths |
574 |
), |
|
575 | 2x |
recursive = FALSE |
576 |
) |
|
577 |
} |
|
578 | ||
579 | ||
580 |
#' Propose Column Widths based on an object's `MatrixPrintForm` form |
|
581 |
#' |
|
582 |
#' The row names are also considered a column for the output |
|
583 |
#' |
|
584 |
#' @param x `MatrixPrintForm` object, or an object with a `matrix_form` |
|
585 |
#' method. |
|
586 |
#' @param indent_size numeric(1). Indent size in characters. Ignored |
|
587 |
#' when `x` is already a `MatrixPrintForm` object in favor of information |
|
588 |
#' there. |
|
589 |
#' |
|
590 |
#' @examples |
|
591 |
#' mf <- basic_matrix_form(mtcars) |
|
592 |
#' propose_column_widths(mf) |
|
593 |
#' |
|
594 |
#' @export |
|
595 |
#' @return a vector of column widths based on the content of \code{x} |
|
596 |
#' for use in printing and pagination. |
|
597 |
## ' @examples |
|
598 |
## ' library(dplyr) |
|
599 |
## ' library(rtables) |
|
600 |
## ' iris2 <- iris %>% |
|
601 |
## ' group_by(Species) %>% |
|
602 |
## ' mutate(group = as.factor(rep_len(c("a", "b"), length.out = n()))) %>% |
|
603 |
## ' ungroup() |
|
604 |
## ' |
|
605 |
## ' l <- basic_table() %>% |
|
606 |
## ' split_cols_by("Species") %>% |
|
607 |
## ' split_cols_by("group") %>% |
|
608 |
## ' analyze(c("Sepal.Length", "Petal.Width"), afun = list_wrap_x(summary) , format = "xx.xx") |
|
609 |
## ' |
|
610 |
## ' tbl <- build_table(l, iris2) |
|
611 |
## ' mf <- matrix_form(tbl) |
|
612 |
## ' propose_column_widths(mf) |
|
613 |
propose_column_widths <- function(x, indent_size = 2) { |
|
614 |
## stopifnot(is(x, "VTableTree")) |
|
615 | 24x |
if (!is(x, "MatrixPrintForm")) { |
616 | ! |
x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size) |
617 |
} |
|
618 | 24x |
body <- x$strings |
619 | 24x |
spans <- x$spans |
620 |
# aligns <- x$aligns |
|
621 | 24x |
display <- x$display |
622 | ||
623 | 24x |
chars <- nchar(body) |
624 | ||
625 |
# first check column widths without colspan |
|
626 | 24x |
has_spans <- spans != 1 |
627 | 24x |
chars_ns <- chars |
628 | 24x |
chars_ns[has_spans] <- 0 |
629 | 24x |
widths <- apply(chars_ns, 2, max) |
630 | ||
631 |
# now check if the colspans require extra width |
|
632 | 24x |
if (any(has_spans)) { |
633 | 1x |
has_row_spans <- apply(has_spans, 1, any) |
634 | ||
635 | 1x |
chars_sp <- chars[has_row_spans, , drop = FALSE] |
636 | 1x |
spans_sp <- spans[has_row_spans, , drop = FALSE] |
637 | 1x |
disp_sp <- display[has_row_spans, , drop = FALSE] |
638 | ||
639 | 1x |
nc <- ncol(spans) |
640 | 1x |
for (i in seq_len(nrow(chars_sp))) { |
641 | 1x |
for (j in seq_len(nc)) { |
642 | 2x |
if (disp_sp[i, j] && spans_sp[i, j] != 1) { |
643 | 1x |
i_cols <- seq(j, j + spans_sp[i, j] - 1) |
644 | ||
645 | 1x |
nchar_i <- chars_sp[i, j] |
646 | 1x |
cw_i <- widths[i_cols] |
647 | 1x |
available_width <- sum(cw_i) |
648 | ||
649 | 1x |
if (nchar_i > available_width) { |
650 |
# need to update widths to fit content with colspans |
|
651 |
# spread width among columns |
|
652 | ! |
widths[i_cols] <- cw_i + spread_integer(nchar_i - available_width, length(cw_i)) |
653 |
} |
|
654 |
} |
|
655 |
} |
|
656 |
} |
|
657 |
} |
|
658 | 24x |
widths |
659 |
} |
|
660 | ||
661 | ||
662 | ||
663 | ||
664 |
#' Pad a string and align within string |
|
665 |
#' |
|
666 |
#' @param x string |
|
667 |
#' @param n number of character of the output string, if `n < |
|
668 |
#' nchar(x)` an error is thrown |
|
669 |
#' @param just character(1). Text alignment justification to |
|
670 |
#' use. Defaults to center. Must be center, right or left. |
|
671 |
#' |
|
672 |
#' @export |
|
673 |
#' @examples |
|
674 |
#' |
|
675 |
#' padstr("abc", 3) |
|
676 |
#' padstr("abc", 4) |
|
677 |
#' padstr("abc", 5) |
|
678 |
#' padstr("abc", 5, "left") |
|
679 |
#' padstr("abc", 5, "right") |
|
680 |
#' |
|
681 |
#' if (interactive()) { |
|
682 |
#' padstr("abc", 1) |
|
683 |
#' } |
|
684 |
#' @return `x`, padded to be a string of `n` characters |
|
685 |
#' |
|
686 |
padstr <- function(x, n, just = c("center", "left", "right")) { |
|
687 | 4087x |
just <- match.arg(just) |
688 | ||
689 | 1x |
if (length(x) != 1) stop("length of x needs to be 1 and not", length(x)) |
690 | 1x |
if (is.na(n) || !is.numeric(n) || n < 0) stop("n needs to be numeric and > 0") |
691 | ||
692 | 1x |
if (is.na(x)) x <- "<NA>" |
693 | ||
694 | 4085x |
nc <- nchar(x) |
695 | ||
696 | ! |
if (n < nc) stop("\"", x, "\" has more than ", n, " characters") |
697 | ||
698 | 4085x |
switch(just, |
699 |
center = { |
|
700 | 172x |
pad <- (n - nc) / 2 |
701 | 172x |
paste0(spaces(floor(pad)), x, spaces(ceiling(pad))) |
702 |
}, |
|
703 | 3912x |
left = paste0(x, spaces(n - nc)), |
704 | 1x |
right = paste0(spaces(n - nc), x) |
705 |
) |
|
706 |
} |
|
707 | ||
708 |
spaces <- function(n) { |
|
709 | 4257x |
strrep(" ", n) |
710 |
} |
|
711 | ||
712 | ||
713 |
.paste_no_na <- function(x, ...) { |
|
714 | 676x |
paste(na.omit(x), ...) |
715 |
} |
|
716 | ||
717 | ||
718 |
#' spread `x` into `len` elements |
|
719 |
#' |
|
720 |
#' @param x numeric(1). The number to spread |
|
721 |
#' @param len numeric(1). The number of times to repeat \code{x} |
|
722 |
#' |
|
723 |
#' @export |
|
724 |
#' @return if \code{x} is a scalar "whole number" value (see \code{\link{is.wholenumber}}), |
|
725 |
#' the value \code{x} repeated \code{len} times. If not, an error is thrown. |
|
726 |
#' @examples |
|
727 |
#' spread_integer(3, 1) |
|
728 |
#' spread_integer(0, 3) |
|
729 |
#' spread_integer(1, 3) |
|
730 |
#' spread_integer(2, 3) |
|
731 |
#' spread_integer(3, 3) |
|
732 |
#' spread_integer(4, 3) |
|
733 |
#' spread_integer(5, 3) |
|
734 |
#' spread_integer(6, 3) |
|
735 |
#' spread_integer(7, 3) |
|
736 |
spread_integer <- function(x, len) { |
|
737 | 2x |
stopifnot( |
738 | 2x |
is.wholenumber(x), length(x) == 1, x >= 0, |
739 | 2x |
is.wholenumber(len), length(len) == 1, len >= 0, |
740 | 2x |
!(len == 0 && x > 0) |
741 |
) |
|
742 | ||
743 | ||
744 | 1x |
if (len == 0) { |
745 | ! |
integer(0) |
746 |
} else { |
|
747 | 1x |
y <- rep(floor(x / len), len) |
748 | 1x |
i <- 1 |
749 | 1x |
while (sum(y) < x) { |
750 | 1x |
y[i] <- y[i] + 1 |
751 | 1x |
if (i == len) { |
752 | ! |
i <- 1 |
753 |
} else { |
|
754 | 1x |
i <- i + 1 |
755 |
} |
|
756 |
} |
|
757 | 1x |
y |
758 |
} |
|
759 |
} |
|
760 | ||
761 | ||
762 | ||
763 |
#' `is.wholenumber` |
|
764 |
#' |
|
765 |
#' @param x numeric(1). A numeric value |
|
766 |
#' @param tol numeric(1). A precision tolerance. |
|
767 |
#' |
|
768 |
#' @return \code{TRUE} if \code{x} is within \code{tol} of zero, |
|
769 |
#' \code{FALSE} otherwise. |
|
770 |
#' |
|
771 |
#' @export |
|
772 |
#' @examples |
|
773 |
#' is.wholenumber(5) |
|
774 |
#' is.wholenumber(5.00000000000000001) |
|
775 |
#' is.wholenumber(.5) |
|
776 |
#' |
|
777 |
is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) { |
|
778 | 3x |
abs(x - round(x)) < tol |
779 |
} |
1 |
#' @import grid |
|
2 |
#' @import grDevices |
|
3 |
NULL |
|
4 |
## https://www.ietf.org/rfc/rfc0678.txt |
|
5 | ||
6 |
## This assumes fixed font size, monospaced font |
|
7 | ||
8 |
std_cpi <- 10L |
|
9 |
std_lpi <- 6L |
|
10 | ||
11 | ||
12 |
std_full_pg_wd_in <- 8.5 |
|
13 | ||
14 |
std_full_pg_ht_in <- 11 |
|
15 | ||
16 |
std_log_pg_wd_chars <- 72 |
|
17 | ||
18 |
std_log_pg_ht_lines <- 60 |
|
19 | ||
20 |
std_marg_ht <- round((std_full_pg_ht_in - std_log_pg_ht_lines / std_lpi) / 2, 2) |
|
21 |
std_marg_wd <- round((std_full_pg_wd_in - std_log_pg_wd_chars / std_cpi) / 2, 2) |
|
22 | ||
23 |
std_margins <- list( |
|
24 |
top = std_marg_ht, |
|
25 |
bottom = std_marg_ht, |
|
26 |
left = std_marg_wd, |
|
27 |
right = std_marg_wd |
|
28 |
) |
|
29 | ||
30 |
## does not appear to be used anywhere |
|
31 |
## to_inches_num <- function(x) { |
|
32 |
## if (is(x, "unit")) { |
|
33 |
## x <- unclass(convertUnit(x, "inches")) |
|
34 |
## } |
|
35 |
## x |
|
36 |
## } |
|
37 | ||
38 |
## Physical size, does not take margins into account |
|
39 |
pg_dim_names <- list( |
|
40 |
letter = c(8.5, 11), |
|
41 |
a4 = c(8.27, 11.69), |
|
42 |
legal = c(8.5, 14) |
|
43 |
) |
|
44 | ||
45 | ||
46 |
#' |
|
47 |
#' Supported Named Page `TypesList` supported named page types |
|
48 |
#' |
|
49 |
#' @return for `page_types` a character vector of supported page types, |
|
50 |
#' for `page_dim` the dimensions (width, then height) of the selected page type. |
|
51 |
#' |
|
52 |
#' @export |
|
53 |
#' @examples |
|
54 |
#' page_types() |
|
55 |
#' page_dim("a4") |
|
56 |
page_types <- function() { |
|
57 | 33x |
names(pg_dim_names) |
58 |
} |
|
59 | ||
60 |
#' @export |
|
61 |
#' @param page_type character(1). The name of a page size specification. Call |
|
62 |
#' `page_types` for supported values. |
|
63 |
#' @rdname page_types |
|
64 |
page_dim <- function(page_type) { |
|
65 | 9x |
if (is.null(page_type)) { |
66 | 4x |
return(NULL) |
67 |
} |
|
68 | 5x |
if (!page_type %in% page_types()) { |
69 | 1x |
stop("Unrecognized page-size specification: ", page_type) |
70 |
} |
|
71 | 4x |
pg_dim_names[[page_type]] |
72 |
} |
|
73 | ||
74 | ||
75 | ||
76 |
#' Calculate lines per inch and characters per inch for font |
|
77 |
#' |
|
78 |
#' @inheritParams page_lcpp |
|
79 |
#' |
|
80 |
#' @details This function creates opens pdf graphics device writing to an temporary file, |
|
81 |
#' then utilizes [grid::convertWidth()] and [grid::convertHeight()] to calculate |
|
82 |
#' lines per inch and characters per inch for the specified font family, size, and |
|
83 |
#' line height. |
|
84 |
#' |
|
85 |
#' An error is thrown if the font is not monospaced (determined by comparing |
|
86 |
#' the effective widths of the `M` and `.` glyphs). |
|
87 |
#' @return named list with `cpi` and `lpi`, the characters and lines per |
|
88 |
#' inch, respectively. |
|
89 |
#' |
|
90 |
#' @export |
|
91 |
#' @examples |
|
92 |
#' font_lcpi() |
|
93 |
#' |
|
94 |
#' font_lcpi(font_size = 8) |
|
95 |
#' |
|
96 |
#' font_lcpi(font_size = 8, lineheight = 1.1) |
|
97 |
font_lcpi <- function(font_family = "Courier", font_size = 8, lineheight = 1) { |
|
98 | 24x |
tmppdf <- tempfile(fileext = ".pdf") |
99 | 24x |
pdf(tmppdf) |
100 | 24x |
on.exit(dev.off()) |
101 | 24x |
grid.newpage() |
102 | 24x |
gp <- gpar(fontfamily = font_family, fontsize = font_size, lineheight = lineheight) |
103 | 24x |
pushViewport(plotViewport(gp = gp)) |
104 | 24x |
if (convertWidth(unit(1, "strwidth", "."), "inches", valueOnly = TRUE) != |
105 | 24x |
convertWidth(unit(1, "strwidth", "M"), "inches", valueOnly = TRUE)) { |
106 | 1x |
stop( |
107 | 1x |
"The font family you selected - ", |
108 | 1x |
font_family, |
109 | 1x |
" - does not appear to be monospaced. This is not supported." |
110 |
) |
|
111 |
} |
|
112 | 23x |
list( |
113 | 23x |
cpi = 1 / convertWidth(unit(1, "strwidth", "h"), "inches", valueOnly = TRUE), |
114 | 23x |
lpi = convertHeight(unit(1, "inches"), "lines", valueOnly = TRUE) |
115 |
) |
|
116 |
} |
|
117 | ||
118 |
marg_order <- c("bottom", "left", "top", "right") |
|
119 | ||
120 |
#' Determine lines per page (`LPP`) and characters per page (`CPP`) based on font and page type |
|
121 |
#' |
|
122 |
#' @param page_type character(1). Name of a page type. See |
|
123 |
#' `page_types`. Ignored when `pg_width` and `pg_height` |
|
124 |
#' are set directly. |
|
125 |
#' @param landscape logical(1). Should the dimensions of `page_type` |
|
126 |
#' be inverted for landscape? Defaults to `FALSE`, ignored when |
|
127 |
#' `pg_width` and `pg_height` are set directly. |
|
128 |
#' @param font_family character(1). Name of a font family. An error |
|
129 |
#' will be thrown if the family named is not monospaced. Defaults |
|
130 |
#' to Courier. |
|
131 |
#' @param font_size numeric(1). Font size, defaults to 12. |
|
132 |
#' @param lineheight numeric(1). Line height, defaults to 1. |
|
133 |
#' @param margins numeric(4). Named numeric vector containing `'bottom'`, |
|
134 |
#' `'left'`, `'top'`, and `'right'` margins in inches. Defaults |
|
135 |
#' to `.5` inches for both vertical margins and `.75` for both |
|
136 |
#' horizontal margins. |
|
137 |
#' @param pg_width numeric(1). Page width in inches. |
|
138 |
#' @param pg_height numeric(1). Page height in inches. |
|
139 |
#' |
|
140 |
#' @return a named list containing `LPP` (lines per page) and `CPP` (characters per page) |
|
141 |
#' elements suitable for use by the pagination machinery. |
|
142 |
#' |
|
143 |
#' @export |
|
144 |
#' @examples |
|
145 |
#' page_lcpp() |
|
146 |
#' page_lcpp(font_size = 10) |
|
147 |
#' page_lcpp("a4", font_size = 10) |
|
148 |
#' |
|
149 |
#' page_lcpp(margins = c(top = 1, bottom = 1, left = 1, right = 1)) |
|
150 |
#' page_lcpp(pg_width = 10, pg_height = 15) |
|
151 |
page_lcpp <- function(page_type = page_types(), |
|
152 |
landscape = FALSE, |
|
153 |
font_family = "Courier", |
|
154 |
font_size = 8, |
|
155 |
lineheight = 1, |
|
156 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
157 |
pg_width = NULL, |
|
158 |
pg_height = NULL) { |
|
159 | 24x |
if(is.null(page_type)) |
160 | 3x |
page_type <- page_types()[1] |
161 |
else |
|
162 | 21x |
page_type <- match.arg(page_type) |
163 | ||
164 | 24x |
if(is.null(names(margins))) |
165 | 6x |
names(margins) <- marg_order |
166 |
else |
|
167 | 18x |
margins <- margins[marg_order] |
168 | 24x |
if(any(is.na(margins))) |
169 | ! |
stop("margins argument must have names 'bottom', 'left', 'top' and 'right'.") |
170 | 24x |
lcpi <- font_lcpi( |
171 | 24x |
font_family = font_family, |
172 | 24x |
font_size = font_size, |
173 | 24x |
lineheight = lineheight |
174 |
) |
|
175 | ||
176 | 23x |
wdpos <- ifelse(landscape, 2, 1) |
177 | 23x |
pg_width <- pg_width %||% pg_dim_names[[page_type]][wdpos] |
178 | 23x |
pg_height <- pg_height %||% pg_dim_names[[page_type]][-wdpos] |
179 | ||
180 | 23x |
pg_width <- pg_width - sum(margins[c("left", "right")]) |
181 | 23x |
pg_height <- pg_height - sum(margins[c("top", "bottom")]) |
182 | ||
183 | 23x |
list( |
184 | 23x |
cpp = floor(lcpi[["cpi"]] * pg_width), |
185 | 23x |
lpp = floor(lcpi[["lpi"]] * pg_height) |
186 |
) |
|
187 |
} |
|
188 | ||
189 |
## pg_types <- list( |
|
190 |
## "fsrp" = c(cpp = 110, lpp = 66), |
|
191 |
## "fsrp8" = c(cpp = 110, lpp = 66), |
|
192 |
## "fsrp7" = c(cpp = 110, lpp = 75), |
|
193 |
## "fsrl" = c(cpp = 149, lpp = 51), |
|
194 |
## "fsrl8" = c(cpp = 149, lpp = 51), |
|
195 |
## "fsrl7" = c(cpp = 150, lpp = 59), |
|
196 |
## "erp" = c(cpp = 96, lpp = 66), |
|
197 |
## "erp8" = c(cpp = 96, lpp = 66), |
|
198 |
## "erl" = c(cpp = 149, lpp = 45), |
|
199 |
## "erl8" = c(cpp = 149, lpp = 45), |
|
200 |
## "sasp" = c(cpp = 93, lpp = 73), |
|
201 |
## "sasp8" = c(cpp = 93, lpp = 73), |
|
202 |
## "sasl" = c(cpp = 134, lpp = 52), |
|
203 |
## "sasl8" = c(cpp = 134, lpp = 52), |
|
204 |
## "sasp7" = c(cpp = 107, lpp = 92), |
|
205 |
## "sasl7" = c(cpp = 154, lpp = 64), |
|
206 |
## "sasp6" = c(cpp = 125, lpp = 108), |
|
207 |
## "sasl6" = c(cpp = 180, lpp = 75), |
|
208 |
## "sasp10" = c(cpp = 78, lpp = 64), |
|
209 |
## "sasl10" = c(cpp = 108, lpp = 45), |
|
210 |
## "sasp9" = c(cpp = 87, lpp = 71), |
|
211 |
## "sasl9" = c(cpp = 120, lpp = 51), |
|
212 |
## "rapidp10" = c(cpp = 78, lpp = 64), |
|
213 |
## "rapidl10" = c(cpp = 108, lpp = 45), |
|
214 |
## "rapidp9" = c(cpp = 87, lpp = 71), |
|
215 |
## "rapidl9" = c(cpp = 120, lpp = 51), |
|
216 |
## "rapidp" = c(cpp = 93, lpp = 73), |
|
217 |
## "rapidp8" = c(cpp = 93, lpp = 73), |
|
218 |
## "rapidl" = c(cpp = 134, lpp = 52), |
|
219 |
## "rapidl8" = c(cpp = 134, lpp = 52), |
|
220 |
## "rapidp7" = c(cpp = 107, lpp = 92), |
|
221 |
## "rapidl7" = c(cpp = 154, lpp = 64), |
|
222 |
## "rapidp6" = c(cpp = 125, lpp = 108), |
|
223 |
## "rapidl6" = c(cpp = 180, lpp = 75), |
|
224 |
## "shibal" = c(cpp = 170, lpp = 48), |
|
225 |
## "shibal10" = c(cpp = 137, lpp = 39), |
|
226 |
## "shibal8" = c(cpp = 170, lpp = 48), |
|
227 |
## "shibal7" = c(cpp = 194, lpp = 56), |
|
228 |
## "shibal6" = c(cpp = 225, lpp = 65), |
|
229 |
## "shibap" = c(cpp = 112, lpp = 78), |
|
230 |
## "shibap10" = c(cpp = 89, lpp = 64), |
|
231 |
## "shibap8" = c(cpp = 112, lpp = 78), |
|
232 |
## "shibap7" = c(cpp = 127, lpp = 92), |
|
233 |
## "shibap6" = c(cpp = 148, lpp = 108)) |
|
234 | ||
235 | ||
236 | ||
237 | ||
238 | ||
239 | ||
240 |
## courier_fontsize_lcpi_df <- tribble( |
|
241 |
## ~courier_size, ~cpi, ~lpi, |
|
242 |
## 6, floor(129 / pg_dim_names[["letter"]][1]), floor(85 / pg_dim_names[["letter"]][2]), |
|
243 |
## 7, floor(110 / pg_dim_names[["letter"]][1]), floor(76 / pg_dim_names[["letter"]][2]), |
|
244 |
## 8, floor(95 / pg_dim_names[["letter"]][1]), floor(68 / pg_dim_names[["letter"]][2]), |
|
245 |
## 9, floor(84 / pg_dim_names[["letter"]][1]), floor(61 / pg_dim_names[["letter"]][2]), |
|
246 |
## 10, floor(75 / pg_dim_names[["letter"]][1]), floor(56 / pg_dim_names[["letter"]][2]) |
|
247 |
## ) |
|
248 | ||
249 |
## courier_lcpi <- function(size) { |
|
250 |
## grid.newpage() |
|
251 |
## gp <- gpar(fontfamily="Courier New", fontsize = size, lineheight = 1) |
|
252 |
## pushViewport(plotViewport( gp = gp)) |
|
253 |
## list(cpi = round(1/convertWidth(unit(1, "strwidth", "h"), "inches", valueOnly = TRUE), 0), |
|
254 |
## lpi = round(convertHeight(unit(1, "inches"), "lines", valueOnly = TRUE), 0)) |
|
255 |
## } |
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 | ||
7 |
#' @import methods |
|
8 |
#' @include matrix_form.R |
|
9 | ||
10 |
#' @title Make row and column layout summary data.frames for use during pagination |
|
11 |
#' @name make_row_df |
|
12 |
#' |
|
13 |
#' |
|
14 |
#' @param tt ANY. Object representing the table-like object to be summarized. |
|
15 |
#' @param visible_only logical(1). Should only visible aspects of the table structure be reflected in this summary. |
|
16 |
#' Defaults to \code{TRUE}. May not be supported by all methods. |
|
17 |
#' @param incontent logical(1). Internal detail do not set manually. |
|
18 |
#' @param repr_ext integer(1). Internal detail do not set manually. |
|
19 |
#' @param repr_inds integer. Internal detail do not set manually. |
|
20 |
#' @param sibpos integer(1). Internal detail do not set manually. |
|
21 |
#' @param nsibs integer(1). Internal detail do not set manually. |
|
22 |
#' @param rownum numeric(1). Internal detail do not set manually. |
|
23 |
#' @param indent integer(1). Internal detail do not set manually. |
|
24 | ||
25 |
#' @param colwidths numeric. Internal detail do not set manually. |
|
26 |
#' @param path character. Path to the (sub)table represented by |
|
27 |
#' \code{tt}. Defaults to \code{character()} |
|
28 |
#' @param max_width numeric(1) or NULL. Maximum width for title/footer |
|
29 |
#' materials. |
|
30 |
#' |
|
31 |
#' @details When \code{visible_only} is \code{TRUE} (the default), |
|
32 |
#' methods should return a data.frame with exactly one row per |
|
33 |
#' visible row in the table-like object. This is useful when |
|
34 |
#' reasoning about how a table will print, but does not reflect |
|
35 |
#' the full pathing space of the structure (though the paths which |
|
36 |
#' are given will all work as is). |
|
37 |
#' |
|
38 |
#' If supported, when \code{visible_only} is \code{FALSE}, every |
|
39 |
#' structural element of the table (in row-space) will be reflected in |
|
40 |
#' the returned data.frame, meaning the full pathing-space will be |
|
41 |
#' represented but some rows in the layout summary will not represent |
|
42 |
#' printed rows in the table as it is displayed. |
|
43 |
#' |
|
44 |
#' Most arguments beyond \code{tt} and \code{visible_only} are present so that |
|
45 |
#' `make_row_df` methods can call `make_row_df` recursively and retain information, |
|
46 |
#' and should not be set during a top-level call |
|
47 |
#' |
|
48 |
#' @note the technically present root tree node is excluded from the summary returned by |
|
49 |
#' both \code{make_row_df} and \code{make_col_df}, as it is simply the |
|
50 |
#' row/column structure of \code{tt} and thus not useful for pathing or pagination. |
|
51 |
#' @export |
|
52 |
#' @return a data.frame of row/column-structure information used by the pagination machinery. |
|
53 |
#' @rdname make_row_df |
|
54 |
#' |
|
55 |
## nocov start |
|
56 |
setGeneric("make_row_df", function(tt, colwidths = NULL, visible_only = TRUE, |
|
57 |
rownum = 0, |
|
58 |
indent = 0L, |
|
59 |
path = character(), |
|
60 |
incontent = FALSE, |
|
61 |
repr_ext = 0L, |
|
62 |
repr_inds = integer(), |
|
63 |
sibpos = NA_integer_, |
|
64 |
nsibs = NA_integer_, |
|
65 |
max_width = NULL) { |
|
66 |
standardGeneric("make_row_df") |
|
67 |
}) |
|
68 |
## nocov end |
|
69 | ||
70 | ||
71 |
#' Transform `rtable` to a list of matrices which can be used for outputting |
|
72 |
#' |
|
73 |
#' Although `rtables` are represented as a tree data structure when outputting the table to ASCII or HTML it is useful to |
|
74 |
#' map the `rtable` to an in between state with the formatted cells in a matrix form. |
|
75 |
#' |
|
76 |
#' @param obj ANY. Object to be transformed into a ready-to-render form (a `MatrixPrintForm` object) |
|
77 |
#' @param indent_rownames logical(1), if TRUE the column with the row names in the `strings` matrix of has indented row |
|
78 |
#' names (strings pre-fixed) |
|
79 |
#' @param expand_newlines logical(1). Should the matrix form generated |
|
80 |
#' expand rows whose values contain newlines into multiple |
|
81 |
#' 'physical' rows (as they will appear when rendered into |
|
82 |
#' ASCII). Defaults to \code{TRUE} |
|
83 |
#' @param indent_size numeric(1). Number of spaces to be used per level of indent (if supported by |
|
84 |
#' the relevant method). Defaults to 2. |
|
85 |
#' @export |
|
86 |
#' |
|
87 |
#' @details |
|
88 |
#' |
|
89 |
#' The strings in the return object are defined as follows: row labels are those determined by \code{summarize_rows} and |
|
90 |
#' cell values are determined using \code{get_formatted_cells}. |
|
91 |
#' (Column labels are calculated using a non-exported internal function. |
|
92 |
#' |
|
93 |
#' @return A `MatrixPrintForm` classed list with the following elements: |
|
94 |
#' \describe{ |
|
95 |
#' \item{strings}{The content, as it should be printed, of the top-left material, column headers, row labels, and |
|
96 |
#' cell values of \code{tt}} |
|
97 |
#' \item{spans}{The column-span information for each print-string in the strings matrix} |
|
98 |
#' \item{aligns}{The text alignment for each print-string in the strings matrix} |
|
99 |
#' \item{display}{Whether each print-string in the strings matrix should be printed or not}. |
|
100 |
#' \item{row_info}{the data.frame generated by \code{summarize_rows(tt)}} |
|
101 |
#' } |
|
102 |
#' |
|
103 |
#' With an additional \code{nrow_header} attribute indicating the number of pseudo "rows" the |
|
104 |
#' column structure defines. |
|
105 |
setGeneric("matrix_form", function(obj, |
|
106 |
indent_rownames = FALSE, |
|
107 |
expand_newlines = TRUE, |
|
108 |
indent_size = 2) { |
|
109 | 113x |
standardGeneric("matrix_form") |
110 |
}) |
|
111 | ||
112 |
#' @rdname matrix_form |
|
113 |
#' @export |
|
114 |
setMethod("matrix_form", "MatrixPrintForm", function(obj, |
|
115 |
indent_rownames = FALSE, |
|
116 |
expand_newlines = TRUE, |
|
117 |
indent_size = 2) { |
|
118 | 113x |
obj |
119 |
}) |
|
120 | ||
121 | ||
122 |
## Generics for `toString` and helper functions |
|
123 | ||
124 | ||
125 |
## this is where we will take word wrapping |
|
126 |
## into account when it is added |
|
127 |
## |
|
128 |
## ALL calculations of vertical space for pagination |
|
129 |
## purposes must go through nlines and divider_height!!!!!!!! |
|
130 | ||
131 |
## this will be customizable someday. I have foreseen it (spooky noises) |
|
132 |
#' Divider Height |
|
133 |
#' |
|
134 |
#' @param obj ANY. Object. |
|
135 |
#' @return The height, in lines of text, of the divider between |
|
136 |
#' header and body. Currently returns \code{1L} for the default method. |
|
137 |
#' @export |
|
138 |
#' @examples |
|
139 |
#' divider_height(mtcars) |
|
140 | 18x |
setGeneric("divider_height", function(obj) standardGeneric("divider_height")) |
141 | ||
142 |
#' @rdname divider_height |
|
143 |
#' @export |
|
144 |
setMethod( |
|
145 |
"divider_height", "ANY", |
|
146 | 18x |
function(obj) 1L |
147 |
) |
|
148 | ||
149 |
#' Number of lines required to print a value |
|
150 |
#' @param x ANY. The object to be printed |
|
151 |
#' @param colwidths numeric. Column widths (if necessary). |
|
152 |
#' @param max_width numeric(1). Width strings should be wrapped to |
|
153 |
#' when determining how many lines they require. |
|
154 |
#' @return A scalar numeric indicating the number of lines needed |
|
155 |
#' to render the object \code{x}. |
|
156 |
#' @export |
|
157 |
setGeneric( |
|
158 |
"nlines", |
|
159 | 22935x |
function(x, colwidths = NULL, max_width = NULL) standardGeneric("nlines") |
160 |
) |
|
161 | ||
162 |
## XXX beware. I think it is dangerous |
|
163 |
#' @export |
|
164 |
#' @rdname nlines |
|
165 |
setMethod( |
|
166 |
"nlines", "list", |
|
167 |
function(x, colwidths, max_width) { |
|
168 | 2x |
if (length(x) == 0) { |
169 | 1x |
0L |
170 |
} else { |
|
171 | 1x |
sum(unlist(vapply(x, nlines, NA_integer_, |
172 | 1x |
colwidths = colwidths, |
173 | 1x |
max_width = max_width |
174 |
))) |
|
175 |
} |
|
176 |
} |
|
177 |
) |
|
178 | ||
179 |
#' @export |
|
180 |
#' @rdname nlines |
|
181 |
setMethod("nlines", "NULL", function(x, colwidths, max_width) 0L) |
|
182 | ||
183 |
#' @export |
|
184 |
#' @rdname nlines |
|
185 |
setMethod("nlines", "character", function(x, colwidths, max_width) { |
|
186 | 22932x |
if (length(x) == 0) { |
187 | 1x |
return(0L) |
188 |
} |
|
189 | ||
190 | 22931x |
sum(vapply(strsplit(x, "\n", fixed = TRUE), |
191 | 22931x |
function(xi, max_width) { |
192 | 22937x |
if (length(xi) == 0) { |
193 | 2373x |
1L |
194 | 20564x |
} else if (length(max_width) == 0) { ## this happens with strsplit("", "\n") |
195 | 20515x |
length(xi) |
196 |
} else { |
|
197 | 49x |
length(wrap_txt(xi, max_width)) |
198 |
} |
|
199 | 22931x |
}, 1L, |
200 | 22931x |
max_width = max_width |
201 |
)) |
|
202 |
}) |
|
203 | ||
204 | ||
205 | ||
206 |
#' @title `toString` |
|
207 |
#' |
|
208 |
#' @description Transform a complex object into a string representation ready |
|
209 |
#' to be printed or written to a plain-text file |
|
210 |
#' |
|
211 |
#' @param x ANY. Object to be prepared for rendering. |
|
212 |
#' @param ... Passed to individual methods. |
|
213 |
#' @rdname tostring |
|
214 |
#' @export |
|
215 |
setGeneric("toString", function(x, ...) standardGeneric("toString")) |
|
216 | ||
217 |
## preserve S3 behavior |
|
218 |
setMethod("toString", "ANY", base::toString) ## nocov |
|
219 | ||
220 |
#' @title Print |
|
221 |
#' |
|
222 |
#' @description Print an R object. see \code{[base::print()]} |
|
223 |
#' @inheritParams base::print |
|
224 |
#' @rdname basemethods |
|
225 |
setMethod("print", "ANY", base::print) ## nocov |
|
226 | ||
227 | ||
228 | ||
229 | ||
230 | ||
231 | ||
232 | ||
233 | ||
234 | ||
235 | ||
236 |
## General/"universal" property `getter` and `setter` generics and stubs |
|
237 | ||
238 |
#' @title Label, Name and Format accessor generics |
|
239 |
#' |
|
240 |
#' @description `Getters` and `setters` for basic, relatively universal attributes |
|
241 |
#' of "table-like" objects" |
|
242 |
#' |
|
243 |
#' @name lab_name |
|
244 |
#' @param obj ANY. The object. |
|
245 |
#' @param value character(1)/FormatSpec. The new value of the attribute. |
|
246 |
#' @return the name, format or label of \code{obj} for `getters`, or \code{obj} after modification |
|
247 |
#' for setters. |
|
248 |
#' @aliases obj_name |
|
249 |
#' @export |
|
250 | ||
251 |
## no exported methods so we do nocov |
|
252 |
# nocov start |
|
253 |
setGeneric("obj_name", function(obj) standardGeneric("obj_name")) |
|
254 | ||
255 | ||
256 |
#' @rdname lab_name |
|
257 |
#' @export |
|
258 |
setGeneric("obj_name<-", function(obj, value) standardGeneric("obj_name<-")) |
|
259 |
# nocov end |
|
260 | ||
261 |
#' @seealso with_label |
|
262 |
#' @rdname lab_name |
|
263 |
#' @export |
|
264 | 3x |
setGeneric("obj_label", function(obj) standardGeneric("obj_label")) |
265 | ||
266 |
#' @rdname lab_name |
|
267 |
#' @param value character(1). The new label |
|
268 |
#' @export |
|
269 | 2x |
setGeneric("obj_label<-", function(obj, value) standardGeneric("obj_label<-")) |
270 | ||
271 |
#' @rdname lab_name |
|
272 |
#' @exportMethod obj_label |
|
273 | 3x |
setMethod("obj_label", "ANY", function(obj) attr(obj, "label")) |
274 | ||
275 |
#' @rdname lab_name |
|
276 |
#' @exportMethod obj_label<- |
|
277 |
setMethod( |
|
278 |
"obj_label<-", "ANY", |
|
279 |
function(obj, value) { |
|
280 | 2x |
attr(obj, "label") <- value |
281 | 2x |
obj |
282 |
} |
|
283 |
) |
|
284 | ||
285 |
#' @rdname lab_name |
|
286 |
#' @export |
|
287 | 83x |
setGeneric("obj_format", function(obj) standardGeneric("obj_format")) |
288 |
## this covers rcell, etc |
|
289 |
#' @rdname lab_name |
|
290 |
#' @exportMethod obj_format |
|
291 | 83x |
setMethod("obj_format", "ANY", function(obj) attr(obj, "format", exact = TRUE)) |
292 | ||
293 | ||
294 |
#' @export |
|
295 |
#' @rdname lab_name |
|
296 | 2x |
setGeneric("obj_format<-", function(obj, value) standardGeneric("obj_format<-")) |
297 |
## this covers rcell, etc |
|
298 |
#' @exportMethod obj_format<- |
|
299 |
#' @rdname lab_name |
|
300 |
setMethod("obj_format<-", "ANY", function(obj, value) { |
|
301 | 2x |
attr(obj, "format") <- value |
302 | 2x |
obj |
303 |
}) |
|
304 | ||
305 |
#' @rdname lab_name |
|
306 |
#' @export |
|
307 | 3x |
setGeneric("obj_na_str", function(obj) standardGeneric("obj_na_str")) |
308 |
#' @rdname lab_name |
|
309 |
#' @exportMethod obj_na_str |
|
310 | 3x |
setMethod("obj_na_str", "ANY", function(obj) attr(obj, "format_na_str", exact = TRUE)) |
311 |
#' @rdname lab_name |
|
312 |
#' @export |
|
313 | 1x |
setGeneric("obj_na_str<-", function(obj, value) standardGeneric("obj_na_str<-")) |
314 |
#' @exportMethod obj_na_str<- |
|
315 |
#' @rdname lab_name |
|
316 |
setMethod("obj_na_str<-", "ANY", function(obj, value) { |
|
317 | 1x |
attr(obj, "format_na_str") <- value |
318 | 1x |
obj |
319 |
}) |
|
320 | ||
321 |
#' @rdname lab_name |
|
322 |
#' @export |
|
323 | 2x |
setGeneric("obj_align", function(obj) standardGeneric("obj_align")) |
324 |
#' @rdname lab_name |
|
325 |
#' @exportMethod obj_align |
|
326 | 2x |
setMethod("obj_align", "ANY", function(obj) attr(obj, "align", exact = TRUE)) |
327 | ||
328 |
#' @rdname lab_name |
|
329 |
#' @export |
|
330 | ! |
setGeneric("obj_align<-", function(obj, value) standardGeneric("obj_align<-")) |
331 |
#' @exportMethod obj_align<- |
|
332 |
#' @rdname lab_name |
|
333 |
setMethod("obj_align<-", "ANY", function(obj, value) { |
|
334 | ! |
attr(obj, "align") <- value |
335 | ! |
obj |
336 |
}) |
|
337 | ||
338 |
#' General title/footer accessors |
|
339 |
#' |
|
340 |
#' @param obj ANY. Object to extract information from. |
|
341 |
#' @export |
|
342 |
#' @rdname title_footer |
|
343 |
#' @return a character scalar (`main_title`, `main_footer`), or |
|
344 |
#' vector of length zero or more (`subtitles`, `page_titles`, |
|
345 |
#' `prov_footer`) containing the relevant title/footer contents |
|
346 | 61x |
setGeneric("main_title", function(obj) standardGeneric("main_title")) |
347 | ||
348 |
#' @export |
|
349 |
#' @rdname title_footer |
|
350 |
setMethod( |
|
351 |
"main_title", "MatrixPrintForm", |
|
352 | 61x |
function(obj) obj$main_title |
353 |
) |
|
354 | ||
355 |
##' @rdname title_footer |
|
356 |
##' @export |
|
357 | 6x |
setGeneric("main_title<-", function(obj, value) standardGeneric("main_title<-")) |
358 |
##' @rdname title_footer |
|
359 |
##' @export |
|
360 |
setMethod( |
|
361 |
"main_title<-", "MatrixPrintForm", |
|
362 |
function(obj, value) { |
|
363 | 6x |
obj$main_title <- value |
364 | 6x |
obj |
365 |
} |
|
366 |
) |
|
367 | ||
368 | ||
369 | ||
370 |
#' @export |
|
371 |
#' @rdname title_footer |
|
372 |
setGeneric("subtitles", function(obj) standardGeneric("subtitles")) ## nocov |
|
373 | ||
374 |
#' @export |
|
375 |
#' @rdname title_footer |
|
376 |
setMethod( |
|
377 |
"subtitles", "MatrixPrintForm", |
|
378 | 62x |
function(obj) obj$subtitles |
379 |
) |
|
380 | ||
381 |
##' @rdname title_footer |
|
382 |
##' @export |
|
383 |
setGeneric("subtitles<-", function(obj, value) standardGeneric("subtitles<-")) ## nocov |
|
384 | ||
385 |
##' @rdname title_footer |
|
386 |
##' @export |
|
387 |
setMethod( |
|
388 |
"subtitles<-", "MatrixPrintForm", |
|
389 |
function(obj, value) { |
|
390 | 5x |
obj$subtitles <- value |
391 | 5x |
obj |
392 |
} |
|
393 |
) |
|
394 | ||
395 |
#' @export |
|
396 |
#' @rdname title_footer |
|
397 | 76x |
setGeneric("page_titles", function(obj) standardGeneric("page_titles")) |
398 | ||
399 |
#' @export |
|
400 |
#' @rdname title_footer |
|
401 |
setMethod( |
|
402 |
"page_titles", "MatrixPrintForm", |
|
403 | 76x |
function(obj) obj$page_titles |
404 |
) |
|
405 |
#' @rdname title_footer |
|
406 |
#' @export |
|
407 | ! |
setMethod("page_titles", "ANY", function(obj) NULL) |
408 | ||
409 |
##' @rdname title_footer |
|
410 |
##' @export |
|
411 | 2x |
setGeneric("page_titles<-", function(obj, value) standardGeneric("page_titles<-")) |
412 | ||
413 |
#' @export |
|
414 |
#' @rdname title_footer |
|
415 |
setMethod( |
|
416 |
"page_titles<-", "MatrixPrintForm", |
|
417 |
function(obj, value) { |
|
418 | 2x |
if (!is.character(value)) { |
419 | ! |
stop("page titles must be in the form of a character vector, got object of class ", class(value)) |
420 |
} |
|
421 | 2x |
obj$page_titles <- value |
422 | 2x |
obj |
423 |
} |
|
424 |
) |
|
425 | ||
426 | ||
427 | ||
428 |
#' @export |
|
429 |
#' @rdname title_footer |
|
430 | 56x |
setGeneric("main_footer", function(obj) standardGeneric("main_footer")) |
431 | ||
432 |
#' @export |
|
433 |
#' @rdname title_footer |
|
434 |
setMethod( |
|
435 |
"main_footer", "MatrixPrintForm", |
|
436 | 56x |
function(obj) obj$main_footer |
437 |
) |
|
438 | ||
439 |
#' @rdname title_footer |
|
440 |
#' @param value character. New value. |
|
441 |
#' @export |
|
442 | 6x |
setGeneric("main_footer<-", function(obj, value) standardGeneric("main_footer<-")) |
443 | ||
444 | ||
445 | ||
446 |
#' @export |
|
447 |
#' @rdname title_footer |
|
448 |
setMethod( |
|
449 |
"main_footer<-", "MatrixPrintForm", |
|
450 |
function(obj, value) { |
|
451 | 6x |
if (!is.character(value)) { |
452 | ! |
stop("main footer must be a character vector. Got object of class ", class(value)) |
453 |
} |
|
454 | 6x |
obj$main_footer <- value |
455 | 6x |
obj |
456 |
} |
|
457 |
) |
|
458 | ||
459 | ||
460 |
#' @export |
|
461 |
#' @rdname title_footer |
|
462 | 66x |
setGeneric("prov_footer", function(obj) standardGeneric("prov_footer")) |
463 | ||
464 |
#' @export |
|
465 |
#' @rdname title_footer |
|
466 |
setMethod( |
|
467 |
"prov_footer", "MatrixPrintForm", |
|
468 | 66x |
function(obj) obj$prov_footer |
469 |
) |
|
470 | ||
471 |
#' @rdname title_footer |
|
472 |
#' @export |
|
473 | 7x |
setGeneric("prov_footer<-", function(obj, value) standardGeneric("prov_footer<-")) |
474 | ||
475 |
#' @export |
|
476 |
#' @rdname title_footer |
|
477 |
setMethod( |
|
478 |
"prov_footer<-", "MatrixPrintForm", |
|
479 |
function(obj, value) { |
|
480 | 7x |
if (!is.character(value)) { |
481 | ! |
stop("provenance footer must be a character vector. Got object of class ", class(value)) |
482 |
} |
|
483 | 7x |
obj$prov_footer <- value |
484 | 7x |
obj |
485 |
} |
|
486 |
) |
|
487 | ||
488 | ||
489 | ||
490 | ||
491 |
#' @rdname title_footer |
|
492 |
#' @export |
|
493 | 1x |
all_footers <- function(obj) c(main_footer(obj), prov_footer(obj)) |
494 | ||
495 |
#' @rdname title_footer |
|
496 |
#' @export |
|
497 | 59x |
all_titles <- function(obj) c(main_title(obj), subtitles(obj), page_titles(obj)) |
498 | ||
499 | ||
500 |
#' Access or (recursively) set table inset. |
|
501 |
#' |
|
502 |
#' Table inset is the amount of characters that the body of |
|
503 |
#' a table, referential footnotes, and main footer material |
|
504 |
#' are inset from the left-alignment of the titles and provenance |
|
505 |
#' footer materials. |
|
506 |
#' |
|
507 |
#' @param obj ANY. Object to get or (recursively if necessary) set |
|
508 |
#' table inset for. |
|
509 |
#' @param value character(1). String to use as new header/body separator. |
|
510 |
#' |
|
511 |
#' @return for `table_inset` the integer value that the table body |
|
512 |
#' (including column heading information and section dividers), |
|
513 |
#' referential footnotes, and main footer should be inset from the |
|
514 |
#' left alignment of the titles and provenance footers during rendering. |
|
515 |
#' For `table_inset<-`, the `obj`, with the new table_inset value |
|
516 |
#' applied recursively to it and all its subtables. |
|
517 |
#' |
|
518 |
#' @export |
|
519 | 125x |
setGeneric("table_inset", function(obj) standardGeneric("table_inset")) |
520 | ||
521 |
#' @rdname table_inset |
|
522 |
#' @export |
|
523 |
setMethod( |
|
524 |
"table_inset", "MatrixPrintForm", |
|
525 | 125x |
function(obj) obj$table_inset |
526 |
) |
|
527 | ||
528 | ||
529 |
#' @rdname table_inset |
|
530 |
#' @export |
|
531 | 4x |
setGeneric("table_inset<-", function(obj, value) standardGeneric("table_inset<-")) |
532 | ||
533 |
#' @rdname table_inset |
|
534 |
#' @export |
|
535 |
setMethod( |
|
536 |
"table_inset<-", "MatrixPrintForm", |
|
537 |
function(obj, value) { |
|
538 | 4x |
newval <- as.integer(value) |
539 | 4x |
if (is.na(newval) || newval < 0) { |
540 | 1x |
stop("Got invalid value for table_inset: ", newval) |
541 |
} |
|
542 | 3x |
obj$table_inset <- newval |
543 | 3x |
obj |
544 |
} |
|
545 |
) |
|
546 | ||
547 | ||
548 | ||
549 | ||
550 |
#' Generic for Performing "Forced Pagination" |
|
551 |
#' |
|
552 |
#' Forced pagination is pagination which happens regardless of |
|
553 |
#' position on page. The object is expected to have all information |
|
554 |
#' necessary to locate such page breaks, and the `do_forced_pag` |
|
555 |
#' method is expected to fully perform those paginations. |
|
556 |
#' |
|
557 |
#' @param obj The object to be paginated. |
|
558 |
#' |
|
559 |
#' The `ANY` method simply returns a list of length one, containing |
|
560 |
#' `obj`. |
|
561 |
#' |
|
562 |
#' @return a list of subobjects, which will be further paginated |
|
563 |
#' by the standard pagination algorithm. |
|
564 |
#' |
|
565 |
#' |
|
566 |
#' @export |
|
567 | 42x |
setGeneric("do_forced_paginate", function(obj) standardGeneric("do_forced_paginate")) |
568 | ||
569 |
#' @export |
|
570 |
#' @rdname do_forced_paginate |
|
571 | 39x |
setMethod("do_forced_paginate", "ANY", function(obj) list(obj)) |
572 | ||
573 |
#' Number of repeated columns |
|
574 |
#' |
|
575 |
#' When called on a table-like object using the formatters framework, |
|
576 |
#' this method should return the number of columns which are mandatorily |
|
577 |
#' repeated after each horizontal pagination. |
|
578 |
#' |
|
579 |
#' Absent a class-specific method, this function returns 0, indicating |
|
580 |
#' no always-repeated columns. |
|
581 |
#' |
|
582 |
#' @param obj ANY. A table-like object. |
|
583 |
#' @note This number \emph{does not include row labels}, the repetition |
|
584 |
#' of which is handled separately. |
|
585 |
#' |
|
586 |
#' @return an integer. |
|
587 |
#' @export |
|
588 |
#' @examples |
|
589 |
#' mpf <- basic_matrix_form(mtcars) |
|
590 |
#' num_rep_cols(mpf) |
|
591 | 24x |
setGeneric("num_rep_cols", function(obj) standardGeneric("num_rep_cols")) |
592 |
#' @export |
|
593 |
#' @rdname num_rep_cols |
|
594 | 24x |
setMethod("num_rep_cols", "ANY", function(obj) 0L) |
1 | ||
2 |
#' @importFrom htmltools tags tagList |
|
3 | ||
4 |
formats_1d <- c( |
|
5 |
"xx", "xx.", "xx.x", "xx.xx", "xx.xxx", "xx.xxxx", |
|
6 |
"xx%", "xx.%", "xx.x%", "xx.xx%", "xx.xxx%", "(N=xx)", ">999.9", ">999.99", |
|
7 |
"x.xxxx | (<0.0001)" |
|
8 |
) |
|
9 | ||
10 |
formats_2d <- c( |
|
11 |
"xx / xx", "xx. / xx.", "xx.x / xx.x", "xx.xx / xx.xx", "xx.xxx / xx.xxx", |
|
12 |
"N=xx (xx%)", "xx (xx%)", "xx (xx.%)", "xx (xx.x%)", "xx (xx.xx%)", |
|
13 |
"xx. (xx.%)", "xx.x (xx.x%)", "xx.xx (xx.xx%)", |
|
14 |
"(xx, xx)", "(xx., xx.)", "(xx.x, xx.x)", "(xx.xx, xx.xx)", |
|
15 |
"(xx.xxx, xx.xxx)", "(xx.xxxx, xx.xxxx)", |
|
16 |
"xx - xx", "xx.x - xx.x", "xx.xx - xx.xx", |
|
17 |
"xx (xx)", "xx. (xx.)", "xx.x (xx.x)", "xx.xx (xx.xx)", |
|
18 |
"xx (xx.)", "xx (xx.x)", "xx (xx.xx)", |
|
19 |
"xx.x, xx.x", |
|
20 |
"xx.x to xx.x" |
|
21 |
) |
|
22 | ||
23 |
formats_3d <- c( |
|
24 |
"xx. (xx. - xx.)", |
|
25 |
"xx.x (xx.x - xx.x)", |
|
26 |
"xx.xx (xx.xx - xx.xx)", |
|
27 |
"xx.xxx (xx.xxx - xx.xxx)" |
|
28 |
) |
|
29 | ||
30 |
#' List with currently support 'xx' style format labels grouped by 1d, 2d and 3d |
|
31 |
#' |
|
32 |
#' Currently valid format labels can not be added dynamically. Format functions |
|
33 |
#' must be used for special cases |
|
34 |
#' |
|
35 |
#' @export |
|
36 |
#' @return A nested list, with elements listing the supported 1d, 2d, and 3d format strings. |
|
37 |
#' @examples |
|
38 |
#' |
|
39 |
#' list_valid_format_labels() |
|
40 |
#' |
|
41 |
list_valid_format_labels <- function() { |
|
42 | 52x |
structure( |
43 | 52x |
list( |
44 | 52x |
"1d" = formats_1d, |
45 | 52x |
"2d" = formats_2d, |
46 | 52x |
"3d" = formats_3d |
47 |
), |
|
48 | 52x |
info = "xx does not modify the element, and xx. rounds a number to 0 digits" |
49 |
) |
|
50 |
} |
|
51 | ||
52 |
#' Check if a format is supported |
|
53 |
#' |
|
54 |
#' @param x either format string or an object returned by \code{sprintf_format} |
|
55 |
#' @param stop_otherwise logical, if \code{x} is not a format should an error be |
|
56 |
#' thrown |
|
57 |
#' @note No check if the function is actually a `formatter` is performed. |
|
58 |
#' @return \code{TRUE} if \code{x} is \code{NULL}, a supported format string, or a function; \code{FALSE} otherwise. |
|
59 |
#' |
|
60 |
#' @export |
|
61 |
#' |
|
62 |
#' @examples |
|
63 |
#' is_valid_format("xx.x") |
|
64 |
#' is_valid_format("fakeyfake") |
|
65 |
is_valid_format <- function(x, stop_otherwise = FALSE) { |
|
66 | 51x |
is_valid <- is.null(x) || |
67 | 51x |
(length(x) == 1 && |
68 | 51x |
(is.function(x) || |
69 | 51x |
x %in% unlist(list_valid_format_labels()))) |
70 | ||
71 | 51x |
if (stop_otherwise && !is_valid) { |
72 | ! |
stop("format needs to be a format label, sprintf_format object, a function, or NULL") |
73 |
} |
|
74 | ||
75 | 51x |
is_valid |
76 |
} |
|
77 | ||
78 | ||
79 |
#' Specify text format via a `sprintf` format string |
|
80 |
#' |
|
81 |
#' |
|
82 |
#' @param format character(1). A format string passed to `sprintf`. |
|
83 |
#' |
|
84 |
#' @export |
|
85 |
#' @return A formatting function which wraps and will apply the specified \code{printf} style format |
|
86 |
#' string \code{format}. |
|
87 |
#' @seealso \code{\link[base]{sprintf}} |
|
88 |
#' |
|
89 |
#' @examples |
|
90 |
#' |
|
91 |
#' fmtfun <- sprintf_format("(N=%i") |
|
92 |
#' format_value(100, format = fmtfun) |
|
93 |
#' |
|
94 |
#' fmtfun2 <- sprintf_format("%.4f - %.2f") |
|
95 |
#' format_value(list(12.23456, 2.724)) |
|
96 |
sprintf_format <- function(format) { |
|
97 | 1x |
function(x, ...) { |
98 | 1x |
do.call(sprintf, c(list(fmt = format), x)) |
99 |
} |
|
100 |
} |
|
101 | ||
102 | ||
103 |
#' Round and prepare a value for display |
|
104 |
#' |
|
105 |
#' This function is used within \code{\link{format_value}} to prepare numeric values within |
|
106 |
#' cells for formatting and display. |
|
107 |
#' |
|
108 |
#' @aliases rounding |
|
109 |
#' @param x numeric(1). Value to format |
|
110 |
#' @param digits numeric(1). Number of digits to round to, or \code{NA} to convert to a |
|
111 |
#' character value with no rounding. |
|
112 |
#' @param na_str character(1). The value to return if \code{x} is \code{NA}. |
|
113 |
#' |
|
114 |
#' @details |
|
115 |
#' This function combines the rounding behavior of R's standards-complaint |
|
116 |
#' \code{\link{round}} function (see the Details section of that documentation) |
|
117 |
#' with the strict decimal display of \code{\link{sprintf}}. The exact behavior |
|
118 |
#' is as follows: |
|
119 |
#' |
|
120 |
#' \enumerate{ |
|
121 |
#' \item{If \code{x} is NA, the value of \code{na_str} is returned} |
|
122 |
#' \item{If \code{x} is non-NA but \code{digits} is NA, \code{x} is converted to a character |
|
123 |
#' and returned} |
|
124 |
#' \item{If \code{x} and \code{digits} are both non-NA, \code{round} is called first, |
|
125 |
#' and then \code{sprintf} is used to convert the rounded value to a character with the |
|
126 |
#' appropriate number of trailing zeros enforced.} |
|
127 |
#' } |
|
128 |
#' |
|
129 |
#' @return A character value representing the value after rounding, containing |
|
130 |
#' containing any trailling zeros required to display \emph{exactly} \code{digits} |
|
131 |
#' elements. |
|
132 |
#' @note |
|
133 |
#' This differs from the base R \code{\link{round}} function in that \code{NA} |
|
134 |
#' digits indicate x should be passed converted to character and returned unchanged |
|
135 |
#' whereas \code{round(x, digits =NA)} returns \code{NA} for all values of \code{x}. |
|
136 |
#' |
|
137 |
#' This behavior will differ from \code{as.character(round(x, digits = digits))} |
|
138 |
#' in the case where there are not at least \code{digits} significant digits |
|
139 |
#' after the decimal that remain after rounding. It \emph{may} differ from |
|
140 |
#' \code{sprintf("\%.Nf", x)} for values ending in \code{5} after the decimal place |
|
141 |
#' on many popular operating systems due to \code{round}'s stricter adherence to the |
|
142 |
#' `IEC 60559` standard, particularly for R versions > 4.0.0 (see Warning in \code{\link[base:round]{round}} |
|
143 |
#' documentation). |
|
144 |
#' |
|
145 |
#' @export |
|
146 |
#' @seealso \code{link{format_value}} \code{\link[base:round]{round}} \code{\link[base:sprintf]{sprintf}} |
|
147 |
#' @examples |
|
148 |
#' |
|
149 |
#' round_fmt(0, digits = 3) |
|
150 |
#' round_fmt(.395, digits = 2) |
|
151 |
#' round_fmt(NA, digits = 1) |
|
152 |
#' round_fmt(NA, digits = 1, na_str = "-") |
|
153 |
#' round_fmt(2.765923, digits = NA) |
|
154 |
round_fmt <- function(x, digits, na_str = "NA") { |
|
155 | 193x |
if (!is.na(digits) && digits < 0) { |
156 | ! |
stop("round_fmt currentlyd does not support non-missing values of digits <0") |
157 |
} |
|
158 | 193x |
if (is.na(x)) { |
159 | 4x |
na_str |
160 | 189x |
} else if (is.na(digits)) { |
161 | 41x |
paste0(x) |
162 |
} else { |
|
163 | 148x |
sprfmt <- paste0("%.", digits, "f") |
164 | 148x |
sprintf(fmt = sprfmt, round(x, digits = digits)) |
165 |
} |
|
166 |
} |
|
167 | ||
168 | ||
169 | ||
170 |
val_pct_helper <- function(x, dig1, dig2, na_str, pct = TRUE) { |
|
171 | 32x |
if (pct) { |
172 | 18x |
x[2] <- x[2] * 100 |
173 |
} |
|
174 | 32x |
if (length(na_str) == 1) { |
175 | ! |
na_str <- rep(na_str, 2) |
176 |
} |
|
177 | 32x |
paste0( |
178 | 32x |
round_fmt(x[1], digits = dig1, na_str = na_str[1]), |
179 |
" (", |
|
180 | 32x |
round_fmt(x[2], digits = dig2, na_str = na_str[2]), |
181 | 32x |
if (pct) "%", ")" |
182 |
) |
|
183 |
} |
|
184 | ||
185 |
sep_2d_helper <- function(x, dig1, dig2, sep, na_str, wrap = NULL) { |
|
186 | 43x |
ret <- paste(mapply(round_fmt, x = x, digits = c(dig1, dig2), na_str = na_str), |
187 | 43x |
collapse = sep |
188 |
) |
|
189 | 43x |
if (!is.null(wrap)) { |
190 | 20x |
ret <- paste(c(wrap[1], ret, wrap[2]), collapse = "") |
191 |
} |
|
192 | 43x |
ret |
193 |
} |
|
194 | ||
195 |
## na_or_round <- function(x, digits, na_str) { |
|
196 |
## if(is.na(x)) |
|
197 |
## na_str |
|
198 |
## else |
|
199 |
## round(x, digits = digits) |
|
200 | ||
201 |
## } |
|
202 | ||
203 |
#' Converts a (possibly compound) value into a string using the \code{format} information |
|
204 |
#' |
|
205 |
#' @details A length-zero value for `na_str` will be interpreted as `"NA"`, as will any |
|
206 |
#' missing values within a non-length-zero `na_str` vector. |
|
207 |
#' |
|
208 |
#' @param x ANY. The value to be formatted |
|
209 |
#' @param format character(1) or function. The format label (string) or `formatter` function to apply to \code{x}. |
|
210 |
#' @param na_str character(1). String that should be displayed when the value of \code{x} is missing. |
|
211 |
#' Defaults to \code{"NA"}. |
|
212 |
#' @param output character(1). output type |
|
213 |
#' |
|
214 |
#' @return formatted text representing the cell \code{x}. |
|
215 |
#' @export |
|
216 |
#' |
|
217 |
#' @seealso [round_fmt()] |
|
218 |
#' @examples |
|
219 |
#' |
|
220 |
#' x <- format_value(pi, format = "xx.xx") |
|
221 |
#' x |
|
222 |
#' |
|
223 |
#' format_value(x, output = "ascii") |
|
224 |
#' |
|
225 |
format_value <- function(x, format = NULL, output = c("ascii", "html"), na_str = "NA") { |
|
226 |
## if(is(x, "CellValue")) |
|
227 |
## x = x[[1]] |
|
228 | ||
229 | 3056x |
if (length(x) == 0) { |
230 | 1x |
return("") |
231 |
} |
|
232 | ||
233 | 3055x |
output <- match.arg(output) |
234 | 3055x |
if (length(na_str) == 0) { |
235 | 1x |
na_str <- "NA" |
236 |
} |
|
237 | 3055x |
if (any(is.na(na_str))) { |
238 | 1x |
na_str[is.na(na_str)] <- "NA" |
239 |
} |
|
240 |
## format <- if (!missing(format)) format else obj_format(x) |
|
241 | ||
242 | ||
243 | 3055x |
txt <- if (all(is.na(x)) && length(na_str) == 1L) { |
244 | 21x |
na_str |
245 | 3055x |
} else if (is.null(format)) { |
246 | ! |
toString(x) |
247 | 3055x |
} else if (is.function(format)) { |
248 | 1x |
format(x, output = output) |
249 | 3055x |
} else if (is.character(format)) { |
250 | 3033x |
l <- if (format %in% formats_1d) { |
251 | 2955x |
1 |
252 | 3033x |
} else if (format %in% formats_2d) { |
253 | 69x |
2 |
254 | 3033x |
} else if (format %in% formats_3d) { |
255 | 8x |
3 |
256 |
} else { |
|
257 | 1x |
stop( |
258 | 1x |
"unknown format label: ", format, |
259 | 1x |
". use list_valid_format_labels() to get a list of all formats" |
260 |
) |
|
261 |
} |
|
262 | 3032x |
if (format != "xx" && length(x) != l) { |
263 | 2x |
stop( |
264 | 2x |
"cell <", paste(x), "> and format ", |
265 | 2x |
format, " are of different length" |
266 |
) |
|
267 |
} |
|
268 | 3030x |
if (length(na_str) < length(x)) { |
269 | 73x |
na_str <- rep(na_str, length.out = length(x)) |
270 |
} |
|
271 | 3030x |
switch(format, |
272 | 2917x |
"xx" = as.character(x), |
273 | 3x |
"xx." = round_fmt(x, digits = 0, na_str = na_str), |
274 | 6x |
"xx.x" = round_fmt(x, digits = 1, na_str = na_str), |
275 | 3x |
"xx.xx" = round_fmt(x, digits = 2, na_str = na_str), |
276 | 3x |
"xx.xxx" = round_fmt(x, digits = 3, na_str = na_str), |
277 | 3x |
"xx.xxxx" = round_fmt(x, digits = 4, na_str = na_str), |
278 | 2x |
"xx%" = paste0(round_fmt(x * 100, digits = NA, na_str = na_str), "%"), |
279 | 2x |
"xx.%" = paste0(round_fmt(x * 100, digits = 0, na_str = na_str), "%"), |
280 | 2x |
"xx.x%" = paste0(round_fmt(x * 100, digits = 1, na_str = na_str), "%"), |
281 | 2x |
"xx.xx%" = paste0(round_fmt(x * 100, digits = 2, na_str = na_str), "%"), |
282 | 2x |
"xx.xxx%" = paste0(round_fmt(x * 100, digits = 3, na_str = na_str), "%"), |
283 | 1x |
"(N=xx)" = paste0("(N=", round_fmt(x, digits = NA, na_str = na_str), ")"), |
284 | 3x |
">999.9" = ifelse(x > 999.9, ">999.9", round_fmt(x, digits = 1, na_str = na_str)), |
285 | 3x |
">999.99" = ifelse(x > 999.99, ">999.99", round_fmt(x, digits = 2, na_str = na_str)), |
286 | 3x |
"x.xxxx | (<0.0001)" = ifelse(x < 0.0001, "<0.0001", round_fmt(x, digits = 4, na_str = na_str)), |
287 | 2x |
"xx / xx" = sep_2d_helper(x, dig1 = NA, dig2 = NA, sep = " / ", na_str = na_str), |
288 | 2x |
"xx. / xx." = sep_2d_helper(x, dig1 = 0, dig2 = 0, sep = " / ", na_str = na_str), |
289 | 2x |
"xx.x / xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " / ", na_str = na_str), |
290 | 2x |
"xx.xx / xx.xx" = sep_2d_helper(x, dig1 = 2, dig2 = 2, sep = " / ", na_str = na_str), |
291 | 2x |
"xx.xxx / xx.xxx" = sep_2d_helper(x, dig1 = 3, dig2 = 3, sep = " / ", na_str = na_str), |
292 | 2x |
"N=xx (xx%)" = paste0("N=", val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str)), |
293 | 3x |
"xx (xx%)" = val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str), |
294 | 2x |
"xx (xx.%)" = val_pct_helper(x, dig1 = NA, dig2 = 0, na_str = na_str), |
295 | 2x |
"xx (xx.x%)" = val_pct_helper(x, dig1 = NA, dig2 = 1, na_str = na_str), |
296 | 2x |
"xx (xx.xx%)" = val_pct_helper(x, dig1 = NA, dig2 = 2, na_str = na_str), |
297 | 2x |
"xx. (xx.%)" = val_pct_helper(x, dig1 = 0, dig2 = 0, na_str = na_str), |
298 | 3x |
"xx.x (xx.x%)" = val_pct_helper(x, dig1 = 1, dig2 = 1, na_str = na_str), |
299 | 2x |
"xx.xx (xx.xx%)" = val_pct_helper(x, dig1 = 2, dig2 = 2, na_str = na_str), |
300 | 2x |
"(xx, xx)" = sep_2d_helper(x, |
301 | 2x |
dig1 = NA, dig2 = NA, sep = ", ", |
302 | 2x |
na_str = na_str, wrap = c("(", ")") |
303 |
), |
|
304 | 2x |
"(xx., xx.)" = sep_2d_helper(x, |
305 | 2x |
dig1 = 0, dig2 = 0, sep = ", ", |
306 | 2x |
na_str = na_str, wrap = c("(", ")") |
307 |
), |
|
308 | 2x |
"(xx.x, xx.x)" = sep_2d_helper(x, |
309 | 2x |
dig1 = 1, dig2 = 1, sep = ", ", |
310 | 2x |
na_str = na_str, wrap = c("(", ")") |
311 |
), |
|
312 | 2x |
"(xx.xx, xx.xx)" = sep_2d_helper(x, |
313 | 2x |
dig1 = 2, dig2 = 2, sep = ", ", |
314 | 2x |
na_str = na_str, wrap = c("(", ")") |
315 |
), |
|
316 | 2x |
"(xx.xxx, xx.xxx)" = sep_2d_helper(x, |
317 | 2x |
dig1 = 3, dig2 = 3, sep = ", ", |
318 | 2x |
na_str = na_str, wrap = c("(", ")") |
319 |
), |
|
320 | 2x |
"(xx.xxxx, xx.xxxx)" = sep_2d_helper(x, |
321 | 2x |
dig1 = 4, dig2 = 4, sep = ", ", |
322 | 2x |
na_str = na_str, wrap = c("(", ")") |
323 |
), |
|
324 | 2x |
"xx - xx" = sep_2d_helper(x, dig1 = NA, dig2 = NA, sep = " - ", na_str = na_str), |
325 | 5x |
"xx.x - xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " - ", na_str = na_str), |
326 | 2x |
"xx.xx - xx.xx" = sep_2d_helper(x, dig1 = 2, dig2 = 2, sep = " - ", na_str = na_str), |
327 | 2x |
"xx (xx)" = val_pct_helper(x, dig1 = NA, dig2 = NA, na_str = na_str, pct = FALSE), |
328 | 2x |
"xx. (xx.)" = val_pct_helper(x, dig1 = 0, dig2 = 0, na_str = na_str, pct = FALSE), |
329 | 2x |
"xx.x (xx.x)" = val_pct_helper(x, dig1 = 1, dig2 = 1, na_str = na_str, pct = FALSE), |
330 | 2x |
"xx.xx (xx.xx)" = val_pct_helper(x, dig1 = 2, dig2 = 2, na_str = na_str, pct = FALSE), |
331 | 2x |
"xx (xx.)" = val_pct_helper(x, dig1 = NA, dig2 = 0, na_str = na_str, pct = FALSE), |
332 | 2x |
"xx (xx.x)" = val_pct_helper(x, dig1 = NA, dig2 = 1, na_str = na_str, pct = FALSE), |
333 | 2x |
"xx (xx.xx)" = val_pct_helper(x, dig1 = NA, dig2 = 2, na_str = na_str, pct = FALSE), |
334 | 2x |
"xx.x, xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = ", ", na_str = na_str), |
335 | 2x |
"xx.x to xx.x" = sep_2d_helper(x, dig1 = 1, dig2 = 1, sep = " to ", na_str = na_str), |
336 | 2x |
"xx.xx (xx.xx - xx.xx)" = paste0( |
337 | 2x |
round_fmt(x[1], digits = 2, na_str = na_str[1]), " ", |
338 | 2x |
sep_2d_helper(x[2:3], |
339 | 2x |
dig1 = 2, dig2 = 2, |
340 | 2x |
sep = " - ", na_str = na_str[2:3], |
341 | 2x |
wrap = c("(", ")") |
342 |
) |
|
343 |
), |
|
344 | 2x |
"xx. (xx. - xx.)" = paste0( |
345 | 2x |
round_fmt(x[1], digits = 0, na_str = na_str[1]), " ", |
346 | 2x |
sep_2d_helper(x[2:3], |
347 | 2x |
dig1 = 0, dig2 = 0, |
348 | 2x |
sep = " - ", na_str = na_str[2:3], |
349 | 2x |
wrap = c("(", ")") |
350 |
) |
|
351 |
), |
|
352 | 2x |
"xx.x (xx.x - xx.x)" = paste0( |
353 | 2x |
round_fmt(x[1], digits = 1, na_str = na_str[1]), " ", |
354 | 2x |
sep_2d_helper(x[2:3], |
355 | 2x |
dig1 = 1, dig2 = 1, |
356 | 2x |
sep = " - ", na_str = na_str[2:3], |
357 | 2x |
wrap = c("(", ")") |
358 |
) |
|
359 |
), |
|
360 | 2x |
"xx.xxx (xx.xxx - xx.xxx)" = paste0( |
361 | 2x |
round_fmt(x[1], digits = 3, na_str = na_str[1]), " ", |
362 | 2x |
sep_2d_helper(x[2:3], |
363 | 2x |
dig1 = 3, dig2 = 3, |
364 | 2x |
sep = " - ", na_str = na_str[2:3], |
365 | 2x |
wrap = c("(", ")") |
366 |
) |
|
367 |
), |
|
368 | ! |
paste("format string", format, "not found") |
369 |
) |
|
370 |
} |
|
371 | 3052x |
txt[is.na(txt)] <- na_str |
372 | 3052x |
if (output == "ascii") { |
373 | 3051x |
txt |
374 | 1x |
} else if (output == "html") { |
375 |
## convert to tagList |
|
376 |
## convert \n to <br/> |
|
377 | ||
378 | 1x |
if (identical(txt, "")) { |
379 | ! |
txt |
380 |
} else { |
|
381 | 1x |
els <- unlist(strsplit(txt, "\n", fixed = TRUE)) |
382 | 1x |
Map(function(el, is.last) { |
383 | 1x |
tagList(el, if (!is.last) tags$br() else NULL) |
384 | 1x |
}, els, c(rep(FALSE, length(els) - 1), TRUE)) |
385 |
} |
|
386 |
} else { |
|
387 | ! |
txt |
388 |
} |
|
389 |
} |
|
390 | ||
391 |
setClassUnion("FormatSpec", c("NULL", "character", "function", "list")) |
|
392 |
setClassUnion("characterOrNULL", c("NULL", "character")) |
|
393 |
setClass("fmt_config", |
|
394 |
slots = c(format = "FormatSpec", format_na_str = "characterOrNULL", align = "characterOrNULL")) |
|
395 | ||
396 |
#' Format Configuration |
|
397 |
#' |
|
398 |
#' @param format character(1) or function. A format label (string) or `formatter` function. |
|
399 |
#' @param na_str character(1). String that should be displayed in place of missing values. |
|
400 |
#' @param align character(1). Alignment values should be rendered with. |
|
401 |
#' |
|
402 |
#' @return An object of class `fmt_config` which contains the following elements: |
|
403 |
#' * `format` |
|
404 |
#' * `na_str` |
|
405 |
#' * `align` |
|
406 |
#' |
|
407 |
#' @examples |
|
408 |
#' fmt_config(format = "xx.xx", na_str = "-", align = "left") |
|
409 |
#' fmt_config(format = "xx.xx - xx.xx", align = "right") |
|
410 |
#' |
|
411 |
#' @export |
|
412 |
fmt_config <- function(format = NULL, na_str = "NA", align = "center") { |
|
413 | 2x |
new("fmt_config", format = format, format_na_str = na_str, align = align) |
414 |
} |
1 | ||
2 | ||
3 | ||
4 | ||
5 |
#' Return an object with a label attribute |
|
6 |
#' |
|
7 |
#' @param x an object |
|
8 |
#' @param label label attribute to to attached to \code{x} |
|
9 |
#' |
|
10 |
#' @export |
|
11 |
#' @return \code{x} labeled by \code{label}. Note: the exact mechanism of labeling should be |
|
12 |
#' considered an internal implementation detail, but the label will always be retrieved via \code{obj_label}. |
|
13 |
#' @examples |
|
14 |
#' x <- with_label(c(1, 2, 3), label = "Test") |
|
15 |
#' obj_label(x) |
|
16 |
with_label <- function(x, label) { |
|
17 | 1x |
obj_label(x) <- label |
18 | 1x |
x |
19 |
} |
|
20 | ||
21 | ||
22 |
#' Get Label Attributes of Variables in a \code{data.frame} |
|
23 |
#' |
|
24 |
#' Variable labels can be stored as a \code{label} attribute for each variable. |
|
25 |
#' This functions returns a named character vector with the variable labels |
|
26 |
#' (empty sting if not specified) |
|
27 |
#' |
|
28 |
#' @param x a \code{data.frame} object |
|
29 |
#' @param fill boolean in case the \code{label} attribute does not exist if |
|
30 |
#' \code{TRUE} the variable names is returned, otherwise \code{NA} |
|
31 |
#' |
|
32 |
#' @return a named character vector with the variable labels, the names |
|
33 |
#' correspond to the variable names |
|
34 |
#' |
|
35 |
#' @export |
|
36 |
#' |
|
37 |
#' @examples |
|
38 |
#' x <- iris |
|
39 |
#' var_labels(x) |
|
40 |
#' var_labels(x) <- paste("label for", names(iris)) |
|
41 |
#' var_labels(x) |
|
42 |
var_labels <- function(x, fill = FALSE) { |
|
43 | 4x |
stopifnot(is.data.frame(x)) |
44 | 4x |
if (NCOL(x) == 0) { |
45 | 1x |
return(character()) |
46 |
} |
|
47 | ||
48 | 3x |
y <- Map(function(col, colname) { |
49 | 33x |
label <- attr(col, "label") |
50 | ||
51 | 33x |
if (is.null(label)) { |
52 | 11x |
if (fill) { |
53 | ! |
colname |
54 |
} else { |
|
55 | 3x |
NA_character_ |
56 |
} |
|
57 |
} else { |
|
58 | 22x |
if (!is.character(label) && !(length(label) == 1)) { |
59 | ! |
stop("label for variable ", colname, "is not a character string") |
60 |
} |
|
61 | 22x |
as.vector(label) |
62 |
} |
|
63 | 3x |
}, x, colnames(x)) |
64 | ||
65 | 3x |
labels <- unlist(y, recursive = FALSE, use.names = TRUE) |
66 | ||
67 | 3x |
if (!is.character(labels)) { |
68 | ! |
stop("label extraction failed") |
69 |
} |
|
70 | ||
71 | 3x |
labels |
72 |
} |
|
73 | ||
74 | ||
75 |
#' Set Label Attributes of All Variables in a \code{data.frame} |
|
76 |
#' |
|
77 |
#' Variable labels can be stored as a \code{label} attribute for each variable. |
|
78 |
#' This functions sets all non-missing (non-NA) variable labels in a \code{data.frame} |
|
79 |
#' |
|
80 |
#' @inheritParams var_labels |
|
81 |
#' @param value new variable labels, \code{NA} removes the variable label |
|
82 |
#' |
|
83 |
#' @return modifies the variable labels of \code{x} |
|
84 |
#' |
|
85 |
#' @export |
|
86 |
#' |
|
87 |
#' @examples |
|
88 |
#' x <- iris |
|
89 |
#' var_labels(x) |
|
90 |
#' var_labels(x) <- paste("label for", names(iris)) |
|
91 |
#' var_labels(x) |
|
92 |
#' |
|
93 |
#' if (interactive()) { |
|
94 |
#' View(x) # in RStudio data viewer labels are displayed |
|
95 |
#' } |
|
96 |
`var_labels<-` <- function(x, value) { |
|
97 | 1x |
stopifnot( |
98 | 1x |
is.data.frame(x), |
99 | 1x |
is.character(value), |
100 | 1x |
ncol(x) == length(value) |
101 |
) |
|
102 | ||
103 | 1x |
theseq <- if (!is.null(names(value))) names(value) else seq_along(x) |
104 |
# across columns of x |
|
105 | 1x |
for (j in theseq) { |
106 | 11x |
attr(x[[j]], "label") <- if (!is.na(value[j])) { |
107 | 11x |
value[j] |
108 |
} else { |
|
109 | ! |
NULL |
110 |
} |
|
111 |
} |
|
112 | ||
113 | 1x |
x |
114 |
} |
|
115 | ||
116 | ||
117 |
#' Copy and Change Variable Labels of a \code{data.frame} |
|
118 |
#' |
|
119 |
#' Relabel a subset of the variables |
|
120 |
#' |
|
121 |
#' @inheritParams var_labels<- |
|
122 |
#' @param ... name-value pairs, where name corresponds to a variable name in |
|
123 |
#' \code{x} and the value to the new variable label |
|
124 |
#' |
|
125 |
#' @return a copy of \code{x} with changed labels according to \code{...} |
|
126 |
#' |
|
127 |
#' @export |
|
128 |
#' |
|
129 |
#' @examples |
|
130 |
#' x <- var_relabel(iris, Sepal.Length = "Sepal Length of iris flower") |
|
131 |
#' var_labels(x) |
|
132 |
#' |
|
133 |
var_relabel <- function(x, ...) { |
|
134 |
# todo: make this function more readable / code easier |
|
135 | 1x |
stopifnot(is.data.frame(x)) |
136 | 1x |
if (missing(...)) { |
137 | ! |
return(x) |
138 |
} |
|
139 | 1x |
dots <- list(...) |
140 | 1x |
varnames <- names(dots) |
141 | 1x |
stopifnot(!is.null(varnames)) |
142 | ||
143 | 1x |
map_varnames <- match(varnames, colnames(x)) |
144 | ||
145 | 1x |
if (any(is.na(map_varnames))) { |
146 | ! |
stop("variables: ", paste(varnames[is.na(map_varnames)], collapse = ", "), " not found") |
147 |
} |
|
148 | ||
149 | 1x |
if (any(vapply(dots, Negate(is.character), logical(1)))) { |
150 | ! |
stop("all variable labels must be of type character") |
151 |
} |
|
152 | ||
153 | 1x |
for (i in seq_along(map_varnames)) { |
154 | 1x |
attr(x[[map_varnames[[i]]]], "label") <- dots[[i]] |
155 |
} |
|
156 | ||
157 | 1x |
x |
158 |
} |
|
159 | ||
160 | ||
161 |
#' Remove Variable Labels of a \code{data.frame} |
|
162 |
#' |
|
163 |
#' Removing labels attributes from a variables in a data frame |
|
164 |
#' |
|
165 |
#' @param x a \code{data.frame} object |
|
166 |
#' |
|
167 |
#' @return the same data frame as \code{x} stripped of variable labels |
|
168 |
#' |
|
169 |
#' @export |
|
170 |
#' |
|
171 |
#' @examples |
|
172 |
#' x <- var_labels_remove(iris) |
|
173 |
var_labels_remove <- function(x) { |
|
174 | 1x |
stopifnot(is.data.frame(x)) |
175 | ||
176 | 1x |
for (i in seq_len(ncol(x))) { |
177 | 11x |
attr(x[[i]], "label") <- NULL |
178 |
} |
|
179 | ||
180 | 1x |
x |
181 |
} |
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 | ||
6 |
#' Export a table-like object to plain (ASCII) text with page break |
|
7 |
#' |
|
8 |
#' This function converts \code{x} to a \code{MatrixPrintForm} object via |
|
9 |
#' \code{matrix_form}, paginates it via \code{paginate}, converts each |
|
10 |
#' page to ASCII text via \code{toString}, and emits the strings to \code{file}, |
|
11 |
#' separated by \code{page_break}. |
|
12 |
#' |
|
13 |
#' @inheritParams paginate_indices |
|
14 |
#' @inheritParams toString |
|
15 |
#' @inheritParams propose_column_widths |
|
16 |
#' @param x ANY. The table-like object to export. Must have an |
|
17 |
#' applicable \code{matrix_form} method. |
|
18 |
#' @param file character(1) or NULL. If non-NULL, the path to write a |
|
19 |
#' text file to containing the \code{x} rendered as ASCII text, |
|
20 |
#' @param page_break character(1). Page break symbol (defaults to |
|
21 |
#' outputting \code{"\\n\\s"}). |
|
22 |
#' @param paginate logical(1). Whether pagination should be performed, |
|
23 |
#' defaults to \code{TRUE} if page size is specified (including |
|
24 |
#' the default). |
|
25 |
#' @details if \code{x} has an \code{num_rep_cols} method, the value |
|
26 |
#' returned by it will be used for \code{rep_cols} by default, if |
|
27 |
#' not, 0 will be used. |
|
28 |
#' |
|
29 |
#' If \code{x} has an applicable \code{do_mand_paginate} method, it will be invoked |
|
30 |
#' during the pagination process. |
|
31 |
#' |
|
32 |
#' @return if \code{file} is NULL, the total paginated and then concatenated |
|
33 |
#' string value, otherwise the file that was written. |
|
34 |
#' @export |
|
35 |
#' @examples |
|
36 |
#' export_as_txt(basic_matrix_form(mtcars), pg_height = 5, pg_width = 4) |
|
37 | ||
38 |
export_as_txt <- function(x, |
|
39 |
file = NULL, |
|
40 |
page_type = NULL, |
|
41 |
landscape = FALSE, |
|
42 |
pg_width = page_dim(page_type)[if(landscape) 2 else 1], |
|
43 |
pg_height = page_dim(page_type)[if(landscape) 1 else 2], |
|
44 |
font_family = "Courier", |
|
45 |
font_size = 8, # grid parameters |
|
46 |
lineheight = 1L, |
|
47 |
margins = c(top = .5, bottom = .5, left = .75, right = .75), |
|
48 |
paginate = TRUE, |
|
49 |
cpp = NA_integer_, |
|
50 |
lpp = NA_integer_, |
|
51 |
..., |
|
52 |
hsep = default_hsep(), |
|
53 |
indent_size = 2, |
|
54 |
tf_wrap = paginate, |
|
55 |
max_width = NULL, |
|
56 |
colwidths = NULL, |
|
57 |
min_siblings = 2, |
|
58 |
nosplitin = character(), |
|
59 |
rep_cols = num_rep_cols(x), |
|
60 |
verbose = FALSE, |
|
61 |
page_break = "\\s\\n") { |
|
62 | ||
63 | 2x |
if(paginate) { |
64 | 2x |
pages <- paginate_to_mpfs(x, |
65 | 2x |
page_type = page_type, |
66 | 2x |
font_family = font_family, |
67 | 2x |
font_size = font_size, |
68 | 2x |
lineheight = lineheight, |
69 | 2x |
landscape = landscape, |
70 | 2x |
pg_width = pg_width, |
71 | 2x |
pg_height = pg_height, |
72 | 2x |
margins = margins, |
73 | 2x |
lpp = lpp, |
74 | 2x |
cpp = cpp, |
75 | 2x |
min_siblings = min_siblings, |
76 | 2x |
nosplitin = nosplitin, |
77 | 2x |
colwidths = colwidths, |
78 | 2x |
tf_wrap = tf_wrap, |
79 | 2x |
max_width = max_width, |
80 | 2x |
indent_size = indent_size, |
81 | 2x |
verbose = verbose) |
82 |
} else { |
|
83 | ! |
mf <- matrix_form(x, TRUE, TRUE, indent_size = indent_size) |
84 | ! |
mf_col_widths(mf) <- colwidths %||% propose_column_widths(mf) |
85 | ! |
pages <- list(mf) |
86 |
} |
|
87 |
## we dont' set widths here because we already but that info on mpf |
|
88 |
## so its on each of the pages. |
|
89 | 2x |
strings <- vapply(pages, toString, "", hsep = hsep, tf_wrap = tf_wrap, max_width = max_width) |
90 | 2x |
res <- paste(strings, collapse = page_break) |
91 | ||
92 | 2x |
if(is.null(file)) |
93 | ! |
res |
94 |
else |
|
95 | 2x |
cat(res, file = file) |
96 |
} |
|
97 | ||
98 | ||
99 | ||
100 |
## ## TODO this needs to be in terms of a MPF, so ncol(tt) needs to change |
|
101 | ||
102 |
## ## if(!is.null(colwidths) && length(colwidths) != ncol(tt) + 1) |
|
103 |
## ## stop("non-null colwidths argument must have length ncol(tt) + 1 [", |
|
104 |
## ## ncol(tt) + 1, "], got length ", length(colwidths)) |
|
105 | ||
106 |
## mpf <- matrix_form(x, indent_rownames = TRUE) |
|
107 | ||
108 |
## ps_spec <- calc_lcpp(page_type = page_type, |
|
109 |
## landscape = landscape, |
|
110 |
## pg_width = pg_width, |
|
111 |
## pg_height = pg_height, |
|
112 |
## font_family = font_family, |
|
113 |
## cpp = cpp, |
|
114 |
## lpp = lpp) |
|
115 | ||
116 |
## ## This needs to return list(x) in cases where no pagination was necessary |
|
117 |
## idx_lst <- paginate(mpf, .page_size_spec = ps_spec, colwidths = colwidths, |
|
118 |
## tf_wrap = tf_wrap, ## XXX I think we don't need this |
|
119 |
## ...) |
|
120 | ||
121 |
## tbls <- lapply(idx_lst, function(ii) |
|
122 |
## ## XXX how do we partition the colwidths ??? |
|
123 |
## ## Also this is gross make it a function!!! |
|
124 |
## res <- paste(mapply(function(tb, cwidths, ...) { |
|
125 |
## ## 1 and +1 are because cwidths includes rowlabel 'column' |
|
126 |
## cinds <- c(1, .figure_out_colinds(tb, tt) + 1L) |
|
127 |
## toString(tb, widths = cwidths[cinds], ...) |
|
128 |
## }, |
|
129 |
## MoreArgs = list(hsep = hsep, |
|
130 |
## indent_size = indent_size, |
|
131 |
## tf_wrap = tf_wrap, |
|
132 |
## max_width = max_width, |
|
133 |
## cwidths = colwidths), |
|
134 |
## SIMPLIFY = FALSE, |
|
135 |
## tb = tbls), |
|
136 |
## collapse = page_break) |
|
137 | ||
138 |
## if(!is.null(file)) |
|
139 |
## cat(res, file = file) |
|
140 |
## else |
|
141 |
## res |
|
142 |
## } |
|
143 | ||
144 | ||
145 | ||
146 | ||
147 |
## In use, must be tested |
|
148 |
prep_header_line <- function(mf, i) { |
|
149 | 2x |
ret <- mf$strings[i, mf$display[i, , drop = TRUE], drop = TRUE] |
150 | 2x |
ret |
151 |
} |
|
152 | ||
153 |
## margin_lines_to_in <- function(margins, font_size, font_family) { |
|
154 |
## tmpfile <- tempfile(fileext = ".pdf") |
|
155 |
## gp_plot <- gpar(fontsize = font_size, fontfamily = font_family) |
|
156 |
## pdf(file = tmpfile, width = 20, height = 20) |
|
157 |
## on.exit({ |
|
158 |
## dev.off() |
|
159 |
## file.remove(tmpfile) |
|
160 |
## }) |
|
161 |
## grid.newpage() |
|
162 |
## pushViewport(plotViewport(margins = margins, gp = gp_plot)) |
|
163 |
## c( |
|
164 |
## bottom = convertHeight(unit(margins["bottom"], "lines"), "inches", valueOnly = TRUE), |
|
165 |
## left = convertWidth(unit(1, "strwidth", strrep("m", margins["left"])), "inches", valueOnly = TRUE), |
|
166 |
## top = convertHeight(unit(margins["top"], "lines"), "inches", valueOnly = TRUE), |
|
167 |
## right = convertWidth(unit(1, "strwidth", strrep("m", margins["right"])), "inches", valueOnly = TRUE) |
|
168 |
## ) |
|
169 |
## } |
|
170 | ||
171 | ||
172 | ||
173 | ||
174 |
mpf_to_dfbody <- function(mpf, colwidths) { |
|
175 | 2x |
mf <- matrix_form(mpf, indent_rownames = TRUE) |
176 | 2x |
nlr <- mf_nlheader(mf) |
177 | 2x |
if (is.null(colwidths)) { |
178 | ! |
colwidths <- propose_column_widths(mf) |
179 |
} |
|
180 | 2x |
mf$strings[1:nlr, 1] <- ifelse(nzchar(mf$strings[1:nlr, 1, drop = TRUE]), |
181 | 2x |
mf$strings[1:nlr, 1, drop = TRUE], |
182 | 2x |
strrep(" ", colwidths) |
183 |
) |
|
184 | ||
185 | ||
186 | 2x |
myfakedf <- as.data.frame(tail(mf$strings, -nlr)) |
187 | 2x |
myfakedf |
188 |
} |
|
189 | ||
190 | ||
191 |
#' Transform `MPF` to `RTF` |
|
192 |
#' |
|
193 |
#' Experimental export to `RTF` via the `r2rtf` package |
|
194 |
#' |
|
195 |
#' @inheritParams page_lcpp |
|
196 |
#' @inheritParams toString |
|
197 |
#' @inheritParams grid::plotViewport |
|
198 |
#' @param mpf `MatrixPrintForm`. `MatrixPrintForm` object. |
|
199 |
#' @param colwidths character(1). Column widths. |
|
200 |
#' @details This function provides a low-level coercion of a |
|
201 |
#' `MatrixPrintForm` object into text containing the corresponding |
|
202 |
#' table in `RTF`. Currently, no pagination is done at this level, |
|
203 |
#' and should be done prior to calling this function, though that |
|
204 |
#' may change in the future. |
|
205 |
#' |
|
206 |
#' @return An `RTF` object |
|
207 |
#' @export |
|
208 |
mpf_to_rtf <- function(mpf, |
|
209 |
colwidths = NULL, |
|
210 |
page_type = "letter", |
|
211 |
pg_width = page_dim(page_type)[if (landscape) 2 else 1], |
|
212 |
pg_height = page_dim(page_type)[if (landscape) 1 else 2], |
|
213 |
landscape = FALSE, |
|
214 |
margins = c(4, 4, 4, 4), |
|
215 |
font_size = 8, |
|
216 |
...) { |
|
217 | 2x |
if (!requireNamespace("r2rtf")) { |
218 | ! |
stop("RTF export requires the 'r2rtf' package, please install it.") |
219 |
} |
|
220 | 2x |
mpf <- matrix_form(mpf, indent_rownames = TRUE) |
221 | 2x |
nlr <- mf_nlheader(mpf) |
222 | 2x |
if (is.null(colwidths)) { |
223 | ! |
colwidths <- propose_column_widths(mpf) |
224 |
} |
|
225 | 2x |
mpf$strings[1:nlr, 1] <- ifelse(nzchar(mpf$strings[1:nlr, 1, drop = TRUE]), |
226 | 2x |
mpf$strings[1:nlr, 1, drop = TRUE], |
227 | 2x |
strrep(" ", colwidths) |
228 |
) |
|
229 | ||
230 | 2x |
myfakedf <- mpf_to_dfbody(mpf, colwidths) |
231 | ||
232 | 2x |
rtfpg <- r2rtf::rtf_page(myfakedf, |
233 | 2x |
width = pg_width, |
234 | 2x |
height = pg_height, |
235 | 2x |
orientation = if (landscape) "landscape" else "portrait", |
236 | 2x |
margin = c(0.1, 0.1, 0.1, 0.1, 0.1, 0.1), |
237 | 2x |
nrow = 10000L |
238 | 2x |
) ## dont allow r2rtf to restrict lines per page beyond actual real eastate |
239 | 2x |
rtfpg <- r2rtf::rtf_title(rtfpg, main_title(mpf), subtitles(mpf), text_font = 1) |
240 | 2x |
for (i in seq_len(nlr)) { |
241 | 2x |
hdrlndat <- prep_header_line(mpf, i) |
242 | 2x |
rtfpg <- r2rtf::rtf_colheader(rtfpg, |
243 | 2x |
paste(hdrlndat, collapse = " | "), |
244 | 2x |
col_rel_width = unlist(tapply(colwidths, |
245 | 2x |
cumsum(mpf$display[i, , drop = TRUE]), |
246 | 2x |
sum, |
247 | 2x |
simplify = FALSE |
248 |
)), |
|
249 | 2x |
border_top = c("", rep(if (i > 1) "single" else "", length(hdrlndat) - 1)), |
250 | 2x |
text_font = 9, ## this means Courier New for some insane reason |
251 | 2x |
text_font_size = font_size |
252 |
) |
|
253 |
} |
|
254 | ||
255 | 2x |
rtfpg <- r2rtf::rtf_body(rtfpg, |
256 | 2x |
col_rel_width = colwidths, |
257 | 2x |
text_justification = c("l", rep("c", ncol(myfakedf) - 1)), |
258 | 2x |
text_format = "", |
259 | 2x |
text_font = 9, |
260 | 2x |
text_font_size = font_size |
261 |
) |
|
262 | ||
263 | 2x |
for (i in seq_along(mpf$ref_footnotes)) { |
264 | 4x |
rtfpg <- r2rtf::rtf_footnote(rtfpg, |
265 | 4x |
mpf$ref_footnotes[i], |
266 | 4x |
border_top = if (i == 1) "single" else "", |
267 | 4x |
border_bottom = if (i == length(mpf$ref_footnotes)) "single" else "", |
268 | 4x |
text_font = 9 |
269 |
) |
|
270 |
} |
|
271 | ||
272 | 2x |
if (length(main_footer(mpf)) > 0) { |
273 | 2x |
rtfpg <- r2rtf::rtf_footnote(rtfpg, main_footer(mpf), text_font = 9) |
274 |
} |
|
275 | 2x |
if (length(prov_footer(mpf)) > 0) { |
276 | 2x |
rtfpg <- r2rtf::rtf_source(rtfpg, prov_footer(mpf), text_font = 9) |
277 |
} |
|
278 | ||
279 | 2x |
rtfpg |
280 |
} |
|
281 | ||
282 |
## Not currently in use, previous alternate ways to get to RTF |
|
283 | ||
284 |
## ## XXX Experimental. Not to be exported without approval |
|
285 |
## mpf_to_huxtable <- function(obj) { |
|
286 |
## if (!requireNamespace("huxtable")) { |
|
287 |
## stop("mpf_to_huxtable requires the huxtable package") |
|
288 |
## } |
|
289 |
## mf <- matrix_form(obj, indent_rownames = TRUE) |
|
290 |
## nlr <- mf_nlheader(mf) |
|
291 |
## myfakedf <- as.data.frame(tail(mf$strings, -nlr)) |
|
292 |
## ret <- huxtable::as_hux(myfakedf, add_colnames = FALSE) |
|
293 |
## mf$strings[!mf$display] <- "" |
|
294 |
## for (i in seq_len(nlr)) { |
|
295 |
## arglist <- c( |
|
296 |
## list(ht = ret, after = i - 1), |
|
297 |
## as.list(mf$strings[i, ]) |
|
298 |
## ) |
|
299 |
## ret <- do.call(huxtable::insert_row, arglist) |
|
300 | ||
301 |
## spanspl <- split( |
|
302 |
## seq_len(ncol(mf$strings)), |
|
303 |
## cumsum(mf$display[i, ]) |
|
304 |
## ) |
|
305 | ||
306 | ||
307 |
## for (j in seq_along(spanspl)) { |
|
308 |
## if (length(spanspl[[j]]) > 1) { |
|
309 |
## ret <- huxtable::merge_cells(ret, row = i, col = spanspl[[j]]) |
|
310 |
## } |
|
311 |
## } |
|
312 |
## } |
|
313 |
## ret <- huxtable::set_header_rows(ret, seq_len(nlr), TRUE) |
|
314 |
## huxtable::font(ret) <- "courier" |
|
315 |
## huxtable::font_size(ret) <- 6 |
|
316 |
## huxtable::align(ret)[ |
|
317 |
## seq_len(nrow(ret)), |
|
318 |
## seq_len(ncol(ret)) |
|
319 |
## ] <- mf$aligns |
|
320 |
## ret |
|
321 |
## } |
|
322 | ||
323 |
## ## XXX Experimental. Not to be exported without approval |
|
324 |
## mpf_to_rtf <- function(obj, ..., file) { |
|
325 |
## huxt <- mpf_to_huxtable(obj) |
|
326 |
## ## a bunch more stuff here |
|
327 |
## huxtable::quick_rtf(huxt, ..., file = file) |
|
328 |
## } |
|
329 | ||
330 | ||
331 | ||
332 | ||
333 |
## ## XXX Experimental. Not to be exported without approval |
|
334 |
## mpf_to_gt <- function(obj) { |
|
335 |
## requireNamespace("gt") |
|
336 |
## mf <- matrix_form(obj, indent_rownames = TRUE) |
|
337 |
## nlh <- mf_nlheader(mf) |
|
338 |
## body_df <- as.data.frame(mf$strings[-1 * seq_len(nlh), ]) |
|
339 |
## varnamerow <- mf_nrheader(mf) |
|
340 |
## ## detect if we have counts |
|
341 |
## if (any(nzchar(mf$formats[seq_len(nlh), ]))) { |
|
342 |
## varnamerow <- varnamerow - 1 |
|
343 |
## } |
|
344 | ||
345 |
## rlbl_lst <- as.list(mf$strings[nlh, , drop = TRUE]) |
|
346 |
## names(rlbl_lst) <- names(body_df) |
|
347 | ||
348 |
## ret <- gt::gt(body_df, rowname_col = "V1") |
|
349 |
## ret <- gt::cols_label(ret, .list = rlbl_lst) |
|
350 |
## if (nlh > 1) { |
|
351 |
## for (i in 1:(nlh - 1)) { |
|
352 |
## linedat <- mf$strings[i, , drop = TRUE] |
|
353 |
## splvec <- cumsum(mf$display[i, , drop = TRUE]) |
|
354 |
## spl <- split(seq_along(linedat), splvec) |
|
355 |
## for (j in seq_along(spl)) { |
|
356 |
## vns <- names(body_df)[spl[[j]]] |
|
357 |
## labval <- linedat[spl[[j]][1]] |
|
358 |
## ret <- gt::tab_spanner(ret, |
|
359 |
## label = labval, |
|
360 |
## columns = {{ vns }}, |
|
361 |
## level = nlh - i, |
|
362 |
## id = paste0(labval, j) |
|
363 |
## ) |
|
364 |
## } |
|
365 |
## } |
|
366 |
## } |
|
367 | ||
368 |
## ret <- gt::opt_css(ret, css = "th.gt_left { white-space:pre;}") |
|
369 | ||
370 |
## ret |
|
371 |
## } |
|
372 | ||
373 | ||
374 | ||
375 |
#' Export table to `RTF` |
|
376 |
#' |
|
377 |
#' Experimental export to the `RTF` format. |
|
378 |
#' |
|
379 |
#' @details `RTF` export occurs by via the following steps |
|
380 |
#' |
|
381 |
#' \itemize{ |
|
382 |
#' \item{the table is paginated to the page size (Vertically and horizontally)} |
|
383 |
#' \item{Each separate page is converted to a `MatrixPrintForm` and from there to `RTF`-encoded text} |
|
384 |
#' \item{Separate `RTFs` text chunks are combined and written out as a single `RTF` file} |
|
385 |
#' } |
|
386 |
#' |
|
387 |
#' Conversion of `MatrixPrintForm` objects to `RTF` is done via [formatters::mpf_to_rtf()]. |
|
388 |
#' @inheritParams export_as_txt |
|
389 |
#' @inheritParams toString |
|
390 |
#' @inheritParams grid::plotViewport |
|
391 |
#' @inheritParams paginate_to_mpfs |
|
392 |
#' @export |
|
393 | ||
394 |
export_as_rtf <- function(x, |
|
395 |
file = NULL, |
|
396 |
colwidths = propose_column_widths(matrix_form(x, TRUE)), |
|
397 |
page_type = "letter", |
|
398 |
pg_width = page_dim(page_type)[if(landscape) 2 else 1], |
|
399 |
pg_height = page_dim(page_type)[if(landscape) 1 else 2], |
|
400 |
landscape = FALSE, |
|
401 |
margins = c(bottom = .5, left = .75, top=.5, right = .75), |
|
402 |
font_size = 8, |
|
403 |
font_family = "Courier", |
|
404 |
...) { |
|
405 | 1x |
if(!requireNamespace("r2rtf")) |
406 | ! |
stop("RTF export requires the r2rtf package, please install it.") |
407 | 1x |
if(is.null(names(margins))) |
408 | ! |
names(margins) <- marg_order |
409 | ||
410 | 1x |
fullmf <- matrix_form(x) |
411 | 1x |
req_ncols <- ncol(fullmf) + as.numeric(mf_has_rlabels(fullmf)) |
412 | 1x |
if(!is.null(colwidths) && length(colwidths) != req_ncols) |
413 | ! |
stop("non-null colwidths argument must have length ncol(x) (+ 1 if row labels are present) [", |
414 | ! |
req_ncols, "], got length ", length(colwidths)) |
415 | ||
416 | 1x |
true_width <- pg_width - sum(margins[c("left", "right")]) |
417 | 1x |
true_height <- pg_height - sum(margins[c("top", "bottom")]) |
418 | ||
419 | 1x |
mpfs <- paginate_to_mpfs(fullmf, font_family = font_family, font_size = font_size, |
420 | 1x |
pg_width = true_width, |
421 | 1x |
pg_height = true_height, |
422 | 1x |
margins = c(bottom = 0, left = 0, top = 0, right = 0), |
423 | 1x |
lineheight = 1.25, |
424 | 1x |
colwidths = colwidths, |
425 |
...) |
|
426 | ||
427 | 1x |
rtftxts <- lapply(mpfs, function(mf) r2rtf::rtf_encode(mpf_to_rtf(mf, |
428 | 1x |
colwidths = mf_col_widths(mf), |
429 | 1x |
page_type = page_type, |
430 | 1x |
pg_width = pg_width, |
431 | 1x |
pg_height = pg_height, |
432 | 1x |
font_size = font_size, |
433 | 1x |
margins = c(top = 0, left = 0, bottom = 0, right = 0)))) |
434 | 1x |
restxt <- paste(rtftxts[[1]]$start, |
435 | 1x |
paste(sapply(rtftxts, function(x) x$body), collapse = "\n{\\pard\\fs2\\par}\\page{\\pard\\fs2\\par}\n"), |
436 | 1x |
rtftxts[[1]]$end) |
437 | 1x |
if(!is.null(file)) |
438 | 1x |
cat(restxt, file = file) |
439 |
else |
|
440 | ! |
restxt |
441 |
} |
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 |
#' @name ifnotlen0 |
|
7 |
#' |
|
8 |
#' |
|
9 |
#' |
|
10 |
#' @param a ANY. Element to select only if it is not length 0 |
|
11 |
#' @param b ANY. Element to select if \code{a} is length 0 |
|
12 |
#' @export |
|
13 |
#' @examples |
|
14 |
#' 6 %||% 10 |
|
15 |
#' |
|
16 |
#' character() %||% "hi" |
|
17 |
#' |
|
18 |
#' NULL %||% "hi" |
|
19 |
#' @return `a`, unless it is length 0, in which case `b` (even in the |
|
20 |
#' case `b` is also length 0) |
|
21 | 185x |
`%||%` <- function(a, b) if (length(a) == 0) b else a |