[BUG] Rounding does not follow round-half-away-from-zero on decimal ties and displayed values show negative zeros
Nobody has claimed this yet.
- Dominant language
- R
- Stars
- 27
- Forks
- 5
- Avg merge
- 17h 4m
- Merged PRs (30d)
- 4
Description
Package Version: v0.1.4
Pinned Commit: bdb23d472b16bc9dadbc774e64c5ca40321e9c6b
Description
In regulatory and clinical trial reporting (CSR tables/listings/figures), numerical formatting must adhere to standard data presentation rule BR-001:
- Decimal Ties Rounding: Values intended as exact decimal ties must round half away from zero (symmetric arithmetic rounding, where halfway values round towards $+\infty$ for positive numbers and towards $-\infty$ for negative numbers).
- Negative Zero Prevention: A displayed/formatted value must never be displayed as a negative zero (e.g.,
"-0.0","(-0.0)","-0.00","(-0.00)").
An audit of metalite.ae at release v0.1.4 (commit bdb23d4) reveals two systematic divergences:
- Formatting helpers in
R/fmt.RandR/format_ae_exp_adj.Ruse base R'sformatC(..., format = "f"), which delegates to standard C library rounding (IEEE 754 / IEC 60559 round-half-to-even / bankers' rounding) and produces negative zeroes for small negative floats. - Row-filtering logic in
R/format_ae_specific.R(line 330) uses base R'sround(), which also performs round-to-even, causing adverse events on boundary ties to be incorrectly filtered out or included.
Affected Files & Locations
R/fmt.R:fmt_pct()(lines 33, 37): Formats percentages as(x.x)usingformatC(..., format = "f").fmt_est()(lines 75, 79): Formats mean/difference estimates and standard errors usingformatC(..., format = "f").fmt_ci()(lines 99, 100): Formats confidence intervals usingformatC(..., format = "f").fmt_pval()(line 122): Formats p-values usingformatC(..., format = "f").
R/format_ae_specific.R:- Line 330:
filter_index <- round(outdata$prop[, index_total], digits_prop)performs round-to-even before applyingfilter_criteria.
- Line 330:
R/format_ae_exp_adj.R:- Line 203 & Line 208: Formats total exposure and exposure-adjusted event rates (
eaer) usingfmt_pct()andformatC(..., format = "f").
- Line 203 & Line 208: Formats total exposure and exposure-adjusted event rates (
Minimal Reproducible Examples (Reprex)
1. Negative Zero Displayed
library(metalite.ae)
# A small negative risk difference or proportion change displays as "-0.0" / "(-0.0)"
fmt_pct(-0.01, digits = 1)
#> [1] "(-0.0)" # Expected: "(0.0)"
fmt_est(-0.01, digits = c(1, 1))
#> [1] " -0.0" # Expected: " 0.0"
fmt_ci(-0.001, 0.50, digits = 2)
#> [1] "(-0.00, 0.50)" # Expected: "( 0.00, 0.50)"
2. Exact Decimal Ties Not Rounding Half Away From Zero
ties <- c(0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95)
# Current behavior:
fmt_pct(ties, digits = 1)
#> [1] "(0.1)" "(0.1)" "(0.2)" "(0.3)" "(0.5)" "(0.6)" "(0.7)" "(0.8)" "(0.8)" "(0.9)"
# Discrepancies on positive ties:
# 0.15 -> "(0.1)" [Expected: "(0.2)"]
# 0.25 -> "(0.2)" [Expected: "(0.3)"]
# 0.35 -> "(0.3)" [Expected: "(0.4)"]
# 0.85 -> "(0.8)" [Expected: "(0.9)"]
# 0.95 -> "(0.9)" [Expected: "(1.0)"]
# Discrepancies on negative ties:
fmt_pct(-ties, digits = 1)
#> [1] "(-0.1)" "(-0.1)" "(-0.2)" "(-0.3)" "(-0.5)" "(-0.6)" "(-0.7)" "(-0.8)" "(-0.8)" "(-0.9)"
# -0.15 -> "(-0.1)" [Expected: "(-0.2)"]
# -0.25 -> "(-0.2)" [Expected: "(-0.3)"]
# -0.35 -> "(-0.3)" [Expected: "(-0.4)"]
# -0.85 -> "(-0.8)" [Expected: "(-0.9)"]
# -0.95 -> "(-0.9)" [Expected: "(-1.0)"]
3. Filtering Inconsistency in AE Specific Reports (format_ae_specific)
# Suppose an adverse event has incidence 2.25% with filter_criteria = 2.3% (digits_prop = 1)
# Line 330 in format_ae_specific.R:
prop <- 2.25
round(prop, 1) >= 2.3
#> [1] FALSE # round(2.25, 1) == 2.2 -> Incorrectly dropped
# Expected under BR-001 (round half away from zero):
# 2.25 rounds to 2.3 -> 2.3 >= 2.3 is TRUE -> Retained
Root Cause
formatC(..., format = "f")andround()implement IEEE 754 "round to nearest, ties to even".- Standard floating point formatting in C
snprintf/formatCretains the sign bit on negative values close to zero (resulting in"-0.0").
Proposed Solution
-
Add a centralized round-half-away-from-zero helper (with double precision epsilon buffer):
round_half_away_from_zero <- function(x, digits = 0) { posneg <- sign(x) z <- abs(x) * 10^digits z <- z + 0.5 + sqrt(.Machine$double.eps) z <- trunc(z) z <- z / 10^digits z * posneg } -
Add a formatting helper that suppresses negative zeroes and applies half-away-from-zero rounding:
format_number <- function(x, digits = 1, width = NULL) { if (length(x) == 0) return(character(0)) # Round half away from zero x_round <- round_half_away_from_zero(x, digits = digits) # Eliminate negative zero x_round[abs(x_round) < 10^(-digits) / 2] <- 0 # Format to fixed decimals res <- ifelse( is.na(x_round), NA_character_, formatC(x_round, digits = digits, format = "f", width = width) ) # Final safeguard against any string "-0", "-0.0", etc. gsub("^-0(\\.0+)?$", "0\\1", res) } -
Refactor
fmt_pct(),fmt_est(),fmt_ci(),fmt_pval(),format_ae_exp_adj(), andformat_ae_specific()(line 330) to route all numeric formatting and cutoff-rounding through these functions.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in R/fmt.R by reading fmt_pct(), fmt_est(), fmt_ci(), and fmt_pval(), then inspect the filtering path at line 330 of R/format_ae_specific.R and the exposure formatting in R/format_ae_exp_adj.R. Use the issue's reproducible examples to check decimal ties, negative-zero output, and the 2.25 cutoff case. Done means all listed formatting and filtering paths follow BR-001 consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- r
- Domain
- analytics
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100