Changelog
Source:NEWS.md
roundwork 0.1.0
CRAN release: 2026-10-26
This release accompanies scrutiny 1.0.0, which now imports its rounding from roundwork instead of keeping copies of the functions. The rounding layer was rewritten inside scrutiny in the run-up to that release and then moved here, so nearly every function changed, and the rounding bounds and the rounding procedures are now derived from a single shared source. See vignette("scrutiny") for how the two packages relate.
New functions
-
round_ties_up(),round_ties_down(),round_ties_away(),round_ties_zero(), andround_ties_even()name a complete tie-breaking procedure each, so thatsymmetricdoes not have to be spelled out separately. The first four are the combinations ofround_up()andround_down()withsymmetric, andround_ties_even()isbase::round(); nothing is deprecated by them. Theroundingstrings"ties_up","ties_down","ties_away","ties_zero", and"ties_even"reach the same procedures throughreround()andunround(). Combining one of these strings withsymmetric = TRUEis an error, because the string already determines how negative ties are broken.They exist because the translation is easy to get backwards: “up” means on the number line here, but away from zero in Excel, Java, and Python.
round_ties_away()is the same procedure asround_ties_to_away()under a name that does not presuppose the IEEE 754 vocabulary. bound_numerators()returns the same bounds asunround(), but as exact integer numerators over a common denominator instead of decimals. It is the low-level entry point that consistency tests build on: deriving candidate values from floating-point bounds can drop a legitimate candidate or admit a phantom one, either of which flips a verdict. See lhdjung/scrutiny#86, which was mostly about a development version of scrutiny.sum_range()returns the least and the greatest whole-number sum that, divided byn_itemsand rounded, gives a reported mean. This is the arithmetic behind GRIM, and behind any other share of a whole such as a percentage. It is built onbound_numerators()and exact in the same way, so packages that check granularity no longer have to derive candidate sums from floating-point products. Likebound_numerators(), it is a low-level primitive that leaves most input checks to the caller.resolve_ties_rounding()translates a"ties_*"string into theroundingandsymmetricit stands for, so that a function that branches onroundingitself agrees withreround()andunround().
Rounding bounds
unround()now derives its bounds from the same machinery that a consistency test derives its candidate range from, so a test and the bounds can no longer disagree about what a rounded number means, about which rounding methods exist, or about whatthresholdandsymmetricmean.unround()gains asymmetricargument, matching the rounding functions it inverts.An infinite
xhasNAbounds, like a missing one. Its “bounds” used to be infinite on both sides, which no rounded number can have.unround()now supports"up_from","down_from","up_from_or_down_from","ceiling_or_floor", and the five"ties_*"methods. These used to throw an error, so a caller could reconstruct with a rounding method whose boundsunround()then refused to give.thresholdno longer affects"up_or_down","up", and"down". Those round from a fixed 5, so athresholdother than 5 used to widen the reconstructed range for a rounding that never happens.With
rounding = "even", both bounds are now reported as inclusive rather than asNA.base::round()breaks ties by the parity of the binary double, so whether a bound is really inclusive cannot be predicted; treating both as inclusive can only widen a range, never narrow it, which is the safe direction for error detection.A vector-valued
digitsnow yields a well-formed tibble. The row count used to be taken fromlength(x)while the columns took whatever lengthpaste0()recycling produced.
Rounding procedures
Every rounding function now shares one floating-point tolerance of about
1.5e-9, andunround()’s bounds assume exactly that tolerance. Shifting a number bydigitsdecimal places is inexact —0.28 * 100is28.000000000000004— and without the shared nudge,round_ceiling(0.28, 2)was0.29. The equality between the two families was implicit before and is now stated in one constant.anti_trunc()andround_anti_trunc()leave a value that already sits on the rounding grid where it is, and0stays0. Previously they moved such a value one step further out, soanti_trunc(3)was4. This makes them exact matches for Excel’s and Google Sheets’ROUNDUP(), Java’sRoundingMode.UP, and Python’sdecimal.ROUND_UP.round_down_from()interpretsthresholdlikeround_up_from(): it rounds down if the part cut off is at mostthresholdtenths of a rounding step. It used to switch at10 - threshold, so"up_from_or_down_from"could span up to 1.8 steps, and a GRIM test withthreshold = 9accepted every mean atn = 57. Results at the defaultthreshold = 5are unchanged.digitsmay be negative, rounding to powers of ten.
reround()
rounding,threshold, andsymmetricmust now each have length 1. They describe a single rounding procedure, which is applied to all ofx; passing vectors paired values with procedures by position, which no consistency test ever wanted.unround()keeps the old behavior for its display use case.In every function,
roundingmust be a string andsymmetrica singleTRUEorFALSE. A numeric or logicalroundingwas silently read as a position in the list of rounding methods, sorounding = 2was"ties_down".The compound methods
"up_or_down","up_from_or_down_from", and"ceiling_or_floor"return their two results per input value interleaved, so that each input value’s own pair stays together. Pooling the pairs across input values compares matches that did not come from the same original value; that was a false-pass bug in a GRIMMER implementation (lhdjung/scrutiny#85).reround()is no longer built onVectorize(). Every rounding function is natively vectorized, so the dispatch now happens once for the whole ofx.
threshold validation
A
thresholdof 5 is now accepted. It used to throw an error, on the theory that it could only be the argument’s default showing through — so any caller that computed a threshold and passed it on failed spuriously at exactly the most common value."up_from"with a threshold of 5 is simply"up", which is a correct answer rather than an error.A
thresholdoutside the open interval from 0 to 10 is now rejected. At 0 or 10, one of the two directions can never be taken, which silently turns the method into a ceiling or a floor.
Fractional rounding
reround_to_fraction()andreround_to_fraction_level()return two values per element ofxunder a compound rounding method, asreround()does. The two procedures used to be paired with the elements ofxinstead, soreround_to_fraction(c(0.4, 0.6), denominator = 2)returned two values rather than four.reround_to_fraction()resolvesdigits = "auto"before validatingdigitsas a whole number. The string used to reach the numeric check and fail there.Subsequent rounding to
digitskeeps each branch of a compound method on its own procedure, instead of re-applying the compound method and doubling the values a second time.Both functions accept one
digitsvalue per element ofx, which is what the argument is documented to take. This used to fail an internal length check.
Rounding bias
-
rounding_bias()now throws an error whenroundingis set to one of the compound methods"up_or_down","up_from_or_down_from", or"ceiling_or_floor", which its documentation has always ruled out. Nothing enforced that before: the compound method left two rounded values per element ofx, subtractingxrecycled it across the pairs, and the result was twice as long asxand misaligned throughout. The error names the two constituent procedures so that they can be compared one at a time.
Other
unround(),reround_to_fraction(), andreround_to_fraction_level()no longer throw a spurious length error when two arguments of the same length are separated by one of length 1, as inunround(x, digits = digits)with a vectorx, a vectordigits, and the defaultrounding. The shared length check deduplicated lengths by an index that counted all arguments rather than only those longer than 1.The tests now sweep every rounding method against its own bounds across signs, decimal counts, thresholds, and
symmetric, checking the inclusivity of each bound as well as its position. This is what pins the forward rounding functions and the inverse bounds to each other.
roundwork 0.0.1
CRAN release: 2024-09-25
- Initial CRAN submission.
- This new package is a spin-off from scrutiny. Much of the code was copied from there; see Github issue lhdjung/scrutiny#68.
- Notable changes compared to scrutiny:
-
unround()no longer guesses whether the bounds are inclusive withrounding = "even". This is highly variable in the underlyingbase::round()function, and the firm statement in earlier versions of scrutiny that the bounds are never inclusive was unwarranted. -
unround(),anti_trunc(), andround_anti_trunc()now correctly return zero in a rare edge case (rounding or unrounding zero while assuming"anti_trunc"rounding). - Five new functions around
round_ties_to_even()implement the IEEE 754 rounding standard by wrapping existing functions.
-