From 0f0606153771e3b66c8be800d2acc179dc82a126 Mon Sep 17 00:00:00 2001 From: Melkiades Date: Mon, 10 Feb 2025 12:59:07 +0100 Subject: [PATCH 1/5] add sas round support --- NAMESPACE | 2 ++ NEWS.md | 2 ++ R/format_value.R | 57 +++++++++++++++++++++++++++----- R/tostring.R | 49 --------------------------- R/zzz.R | 37 +++++++++++++++++++++ formatters.Rproj | 1 - man/default_horizontal_sep.Rd | 17 +--------- man/default_rounding_type.Rd | 28 ++++++++++++++++ man/round_fmt.Rd | 14 +++++--- tests/testthat/test-formatters.R | 9 ----- tests/testthat/test_zzz.R | 42 +++++++++++++++++++++++ 11 files changed, 169 insertions(+), 89 deletions(-) create mode 100644 man/default_rounding_type.Rd create mode 100644 tests/testthat/test_zzz.R diff --git a/NAMESPACE b/NAMESPACE index 93dc9004e..c8f1f6de2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,6 +39,7 @@ export(debug_font_dev) export(decimal_align) export(default_hsep) export(default_page_number) +export(default_rounding) export(diagnose_pagination) export(divider_height) export(do_forced_paginate) @@ -99,6 +100,7 @@ export(ref_df_row) export(round_fmt) export(set_default_hsep) export(set_default_page_number) +export(set_default_rounding) export(spans_to_viscell) export(split_word_ttype) export(spread_integer) diff --git a/NEWS.md b/NEWS.md index 4b148e3cc..852de4314 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ ## formatters 0.5.10.9001 * Fixed a bug in `mform_handle_newlines` that caused string matrix column names to be removed. This prevented paginated listing key column info from being repeated when vertically spanning multiple pages. +* Fixed handling for `format_value(format = fun())` for cases where a custom function is used. +* Added handling for rounding types. Specifically, `default_rounding()` and `get_default_rounding()` now allow for setting and getting the rounding type for `format_value()`. ## formatters 0.5.10 * Fixed a bug in `mf_update_cinfo` causing an error when `export_as_txt` was applied to empty listings. diff --git a/R/format_value.R b/R/format_value.R index 80cda3a7b..f59a2fb31 100644 --- a/R/format_value.R +++ b/R/format_value.R @@ -1,3 +1,20 @@ +fun_takes <- function(f, nm) { + nm %in% names(formals(f)) +} + +call_format_fun <- function(f, + value, + na_str, + output) { + args <- c( + list(value), + if (fun_takes(f, "na_str")) list(na_str = na_str), + if (fun_takes(f, "output")) list(output = output) + ) + do.call(f, args) +} + + formats_1d <- c( "xx", "xx.", "xx.x", "xx.xx", "xx.xxx", "xx.xxxx", "xx%", "xx.%", "xx.x%", "xx.xx%", "xx.xxx%", "(N=xx)", "N=xx", ">999.9", ">999.99", @@ -146,9 +163,10 @@ sprintf_format <- function(format) { #' @param na_str (`string`)\cr the value to return if `x` is `NA`. #' #' @details -#' This function combines the rounding behavior of R's standards-compliant [round()] -#' function (see the Details section of that documentation) with the strict decimal display -#' of [sprintf()]. The exact behavior is as follows: +#' This function combines rounding behavior with the strict decimal display of +#' [sprintf()]. By default, R's standards-compliant [round()] +#' function (see the Details section of that documentation) is used. The exact +#' behavior is as follows: #' #' \enumerate{ #' \item{If `x` is `NA`, the value of `na_str` is returned.} @@ -156,6 +174,9 @@ sprintf_format <- function(format) { #' \item{If `x` and `digits` are both non-NA, [round()] is called first, and then [sprintf()] #' is used to convert the rounded value to a character with the appropriate number of trailing #' zeros enforced.} +#' \item{If you need to change the type of rounding to perform, please use `set_default_rounding(round_type)`. +#' With `round_type = "iec"`, the default, rounding is compliant with IEC 60559 (see details), while +#' `round_type = "sas"` performs nearest-value rounding consistent with rounding within SAS.} #' } #' #' @return A character value representing the value after rounding, containing any trailing zeros @@ -169,9 +190,9 @@ sprintf_format <- function(format) { #' not at least `digits` significant digits after the decimal that remain after rounding. It *may* differ from #' `sprintf("\%.Nf", x)` for values ending in `5` after the decimal place on many popular operating systems #' due to `round`'s stricter adherence to the IEC 60559 standard, particularly for R versions > 4.0.0 (see -#' warning in [round()] documentation). +#' warning in [round()] documentation). For changing the rounding behavior, see `set_default_rounding()`. #' -#' @seealso [format_value()], [round()], [sprintf()] +#' @seealso [set_default_rounding()], [format_value()], [round()], [sprintf()] #' #' @examples #' round_fmt(0, digits = 3) @@ -191,11 +212,28 @@ round_fmt <- function(x, digits, na_str = "NA") { } else if (is.na(digits)) { paste0(x) } else { + rndx <- switch(default_rounding(), + iec = round(x, digits), + sas = round_sas(x, digits) + ) sprfmt <- paste0("%.", digits, "f") - sprintf(fmt = sprfmt, round(x, digits = digits)) + sprintf(fmt = sprfmt, rndx) } } +## https://stackoverflow.com/questions/12688717/round-up-from-5 +round_sas <- function(x, digits = 0) { + # perform SAS rounding + posneg <- sign(x) + z <- abs(x) * 10^digits + z <- z + 0.5 + sqrt(.Machine$double.eps) + z <- trunc(z) + z <- z / 10^digits + z <- z * posneg + ## return numeric vector of rounded values + z +} + val_pct_helper <- function(x, dig1, dig2, na_str, pct = TRUE) { if (pct) { x[2] <- x[2] * 100 @@ -272,10 +310,11 @@ format_value <- function(x, format = NULL, output = c("ascii", "html"), na_str = } if (length(na_str) == 1) { if (!all(is.na(x))) { - na_str <- array(na_str, dim = length(x)) + ## array adds an unneeded dim attribute which causes problems + na_str <- rep(na_str, length(x)) } } else { # length(na_str) > 1 - tmp_na_str <- array("NA", dim = length(x)) + tmp_na_str <- rep("NA", length(x)) tmp_na_str[is.na(x)] <- na_str[seq(sum(is.na(x)))] na_str <- tmp_na_str } @@ -288,7 +327,7 @@ format_value <- function(x, format = NULL, output = c("ascii", "html"), na_str = } else if (is.null(format)) { toString(x) } else if (is.function(format)) { - format(x, output = output) + call_format_fun(f = format, value = x, na_str = na_str, output = output) } else if (is.character(format)) { l <- if (format %in% formats_1d) { 1 diff --git a/R/tostring.R b/R/tostring.R index 0d07804d2..f0fecf7a2 100644 --- a/R/tostring.R +++ b/R/tostring.R @@ -205,55 +205,6 @@ gpar_from_fspec <- function(fontspec) { font_dev_is_open <- function() font_dev_state$open -#' Default horizontal separator -#' -#' The default horizontal separator character which can be displayed in the current -#' charset for use in rendering table-like objects. -#' -#' @param hsep_char (`string`)\cr character that will be set in the R environment -#' options as the default horizontal separator. Must be a single character. Use -#' `getOption("formatters_default_hsep")` to get its current value (`NULL` if not set). -#' -#' @return unicode 2014 (long dash for generating solid horizontal line) if in a -#' locale that uses a UTF character set, otherwise an ASCII hyphen with a -#' once-per-session warning. -#' -#' @examples -#' default_hsep() -#' set_default_hsep("o") -#' default_hsep() -#' -#' @name default_horizontal_sep -#' @export -default_hsep <- function() { - system_default_hsep <- getOption("formatters_default_hsep") - - if (is.null(system_default_hsep)) { - if (any(grepl("^UTF", utils::localeToCharset()))) { - hsep <- "\u2014" - } else { - if (interactive()) { - warning( - "Detected non-UTF charset. Falling back to '-' ", - "as default header/body separator. This warning ", - "will only be shown once per R session." - ) # nocov - } # nocov - hsep <- "-" # nocov - } - } else { - hsep <- system_default_hsep - } - hsep -} - -#' @name default_horizontal_sep -#' @export -set_default_hsep <- function(hsep_char) { - checkmate::assert_character(hsep_char, n.chars = 1, len = 1, null.ok = TRUE) - options("formatters_default_hsep" = hsep_char) -} - .calc_cell_widths <- function(mat, colwidths, col_gap) { spans <- mat$spans keep_mat <- mat$display diff --git a/R/zzz.R b/R/zzz.R index c4d69e39a..ead52df82 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -47,6 +47,43 @@ set_default_hsep <- function(hsep_char) { options("formatters_default_hsep" = hsep_char) } +#' Default rounding type +#' +#' The default rounding type for numeric values in any formatting outputs like [format_value()]. +#' +#' @param round_type (`string`)\cr single character value to set the default rounding type. It can be +#' either `"iec"` or `"sas"` for IEC 60559 or SAS rounding (nearest-value rounding), respectively. +#' +#' @return The default rounding type (`"iec"` if not set). +#' +#' @examples +#' default_rounding() +#' set_default_rounding("sas") +#' default_rounding() +#' +#' @name default_rounding_type +#' @export +default_rounding <- function() { + formatters_default_rounding <- getOption("formatters_default_rounding") + + rounding <- if (is.null(formatters_default_rounding)) { + "iec" + } else { + formatters_default_rounding + } + rounding +} + +#' @name default_rounding_type +#' @export +set_default_rounding <- function(round_type = c("iec", "sas")) { + round_type <- round_type[1] + checkmate::assert_character(round_type, len = 1, null.ok = TRUE) + checkmate::assert_choice(round_type, c("iec", "sas"), null.ok = TRUE) + options("formatters_default_rounding" = round_type) +} + + #' Default page number format #' #' If set, the default page number string will appear on the bottom right of diff --git a/formatters.Rproj b/formatters.Rproj index 8d2f37c23..270314b87 100644 --- a/formatters.Rproj +++ b/formatters.Rproj @@ -1,5 +1,4 @@ Version: 1.0 -ProjectId: 91d83c00-f38c-4245-80dc-783a2cfe8487 RestoreWorkspace: Default SaveWorkspace: Default diff --git a/man/default_horizontal_sep.Rd b/man/default_horizontal_sep.Rd index 8b81d826c..1f44ee13d 100644 --- a/man/default_horizontal_sep.Rd +++ b/man/default_horizontal_sep.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tostring.R, R/zzz.R +% Please edit documentation in R/zzz.R \name{default_horizontal_sep} \alias{default_horizontal_sep} \alias{default_hsep} @@ -8,10 +8,6 @@ \usage{ default_hsep() -set_default_hsep(hsep_char) - -default_hsep() - set_default_hsep(hsep_char) } \arguments{ @@ -20,18 +16,11 @@ options as the default horizontal separator. Must be a single character. Use \code{getOption("formatters_default_hsep")} to get its current value (\code{NULL} if not set).} } \value{ -unicode 2014 (long dash for generating solid horizontal line) if in a -locale that uses a UTF character set, otherwise an ASCII hyphen with a -once-per-session warning. - unicode 2014 (long dash for generating solid horizontal line) if in a locale that uses a UTF character set, otherwise an ASCII hyphen with a once-per-session warning. } \description{ -The default horizontal separator character which can be displayed in the current -charset for use in rendering table-like objects. - The default horizontal separator character which can be displayed in the current charset for use in rendering table-like objects. } @@ -40,8 +29,4 @@ default_hsep() set_default_hsep("o") default_hsep() -default_hsep() -set_default_hsep("o") -default_hsep() - } diff --git a/man/default_rounding_type.Rd b/man/default_rounding_type.Rd new file mode 100644 index 000000000..bff06f823 --- /dev/null +++ b/man/default_rounding_type.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{default_rounding_type} +\alias{default_rounding_type} +\alias{default_rounding} +\alias{set_default_rounding} +\title{Default rounding type} +\usage{ +default_rounding() + +set_default_rounding(round_type = "ise") +} +\arguments{ +\item{round_type}{(\code{string})\cr single character value to set the default rounding type. It can be +either \code{"iec"} or \code{"sas"} for IEC 60559 or SAS rounding (nearest-value rounding), respectively.} +} +\value{ +The default rounding type (\code{"iec"} if not set). +} +\description{ +The default rounding type for numeric values in any formatting outputs like \code{\link[=format_value]{format_value()}}. +} +\examples{ +default_rounding() +set_default_rounding("sas") +default_rounding() + +} diff --git a/man/round_fmt.Rd b/man/round_fmt.Rd index fe3d24cb3..7c5f2d1ed 100644 --- a/man/round_fmt.Rd +++ b/man/round_fmt.Rd @@ -24,9 +24,10 @@ This function is used within \code{\link[=format_value]{format_value()}} to prep cells for formatting and display. } \details{ -This function combines the rounding behavior of R's standards-compliant \code{\link[=round]{round()}} -function (see the Details section of that documentation) with the strict decimal display -of \code{\link[=sprintf]{sprintf()}}. The exact behavior is as follows: +This function combines rounding behavior with the strict decimal display of +\code{\link[=sprintf]{sprintf()}}. By default, R's standards-compliant \code{\link[=round]{round()}} +function (see the Details section of that documentation) is used. The exact +behavior is as follows: \enumerate{ \item{If \code{x} is \code{NA}, the value of \code{na_str} is returned.} @@ -34,6 +35,9 @@ of \code{\link[=sprintf]{sprintf()}}. The exact behavior is as follows: \item{If \code{x} and \code{digits} are both non-NA, \code{\link[=round]{round()}} is called first, and then \code{\link[=sprintf]{sprintf()}} is used to convert the rounded value to a character with the appropriate number of trailing zeros enforced.} +\item{If you need to change the type of rounding to perform, please use \code{set_default_rounding(round_type)}. +With \code{round_type = "iec"}, the default, rounding is compliant with IEC 60559 (see details), while +\code{round_type = "sas"} performs nearest-value rounding consistent with rounding within SAS.} } } \note{ @@ -44,7 +48,7 @@ This behavior will differ from \code{as.character(round(x, digits = digits))} in not at least \code{digits} significant digits after the decimal that remain after rounding. It \emph{may} differ from \code{sprintf("\\\%.Nf", x)} for values ending in \code{5} after the decimal place on many popular operating systems due to \code{round}'s stricter adherence to the IEC 60559 standard, particularly for R versions > 4.0.0 (see -warning in \code{\link[=round]{round()}} documentation). +warning in \code{\link[=round]{round()}} documentation). For changing the rounding behavior, see \code{set_default_rounding()}. } \examples{ round_fmt(0, digits = 3) @@ -55,5 +59,5 @@ round_fmt(2.765923, digits = NA) } \seealso{ -\code{\link[=format_value]{format_value()}}, \code{\link[=round]{round()}}, \code{\link[=sprintf]{sprintf()}} +\code{\link[=set_default_rounding]{set_default_rounding()}}, \code{\link[=format_value]{format_value()}}, \code{\link[=round]{round()}}, \code{\link[=sprintf]{sprintf()}} } diff --git a/tests/testthat/test-formatters.R b/tests/testthat/test-formatters.R index 5a88300c4..53302b23f 100644 --- a/tests/testthat/test-formatters.R +++ b/tests/testthat/test-formatters.R @@ -1,14 +1,5 @@ values <- c(5.123456, 7.891112) -test_that("Default horizontal separator works", { - expect_true(is.null(getOption("formatters_default_hsep"))) - expect_error(set_default_hsep("foo")) - expect_silent(set_default_hsep("a")) - expect_equal(default_hsep(), "a") - expect_silent(set_default_hsep(NULL)) - expect_true(default_hsep() %in% c("\u2014", "-")) -}) - test_that("Default horizontal separator works", { expect_true(is.null(getOption("formatters_default_page_number"))) expect_true(is.null(default_page_number())) diff --git a/tests/testthat/test_zzz.R b/tests/testthat/test_zzz.R new file mode 100644 index 000000000..1b94b997b --- /dev/null +++ b/tests/testthat/test_zzz.R @@ -0,0 +1,42 @@ +test_that("Default horizontal separator works", { + expect_true(is.null(getOption("formatters_default_hsep"))) + expect_error(set_default_hsep("foo")) + expect_silent(set_default_hsep("a")) + expect_equal(default_hsep(), "a") + expect_silent(set_default_hsep(NULL)) + expect_true(default_hsep() %in% c("\u2014", "-")) +}) + +test_that("Default rounding type setting and retrieval work", { + expect_true(is.null(getOption("formatters_default_rounding"))) + expect_error(set_default_rounding("foo")) + expect_equal(default_rounding(), "iec") + expect_silent(set_default_rounding("sas")) + expect_equal(default_rounding(), "sas") + expect_silent(set_default_rounding(NULL)) + expect_equal(default_rounding(), "iec") +}) + +test_that("Changing rounding type works", { + # optional sas-style rounding + tricky_val <- 0.845 + + expect_identical( + format_value(tricky_val, "xx.xx"), + "0.84" + ) + + set_default_rounding("sas") + expect_identical( + format_value(tricky_val, "xx.xx"), + "0.85" + ) + + format_fun_rtype <- function(x, output) default_rounding() + expect_identical(format_value(tricky_val, format_fun_rtype), "sas") + + # passing down na_str + format_fun_na_str <- function(x, na_str) na_str + expect_identical(format_value(tricky_val, format_fun_na_str), "NA") + expect_identical(format_value(tricky_val, format_fun_na_str, na_str = "-"), "-") +}) From 470b1939b19495eeb66d0e10c3aea815030ae2e7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:03:11 +0000 Subject: [PATCH 2/5] [skip style] [skip vbump] Restyle files --- R/format_value.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/format_value.R b/R/format_value.R index f59a2fb31..c5783d815 100644 --- a/R/format_value.R +++ b/R/format_value.R @@ -213,8 +213,8 @@ round_fmt <- function(x, digits, na_str = "NA") { paste0(x) } else { rndx <- switch(default_rounding(), - iec = round(x, digits), - sas = round_sas(x, digits) + iec = round(x, digits), + sas = round_sas(x, digits) ) sprfmt <- paste0("%.", digits, "f") sprintf(fmt = sprfmt, rndx) From 26e3293461df8c7e513c9fae213eace190078454 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:06:42 +0000 Subject: [PATCH 3/5] [skip roxygen] [skip vbump] Roxygen Man Pages Auto Update --- man/default_rounding_type.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/default_rounding_type.Rd b/man/default_rounding_type.Rd index bff06f823..bb869a8c6 100644 --- a/man/default_rounding_type.Rd +++ b/man/default_rounding_type.Rd @@ -8,7 +8,7 @@ \usage{ default_rounding() -set_default_rounding(round_type = "ise") +set_default_rounding(round_type = c("iec", "sas")) } \arguments{ \item{round_type}{(\code{string})\cr single character value to set the default rounding type. It can be From c36da5b86880a2b309979fdafc6401422b4dd228 Mon Sep 17 00:00:00 2001 From: Melkiades Date: Mon, 10 Feb 2025 15:28:21 +0100 Subject: [PATCH 4/5] empty From 8acfc4644ea3cd718aedaec041ea519ac7ac05b9 Mon Sep 17 00:00:00 2001 From: Melkiades Date: Mon, 10 Feb 2025 16:23:37 +0100 Subject: [PATCH 5/5] fix docs --- R/zzz.R | 4 ++-- _pkgdown.yml | 1 + man/{default_rounding_type.Rd => default_rounding.Rd} | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) rename man/{default_rounding_type.Rd => default_rounding.Rd} (92%) diff --git a/R/zzz.R b/R/zzz.R index ead52df82..9a0ead3c4 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -61,7 +61,7 @@ set_default_hsep <- function(hsep_char) { #' set_default_rounding("sas") #' default_rounding() #' -#' @name default_rounding_type +#' @name default_rounding #' @export default_rounding <- function() { formatters_default_rounding <- getOption("formatters_default_rounding") @@ -74,7 +74,7 @@ default_rounding <- function() { rounding } -#' @name default_rounding_type +#' @name default_rounding #' @export set_default_rounding <- function(round_type = c("iec", "sas")) { round_type <- round_type[1] diff --git a/_pkgdown.yml b/_pkgdown.yml index 0652cd695..b96591f62 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -104,6 +104,7 @@ reference: - ifnotlen0 - table_inset - default_horizontal_sep + - default_rounding - mf_strings - page_lcpp - page_types diff --git a/man/default_rounding_type.Rd b/man/default_rounding.Rd similarity index 92% rename from man/default_rounding_type.Rd rename to man/default_rounding.Rd index bb869a8c6..525322099 100644 --- a/man/default_rounding_type.Rd +++ b/man/default_rounding.Rd @@ -1,7 +1,6 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/zzz.R -\name{default_rounding_type} -\alias{default_rounding_type} +\name{default_rounding} \alias{default_rounding} \alias{set_default_rounding} \title{Default rounding type}