A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://insightsengineering.github.io/formatters/main/coverage-report/ below:

1
## state completely sucks and I hate it, but
2
## we need a pdf device open to calculate the
3
## print width of strings, and we can't be opening
4
## a new one every time we want to
5
font_dev_state <- new.env()
6
font_dev_state$open <- FALSE
7
font_dev_state$fontspec <- list()
8
font_dev_state$spacewidth <- NA_real_
9
font_dev_state$ismonospace <- NA
10
font_dev_state$max_ratio <- NA_real_
11
font_dev_state$dev_num <- NA_integer_
12
font_dev_state$debug_active <- FALSE
13
 14 
 15 
cwidth_inches_unsafe <- function(x) {
16 1136x
  convertWidth(unit(1, "strwidth", x), "inches", valueOnly = TRUE)
17
}
18
 19 
## returns whether it opened a new device or not
20
#' Activate font state
21
#'
22
#' @param fontspec (`font_spec`)\cr a font_spec object specifying the font information to use for
23
#'   calculating string widths and heights, as returned by [font_spec()].
24
#' @param silent (`logical(1)`)\cr If `FALSE`, the default, a warning will be
25
#'   emitted if this function switches away from an active graphics device.
26
#'
27
#' @details The font device state is an environment with
28
#' four variables guaranteed to be set:
29
#'
30
#' \describe{
31
#'   \item{`open`}{(`logical(1)`)\cr whether a device is already open with font info}
32
#'   \item{`fontspec`}{(`font_spec`)\cr the font specification, if any, that is currently active (`list()` if none is).}
33
#'   \item{`spacewidth`}{(`numeric(1)`)\cr the width of the space character in the currently active font.}
34
#'   \item{`ismonospace`}{(`logical(1)`)\cr whether the specified font is monospaced.}
35
#' }
36
#'
37
#' `open_font_dev` opens a pdf device with the specified font
38
#' only if there is not one currently open with the same font.
39
#' If a new device is opened, it caches `spacewidth` and
40
#' `ismonospace` for use in `nchar_ttype`).
41
#'
42
#' `close_font_dev` closes any open font state device
43
#' and clears the cached values.
44
#'
45
#' `debug_font_dev` and `undebug_font_dev` activate and deactivate, respectively,
46
#' logging of where in the call stack font devices are being opened.
47
#'
48
#' @return
49
#' - `open_font_dev` returns a logical value indicating whether a *new* pdf device was opened.
50
#' - `close_font_dev`, `debug_font_dev` and `undebug_font_dev` return `NULL`.
51
#'
52
#' In all cases the value is returned invisibly.
53
#'
54
#' @examples
55
#' open_font_dev(font_spec("Times"))
56
#' nchar_ttype("Hiya there", font_spec("Times"))
57
#' close_font_dev()
58
#'
59
#' @export
60
open_font_dev <- function(fontspec, silent = FALSE) {
61 82300x
  if (is.null(fontspec)) {
62 1x
    return(invisible(FALSE))
63 82298x
  } else if (font_dev_is_open()) {
64 81788x
    if (identical(font_dev_state$fontspec, fontspec)) {
65 81787x
      if (!silent && dev.cur() != font_dev_state$dev_num) {
66 1x
        warning(
67 1x
          "formatters is switching to the font state graphics device ",
68 1x
          "to perform string width calculations. You may need to switch ",
69 1x
          "to your currently open graphics device, depending on whether ",
70 1x
          "the font device is closed and what other devices you have open."
71
        )
72 1x
        dev.set(font_dev_state$dev_num)
73
      }
74 81787x
      return(invisible(FALSE))
75
    } else {
76 1x
      close_font_dev()
77
    }
78
  }
79 511x
  if (font_dev_state$debug_active && !font_dev_is_open()) { ## call debug_font_dev beforehand to get debugging info to helplocate places which aren't receiving/using the state properly # nolint
80
    ## dump the call stack any time we have cache misses
81
    ## and have to open a completely new font state device
82 1x
    scalls <- sys.calls()
83 1x
    msg <- sapply(
84 1x
      scalls[2:length(scalls)],
85 1x
      function(sci) {
86 42x
        toret <- deparse(sci[[1]], nlines = 3)
87 42x
        if (substr(toret[1], 1, 8) == "function") {
88 !
          toret <- "anon function"
89
        }
90 42x
        toret
91
      }
92
    )
93 1x
    message(paste("\n***** START font dev debugging dump *****\n",
94 1x
      paste(msg, collapse = " -> "),
95 1x
      paste(capture.output(print(fontspec)), collapse = "\n"),
96 1x
      sep = "\n"
97
    ))
98
  }
99 511x
  tmppdf <- tempfile(fileext = ".pdf")
100 511x
  pdf(tmppdf)
101 511x
  grid.newpage()
102 511x
  gp <- gpar_from_fspec(fontspec)
103 511x
  pushViewport(plotViewport(gp = gp))
104 511x
  spcwidth <- cwidth_inches_unsafe(" ")
105 511x
  assign("open", TRUE, envir = font_dev_state)
106 511x
  assign("fontspec", fontspec, envir = font_dev_state)
107 511x
  assign("spacewidth", spcwidth, envir = font_dev_state)
108 511x
  assign("ismonospace", spcwidth == cwidth_inches_unsafe("W"),
109 511x
    envir = font_dev_state
110
  )
111 511x
  assign("dev_num", dev.cur(),
112 511x
    envir = font_dev_state
113
  )
114 511x
  invisible(TRUE)
115
}
116
 117 
#' @rdname open_font_dev
118
#' @export
119
close_font_dev <- function() {
120 511x
  if (font_dev_state$open) {
121 511x
    dev.off(font_dev_state$dev_num)
122 511x
    assign("open", FALSE, envir = font_dev_state)
123 511x
    assign("fontspec", list(), envir = font_dev_state)
124 511x
    assign("spacewidth", NA_real_, envir = font_dev_state)
125 511x
    assign("ismonospace", NA, envir = font_dev_state)
126 511x
    assign("dev_num", NA_integer_, envir = font_dev_state)
127
  }
128 511x
  invisible(NULL)
129
}
130
 131 
#' @rdname open_font_dev
132
#' @export
133
debug_font_dev <- function() {
134 1x
  message("debugging font device swapping. call undebug_font_dev() to turn debugging back off.")
135 1x
  font_dev_state$debug_active <- TRUE
136 1x
  invisible(NULL)
137
}
138
 139 
#' @rdname open_font_dev
140
#' @export
141
undebug_font_dev <- function() {
142 1x
  message("no longer debugging font device swapping.")
143 1x
  font_dev_state$debug_active <- FALSE
144 1x
  invisible(NULL)
145
}
146
 147 
 148 
## can only be called when font_dev_state$open is TRUE
149
get_space_width <- function() {
150 25x
  if (!font_dev_is_open()) {
151 1x
    stop(
152 1x
      "get_space_width called when font dev state is not open. ",
153 1x
      "This shouldn't happen, please contact the maintainers."
154
    )
155
  }
156 24x
  font_dev_state$spacewidth
157
}
158
 159 
.open_fdev_is_monospace <- function() {
160 32671x
  if (!font_dev_is_open()) {
161 2x
    stop(
162 2x
      ".open_fdev_is_monospace called when font dev state is not open. ",
163 2x
      "This shouldn't happen, please contact the maintainers."
164
    )
165
  }
166 32669x
  font_dev_state$ismonospace
167
}
168
 169 
## safe wrapper around .open_fdev_is_monospace
170
is_monospace <- function(fontspec = font_spec(font_family, font_size, lineheight),
171
                         font_family = "Courier",
172
                         font_size = 8,
173
                         lineheight = 1) {
174 32670x
  if (is.null(fontspec)) {
175 1x
    return(TRUE)
176
  }
177 32669x
  new_dev <- open_font_dev(fontspec)
178 32669x
  if (new_dev) {
179 31x
    on.exit(close_font_dev())
180
  }
181 32669x
  .open_fdev_is_monospace()
182
}
183
 184 
## get_max_wratio <- function() {
185
##   if (!font_dev_state$open) {
186
##     stop(
187
##       "get_space_width called when font dev state is not open. ",
188
##       "This shouldn't happen, please contact the maintainers."
189
##     )
190
##   }
191
##   if (.open_fdev_is_monospace()) {
192
##     1
193
##   } else {
194
##     font_dev_state$maxratio
195
##   }
196
## }
197
 198 
gpar_from_fspec <- function(fontspec) {
199 517x
  gpar(
200 517x
    fontfamily = fontspec$family,
201 517x
    fontsize = fontspec$size,
202 517x
    lineheight = fontspec$lineheight
203
  )
204
}
205
 206 114995x
font_dev_is_open <- function() font_dev_state$open
207
 208 
#' Default horizontal separator
209
#'
210
#' The default horizontal separator character which can be displayed in the current
211
#' charset for use in rendering table-like objects.
212
#'
213
#' @param hsep_char (`string`)\cr character that will be set in the R environment
214
#'   options as the default horizontal separator. Must be a single character. Use
215
#'   `getOption("formatters_default_hsep")` to get its current value (`NULL` if not set).
216
#'
217
#' @return unicode 2014 (long dash for generating solid horizontal line) if in a
218
#'   locale that uses a UTF character set, otherwise an ASCII hyphen with a
219
#'   once-per-session warning.
220
#'
221
#' @examples
222
#' default_hsep()
223
#' set_default_hsep("o")
224
#' default_hsep()
225
#'
226
#' @name default_horizontal_sep
227
#' @export
228
default_hsep <- function() {
229
  system_default_hsep <- getOption("formatters_default_hsep")
230
 231 
  if (is.null(system_default_hsep)) {
232
    if (any(grepl("^UTF", utils::localeToCharset()))) {
233
      hsep <- "\u2014"
234
    } else {
235
      if (interactive()) {
236
        warning(
237
          "Detected non-UTF charset. Falling back to '-' ",
238
          "as default header/body separator. This warning ",
239
          "will only be shown once per R session."
240
        ) # nocov
241
      } # nocov
242
      hsep <- "-" # nocov
243
    }
244
  } else {
245
    hsep <- system_default_hsep
246
  }
247
  hsep
248
}
249
 250 
#' @name default_horizontal_sep
251
#' @export
252
set_default_hsep <- function(hsep_char) {
253
  checkmate::assert_character(hsep_char, n.chars = 1, len = 1, null.ok = TRUE)
254
  options("formatters_default_hsep" = hsep_char)
255
}
256
 257 
.calc_cell_widths <- function(mat, colwidths, col_gap) {
258 369x
  spans <- mat$spans
259 369x
  keep_mat <- mat$display
260 369x
  body <- mat$strings
261
 262 369x
  nr <- nrow(body)
263
 264 369x
  cell_widths_mat <- matrix(rep(colwidths, nr), nrow = nr, byrow = TRUE)
265 369x
  nc <- ncol(cell_widths_mat)
266
 267 369x
  for (i in seq_len(nrow(body))) {
268 6545x
    if (any(!keep_mat[i, ])) { # any spans?
269 6x
      j <- 1
270 6x
      while (j <= nc) {
271 10x
        nj <- spans[i, j]
272 10x
        j <- if (nj > 1) {
273 6x
          js <- seq(j, j + nj - 1)
274 6x
          cell_widths_mat[i, js] <- sum(cell_widths_mat[i, js]) + col_gap * (nj - 1)
275 6x
          j + nj
276
        } else {
277 4x
          j + 1
278
        }
279
      }
280
    }
281
  }
282 369x
  cell_widths_mat
283
}
284
 285 
# Main function that does the wrapping
286
do_cell_fnotes_wrap <- function(mat, widths, max_width, tf_wrap, fontspec, expand_newlines = TRUE) {
287 213x
  col_gap <- mf_colgap(mat)
288 213x
  ncchar <- sum(widths) + (length(widths) - as.integer(mf_has_rlabels(mat))) * col_gap
289 213x
  inset <- table_inset(mat)
290
 291 
  ## Text wrapping checks
292 213x
  if (tf_wrap) {
293 92x
    if (is.null(max_width)) {
294 24x
      max_width <- getOption("width", 80L)
295 68x
    } else if (is.character(max_width) && identical(max_width, "auto")) {
296 !
      max_width <- ncchar + inset
297
    }
298 92x
    assert_number(max_width, lower = 0)
299
  }
300
 301 
  ## Check for having the right number of widths
302 213x
  stopifnot(length(widths) == ncol(mat$strings))
303
 304 
  ## format the to ASCII
305 213x
  cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap)
306
 307 
  # Check that indentation is correct (it works only for body)
308 213x
  .check_indentation(mat, row_col_width = cell_widths_mat[, 1, drop = TRUE])
309 210x
  mod_ind_list <- .modify_indentation(mat, cell_widths_mat, do_what = "remove")
310 210x
  mfs <- mod_ind_list[["mfs"]]
311 210x
  cell_widths_mat <- mod_ind_list[["cell_widths_mat"]]
312
 313 
  # Main wrapper
314 210x
  mf_strings(mat) <- matrix(
315 210x
    unlist(mapply(wrap_string,
316 210x
      str = mfs,
317 210x
      width = cell_widths_mat,
318 210x
      collapse = "\n",
319 210x
      MoreArgs = list(fontspec = fontspec)
320
    )),
321 210x
    ncol = ncol(mfs)
322
  )
323
 324 210x
  if (expand_newlines) {
325
    ## XXXXX this is wrong and will break for listings cause we don't know when
326
    ## we need has_topleft to be FALSE!!!!!!!!!!
327 210x
    mat <- mform_handle_newlines(mat)
328
 329 
    ## this updates extents in rinfo AND nlines in ref_fnotes_df
330
    ## mat already has fontspec on it so no need to pass that down
331 210x
    mat <- update_mf_nlines(mat, max_width = max_width)
332
 333 
    # Re-indenting
334 210x
    mf_strings(mat) <- .modify_indentation(mat, cell_widths_mat, do_what = "add")[["mfs"]]
335 210x
    .check_indentation(mat) # all went well
336
  }
337 210x
  mat
338
}
339
 340 
# Helper function to see if body indentation matches (minimum)
341
# It sees if there is AT LEAST the indentation contained in rinfo
342
.check_indentation <- function(mat, row_col_width = NULL) {
343
  # mf_nrheader(mat) # not useful
344 424x
  mf_nlh <- mf_nlheader(mat)
345 424x
  mf_lgrp <- mf_lgrouping(mat)
346 424x
  mf_str <- mf_strings(mat)
347
  # we base everything on the groupings -> unique indentation identifiers
348 424x
  if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()
349 424x
    mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft
350
  } else {
351 !
    mf_ind <- rep(0, mf_nrheader(mat))
352
  }
353 424x
  ind_std <- paste0(rep(" ", mat$indent_size), collapse = "")
354
 355 
  # Expected indent (-x negative numbers should not appear at this stage)
356 424x
  stopifnot(all(mf_ind >= 0))
357 424x
  real_indent <- vapply(mf_ind, function(ii) {
358 7833x
    paste0(rep(ind_std, ii), collapse = "")
359 424x
  }, character(1))
360
 361 424x
  if (!is.null(row_col_width) && any(row_col_width > 0) && !is.null(mf_rinfo(mat))) { # third is rare case
362
    # Self consistency test for row_col_width (same groups should have same width)
363
    # This should not be necessary (nocov)
364 213x
    consistency_check <- vapply(unique(mf_lgrp), function(ii) {
365 3935x
      width_per_grp <- row_col_width[which(mf_lgrp == ii)]
366 3935x
      all(width_per_grp == width_per_grp[1])
367 213x
    }, logical(1))
368 213x
    stopifnot(all(consistency_check))
369
 370 
    # Taking only one width for each indentation grouping
371 213x
    unique_row_col_width <- row_col_width[match(unique(mf_lgrp), mf_lgrp)]
372
 373 
    # Exception for check: case with summarize_row_groups and (hence) content_rows
374 213x
    nchar_real_indent <- nchar(real_indent)
375 213x
    body_rows <- seq(mf_nrheader(mat) + 1, length(nchar_real_indent))
376 213x
    nchar_real_indent[body_rows] <- nchar_real_indent[body_rows] +
377 213x
      as.numeric(mf_rinfo(mat)$node_class != "ContentRow")
378
    # xxx I think all of the above is a bit buggy honestly (check ContentRows!!!)
379
 380 213x
    if (any(nchar_real_indent > unique_row_col_width)) {
381 2x
      stop(
382 2x
        "Inserted width for row label column is not wide enough. ",
383 2x
        "We found the following rows that do not have at least indentation * ind_size + 1",
384 2x
        " characters to allow text to be shown after indentation: ",
385 2x
        paste0(which(nchar(real_indent) + 1 > unique_row_col_width), collapse = " ")
386
      )
387
    }
388
  }
389
 390 
  # Main detector
391 422x
  correct_indentation <- vapply(seq_along(mf_lgrp), function(xx) {
392 8277x
    grouping <- mf_lgrp[xx]
393 8277x
    if (nzchar(real_indent[grouping])) {
394 33x
      has_correct_indentation <- stringi::stri_detect(
395 33x
        mf_str[xx, 1],
396 33x
        regex = paste0("^", real_indent[grouping])
397
      )
398 33x
      return(has_correct_indentation || !nzchar(mf_str[xx, 1])) # "" is still an ok indentation
399
    }
400
    # Cases where no indent are true by definition
401 8244x
    TRUE
402 422x
  }, logical(1))
403
 404 422x
  if (any(!correct_indentation)) {
405 1x
    stop(
406 1x
      "We discovered indentation mismatches between the matrix_form and the indentation",
407 1x
      " predefined in mf_rinfo. This should not happen. Contact the maintainer."
408 1x
    ) # nocov
409
  }
410
}
411
 412 
# Helper function that takes out or adds the proper indentation
413
.modify_indentation <- function(mat, cell_widths_mat, do_what = c("remove", "add")) {
414
  # Extract info
415 420x
  mfs <- mf_strings(mat) # we work on mfs
416 420x
  mf_nlh <- mf_nlheader(mat)
417 420x
  mf_l <- mf_lgrouping(mat)
418 420x
  if (!is.null(mf_rinfo(mat))) { # this happens in rare cases with rtables::rtable()
419 420x
    mf_ind <- c(rep(0, mf_nrheader(mat)), mf_rinfo(mat)$indent) # XXX to fix with topleft
420
  } else {
421 !
    mf_ind <- rep(0, mf_nrheader(mat))
422
  }
423 420x
  stopifnot(length(mf_ind) == length(unique(mf_l))) # Check for indentation and grouping
424 420x
  ind_std <- paste0(rep(" ", mat$indent_size), collapse = "") # standard size of indent 1
425
 426 
  # Create real indentation
427 420x
  real_indent <- sapply(mf_ind, function(ii) paste0(rep(ind_std, ii), collapse = ""))
428
 429 
  # Use groupings to add or remove proper indentation
430 420x
  lbl_row <- mfs[, 1, drop = TRUE]
431 420x
  for (ii in seq_along(lbl_row)) {
432 8268x
    grp <- mf_l[ii]
433 8268x
    if (nzchar(real_indent[grp])) {
434
      # Update also the widths!!
435 29x
      if (do_what[1] == "remove") {
436 9x
        cell_widths_mat[ii, 1] <- cell_widths_mat[ii, 1] - nchar(real_indent[grp])
437 9x
        mfs[ii, 1] <- stringi::stri_replace(lbl_row[ii], "", regex = paste0("^", real_indent[grp]))
438 20x
      } else if (do_what[1] == "add") {
439 20x
        mfs[ii, 1] <- paste0(real_indent[grp], lbl_row[ii])
440
      } else {
441
        stop("do_what needs to be remove or add.") # nocov
442
      }
443
    } else {
444 8239x
      mfs[ii, 1] <- lbl_row[ii]
445
    }
446
  }
447
  # Final return
448 420x
  return(list("mfs" = mfs, "cell_widths_mat" = cell_widths_mat))
449
}
450
 451 
## take a character vector and return whether the value is
452
## a string version of a number or not
453
is_number_str <- function(vec) {
454 !
  is.na(as.numeric(vec))
455
}
456
 457 
is_dec_align <- function(vec) {
458
  # "c" is not an alignment method we define in `formatters`,
459
  # but the reverse dependency package `tables` will need
460 604x
  sdiff <- setdiff(vec, c(list_valid_aligns(), "c"))
461 604x
  if (length(sdiff) > 0) {
462 !
    stop("Invalid text-alignment(s): ", paste(sdiff, collapse = ", "))
463
  }
464 604x
  grepl("dec", vec)
465
}
466
 467 459x
any_dec_align <- function(vec) any(is_dec_align(vec))
468
 469 
#' Decimal alignment
470
#'
471
#' Aligning decimal values of string matrix. Allowed alignments are: `dec_left`, `dec_right`,
472
#' and `decimal`.
473
#'
474
#' @param string_mat (`character matrix`)\cr "string" matrix component of `MatrixPrintForm` object.
475
#' @param align_mat (`character matrix`)\cr "aligns" matrix component of `MatrixPrintForm` object.
476
#'   Should contain either `dec_left`, `dec_right`, or `decimal` for values to be decimal aligned.
477
#'
478
#' @details Left and right decimal alignment (`dec_left` and `dec_right`) differ from center decimal
479
#'   alignment (`decimal`) only when there is padding present. This may occur if column widths are
480
#'   set wider via parameters `widths` in `toString` or `colwidths` in `paginate_*`. More commonly,
481
#'   it also occurs when column names are wider. Cell wrapping is not supported when decimal
482
#'   alignment is used.
483
#'
484
#' @return A processed string matrix of class `MatrixPrintForm` with decimal-aligned values.
485
#'
486
#' @seealso [toString()], [MatrixPrintForm()]
487
#'
488
#' @examples
489
#' dfmf <- basic_matrix_form(mtcars[1:5, ])
490
#' aligns <- mf_aligns(dfmf)
491
#' aligns[, -c(1)] <- "dec_left"
492
#' decimal_align(mf_strings(dfmf), aligns)
493
#'
494
#' @export
495
decimal_align <- function(string_mat, align_mat) {
496
  ## Evaluate if any values are to be decimal aligned
497 45x
  if (!any_dec_align(align_mat)) {
498 !
    return(string_mat)
499
  }
500 45x
  for (i in seq(1, ncol(string_mat))) {
501
    ## Take a column and its decimal alignments
502 145x
    col_i <- as.character(string_mat[, i])
503 145x
    align_col_i <- is_dec_align(align_mat[, i])
504
 505 
    ## !( A || B) -> !A && !B  DeMorgan's Law
506
    ## Are there any values to be decimal aligned? safe if
507 145x
    if (any(align_col_i) && any(!grepl("^[0-9]\\.", col_i))) {
508
      ## Extract values not to be aligned (NAs, non-numbers,
509
      ##  doesn't say "decimal" in alignment matrix)
510
      ## XXX FIXME because this happens after formatting, we can't tell the difference between
511
      ## non-number strings which come from na_str+ NA  value and strings which just aren't numbers.
512
      ## this is a problem that should eventually be fixed.
513 82x
      nas <- vapply(col_i, is.na, FUN.VALUE = logical(1))
514 82x
      nonnum <- !grepl("[0-9]", col_i)
515
      ## No grepl("[a-zA-Z]", col_i) because this excludes N=xx, e.g.
516 82x
      nonalign <- nas | nonnum | !align_col_i
517 82x
      col_ia <- col_i[!nonalign]
518
 519 
      ## Do decimal alignment
520 82x
      if (length(col_ia) > 0) {
521
        # Special case: scientific notation
522 82x
        has_sc_not <- grepl("\\d+[e|E][\\+|\\-]\\d+", col_ia)
523 82x
        if (any(has_sc_not)) {
524 1x
          stop(
525 1x
            "Found values using scientific notation between the ones that",
526 1x
            " needs to be decimal aligned (aligns is decimal, dec_left or dec_right).",
527 1x
            " Please consider using format functions to get a complete decimal ",
528 1x
            "(e.g. formatC)."
529
          )
530
        }
531
 532 
        ## Count the number of numbers in the string
533 81x
        matches <- gregexpr("\\d+\\.\\d+|\\d+", col_ia)
534 81x
        more_than_one <- vapply(matches, function(x) {
535 697x
          sum(attr(x, "match.length") > 0) > 1
536 81x
        }, logical(1))
537
        ## Throw error in case any have more than 1 numbers
538 81x
        if (any(more_than_one)) {
539 2x
          stop(
540 2x
            "Decimal alignment is not supported for multiple values. ",
541 2x
            "Found the following string with multiple numbers ",
542 2x
            "(first 3 selected from column ", col_i[1], "): '",
543 2x
            paste0(col_ia[more_than_one][seq(1, 3)], collapse = "', '"),
544
            "'"
545
          )
546
        }
547
        ## General split (only one match -> the first)
548 79x
        main_regexp <- regexpr("\\d+", col_ia)
549 79x
        left <- regmatches(col_ia, main_regexp, invert = FALSE)
550 79x
        right <- regmatches(col_ia, main_regexp, invert = TRUE)
551 79x
        right <- sapply(right, "[[", 2)
552 79x
        something_left <- sapply(strsplit(col_ia, "\\d+"), "[[", 1)
553 79x
        left <- paste0(something_left, left)
554 79x
        if (!checkmate::test_set_equal(paste0(left, right), col_ia)) {
555 !
          stop(
556 !
            "Split string list lost some piece along the way. This ",
557 !
            "should not have happened. Please contact the maintainer."
558
          )
559
        } # nocov
560 79x
        separator <- sapply(right, function(x) {
561 645x
          if (nzchar(x)) {
562 349x
            substr(x, 1, 1)
563
          } else {
564 296x
            c(" ")
565
          }
566 79x
        }, USE.NAMES = FALSE)
567 79x
        right <- sapply(right, function(x) {
568 645x
          if (nchar(x) > 1) {
569 317x
            substr(x, 2, nchar(x))
570
          } else {
571 328x
            c("")
572
          }
573 79x
        }, USE.NAMES = FALSE)
574
        ## figure out whether we need space separators (at least one had a "." or not)
575 79x
        if (!any(grepl("[^[:space:]]", separator))) {
576 26x
          separator <- gsub("[[:space:]]*", "", separator)
577
        }
578
        ## modify the piece with spaces
579 79x
        left_mod <- paste0(spaces(max(nchar(left), na.rm = TRUE) - nchar(left)), left)
580 79x
        right_mod <- paste0(right, spaces(max(nchar(right), na.rm = TRUE) - nchar(right)))
581
        # Put everything together
582 79x
        aligned <- paste(left_mod, separator, right_mod, sep = "")
583 79x
        string_mat[!nonalign, i] <- aligned
584
      }
585
    }
586
  }
587 42x
  string_mat
588
}
589
 590 
## this gives the conversion from number of spaces to number of characters
591
## for use in, e.g., repping out divider lines.
592
calc_str_adj <- function(str, fontspec) {
593 160x
  if (nchar(str) == 0) {
594 !
    return(0)
595
  }
596 160x
  nchar(str) / nchar_ttype(str, fontspec, raw = TRUE)
597
}
598
 599 
# toString ---------------------------------------------------------------------
600
# main printing code for MatrixPrintForm
601
 602 
#' @description
603
#' All objects that are printed to console pass via `toString`. This function allows
604
#' fundamental formatting specifications to be applied to final output, like column widths
605
#' and relative wrapping (`width`), title and footer wrapping (`tf_wrap = TRUE` and
606
#' `max_width`), and horizontal separator character (e.g. `hsep = "+"`).
607
#'
608
#' @inheritParams MatrixPrintForm
609
#' @inheritParams open_font_dev
610
#' @inheritParams format_value
611
#' @param widths (`numeric` or  `NULL`)\cr Proposed widths for the columns of `x`. The expected
612
#'   length of this numeric vector can be retrieved with `ncol(x) + 1` as the column of row names
613
#'   must also be considered.
614
#' @param hsep (`string`)\cr character to repeat to create header/body separator line. If
615
#'   `NULL`, the object value will be used. If `" "`, an empty separator will be printed. See
616
#'   [default_hsep()] for more information.
617
#' @param tf_wrap (`flag`)\cr whether the text for title, subtitles, and footnotes should be wrapped.
618
#' @param max_width (`integer(1)`, `string` or `NULL`)\cr width that title and footer (including
619
#'   footnotes) materials should be word-wrapped to. If `NULL`, it is set to the current print width of the
620
#'   session (`getOption("width")`). If set to `"auto"`, the width of the table (plus any table inset) is
621
#'   used. Parameter is ignored if `tf_wrap = FALSE`.
622
#' @param ttype_ok (`logical(1)`)\cr should truetype (non-monospace) fonts be
623
#'   allowed via `fontspec`. Defaults to `FALSE`. This parameter is primarily
624
#'   for internal testing and generally should not be set by end users.
625
#'
626
#' @details
627
#' Manual insertion of newlines is not supported when `tf_wrap = TRUE` and will result in a warning and
628
#' undefined wrapping behavior. Passing vectors of already split strings remains supported, however in this
629
#' case each string is word-wrapped separately with the behavior described above.
630
#'
631
#' @return A character string containing the ASCII rendering of the table-like object represented by `x`.
632
#'
633
#' @seealso [wrap_string()]
634
#'
635
#' @examples
636
#' mform <- basic_matrix_form(mtcars)
637
#' cat(toString(mform))
638
#'
639
#' @rdname tostring
640
#' @exportMethod toString
641
setMethod("toString", "MatrixPrintForm", function(x,
642
                                                  widths = NULL,
643
                                                  tf_wrap = FALSE,
644
                                                  max_width = NULL,
645
                                                  col_gap = mf_colgap(x),
646
                                                  hsep = NULL,
647
                                                  fontspec = font_spec(),
648
                                                  ttype_ok = FALSE,
649
                                                  round_type = c("iec", "sas")) {
650 163x
  checkmate::assert_flag(tf_wrap)
651
 652 
  ## we are going to use the pdf device and grid to understand the actual
653
  ## print width of things given our font family and font size
654 163x
  new_dev <- open_font_dev(fontspec)
655 163x
  if (new_dev) {
656 153x
    on.exit(close_font_dev())
657
  }
658
 659 163x
  if (!is_monospace(fontspec = fontspec) && !ttype_ok) {
660 !
    stop(
661 !
      "non-monospace font specified in toString call; this would result in cells contents not lining up exactly. ",
662 !
      "If you truly want this behavior please set ttype_ok = TRUE in the call to toString/export_as_txt/export_as_pdf"
663
    )
664
  }
665 163x
  mat <- matrix_form(x, indent_rownames = TRUE, fontspec = fontspec, round_type = round_type)
666
 667 
  # Check for \n in mat strings -> if there are any, matrix_form did not work
668 163x
  if (any(grepl("\n", mf_strings(mat)))) {
669 !
    stop(
670 !
      "Found newline characters (\\n) in string matrix produced by matrix_form. ",
671 !
      "This is not supported and implies missbehavior on the first parsing (in matrix_form). ",
672 !
      "Please contact the maintainer or file an issue."
673 !
    ) # nocov
674
  }
675 163x
  if (any(grepl("\r", mf_strings(mat)))) {
676 !
    stop(
677 !
      "Found recursive special characters (\\r) in string matrix produced by matrix_form. ",
678 !
      "This special character is not supported and should be removed."
679 !
    ) # nocov
680
  }
681
 682 
  # Check that expansion worked for header -> should not happen
683 163x
  if (!is.null(mf_rinfo(mat)) && # rare case of rtables::rtable()
684 163x
    (length(mf_lgrouping(mat)) != nrow(mf_strings(mat)) || # non-unique grouping test # nolint
685 163x
      mf_nrheader(mat) + nrow(mf_rinfo(mat)) != length(unique(mf_lgrouping(mat))))) { # nolint
686 !
    stop(
687 !
      "The sum of the expected nrows header and nrows of content table does ",
688 !
      "not match the number of rows in the string matrix. To our knowledge, ",
689 !
      "this is usually of a problem in solving newline characters (\\n) in the header. ",
690 !
      "Please contact the maintaner or file an issue."
691 !
    ) # nocov
692
  }
693
 694 163x
  inset <- table_inset(mat)
695
 696 
  # if cells are decimal aligned, run propose column widths
697
  # if the provided widths is less than proposed width, return an error
698 163x
  if (any_dec_align(mf_aligns(mat))) {
699 22x
    aligned <- propose_column_widths(x, fontspec = fontspec, round_type = round_type)
700
 701 
    # catch any columns that require widths more than what is provided
702 20x
    if (!is.null(widths)) {
703 9x
      how_wide <- sapply(seq_along(widths), function(i) c(widths[i] - aligned[i]))
704 9x
      too_wide <- how_wide < 0
705 9x
      if (any(too_wide)) {
706 2x
        desc_width <- paste(paste(
707 2x
          names(which(too_wide)),
708 2x
          paste0("(", how_wide[too_wide], ")")
709 2x
        ), collapse = ", ")
710 2x
        stop(
711 2x
          "Inserted width(s) for column(s) ", desc_width,
712 2x
          " is(are) not wide enough for the desired alignment."
713
        )
714
      }
715
    }
716
  }
717
 718 
  # Column widths are fixed here
719 159x
  if (is.null(widths)) {
720
    # if mf does not have widths -> propose them
721 133x
    widths <- mf_col_widths(x) %||% propose_column_widths(x, fontspec = fontspec, round_type = round_type)
722
  } else {
723 26x
    mf_col_widths(x) <- widths
724
  }
725
 726 
  ## Total number of characters for the table
727
  ## col_gap (and table inset) are in number of spaces
728
  ## so we're ok here even in the truetype case
729 159x
  ncchar <- sum(widths) + (length(widths) - 1) * col_gap
730
 731 
  ## max_width for wrapping titles and footers (not related to ncchar if not indirectly)
732 159x
  max_width <- .handle_max_width(
733 159x
    tf_wrap = tf_wrap,
734 159x
    max_width = max_width,
735 159x
    colwidths = widths,
736 159x
    col_gap = col_gap,
737 159x
    inset = inset
738
  )
739
 740 
  # Main wrapper function for table core
741 159x
  mat <- do_cell_fnotes_wrap(mat, widths, max_width = max_width, tf_wrap = tf_wrap, fontspec = fontspec)
742
 743 156x
  body <- mf_strings(mat)
744 156x
  aligns <- mf_aligns(mat)
745 156x
  keep_mat <- mf_display(mat)
746
  ## spans <- mat$spans
747 156x
  mf_ri <- mf_rinfo(mat)
748 156x
  ref_fnotes <- mf_rfnotes(mat)
749 156x
  nl_header <- mf_nlheader(mat)
750
 751 156x
  cell_widths_mat <- .calc_cell_widths(mat, widths, col_gap)
752
 753 
  # decimal alignment
754 156x
  if (any_dec_align(aligns)) {
755 18x
    body <- decimal_align(body, aligns)
756
  }
757
 758 
  # Content is a matrix of cells with the right amount of spaces
759 156x
  content <- matrix(
760 156x
    mapply(padstr, body, cell_widths_mat, aligns, MoreArgs = list(fontspec = fontspec)),
761 156x
    ncol = ncol(body)
762
  )
763 156x
  content[!keep_mat] <- NA
764
 765 
  # Define gap string and divisor string
766 156x
  gap_str <- strrep(" ", col_gap)
767 156x
  if (is.null(hsep)) {
768 121x
    hsep <- horizontal_sep(mat)
769
  }
770 156x
  adj_hsep <- calc_str_adj(hsep, fontspec)
771 156x
  div <- substr(strrep(hsep, ceiling(ncchar * adj_hsep)), 1, ceiling(ncchar * adj_hsep))
772 156x
  hsd <- header_section_div(mat)
773 156x
  if (!is.na(hsd)) {
774 !
    adj_hsd <- calc_str_adj(hsd, fontspec)
775 !
    hsd <- substr(strrep(hsd, ceiling(ncchar * adj_hsd)), 1, ceiling(ncchar * adj_hsd))
776
  } else {
777 156x
    hsd <- NULL # no divisor
778
  }
779
 780 
  # text head (paste w/o NA content header and gap string)
781 156x
  txt_head <- apply(head(content, nl_header), 1, .paste_no_na, collapse = gap_str)
782
 783 
  # txt body
784 156x
  sec_seps_df <- mf_ri[, c("abs_rownumber", "trailing_sep"), drop = FALSE]
785 156x
  if (!is.null(sec_seps_df) && any(!is.na(sec_seps_df$trailing_sep))) {
786 2x
    bdy_cont <- tail(content, -nl_header)
787
    ## unfortunately we count "header rows" wrt line grouping so it
788
    ## doesn't match the real (i.e. body) rows as is
789 2x
    row_grouping <- tail(mf_lgrouping(mat), -nl_header) - mf_nrheader(mat)
790 2x
    nrbody <- NROW(bdy_cont)
791 2x
    stopifnot(length(row_grouping) == nrbody)
792
    ## all rows with non-NA section divs and the final row (regardless of NA status)
793
    ## fixes #77
794 2x
    sec_seps_df <- sec_seps_df[unique(c(
795 2x
      which(!is.na(sec_seps_df$trailing_sep)),
796 2x
      NROW(sec_seps_df)
797
    )), ]
798 2x
    txt_body <- character()
799 2x
    sec_strt <- 1
800 2x
    section_rws <- sec_seps_df$abs_rownumber
801 2x
    for (i in seq_len(NROW(section_rws))) {
802 6x
      cur_rownum <- section_rws[i]
803 6x
      sec_end <- max(which(row_grouping == cur_rownum))
804 6x
      txt_body <- c(
805 6x
        txt_body,
806 6x
        apply(bdy_cont[seq(sec_strt, sec_end), , drop = FALSE],
807 6x
          1,
808 6x
          .paste_no_na,
809 6x
          collapse = gap_str
810
        ),
811
        ## don't print section dividers if they would be the last thing before the
812
        ## footer divider
813
        ## this also ensures an extraneous sec div won't be printed if we have non-sec-div
814
        ## rows after the last sec div row (#77)
815 6x
        if (sec_end < nrbody) {
816 4x
          adj_i <- calc_str_adj(sec_seps_df$trailing_sep[i], fontspec)
817 4x
          substr(
818 4x
            strrep(sec_seps_df$trailing_sep[i], ceiling(ncchar * adj_i)), 1,
819 4x
            ceiling((ncchar - inset) * adj_i)
820
          )
821
        }
822
      )
823 6x
      sec_strt <- sec_end + 1
824
    }
825
  } else {
826
    # This is the usual default pasting
827 154x
    txt_body <- apply(tail(content, -nl_header), 1, .paste_no_na, collapse = gap_str)
828
  }
829
 830 
  # retrieving titles and footers
831 156x
  allts <- all_titles(mat)
832
 833 156x
  ref_fnotes <- reorder_ref_fnotes(ref_fnotes)
834
  # Fix for ref_fnotes with \n characters XXX this does not count in the pagination
835 156x
  if (any(grepl("\\n", ref_fnotes))) {
836 2x
    ref_fnotes <- unlist(strsplit(ref_fnotes, "\n", fixed = TRUE))
837
  }
838
 839 156x
  allfoots <- list(
840 156x
    "main_footer" = main_footer(mat),
841 156x
    "prov_footer" = prov_footer(mat),
842 156x
    "ref_footnotes" = ref_fnotes
843
  )
844 156x
  allfoots <- allfoots[!sapply(allfoots, is.null)]
845
 846 
  ## Wrapping titles if they go beyond the horizontally allowed space
847 156x
  if (tf_wrap) {
848 68x
    new_line_warning(allts)
849 68x
    allts <- wrap_txt(allts, max_width, fontspec = fontspec)
850
  }
851 156x
  titles_txt <- if (any(nzchar(allts))) c(allts, "", .do_inset(div, inset)) else NULL
852
 853 
  # Wrapping footers if they go beyond the horizontally allowed space
854 156x
  if (tf_wrap) {
855 68x
    new_line_warning(allfoots)
856 68x
    allfoots$main_footer <- wrap_txt(allfoots$main_footer, max_width - inset, fontspec = fontspec)
857 68x
    allfoots$ref_footnotes <- wrap_txt(allfoots$ref_footnotes, max_width - inset, fontspec = fontspec)
858
    ## no - inset here because the prov_footer is not inset
859 68x
    allfoots$prov_footer <- wrap_txt(allfoots$prov_footer, max_width, fontspec = fontspec)
860
  }
861
 862 
  # Final return
863 156x
  paste0(
864 156x
    paste(c(
865 156x
      titles_txt, # .do_inset(div, inset) happens if there are any titles
866 156x
      .do_inset(txt_head, inset),
867 156x
      .do_inset(div, inset),
868 156x
      .do_inset(hsd, inset), # header_section_div if present
869 156x
      .do_inset(txt_body, inset),
870 156x
      .footer_inset_helper(allfoots, div, inset)
871 156x
    ), collapse = "\n"),
872 156x
    "\n"
873
  )
874
})
875
 876 
# Switcher for the 3 options for max_width (NULL, numeric, "auto"))
877
.handle_max_width <- function(tf_wrap, max_width,
878
                              cpp = NULL, # Defaults to getOption("width", 80L)
879
                              # Things for auto
880
                              inset = NULL, colwidths = NULL, col_gap = NULL) {
881 237x
  max_width <- if (!tf_wrap) {
882 117x
    if (!is.null(max_width)) {
883 1x
      warning("tf_wrap is FALSE - ignoring non-null max_width value.")
884
    }
885 117x
    NULL
886 237x
  } else if (tf_wrap) {
887 120x
    if (is.null(max_width)) {
888 36x
      if (is.null(cpp) || is.na(cpp)) {
889 7x
        getOption("width", 80L)
890
      } else {
891 29x
        cpp
892
      }
893 84x
    } else if (is.numeric(max_width)) {
894 79x
      max_width
895 5x
    } else if (is.character(max_width) && identical(max_width, "auto")) {
896
      # This should not happen, but just in case
897 4x
      if (any(sapply(list(inset, colwidths, col_gap), is.null))) {
898 1x
        stop("inset, colwidths, and col_gap must all be non-null when max_width is \"auto\".")
899
      }
900 3x
      inset + sum(colwidths) + (length(colwidths) - 1) * col_gap
901
    } else {
902 1x
      stop("max_width must be NULL, a numeric value, or \"auto\".")
903
    }
904
  }
905 235x
  return(max_width)
906
}
907
 908 
.do_inset <- function(x, inset) {
909 1053x
  if (inset == 0 || !any(nzchar(x))) {
910 1034x
    return(x)
911
  }
912 19x
  padding <- strrep(" ", inset)
913 19x
  if (is.character(x)) {
914 19x
    x <- paste0(padding, x)
915 !
  } else if (is(x, "matrix")) {
916 !
    x[, 1] <- .do_inset(x[, 1, drop = TRUE], inset)
917
  }
918 19x
  x
919
}
920
 921 
.inset_div <- function(txt, div, inset) {
922 105x
  c(.do_inset(div, inset), "", txt)
923
}
924
 925 
.footer_inset_helper <- function(footers_v, div, inset) {
926 156x
  div_done <- FALSE # nolint
927 156x
  fter <- footers_v$main_footer
928 156x
  prvf <- footers_v$prov_footer
929 156x
  rfn <- footers_v$ref_footnotes
930 156x
  footer_txt <- .do_inset(rfn, inset)
931 156x
  if (any(nzchar(footer_txt))) {
932 14x
    footer_txt <- .inset_div(footer_txt, div, inset)
933
  }
934 156x
  if (any(vapply(
935 156x
    footers_v, function(x) any(nzchar(x)),
936 156x
    TRUE
937
  ))) {
938 91x
    if (any(nzchar(prvf))) {
939 89x
      provtxt <- c(
940 89x
        if (any(nzchar(fter))) "",
941 89x
        prvf
942
      )
943
    } else {
944 2x
      provtxt <- character()
945
    }
946 91x
    footer_txt <- c(
947 91x
      footer_txt,
948 91x
      .inset_div(
949 91x
        c(
950 91x
          .do_inset(fter, inset),
951 91x
          provtxt
952
        ),
953 91x
        div,
954 91x
        inset
955
      )
956
    )
957
  }
958 156x
  footer_txt
959
}
960
 961 
reorder_ref_fnotes <- function(fns) {
962 159x
  ind <- gsub("\\{(.*)\\}.*", "\\1", fns)
963 159x
  ind_num <- suppressWarnings(as.numeric(ind))
964 159x
  is_num <- !is.na(ind_num)
965 159x
  is_asis <- ind == fns
966
 967 159x
  if (all(is_num)) {
968 143x
    ord_num <- order(ind_num)
969 143x
    ord_char <- NULL
970 143x
    ord_other <- NULL
971
  } else {
972 16x
    ord_num <- order(ind_num[is_num])
973 16x
    ord_char <- order(ind[!is_num & !is_asis])
974 16x
    ord_other <- order(ind[is_asis])
975
  }
976 159x
  c(fns[is_num][ord_num], fns[!is_num & !is_asis][ord_char], ind[is_asis][ord_other])
977
}
978
 979 
new_line_warning <- function(str_v) {
980 136x
  if (any(unlist(sapply(str_v, grepl, pattern = "\n")))) {
981 !
    msg <- c(
982 !
      "Detected manual newlines when automatic title/footer word-wrapping is on.",
983 !
      "This is unsupported and will result in undefined behavior. Please either ",
984 !
      "utilize automatic word-wrapping with newline characters inserted, or ",
985 !
      "turn off automatic wrapping and wordwrap all contents manually by inserting ",
986 !
      "newlines."
987
    )
988 !
    warning(paste0(msg, collapse = ""))
989
  }
990
}
991
 992 
#' Wrap a string to a precise width
993
#'
994
#' Core wrapping functionality that preserves whitespace. Newline character `"\n"` is not supported
995
#' by core functionality [stringi::stri_wrap()]. This is usually solved beforehand by [matrix_form()].
996
#' If the width is smaller than any large word, these will be truncated after `width` characters. If
997
#' the split leaves trailing groups of empty spaces, they will be dropped.
998
#'
999
#' @inheritParams open_font_dev
1000
#' @param str (`string`, `character`, or `list`)\cr string to be wrapped. If it is a `vector` or
1001
#'   a `list`, it will be looped as a `list` and returned with `unlist(use.names = FALSE)`.
1002
#' @param width (`numeric(1)`)\cr width, in characters, that the text should be wrapped to.
1003
#' @param collapse (`string` or `NULL`)\cr collapse character used to separate segments of words that
1004
#'   have been split and should be pasted together. This is usually done internally with `"\n"` to update
1005
#'   the wrapping along with other internal values.
1006
#'
1007
#' @details Word wrapping happens similarly to [stringi::stri_wrap()] with the following difference: individual
1008
#'   words which are longer than `max_width` are broken up in a way that fits with other word wrapping.
1009
#'
1010
#' @return A string if `str` is one element and if `collapse = NULL`. Otherwise, a list of elements
1011
#'   (if `length(str) > 1`) that can contain strings or vectors of characters (if `collapse = NULL`).
1012
#'
1013
#' @examples
1014
#' str <- list(
1015
#'   "  , something really  \\tnot  very good", # \t needs to be escaped
1016
#'   "  but I keep it12   "
1017
#' )
1018
#' wrap_string(str, 5, collapse = "\n")
1019
#'
1020
#' @export
1021
wrap_string <- function(str, width, collapse = NULL, fontspec = font_spec()) {
1022 36488x
  if (length(str) > 1) {
1023 114x
    return(
1024 114x
      unlist(
1025 114x
        lapply(str, wrap_string, width = width, collapse = collapse, fontspec = fontspec),
1026 114x
        use.names = FALSE
1027
      )
1028
    )
1029
  }
1030 36374x
  str <- unlist(str, use.names = FALSE) # it happens is one list element
1031 36374x
  if (!length(str) || !nzchar(str) || is.na(str)) {
1032 3872x
    return(str)
1033
  }
1034 32502x
  checkmate::assert_character(str)
1035 32502x
  checkmate::assert_int(width, lower = 1)
1036
 1037 32502x
  if (any(grepl("\\n", str))) {
1038 !
    stop(
1039 !
      "Found \\n in a string that was meant to be wrapped. This should not happen ",
1040 !
      "because matrix_form should take care of them before this step (toString, ",
1041 !
      "i.e. the printing machinery). Please contact the maintaner or file an issue."
1042
    )
1043
  }
1044
 1045 32502x
  if (!is_monospace(fontspec)) {
1046 3x
    return(wrap_string_ttype(str, width, fontspec, collapse = collapse))
1047
  }
1048
 1049 
  # str can be also a vector or list. In this case simplify manages the output
1050 32499x
  ret <- .go_stri_wrap(str, width)
1051
 1052 
  # Check if it went fine
1053 32499x
  if (any(nchar_ttype(ret, fontspec) > width)) {
1054 68x
    which_exceeded <- which(nchar_ttype(ret, fontspec) > width)
1055
 1056 
    # Recursive for loop to take word interval
1057 68x
    while (length(which_exceeded) > 0) {
1058 75x
      we_i <- which_exceeded[1]
1059
      # Is there space for some part of the next word?
1060 75x
      char_threshold <- width * (2 / 3) + 0.01 # if too little space -> no previous word
1061 75x
      smart_condition <- nchar_ttype(ret[we_i - 1], fontspec) + 1 < char_threshold # +1 is for spaces
1062 75x
      if (we_i - 1 > 0 && smart_condition) {
1063 6x
        we_interval <- unique(c(we_i - 1, we_i))
1064 6x
        we_interval <- we_interval[
1065 6x
          (we_interval < (length(ret) + 1)) &
1066 6x
            (we_interval > 0)
1067
        ]
1068
      } else {
1069 69x
        we_interval <- we_i
1070
      }
1071
      # Split words and collapse (needs unique afterwards)
1072 75x
      ret[we_interval] <- split_words_by(
1073 75x
        paste0(ret[we_interval], collapse = " "),
1074 75x
        width
1075
      )
1076
      # Taking out repetitions if there are more than one
1077 75x
      if (length(we_interval) > 1) {
1078 6x
        ret <- ret[-we_interval[-1]]
1079 6x
        we_interval <- we_interval[1]
1080
      }
1081
      # Paste together and rerun if it is not the same as original ret
1082 75x
      ret_collapse <- paste0(ret, collapse = " ")
1083
 1084 
      # Checking if we are stuck in a loop
1085 75x
      ori_wrapped_txt_v <- .go_stri_wrap(str, width)
1086 75x
      cur_wrapped_txt_v <- .go_stri_wrap(ret_collapse, width)
1087 75x
      broken_char_ori <- sum(nchar_ttype(ori_wrapped_txt_v, fontspec) > width) # how many issues there were
1088 75x
      broken_char_cur <- sum(nchar_ttype(cur_wrapped_txt_v, fontspec) > width) # how many issues there are
1089
 1090 
      # if still broken, we did not solve the current issue!
1091 75x
      if (setequal(ori_wrapped_txt_v, cur_wrapped_txt_v) || broken_char_cur >= broken_char_ori) {
1092
        # help function: Very rare case where the recursion is stuck in a loop
1093 14x
        ret_tmp <- force_split_words_by(ret[we_interval], width) # here we_interval is only one ind
1094 14x
        ret <- append(ret, ret_tmp, we_interval)[-we_interval]
1095 14x
        which_exceeded <- which(nchar_ttype(ret, fontspec) > width)
1096
      } else {
1097 61x
        return(wrap_string(str = ret_collapse, width = width, collapse = collapse))
1098
      }
1099
    }
1100
  }
1101
 1102 32438x
  if (!is.null(collapse)) {
1103 31987x
    return(paste0(ret, collapse = collapse))
1104
  }
1105
 1106 451x
  return(ret)
1107
}
1108
 1109 
.go_stri_wrap <- function(str, w) {
1110 32649x
  if (w < 1) {
1111 !
    return(str)
1112
  }
1113 32649x
  stringi::stri_wrap(str,
1114 32649x
    width = w,
1115 32649x
    normalize = FALSE, # keeps spaces
1116 32649x
    simplify = TRUE, # If FALSE makes it a list with str elements
1117 32649x
    indent = 0,
1118 32649x
    use_length = FALSE # incase the defaul changes, use actual char widths
1119
  )
1120
}
1121
 1122 
#' @rdname wrap_string_ttype
1123
#' @export
1124
split_word_ttype <- function(str, width, fontspec, min_ok_chars) {
1125 11x
  chrs <- strsplit(str, "")[[1]]
1126 11x
  nctt_chars <- nchar_ttype(chrs, fontspec, raw = TRUE)
1127 11x
  ok <- which(cumsum(nctt_chars) <= width)
1128 11x
  if (length(ok) < min_ok_chars || length(chrs) - length(ok) < min_ok_chars) {
1129 3x
    list(
1130 3x
      ok = character(),
1131 3x
      remainder = str
1132
    )
1133
  } else {
1134 8x
    list(
1135 8x
      ok = substr(str, 1, length(ok)),
1136 8x
      remainder = substr(str, length(ok) + 1, nchar(str))
1137
    )
1138
  }
1139
}
1140
 1141 
## need a separate path here because **the number of characters**
1142
## in each part is no longer going to be constant the way it
1143
## was for monospace
1144
## this is much slower but still shouldn't be a bottleneck, if it is we'll
1145
## have to do something else
1146
#' wrap string given a Truetype font
1147
#'
1148
#' @inheritParams wrap_string
1149
#' @param min_ok_chars (`numeric(1)`)\cr number of minimum characters that remain
1150
#'   on either side when a word is split.
1151
#' @param wordbreak_ok (`logical(1)`)\cr should breaking within a word be allowed? If, `FALSE`,
1152
#'   attempts to wrap a string to a width narrower than its widest word will result
1153
#'   in an error.
1154
#' @return `str`, broken up into a word-wrapped vector
1155
#' @export
1156
wrap_string_ttype <- function(str,
1157
                              width,
1158
                              fontspec,
1159
                              collapse = NULL,
1160
                              min_ok_chars = min(floor(nchar(str) / 2), 4, floor(width / 2)),
1161
                              wordbreak_ok = TRUE) {
1162 12x
  newdev <- open_font_dev(fontspec)
1163 11x
  if (newdev) {
1164 !
    on.exit(close_font_dev())
1165
  }
1166
 1167 11x
  rawspls <- strsplit(str, "[[:space:]](?=[^[:space:]])", perl = TRUE)[[1]] # preserve all but one space
1168 11x
  nctt <- nchar_ttype(rawspls, fontspec, raw = TRUE)
1169 11x
  pts <- which(cumsum(nctt) <= width)
1170 11x
  if (length(pts) == length(rawspls)) { ## no splitting needed
1171 3x
    return(str)
1172 8x
  } else if (length(pts) == 0) { ## no spaces, all one word, split it and keep going
1173 7x
    if (wordbreak_ok) {
1174 7x
      inner_res <- list()
1175 7x
      min_ok_inner <- min_ok_chars
1176 7x
      while (length(inner_res$ok) == 0) {
1177 10x
        inner_res <- split_word_ttype(rawspls[1], width, fontspec, min_ok_inner) # min_ok_chars)
1178 10x
        min_ok_inner <- floor(min_ok_inner / 2)
1179
      }
1180 7x
      done <- inner_res$ok
1181 7x
      remainder <- paste(c(inner_res$remainder, rawspls[-1]), collapse = " ")
1182
    } else {
1183 !
      stop(
1184 !
        "Unable to find word wrapping solution without breaking word: ",
1185 !
        rawspls[[1]], " [requires  ", nchar_ttype(rawspls[[1]], fontspec), " spaces of width, out of ",
1186 !
        width, " available]."
1187
      )
1188
    }
1189
  } else { ## some words fit, and some words don't
1190 1x
    done_tmp <- paste(rawspls[pts], collapse = " ")
1191 1x
    tospl_tmp <- rawspls[length(pts) + 1]
1192 1x
    width_tmp <- width - sum(nctt[pts])
1193 1x
    if (wordbreak_ok && width_tmp / width > .33) {
1194 1x
      inner_res <- split_word_ttype(tospl_tmp, width_tmp, fontspec,
1195 1x
        min_ok_chars = min_ok_chars
1196
      )
1197
    } else {
1198 !
      inner_res <- list(done = "", remainder = tospl_tmp)
1199
    }
1200 1x
    done <- paste(c(rawspls[pts], inner_res$ok),
1201 1x
      collapse = " "
1202
    )
1203 1x
    remainder <- paste(
1204 1x
      c(
1205 1x
        inner_res$remainder,
1206 1x
        if (length(rawspls) > length(pts) + 1) tail(rawspls, -(length(pts) + 1))
1207
      ),
1208 1x
      collapse = " "
1209
    )
1210
  }
1211 8x
  ret <- c(
1212 8x
    done,
1213 8x
    wrap_string_ttype(remainder, width, fontspec)
1214
  )
1215 8x
  if (!is.null(collapse)) {
1216 !
    ret <- paste(ret, collapse = collapse)
1217
  }
1218 8x
  ret
1219
}
1220
 1221 
# help function: Very rare case where the recursion is stuck in a loop
1222
force_split_words_by <- function(ret, width) {
1223 14x
  which_exceeded <- which(nchar(ret) > width)
1224 14x
  ret_tmp <- NULL
1225 14x
  for (ii in seq_along(ret)) {
1226 14x
    if (ii %in% which_exceeded) {
1227 14x
      wrd_i <- ret[ii]
1228 14x
      init_v <- seq(1, nchar(wrd_i), by = width)
1229 14x
      end_v <- c(init_v[-1] - 1, nchar(wrd_i))
1230 14x
      str_v_tmp <- stringi::stri_sub(wrd_i, from = init_v, to = end_v)
1231 14x
      ret_tmp <- c(ret_tmp, str_v_tmp[!grepl("^\\s+$", str_v_tmp) & nzchar(str_v_tmp)])
1232
    } else {
1233 !
      ret_tmp <- c(ret_tmp, ret[ii])
1234
    }
1235
  }
1236 14x
  ret_tmp
1237
}
1238
 1239 
# Helper fnc to split the words and collapse them with space
1240
split_words_by <- function(wrd, width) {
1241 75x
  vapply(wrd, function(wrd_i) {
1242 75x
    init_v <- seq(1, nchar(wrd_i), by = width)
1243 75x
    end_v <- c(init_v[-1] - 1, nchar(wrd_i))
1244 75x
    fin_str_v <- substring(wrd_i, init_v, end_v)
1245 75x
    is_only_spaces <- grepl("^\\s+$", fin_str_v)
1246
    # We pop only spaces at this point
1247 75x
    if (all(is_only_spaces)) {
1248 !
      fin_str_v <- fin_str_v[1] # keep only one width-sized empty
1249
    } else {
1250 75x
      fin_str_v <- fin_str_v[!is_only_spaces] # hybrid text + \s
1251
    }
1252
 1253 
    # Collapse the string
1254 75x
    paste0(fin_str_v, collapse = " ")
1255 75x
  }, character(1), USE.NAMES = FALSE)
1256
}
1257
 1258 
#' @describeIn wrap_string Deprecated function. Please use [wrap_string()] instead.
1259
#'
1260
#' @examples
1261
#' wrap_txt(str, 5, collapse = NULL)
1262
#'
1263
#' @export
1264
wrap_txt <- function(str, width, collapse = NULL, fontspec = font_spec()) {
1265 396x
  new_dev <- open_font_dev(fontspec)
1266 396x
  if (new_dev) {
1267 2x
    on.exit(close_font_dev())
1268
  }
1269
 1270 396x
  unlist(wrap_string(str, width, collapse, fontspec = fontspec), use.names = FALSE)
1271
}
1272
 1273 
pad_vert_top <- function(x, len, default = "") {
1274 5546x
  c(x, rep(default, len - length(x)))
1275
}
1276
 1277 
pad_vert_bottom <- function(x, len, default = "") {
1278 362x
  c(rep(default, len - length(x)), x)
1279
}
1280
 1281 
pad_vec_to_len <- function(vec, len, cpadder = pad_vert_top, rlpadder = cpadder) {
1282 741x
  dat <- unlist(lapply(vec[-1], cpadder, len = len))
1283 741x
  dat <- c(rlpadder(vec[[1]], len = len), dat)
1284 741x
  matrix(dat, nrow = len)
1285
}
1286
 1287 
rep_vec_to_len <- function(vec, len, ...) {
1288 698x
  matrix(unlist(lapply(vec, rep, times = len)),
1289 698x
    nrow = len
1290
  )
1291
}
1292
 1293 
safe_strsplit <- function(x, split, ...) {
1294 857x
  ret <- strsplit(x, split, ...)
1295 857x
  lapply(ret, function(reti) if (length(reti) == 0) "" else reti)
1296
}
1297
 1298 
.expand_mat_rows_inner <- function(i, mat, row_nlines, expfun, ...) {
1299 1439x
  leni <- row_nlines[i]
1300 1439x
  rw <- mat[i, ]
1301 1439x
  if (is.character(rw)) {
1302 857x
    rw <- safe_strsplit(rw, "\n", fixed = TRUE)
1303
  }
1304 1439x
  expfun(rw, len = leni, ...)
1305
}
1306
 1307 
expand_mat_rows <- function(mat, row_nlines = apply(mat, 1, nlines), expfun = pad_vec_to_len, ...) {
1308 280x
  rinds <- seq_len(nrow(mat))
1309 280x
  exprows <- lapply(rinds, .expand_mat_rows_inner,
1310 280x
    mat = mat,
1311 280x
    row_nlines = row_nlines,
1312 280x
    expfun = expfun,
1313
    ...
1314
  )
1315 280x
  do.call(rbind, exprows)
1316
}
1317
 1318 
#' Transform a vector of spans (with duplication) into a visibility vector
1319
#'
1320
#' @param spans (`numeric`)\cr a vector of spans, with each span value repeated
1321
#'   for the cells it covers.
1322
#'
1323
#' @details
1324
#' The values of `spans` are assumed to be repeated such that each individual position covered by the
1325
#' span has the repeated value.
1326
#'
1327
#' This means that each block of values in `spans` must be of a length at least equal to its value
1328
#' (i.e. two 2s, three 3s, etc).
1329
#'
1330
#' This function correctly handles cases where two spans of the same size are next to each other;
1331
#' i.e., a block of four 2s represents two large cells each of which spans two individual cells.
1332
#'
1333
#' @return A logical vector the same length as `spans` indicating whether the contents of a string vector
1334
#'   with those spans is valid.
1335
#'
1336
#' @note
1337
#' Currently no checking or enforcement is done to verify that the vector of spans is valid according to
1338
#' the specifications described in the Details section above.
1339
#'
1340
#' @examples
1341
#' spans_to_viscell(c(2, 2, 2, 2, 1, 3, 3, 3))
1342
#'
1343
#' @export
1344
spans_to_viscell <- function(spans) {
1345 2x
  if (!is.vector(spans)) {
1346 !
    spans <- as.vector(spans)
1347
  }
1348 2x
  myrle <- rle(spans)
1349 2x
  unlist(
1350 2x
    mapply(
1351 2x
      function(vl, ln) {
1352 4x
        rep(c(TRUE, rep(FALSE, vl - 1L)), times = ln / vl)
1353
      },
1354 2x
      SIMPLIFY = FALSE,
1355 2x
      vl = myrle$values,
1356 2x
      ln = myrle$lengths
1357
    ),
1358 2x
    recursive = FALSE
1359
  )
1360
}
1361
 1362 
#' Propose column widths based on the `MatrixPrintForm` of an object
1363
#'
1364
#' Row names are also considered a column for the output.
1365
#'
1366
#' @inheritParams open_font_dev
1367
#' @inheritParams format_value
1368
#' @param x (`ANY`)\cr a `MatrixPrintForm` object, or an object with a `matrix_form` method.
1369
#' @param indent_size (`numeric(1)`)\cr indent size, in characters. Ignored when `x` is already
1370
#'   a `MatrixPrintForm` object in favor of information there.
1371
#'
1372
#' @return A vector of column widths based on the content of `x` for use in printing and pagination.
1373
#'
1374
#' @examples
1375
#' mf <- basic_matrix_form(mtcars)
1376
#' propose_column_widths(mf)
1377
#'
1378
#' @export
1379
propose_column_widths <- function(x,
1380
                                  indent_size = 2,
1381
                                  fontspec = font_spec(),
1382
                                  round_type = c("iec", "sas")) {
1383 95x
  new_dev <- open_font_dev(fontspec)
1384 95x
  if (new_dev) {
1385 65x
    on.exit(close_font_dev())
1386
  }
1387
 1388 95x
  if (!is(x, "MatrixPrintForm")) {
1389 !
    x <- matrix_form(x, indent_rownames = TRUE, indent_size = indent_size, fontspec = fontspec, round_type = round_type)
1390
  }
1391 95x
  body <- mf_strings(x)
1392 95x
  spans <- mf_spans(x)
1393 95x
  aligns <- mf_aligns(x)
1394 95x
  display <- mf_display(x)
1395
 1396 
  # compute decimal alignment if asked in alignment matrix
1397 95x
  if (any_dec_align(aligns)) {
1398 27x
    body <- decimal_align(body, aligns)
1399
  }
1400
 1401 
  ## chars <- nchar(body) #old monospace assumption
1402
  ## we now use widths in terms of the printwidth of the space (" ")
1403
  ## character. This collapses to the same thing in the monospace
1404
  ## case but allows us to reasonably support truetype fonts
1405 92x
  chars <- nchar_ttype(body, fontspec)
1406
 1407 
  # first check column widths without colspan
1408 92x
  has_spans <- spans != 1
1409 92x
  chars_ns <- chars
1410 92x
  chars_ns[has_spans] <- 0
1411 92x
  widths <- apply(chars_ns, 2, max)
1412
 1413 
  # now check if the colspans require extra width
1414 92x
  if (any(has_spans)) {
1415 1x
    has_row_spans <- apply(has_spans, 1, any)
1416
 1417 1x
    chars_sp <- chars[has_row_spans, , drop = FALSE]
1418 1x
    spans_sp <- spans[has_row_spans, , drop = FALSE]
1419 1x
    disp_sp <- display[has_row_spans, , drop = FALSE]
1420
 1421 1x
    nc <- ncol(spans)
1422 1x
    for (i in seq_len(nrow(chars_sp))) {
1423 1x
      for (j in seq_len(nc)) {
1424 2x
        if (disp_sp[i, j] && spans_sp[i, j] != 1) {
1425 1x
          i_cols <- seq(j, j + spans_sp[i, j] - 1)
1426
 1427 1x
          nchar_i <- chars_sp[i, j]
1428 1x
          cw_i <- widths[i_cols]
1429 1x
          available_width <- sum(cw_i)
1430
 1431 1x
          if (nchar_i > available_width) {
1432
            # need to update widths to fit content with colspans
1433
            # spread width among columns
1434 !
            widths[i_cols] <- cw_i + spread_integer(nchar_i - available_width, length(cw_i))
1435
          }
1436
        }
1437
      }
1438
    }
1439
  }
1440 92x
  widths
1441
}
1442
 1443 
## "number of characters" width in terms of
1444
## width of " " for the chosen font family
1445
 1446 
## pdf device with font specification MUST already be open
1447
 1448 
#' Calculate font-specific string width
1449
#'
1450
#' This function returns the width of each element `x`
1451
#' *as a multiple of the width of the space character
1452
#' for in declared font*, rounded up to the nearest
1453
#' integer. This is used extensively in the text rendering
1454
#' ([toString()]) and pagination machinery for
1455
#' calculating word wrapping, default column widths,
1456
#' lines per page, etc.
1457
#'
1458
#' @param x (`character`)\cr the string(s) to calculate width(s) for.
1459
#' @param fontspec (`font_spec` or `NULL`)\cr if non-NULL, the font to use for
1460
#'   the calculations (as returned by [font_spec()]). Defaults to "Courier",
1461
#'   which is a monospace font. If NULL, the width will be returned
1462
#'   in number of characters by calling `nchar` directly.
1463
#' @param tol (`numeric(1)`)\cr the tolerance to use when determining
1464
#'   if a multiple needs to be rounded up to the next integer. See
1465
#'   Details.
1466
#' @param raw (`logical(1)`)\cr whether unrounded widths should be returned. Defaults to `FALSE`.
1467
#'
1468
#' @details String width is defined in terms of spaces within
1469
#' the specified font. For monospace fonts, this definition
1470
#' collapses to the number of characters in the string
1471
#' ([nchar()]), but for truetype fonts it does not.
1472
#'
1473
#' For `raw = FALSE`, non-integer values (the norm in a truetype
1474
#' setting) for the number of spaces a string takes up is rounded
1475
#' up, *unless the multiple is less than `tol` above the last integer
1476
#' before it*. E.g., if `k - num_spaces < tol` for an integer
1477
#' `k`, `k` is returned instead of `k+1`.
1478
#'
1479
#' @seealso [font_spec()]
1480
#'
1481
#' @examples
1482
#' nchar_ttype("hi there!")
1483
#'
1484
#' nchar_ttype("hi there!", font_spec("Times"))
1485
#'
1486
#' @export
1487
nchar_ttype <- function(x, fontspec = font_spec(), tol = sqrt(.Machine$double.eps), raw = FALSE) {
1488
  ## escape hatch because sometimes we need to call, e.g. make_row_df
1489
  ## but we dont' care about getting the word wrapping right and the
1490
  ## performance penalty was KILLING us. Looking at you
1491
  ## rtables::update_ref_indexing @.@
1492 48730x
  if (is.null(fontspec)) {
1493 1x
    return(nchar(x))
1494
  }
1495 48729x
  new_dev <- open_font_dev(fontspec)
1496 48729x
  if (new_dev) {
1497 149x
    on.exit(close_font_dev())
1498
  }
1499 48729x
  if (font_dev_state$ismonospace) { ## WAY faster if we can do it
1500 48705x
    return(nchar(x))
1501
  }
1502 24x
  space_width <- get_space_width()
1503
  ## cwidth_inches_unsafe is ok here because if we don't
1504
  ## have a successfully opened state (somehow), get_space_width
1505
  ## above will error.
1506 24x
  num_inches_raw <- vapply(x, cwidth_inches_unsafe, 1.0)
1507 24x
  num_spaces_raw <- num_inches_raw / space_width
1508 24x
  if (!raw) {
1509 1x
    num_spaces_ceil <- ceiling(num_spaces_raw)
1510
    ## we don't want to add one when the answer is e.g, 3.0000000000000953
1511 1x
    within_tol <- which(num_spaces_raw + 1 - num_spaces_ceil <= tol)
1512 1x
    ret <- num_spaces_ceil
1513 1x
    if (length(within_tol) == 0L) {
1514 1x
      ret[within_tol] <- floor(num_spaces_raw[within_tol])
1515
    }
1516
  } else {
1517 23x
    ret <- num_spaces_raw
1518
  }
1519 24x
  if (!is.null(dim(x))) {
1520 !
    dim(ret) <- dim(x)
1521
  } else {
1522 24x
    names(ret) <- NULL
1523
  }
1524 24x
  ret
1525
}
1526
 1527 
#' Pad a string and align within string
1528
#'
1529
#' @inheritParams open_font_dev
1530
#' @param x (`string`)\cr a string.
1531
#' @param n (`integer(1)`)\cr number of characters in the output string. If `n < nchar(x)`, an error is thrown.
1532
#' @param just (`string`)\cr text alignment justification to use. Defaults to `"center"`. Must be one of
1533
#'   `"center"`, `"right"`, `"left"`, `"dec_right"`, `"dec_left"`, or `"decimal"`.
1534
#'
1535
#' @return `x`, padded to be a string of length `n`.
1536
#'
1537
#' @examples
1538
#' padstr("abc", 3)
1539
#' padstr("abc", 4)
1540
#' padstr("abc", 5)
1541
#' padstr("abc", 5, "left")
1542
#' padstr("abc", 5, "right")
1543
#'
1544
#' \dontrun{
1545
#' # Expect error: "abc" has more than 1 characters
1546
#' padstr("abc", 1)
1547
#' }
1548
#'
1549
#' @export
1550
padstr <- function(x, n, just = list_valid_aligns(), fontspec = font_spec()) {
1551 15649x
  just <- match.arg(just)
1552
 1553 1x
  if (length(x) != 1) stop("length of x needs to be 1 and not", length(x))
1554 1x
  if (is.na(n) || !is.numeric(n) || n < 0) stop("n needs to be numeric and > 0")
1555
 1556 2x
  if (is.na(x)) x <- "<NA>"
1557
 1558 15647x
  nc <- nchar_ttype(x, fontspec)
1559 !
  if (n < nc) stop("\"", x, "\" has more than ", n, " characters")
1560
 1561 15647x
  switch(just,
1562
    center = {
1563 13742x
      pad <- (n - nc) / 2
1564 13742x
      paste0(spaces(floor(pad)), x, spaces(ceiling(pad)))
1565
    },
1566 1754x
    left = paste0(x, spaces(n - nc)),
1567 10x
    right = paste0(spaces(n - nc), x),
1568
    decimal = {
1569 61x
      pad <- (n - nc) / 2
1570 61x
      paste0(spaces(floor(pad)), x, spaces(ceiling(pad)))
1571
    },
1572 45x
    dec_left = paste0(x, spaces(n - nc)),
1573 35x
    dec_right = paste0(spaces(n - nc), x)
1574
  )
1575
}
1576
 1577 
spaces <- function(n) {
1578 29608x
  strrep(" ", n)
1579
}
1580
 1581 
.paste_no_na <- function(x, ...) {
1582 2408x
  paste(na.omit(x), ...)
1583
}
1584
 1585 
#' Spread an integer to a given length
1586
#'
1587
#' @param x (`integer(1)`)\cr number to spread.
1588
#' @param len (`integer(1)`)\cr number of times to repeat `x`.
1589
#'
1590
#' @return If `x` is a scalar whole number value (see [is.wholenumber()]), the value `x` is repeated `len` times.
1591
#'   Otherwise, an error is thrown.
1592
#'
1593
#' @examples
1594
#' spread_integer(3, 1)
1595
#' spread_integer(0, 3)
1596
#' spread_integer(1, 3)
1597
#' spread_integer(2, 3)
1598
#' spread_integer(3, 3)
1599
#' spread_integer(4, 3)
1600
#' spread_integer(5, 3)
1601
#' spread_integer(6, 3)
1602
#' spread_integer(7, 3)
1603
#'
1604
#' @export
1605
spread_integer <- function(x, len) {
1606 2x
  stopifnot(
1607 2x
    is.wholenumber(x), length(x) == 1, x >= 0,
1608 2x
    is.wholenumber(len), length(len) == 1, len >= 0,
1609 2x
    !(len == 0 && x > 0)
1610
  )
1611
 1612 1x
  if (len == 0) {
1613 !
    integer(0)
1614
  } else {
1615 1x
    y <- rep(floor(x / len), len)
1616 1x
    i <- 1
1617 1x
    while (sum(y) < x) {
1618 1x
      y[i] <- y[i] + 1
1619 1x
      if (i == len) {
1620 !
        i <- 1
1621
      } else {
1622 1x
        i <- i + 1
1623
      }
1624
    }
1625 1x
    y
1626
  }
1627
}
1628
 1629 
#' Check if a value is a whole number
1630
#'
1631
#' @param x (`numeric(1)`)\cr a numeric value.
1632
#' @param tol (`numeric(1)`)\cr a precision tolerance.
1633
#'
1634
#' @return `TRUE` if `x` is within `tol` of zero, `FALSE` otherwise.
1635
#'
1636
#' @examples
1637
#' is.wholenumber(5)
1638
#' is.wholenumber(5.00000000000000001)
1639
#' is.wholenumber(.5)
1640
#'
1641
#' @export
1642
is.wholenumber <- function(x, tol = .Machine$double.eps^0.5) {
1643 3x
  abs(x - round(x)) < tol
1644
}

RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4