Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the fmt_fraction() formatter function #753

Merged
merged 70 commits into from Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
a4e1d19
Modify URL for Google Fonts
rich-iannone May 3, 2021
cabd3b9
Add R script for generating fraction tables
rich-iannone May 3, 2021
b7375ec
Create X06-fractions.R
rich-iannone May 3, 2021
2637e69
Add CSV for fraction tables
rich-iannone May 3, 2021
ac8ed0f
Update zz_process_datasets_ext.R
rich-iannone May 3, 2021
dab600d
Update sysdata.rda
rich-iannone May 3, 2021
7b14011
Add draft `fmt_fraction()` fn
rich-iannone May 4, 2021
d9f91bb
Merge branch 'master' into fmt-fraction
rich-iannone May 4, 2021
7e0eac4
Update frac_generation.R
rich-iannone May 4, 2021
1f7feb6
Add CSVs with fraction values
rich-iannone May 4, 2021
ea841b8
Update X06-fractions.R
rich-iannone May 4, 2021
d841c8d
Update zz_process_datasets_ext.R
rich-iannone May 4, 2021
017700c
Update sysdata.rda
rich-iannone May 4, 2021
dd9c5df
Refactor `fmt_fraction()`
rich-iannone May 4, 2021
7f96066
Update fmt_fraction.Rd
rich-iannone May 4, 2021
7db71fd
Update format_data.R
rich-iannone May 4, 2021
835c66e
Refactor `fmt_fraction()`
rich-iannone May 4, 2021
d4f876b
Create test-fmt_fraction.R
rich-iannone May 4, 2021
e82f321
Refactor and modify `layout` option names
rich-iannone May 5, 2021
cf016ca
Update fmt_fraction.Rd
rich-iannone May 5, 2021
34b40e6
Update test-fmt_fraction.R
rich-iannone May 5, 2021
413c8bd
Update _pkgdown.yml
rich-iannone May 5, 2021
45cdf54
Avoid partial argument match
rich-iannone May 5, 2021
e007402
Fix and update documentation
rich-iannone May 5, 2021
efcc0c2
Update test-fmt_fraction.R
rich-iannone May 5, 2021
a0aa613
Create custom rounding function for `fmt_fraction()`
rich-iannone May 5, 2021
ccffd31
Merge branch 'master' into fmt-fraction
rich-iannone Jun 11, 2021
49b3604
Merge branch 'master' into fmt-fraction
rich-iannone Jun 17, 2021
c9e56b8
Re-number some formatter functions
rich-iannone Jun 17, 2021
e75a00f
Merge branch 'master' into fmt-fraction
rich-iannone Jul 15, 2021
083cbb0
Merge branch 'master' into fmt-fraction
rich-iannone Feb 1, 2022
5690c19
`devtools::document()` (GitHub Actions)
rich-iannone Feb 1, 2022
e75ebf1
Remove unneeded CSV files
rich-iannone Feb 1, 2022
43dbb11
Add the `fractions.csv` file
rich-iannone Feb 1, 2022
8a92301
Revise reading of CSV
rich-iannone Feb 1, 2022
384de29
Update zz_process_datasets_ext.R
rich-iannone Feb 1, 2022
3da8a75
Delete frac_generation.R
rich-iannone Feb 1, 2022
43935b8
Update X04-palettes_strips.R
rich-iannone Feb 1, 2022
64543c9
Merge branch 'fmt-fraction' of https://github.com/rstudio/gt into fmt…
rich-iannone Feb 1, 2022
faabc9e
Update sysdata.rda
rich-iannone Feb 1, 2022
5e143a1
Update zz_process_datasets_ext.R
rich-iannone Feb 1, 2022
7d9aa6d
Update implementation for obtaining fractions
rich-iannone Feb 1, 2022
380b4cb
`devtools::document()` (GitHub Actions)
rich-iannone Feb 1, 2022
6a8a516
Merge branch 'master' into fmt-fraction
rich-iannone Feb 2, 2022
a785d01
`devtools::document()` (GitHub Actions)
rich-iannone Feb 2, 2022
e8f4539
Update gt_styles_default.scss
rich-iannone Feb 2, 2022
f504b1e
Rewrite statements to account for all edge cases
rich-iannone Feb 2, 2022
6ec1f26
Merge branch 'fmt-fraction' of https://github.com/rstudio/gt into fmt…
rich-iannone Feb 2, 2022
940dfcd
Use specific formatting of numbers internally
rich-iannone Feb 2, 2022
0a3c19d
Update test-fmt_fraction.R
rich-iannone Feb 2, 2022
e88af40
Add snapshot testing
rich-iannone Feb 2, 2022
fc18fe1
Update snapshot testing for HTML outputs
rich-iannone Feb 2, 2022
1551ee5
Improve fraction formatting for LaTeX outputs
rich-iannone Feb 2, 2022
30cb581
Add snapshot tests for LaTeX outputs
rich-iannone Feb 2, 2022
e7465a9
Merge branch 'master' into fmt-fraction
rich-iannone Feb 2, 2022
845a0a9
Handle display of fractions in RTF output
rich-iannone Feb 2, 2022
63b5caa
Add snapshot tests for RTF output
rich-iannone Feb 2, 2022
19537b6
Merge branch 'master' into fmt-fraction
rich-iannone Feb 4, 2022
06e5f08
Add WIP re-implementation of fraction generation
rich-iannone Feb 5, 2022
9492439
`devtools::document()` (GitHub Actions)
rich-iannone Feb 5, 2022
1d6dad8
Revise formatting of fractions
rich-iannone Feb 5, 2022
d719a9b
Update testthat tests
rich-iannone Feb 5, 2022
ce6ca55
Merge branch 'fmt-fraction' of https://github.com/rstudio/gt into fmt…
rich-iannone Feb 5, 2022
c4063c9
Handle formatting of very large fraction parts
rich-iannone Feb 6, 2022
301e948
Add several testthat tests
rich-iannone Feb 6, 2022
b21846c
Update documentation for `fmt_fraction()`
rich-iannone Feb 6, 2022
686d06f
Update `fractions` dataset
rich-iannone Feb 6, 2022
202c391
Simplify lookup of fraction value
rich-iannone Feb 6, 2022
2128963
Update documentation for `fmt_fraction()`
rich-iannone Feb 7, 2022
8c6bd85
Code review
jcheng5 Feb 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Expand Up @@ -52,6 +52,7 @@ export(fmt_currency)
export(fmt_date)
export(fmt_datetime)
export(fmt_engineering)
export(fmt_fraction)
export(fmt_integer)
export(fmt_markdown)
export(fmt_missing)
Expand Down
308 changes: 306 additions & 2 deletions R/format_data.R
Expand Up @@ -996,6 +996,310 @@ fmt_percent <- function(data,
)
}

#' Format values as a mixed fractions
#'
#' @description
#' With numeric values in a **gt** table, we can perform mixed-fraction-based
#' formatting. There are several options for setting the accuracy of the
#' fractions (e.g., setting the number of desired figures in the denominator or
#' expressing fractions with fixed denominators). The following options are
#' available for controlling this type of formatting:
#'
#' - accuracy: how to express the fractional part of the mixed fractions; there
#' are nine named options for this.
#' - layout: for HTML output, we can choose to use diagonal or inline fractions
#' - digit grouping separators: options to enable/disable digit separators
#' and provide a choice of separator symbol for the whole number portion
#' - pattern: option to use a text pattern for decoration of the formatted
#' mixed fractions
#' - locale-based formatting: providing a locale ID will result in number
#' formatting specific to the chosen locale
#'
#' @details
#' Targeting of values is done through `columns` and additionally by `rows` (if
#' nothing is provided for `rows` then entire columns are selected). A number of
#' helper functions exist to make targeting more effective. Conditional
#' formatting is possible by providing a conditional expression to the `rows`
#' argument. See the Arguments section for more information on this.
#'
#' @inheritParams fmt_number
#' @param accuracy The type of fractions to generate. With either of `"low"`,
#' `"med"`, and `"high"` we can generate fractions with denominators of up to
#' 1, 2, or 3 digits. Several options provide a fixed denominator: `"/2"` (for
#' fractions as halves), `"/4"` (quarters), `"/8"` (eighths), `"/16"`
#' (sixteenths), and `"1/100"` (hundredths).
#' @param layout For HTML output, the `"diagonal"` layout is the default. This
#' will generate fractions that are typeset with raised/lowered numerals and a
#' virgule. The `"inline"` layout places the numerals of the fraction on the
#' baseline and uses a standard slash character.
#'
#' @return An object of class `gt_tbl`.
#'
#' @family Format Data
#' @section Function ID:
#' 3-6
#'
#' @import rlang
#' @export
fmt_fraction <- function(
data,
columns,
rows = everything(),
accuracy = c("low", "med", "high", "/2", "/4", "/8", "/16", "/100"),
layout = c("diagonal", "inline"),
use_seps = TRUE,
pattern = "{x}",
sep_mark = ",",
dec_mark = ".",
locale = NULL
) {

accuracy <- match.arg(accuracy)
layout <- match.arg(layout)

# Perform input object validation
stop_if_not_gt(data = data)

# Resolve the `locale` value here with the global locale value
locale <- resolve_locale(data = data, locale = locale)

# Stop function if any columns have data that is incompatible
# with this formatter
if (!column_classes_are_valid(
data = data,
columns = {{ columns }},
valid_classes = c("numeric", "integer")
)
) {
stop(
"The `fmt_fraction()` function can only be used on `columns` with numeric data",
call. = FALSE
)
}

# In the inline layout (where there is no special typesetting of the
# fraction part) we must include a space to distinguish between the
# whole number and fraction parts
# if (layout == "inline") {
# incl_space <- TRUE
# }

# Use locale-based marks if a locale ID is provided
sep_mark <- get_locale_sep_mark(locale, sep_mark, use_seps)
dec_mark <- get_locale_dec_mark(locale, dec_mark)

# Pass `data`, `columns`, `rows`, and the formatting
# functions as a function list to `fmt()`
fmt(
data = data,
columns = {{ columns }},
rows = {{ rows }},
fns = num_fmt_factory_multi(
pattern = pattern,
format_fn = function(x, context) {

# Round all values of x to 3 digits with the R-H-U method of
# rounding (for reproducibility purposes)
x <- round_gt(x, 3)

# Determine which of `x` are finite values
x_is_a_number <- is.finite(x)

# Divide the `x` values in 'big' and 'small' components; delay the
# formatting of `big_x` until it is appropriately rounded on the
# basis of the fractions obtained at the desired accuracy
big_x <- trunc(x)
small_x <- abs(x - big_x)

# Get the correct minus mark based on the output context
minus_mark <- context_minus_mark(context = context)

rich-iannone marked this conversation as resolved.
Show resolved Hide resolved
# Format the 'small' portion of the numeric values
# to character-based numbers with exactly 3 decimal places
small_x[x_is_a_number] <-
format_num_to_str(
small_x[x_is_a_number],
context = context, decimals = 3, n_sigfig = NULL,
sep_mark = ",", dec_mark = ".",
drop_trailing_zeros = FALSE,
drop_trailing_dec_mark = TRUE,
format = "f"
)

# Map the `accuracy` keyword to a column index in the
# internal `fractions` dataset
fractions_col_idx <-
which(c("low", "med", "high", "/2", "/4", "/8", "/16", "/100") %in% accuracy) + 1

# Generate an vector of empty strings that will eventually contain
# all of the fractional parts of the finalized numbers
fraction_x <- rep("", length(x))

# For every `small_x` value that corresponds to a number
# (i.e., not Inf), get the fractional part from the `fractions`
# lookup table
fraction_x[x_is_a_number] <-
vapply(
small_x[x_is_a_number],
FUN.VALUE = character(1),
USE.NAMES = FALSE,
FUN = function(x) {
fractions[fractions[["x"]] == x, ][[fractions_col_idx]]
}
)
rich-iannone marked this conversation as resolved.
Show resolved Hide resolved

# Round up or down the `big_x` values when necessary; values
# of exactly "1" indicate a requirement for rounding and this
# is a two-pass operation to handle positive and then negative
# values of `big_x`
big_x[big_x >= 0 & fraction_x == "1"] <-
big_x[big_x >= 0 & fraction_x == "1"] + 1

big_x[big_x <= 0 & fraction_x == "1"] <-
big_x[big_x <= 0 & fraction_x == "1"] - 1

# Remove whole number values from `fraction_x`; they were only
# needed for rounding guidance and they signal the lack of a
# fractional part
fraction_x[fraction_x %in% c("0", "1")] <- ""

# Format the 'big' portion of the numeric values
# to character-based numbers
big_x <-
format_num_to_str(
big_x,
context = context, decimals = 0, n_sigfig = NULL,
sep_mark = sep_mark, dec_mark = dec_mark,
drop_trailing_zeros = TRUE,
drop_trailing_dec_mark = TRUE,
format = "f"
)

# Initialize a vector that will contain the finalized strings
x_str <- character(length(x))

# Generate the mixed fractions by pasting `big_x` and `small_x`
# while ensuring there is a single space between these components
x_str[x_is_a_number] <-
paste(
big_x[x_is_a_number],
fraction_x[x_is_a_number],
sep = " "
)

# Trim any whitespace
x_str <- gsub("(^ | $)", "", x_str)

# Eliminate the display of leading zeros in mixed fractions
x_str <- gsub("^0\\s+?", "", x_str)

# There are situations where small fractions (not mixed) require
# a minus mark; these conditions are specific so we need to ascertain
# which values in `x_str` require this and then apply the mark to
# the targets
x_is_negative <- x < 0
x_is_zero <- x_str == "0"
x_has_minus_mark <- grepl(minus_mark, big_x)
x_needs_minus_mark <- x_is_negative & !x_is_zero & !x_has_minus_mark

x_str[x_needs_minus_mark] <- paste0(minus_mark, x_str[x_needs_minus_mark])

# Generate diagonal fractions if the `layout = "diagonal"` option was chosen
if (layout == "diagonal") {

has_a_fraction <- grepl("/", x_str)

non_fraction_part <- gsub("^(.*?)[0-9]*/[0-9]*", "\\1", x_str[has_a_fraction])

fraction_part <- gsub("^(.*?)([0-9]*/[0-9]*)", "\\2", x_str[has_a_fraction])

num_vec <- strsplit(fraction_part, "/") %>% lapply(`[[`, 1) %>% unlist()
denom_vec <- strsplit(fraction_part, "/") %>% lapply(`[[`, 2) %>% unlist()

if (context == "html") {

num_vec <-
paste0("<span class=\"gt_fraction_numerator\">", num_vec, "</span>")

denom_vec <-
paste0("<span class=\"gt_fraction_denominator\">", denom_vec, "</span>")

slash_mark <-
htmltools::tags$span(
class = "gt_slash_mark",
htmltools::HTML("&frasl;")
)

x_str[has_a_fraction] <-
paste0(
gsub(" ", "&#8239;", non_fraction_part),
num_vec, slash_mark, denom_vec
)

} else if (context == "latex") {

x_str[has_a_fraction] <-
paste0(
gsub(" ", "\\\\, ", non_fraction_part),
paste0("{{}^{", num_vec, "}\\!/_{", denom_vec, "}}")
)

} else if (context == "rtf") {

x_str[has_a_fraction] <-
paste0(
gsub(" ", "", non_fraction_part),
paste0("{\\super ", num_vec, "}/{\\sub ", denom_vec, "}")
)
}
}

# For the `layout = "inline"` option, LaTeX outputs in math mode
# disregard space characters so the `\ ` spacing command must used
if (layout == "inline" && context == "latex") {
x_str <- gsub(" ", "\\\\ ", x_str)
}

# In rare cases that Inf or -Inf appear, ensure that these
# special values are printed correctly
x_str[is.infinite(x)] <- x[is.infinite(x)]

x_str
}
)
)
}

get_fractions <- function(small_x, accuracy) {

fractions_col_idx <-
which(c("low", "med", "high", "/2", "/4", "/8", "/16", "/100") %in% accuracy) + 1

vapply(
small_x,
FUN.VALUE = character(1),
USE.NAMES = FALSE,
FUN = function(x) {
res <- fractions[fractions[["x"]] == x, ][[fractions_col_idx]]
if (res %in% c("0", "1")) res <- ""
res
}
)
}

# The `round_gt()` function is used in gt over `base::round()` for consistency
# in rounding across R versions; it uses the 'Round-Half-Up' (R-H-U) algorithm,
# which is *not* used in R >= 4.0
round_gt <- function(x, digits = 0) {

x_sign <- sign(x)
z <- abs(x) * 10^digits
z <- 0.5 + z + sqrt(.Machine$double.eps)
z <- trunc(z)
z <- z / 10^digits
z * x_sign
}

#' Format values as currencies
#'
#' @description
Expand Down Expand Up @@ -1101,7 +1405,7 @@ fmt_percent <- function(data,
#'
#' @family Format Data
#' @section Function ID:
#' 3-6
#' 3-7
#'
#' @import rlang
#' @export
Expand Down Expand Up @@ -1246,7 +1550,7 @@ fmt_currency <- function(data,
#'
#' @family Format Data
#' @section Function ID:
#' 3-7
#' 3-8
#'
#' @import rlang
#' @export
Expand Down
Binary file modified R/sysdata.rda
Binary file not shown.
1 change: 1 addition & 0 deletions _pkgdown.yml
Expand Up @@ -58,6 +58,7 @@ reference:
- fmt_scientific
- fmt_engineering
- fmt_percent
- fmt_fraction
- fmt_currency
- fmt_bytes
- fmt_date
Expand Down
7 changes: 5 additions & 2 deletions data-raw/X04-palettes_strips.R
Expand Up @@ -56,10 +56,13 @@ get_d_palettes <- function(color_packages = c(
dplyr::filter(package %in% color_packages) %>%
mutate(colors = "NA_character_")

for (i in seq(nrow(palettes))) {
for (i in seq_len(nrow(palettes))) {

pkg <- palettes[[i, "package"]]
pal <- palettes[[i, "palette"]]

color_strip <-
palettes_d[[palettes[i, "package"]]][[palettes[i, "palette"]]] %>%
palettes_d[[pkg]][[pal]] %>%
make_color_strip_svg()

palettes[i, "colors"] <- color_strip
Expand Down
2 changes: 1 addition & 1 deletion data-raw/X05-google_fonts.R
Expand Up @@ -9,7 +9,7 @@ fs::dir_create(work_dir)

# Download and extract the tarball for the google/font repository
downloader::download(
"https://github.com/google/fonts/tarball/master",
"https://github.com/google/fonts/tarball/main",
destfile = file.path(work_dir, "google_fonts.tar.gz")
)
utils::untar(file.path(work_dir, "google_fonts.tar.gz"), exdir = work_dir)
Expand Down
7 changes: 7 additions & 0 deletions data-raw/X07-fractions.R
@@ -0,0 +1,7 @@
library(tidyverse)

fractions <-
readr::read_csv(
file = "data-raw/fractions.csv",
col_types = "ccccccccc"
)