summaryrefslogtreecommitdiff
path: root/crates/typst-utils/src
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2024-10-14 13:14:06 -0300
committerGitHub <noreply@github.com>2024-10-14 16:14:06 +0000
commit382787d7995775cc85052419896e308a2de8de28 (patch)
tree64e4ce35a218a101ea442f4553b95ecbdd1434b3 /crates/typst-utils/src
parent03a766444a2770246a847f71980aebe71964e3af (diff)
(Re-)implement rounding with negative digits (#5198)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-utils/src')
-rw-r--r--crates/typst-utils/src/lib.rs2
-rw-r--r--crates/typst-utils/src/round.rs313
2 files changed, 257 insertions, 58 deletions
diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs
index 77581860..8488b72d 100644
--- a/crates/typst-utils/src/lib.rs
+++ b/crates/typst-utils/src/lib.rs
@@ -17,7 +17,7 @@ pub use self::deferred::Deferred;
pub use self::duration::format_duration;
pub use self::hash::LazyHash;
pub use self::pico::PicoStr;
-pub use self::round::round_with_precision;
+pub use self::round::{round_int_with_precision, round_with_precision};
pub use self::scalar::Scalar;
use std::fmt::{Debug, Formatter};
diff --git a/crates/typst-utils/src/round.rs b/crates/typst-utils/src/round.rs
index e72b45f7..3b0d282e 100644
--- a/crates/typst-utils/src/round.rs
+++ b/crates/typst-utils/src/round.rs
@@ -1,17 +1,29 @@
/// Returns value with `n` digits after floating point where `n` is `precision`.
-/// Standard rounding rules apply (if `n+1`th digit >= 5, round up).
+/// Standard rounding rules apply (if `n+1`th digit >= 5, round away from zero).
+///
+/// If `precision` is negative, returns value with `n` less significant integer
+/// digits before floating point where `n` is `-precision`. Standard rounding
+/// rules apply to the first remaining significant digit (if `n`th digit from
+/// the floating point >= 5, round away from zero).
///
/// If rounding the `value` will have no effect (e.g., it's infinite or NaN),
/// returns `value` unchanged.
///
+/// Note that rounding with negative precision may return plus or minus
+/// infinity if the result would overflow or underflow (respectively) the range
+/// of floating-point numbers.
+///
/// # Examples
///
/// ```
/// # use typst_utils::round_with_precision;
/// let rounded = round_with_precision(-0.56553, 2);
/// assert_eq!(-0.57, rounded);
+///
+/// let rounded_negative = round_with_precision(823543.0, -3);
+/// assert_eq!(824000.0, rounded_negative);
/// ```
-pub fn round_with_precision(value: f64, precision: u8) -> f64 {
+pub fn round_with_precision(value: f64, precision: i16) -> f64 {
// Don't attempt to round the float if that wouldn't have any effect.
// This includes infinite or NaN values, as well as integer values
// with a filled mantissa (which can't have a fractional part).
@@ -23,83 +35,270 @@ pub fn round_with_precision(value: f64, precision: u8) -> f64 {
// `value * offset` multiplication) does not.
if value.is_infinite()
|| value.is_nan()
- || value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
- || precision as u32 >= f64::DIGITS
+ || precision >= 0 && value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
+ || precision >= f64::DIGITS as i16
{
return value;
}
- let offset = 10_f64.powi(precision.into());
- assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
- (value * offset).round() / offset
+ // Floats cannot have more than this amount of base-10 integer digits.
+ if precision < -(f64::MAX_10_EXP as i16) {
+ // Multiply by zero to ensure sign is kept.
+ return value * 0.0;
+ }
+ if precision > 0 {
+ let offset = 10_f64.powi(precision.into());
+ assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
+ (value * offset).round() / offset
+ } else {
+ // Divide instead of multiplying by a negative exponent given that
+ // `f64::MAX_10_EXP` is larger than `f64::MIN_10_EXP` in absolute value
+ // (|308| > |-307|), allowing for the precision of -308 to be used.
+ let offset = 10_f64.powi((-precision).into());
+ (value / offset).round() * offset
+ }
+}
+
+/// This is used for rounding into integer digits, and is a no-op for positive
+/// `precision`.
+///
+/// If `precision` is negative, returns value with `n` less significant integer
+/// digits from the first digit where `n` is `-precision`. Standard rounding
+/// rules apply to the first remaining significant digit (if `n`th digit from
+/// the first digit >= 5, round away from zero).
+///
+/// Note that this may return `None` for negative precision when rounding
+/// beyond [`i64::MAX`] or [`i64::MIN`].
+///
+/// # Examples
+///
+/// ```
+/// # use typst_utils::round_int_with_precision;
+/// let rounded = round_int_with_precision(-154, -2);
+/// assert_eq!(Some(-200), rounded);
+///
+/// let rounded = round_int_with_precision(823543, -3);
+/// assert_eq!(Some(824000), rounded);
+/// ```
+pub fn round_int_with_precision(value: i64, precision: i16) -> Option<i64> {
+ if precision >= 0 {
+ return Some(value);
+ }
+
+ let digits = -precision as u32;
+ let Some(ten_to_digits) = 10i64.checked_pow(digits - 1) else {
+ // Larger than any possible amount of integer digits.
+ return Some(0);
+ };
+
+ // Divide by 10^(digits - 1).
+ //
+ // We keep the last digit we want to remove as the first digit of this
+ // number, so we can check it with mod 10 for rounding purposes.
+ let truncated = value / ten_to_digits;
+ if truncated == 0 {
+ return Some(0);
+ }
+
+ let rounded = if (truncated % 10).abs() >= 5 {
+ // Round away from zero (towards the next multiple of 10).
+ //
+ // This may overflow in the particular case of rounding MAX/MIN
+ // with -1.
+ truncated.checked_add(truncated.signum() * (10 - (truncated % 10).abs()))?
+ } else {
+ // Just replace the last digit with zero, since it's < 5.
+ truncated - (truncated % 10)
+ };
+
+ // Multiply back by 10^(digits - 1).
+ //
+ // May overflow / underflow, in which case we fail.
+ rounded.checked_mul(ten_to_digits)
}
#[cfg(test)]
mod tests {
- use super::*;
+ use super::{round_int_with_precision as rip, round_with_precision as rp};
#[test]
fn test_round_with_precision_0() {
- let round = |value| round_with_precision(value, 0);
- assert_eq!(0.0, round(0.0));
- assert_eq!(-0.0, round(-0.0));
- assert_eq!(0.0, round(0.4));
- assert_eq!(-0.0, round(-0.4));
- assert_eq!(1.0, round(0.56453));
- assert_eq!(-1.0, round(-0.56453));
+ let round = |value| rp(value, 0);
+ assert_eq!(round(0.0), 0.0);
+ assert_eq!(round(-0.0), -0.0);
+ assert_eq!(round(0.4), 0.0);
+ assert_eq!(round(-0.4), -0.0);
+ assert_eq!(round(0.56453), 1.0);
+ assert_eq!(round(-0.56453), -1.0);
}
#[test]
fn test_round_with_precision_1() {
- let round = |value| round_with_precision(value, 1);
- assert_eq!(0.0, round(0.0));
- assert_eq!(-0.0, round(-0.0));
- assert_eq!(0.4, round(0.4));
- assert_eq!(-0.4, round(-0.4));
- assert_eq!(0.4, round(0.44));
- assert_eq!(-0.4, round(-0.44));
- assert_eq!(0.6, round(0.56453));
- assert_eq!(-0.6, round(-0.56453));
- assert_eq!(1.0, round(0.96453));
- assert_eq!(-1.0, round(-0.96453));
+ let round = |value| rp(value, 1);
+ assert_eq!(round(0.0), 0.0);
+ assert_eq!(round(-0.0), -0.0);
+ assert_eq!(round(0.4), 0.4);
+ assert_eq!(round(-0.4), -0.4);
+ assert_eq!(round(0.44), 0.4);
+ assert_eq!(round(-0.44), -0.4);
+ assert_eq!(round(0.56453), 0.6);
+ assert_eq!(round(-0.56453), -0.6);
+ assert_eq!(round(0.96453), 1.0);
+ assert_eq!(round(-0.96453), -1.0);
}
#[test]
fn test_round_with_precision_2() {
- let round = |value| round_with_precision(value, 2);
- assert_eq!(0.0, round(0.0));
- assert_eq!(-0.0, round(-0.0));
- assert_eq!(0.4, round(0.4));
- assert_eq!(-0.4, round(-0.4));
- assert_eq!(0.44, round(0.44));
- assert_eq!(-0.44, round(-0.44));
- assert_eq!(0.44, round(0.444));
- assert_eq!(-0.44, round(-0.444));
- assert_eq!(0.57, round(0.56553));
- assert_eq!(-0.57, round(-0.56553));
- assert_eq!(1.0, round(0.99553));
- assert_eq!(-1.0, round(-0.99553));
+ let round = |value| rp(value, 2);
+ assert_eq!(round(0.0), 0.0);
+ assert_eq!(round(-0.0), -0.0);
+ assert_eq!(round(0.4), 0.4);
+ assert_eq!(round(-0.4), -0.4);
+ assert_eq!(round(0.44), 0.44);
+ assert_eq!(round(-0.44), -0.44);
+ assert_eq!(round(0.444), 0.44);
+ assert_eq!(round(-0.444), -0.44);
+ assert_eq!(round(0.56553), 0.57);
+ assert_eq!(round(-0.56553), -0.57);
+ assert_eq!(round(0.99553), 1.0);
+ assert_eq!(round(-0.99553), -1.0);
}
#[test]
- fn test_round_with_precision_fuzzy() {
- let round = |value| round_with_precision(value, 0);
- assert_eq!(f64::INFINITY, round(f64::INFINITY));
- assert_eq!(f64::NEG_INFINITY, round(f64::NEG_INFINITY));
- assert!(round(f64::NAN).is_nan());
+ fn test_round_with_precision_negative_1() {
+ let round = |value| rp(value, -1);
+ assert_eq!(round(0.0), 0.0);
+ assert_eq!(round(-0.0), -0.0);
+ assert_eq!(round(0.4), 0.0);
+ assert_eq!(round(-0.4), -0.0);
+ assert_eq!(round(1234.5), 1230.0);
+ assert_eq!(round(-1234.5), -1230.0);
+ assert_eq!(round(1245.232), 1250.0);
+ assert_eq!(round(-1245.232), -1250.0);
+ }
+
+ #[test]
+ fn test_round_with_precision_negative_2() {
+ let round = |value| rp(value, -2);
+ assert_eq!(round(0.0), 0.0);
+ assert_eq!(round(-0.0), -0.0);
+ assert_eq!(round(0.4), 0.0);
+ assert_eq!(round(-0.4), -0.0);
+ assert_eq!(round(1243.232), 1200.0);
+ assert_eq!(round(-1243.232), -1200.0);
+ assert_eq!(round(1253.232), 1300.0);
+ assert_eq!(round(-1253.232), -1300.0);
+ }
+ #[test]
+ fn test_round_with_precision_fuzzy() {
let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
- let f64_digits = f64::DIGITS as u8;
-
- // max
- assert_eq!(max_int, round(max_int));
- assert_eq!(0.123456, round_with_precision(0.123456, f64_digits));
- assert_eq!(max_int, round_with_precision(max_int, f64_digits));
-
- // max - 1
- assert_eq!(max_int - 1f64, round(max_int - 1f64));
- assert_eq!(0.123456, round_with_precision(0.123456, f64_digits - 1));
- assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits));
- assert_eq!(max_int, round_with_precision(max_int, f64_digits - 1));
- assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits - 1));
+ let max_digits = f64::DIGITS as i16;
+
+ // Special cases.
+ assert_eq!(rp(f64::INFINITY, 0), f64::INFINITY);
+ assert_eq!(rp(f64::NEG_INFINITY, 0), f64::NEG_INFINITY);
+ assert!(rp(f64::NAN, 0).is_nan());
+
+ // Max
+ assert_eq!(rp(max_int, 0), max_int);
+ assert_eq!(rp(0.123456, max_digits), 0.123456);
+ assert_eq!(rp(max_int, max_digits), max_int);
+
+ // Max - 1
+ assert_eq!(rp(max_int - 1.0, 0), max_int - 1.0);
+ assert_eq!(rp(0.123456, max_digits - 1), 0.123456);
+ assert_eq!(rp(max_int - 1.0, max_digits), max_int - 1.0);
+ assert_eq!(rp(max_int, max_digits - 1), max_int);
+ assert_eq!(rp(max_int - 1.0, max_digits - 1), max_int - 1.0);
+ }
+
+ #[test]
+ fn test_round_with_precision_fuzzy_negative() {
+ let exp10 = |exponent: i16| 10_f64.powi(exponent.into());
+ let max_digits = f64::MAX_10_EXP as i16;
+ let max_up = max_digits + 1;
+ let max_down = max_digits - 1;
+
+ // Special cases.
+ assert_eq!(rp(f64::INFINITY, -1), f64::INFINITY);
+ assert_eq!(rp(f64::NEG_INFINITY, -1), f64::NEG_INFINITY);
+ assert!(rp(f64::NAN, -1).is_nan());
+
+ // Max
+ assert_eq!(rp(f64::MAX, -max_digits), f64::INFINITY);
+ assert_eq!(rp(f64::MIN, -max_digits), f64::NEG_INFINITY);
+ assert_eq!(rp(1.66 * exp10(max_digits), -max_digits), f64::INFINITY);
+ assert_eq!(rp(-1.66 * exp10(max_digits), -max_digits), f64::NEG_INFINITY);
+ assert_eq!(rp(1.66 * exp10(max_down), -max_digits), 0.0);
+ assert_eq!(rp(-1.66 * exp10(max_down), -max_digits), -0.0);
+ assert_eq!(rp(1234.5678, -max_digits), 0.0);
+ assert_eq!(rp(-1234.5678, -max_digits), -0.0);
+
+ // Max + 1
+ assert_eq!(rp(f64::MAX, -max_up), 0.0);
+ assert_eq!(rp(f64::MIN, -max_up), -0.0);
+ assert_eq!(rp(1.66 * exp10(max_digits), -max_up), 0.0);
+ assert_eq!(rp(-1.66 * exp10(max_digits), -max_up), -0.0);
+ assert_eq!(rp(1.66 * exp10(max_down), -max_up), 0.0);
+ assert_eq!(rp(-1.66 * exp10(max_down), -max_up), -0.0);
+ assert_eq!(rp(1234.5678, -max_up), 0.0);
+ assert_eq!(rp(-1234.5678, -max_up), -0.0);
+
+ // Max - 1
+ assert_eq!(rp(f64::MAX, -max_down), f64::INFINITY);
+ assert_eq!(rp(f64::MIN, -max_down), f64::NEG_INFINITY);
+ assert_eq!(rp(1.66 * exp10(max_down), -max_down), 2.0 * exp10(max_down));
+ assert_eq!(rp(-1.66 * exp10(max_down), -max_down), -2.0 * exp10(max_down));
+ assert_eq!(rp(1234.5678, -max_down), 0.0);
+ assert_eq!(rp(-1234.5678, -max_down), -0.0);
+
+ // Must be approx equal to 1.7e308. Using some division and flooring
+ // to avoid weird results due to imprecision.
+ assert_eq!(
+ (rp(1.66 * exp10(max_digits), -max_down) / exp10(max_down)).floor(),
+ 17.0,
+ );
+ assert_eq!(
+ (rp(-1.66 * exp10(max_digits), -max_down) / exp10(max_down)).floor(),
+ -17.0,
+ );
+ }
+
+ #[test]
+ fn test_round_int_with_precision_positive() {
+ assert_eq!(rip(0, 0), Some(0));
+ assert_eq!(rip(10, 0), Some(10));
+ assert_eq!(rip(23, 235), Some(23));
+ assert_eq!(rip(i64::MAX, 235), Some(i64::MAX));
+ }
+
+ #[test]
+ fn test_round_int_with_precision_negative_1() {
+ let round = |value| rip(value, -1);
+ assert_eq!(round(0), Some(0));
+ assert_eq!(round(3), Some(0));
+ assert_eq!(round(5), Some(10));
+ assert_eq!(round(13), Some(10));
+ assert_eq!(round(1234), Some(1230));
+ assert_eq!(round(-1234), Some(-1230));
+ assert_eq!(round(1245), Some(1250));
+ assert_eq!(round(-1245), Some(-1250));
+ assert_eq!(round(i64::MAX), None);
+ assert_eq!(round(i64::MIN), None);
+ }
+
+ #[test]
+ fn test_round_int_with_precision_negative_2() {
+ let round = |value| rip(value, -2);
+ assert_eq!(round(0), Some(0));
+ assert_eq!(round(3), Some(0));
+ assert_eq!(round(5), Some(0));
+ assert_eq!(round(13), Some(0));
+ assert_eq!(round(1245), Some(1200));
+ assert_eq!(round(-1245), Some(-1200));
+ assert_eq!(round(1253), Some(1300));
+ assert_eq!(round(-1253), Some(-1300));
+ assert_eq!(round(i64::MAX), Some(i64::MAX - 7));
+ assert_eq!(round(i64::MIN), Some(i64::MIN + 8));
}
}