mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
(Re-)implement rounding with negative digits (#5198)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
03a766444a
commit
382787d799
@ -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};
|
||||
|
@ -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_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 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());
|
||||
|
||||
let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
|
||||
let f64_digits = f64::DIGITS as u8;
|
||||
let max_digits = f64::DIGITS as i16;
|
||||
|
||||
// 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));
|
||||
// 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 - 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));
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::eval::ops;
|
||||
use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value};
|
||||
use crate::layout::{Angle, Fr, Length, Ratio};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::utils::round_with_precision;
|
||||
use crate::utils::{round_int_with_precision, round_with_precision};
|
||||
|
||||
/// A module with calculation definitions.
|
||||
pub fn module() -> Module {
|
||||
@ -714,10 +714,13 @@ pub fn fract(
|
||||
}
|
||||
}
|
||||
|
||||
/// Rounds a number to the nearest integer.
|
||||
/// Rounds a number to the nearest integer away from zero.
|
||||
///
|
||||
/// Optionally, a number of decimal places can be specified.
|
||||
///
|
||||
/// If the number of digits is negative, its absolute value will indicate the
|
||||
/// amount of significant integer digits to remove before the decimal point.
|
||||
///
|
||||
/// Note that this function will return the same type as the operand. That is,
|
||||
/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`],
|
||||
/// another `decimal`. You may explicitly convert the output of this function to
|
||||
@ -725,29 +728,48 @@ pub fn fract(
|
||||
/// `float` or `decimal` is larger than the maximum 64-bit signed integer or
|
||||
/// smaller than the minimum integer.
|
||||
///
|
||||
/// In addition, this function can error if there is an attempt to round beyond
|
||||
/// the maximum or minimum integer or `decimal`. If the number is a `float`,
|
||||
/// such an attempt will cause `{float.inf}` or `{-float.inf}` to be returned
|
||||
/// for maximum and minimum respectively.
|
||||
///
|
||||
/// ```example
|
||||
/// #assert(calc.round(3) == 3)
|
||||
/// #assert(calc.round(3.14) == 3)
|
||||
/// #assert(calc.round(3.5) == 4.0)
|
||||
/// #assert(calc.round(3333.45, digits: -2) == 3300.0)
|
||||
/// #assert(calc.round(-48953.45, digits: -3) == -49000.0)
|
||||
/// #assert(calc.round(3333, digits: -2) == 3300)
|
||||
/// #assert(calc.round(-48953, digits: -3) == -49000)
|
||||
/// #assert(calc.round(decimal("-6.5")) == decimal("-7"))
|
||||
/// #assert(calc.round(decimal("7.123456789"), digits: 6) == decimal("7.123457"))
|
||||
/// #assert(calc.round(decimal("3333.45"), digits: -2) == decimal("3300"))
|
||||
/// #assert(calc.round(decimal("-48953.45"), digits: -3) == decimal("-49000"))
|
||||
/// #calc.round(3.1415, digits: 2)
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn round(
|
||||
/// The number to round.
|
||||
value: DecNum,
|
||||
/// The number of decimal places. Must not be negative.
|
||||
/// If positive, the number of decimal places.
|
||||
///
|
||||
/// If negative, the number of significant integer digits that should be
|
||||
/// removed before the decimal point.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
digits: u32,
|
||||
) -> DecNum {
|
||||
digits: i64,
|
||||
) -> StrResult<DecNum> {
|
||||
match value {
|
||||
DecNum::Int(n) => DecNum::Int(n),
|
||||
DecNum::Int(n) => Ok(DecNum::Int(
|
||||
round_int_with_precision(n, digits.saturating_as::<i16>())
|
||||
.ok_or_else(too_large)?,
|
||||
)),
|
||||
DecNum::Float(n) => {
|
||||
DecNum::Float(round_with_precision(n, digits.saturating_as::<u8>()))
|
||||
Ok(DecNum::Float(round_with_precision(n, digits.saturating_as::<i16>())))
|
||||
}
|
||||
DecNum::Decimal(n) => DecNum::Decimal(n.round(digits)),
|
||||
DecNum::Decimal(n) => Ok(DecNum::Decimal(
|
||||
n.round(digits.saturating_as::<i32>()).ok_or_else(too_large)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,8 @@ pub struct Decimal(rust_decimal::Decimal);
|
||||
impl Decimal {
|
||||
pub const ZERO: Self = Self(rust_decimal::Decimal::ZERO);
|
||||
pub const ONE: Self = Self(rust_decimal::Decimal::ONE);
|
||||
pub const MIN: Self = Self(rust_decimal::Decimal::MIN);
|
||||
pub const MAX: Self = Self(rust_decimal::Decimal::MAX);
|
||||
|
||||
/// Whether this decimal value is zero.
|
||||
pub const fn is_zero(self) -> bool {
|
||||
@ -146,11 +148,46 @@ impl Decimal {
|
||||
/// Rounds this decimal up to the specified amount of digits with the
|
||||
/// traditional rounding rules, using the "midpoint away from zero"
|
||||
/// strategy (6.5 -> 7, -6.5 -> -7).
|
||||
pub fn round(self, digits: u32) -> Self {
|
||||
Self(self.0.round_dp_with_strategy(
|
||||
digits,
|
||||
///
|
||||
/// If given a negative amount of digits, rounds to integer digits instead
|
||||
/// with the same rounding strategy. For example, rounding to -3 digits
|
||||
/// will turn 34567.89 into 35000.00 and -34567.89 into -35000.00.
|
||||
///
|
||||
/// Note that this can return `None` when using negative digits where the
|
||||
/// rounded number would overflow the available range for decimals.
|
||||
pub fn round(self, digits: i32) -> Option<Self> {
|
||||
// Positive digits can be handled by just rounding with rust_decimal.
|
||||
if let Ok(positive_digits) = u32::try_from(digits) {
|
||||
return Some(Self(self.0.round_dp_with_strategy(
|
||||
positive_digits,
|
||||
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
|
||||
)));
|
||||
}
|
||||
|
||||
// We received negative digits, so we round to integer digits.
|
||||
let mut num = self.0;
|
||||
let old_scale = num.scale();
|
||||
let digits = -digits as u32;
|
||||
|
||||
let (Ok(_), Some(ten_to_digits)) = (
|
||||
// Same as dividing by 10^digits.
|
||||
num.set_scale(old_scale + digits),
|
||||
rust_decimal::Decimal::TEN.checked_powi(digits as i64),
|
||||
) else {
|
||||
// Scaling more than any possible amount of integer digits.
|
||||
let mut zero = rust_decimal::Decimal::ZERO;
|
||||
zero.set_sign_negative(self.is_negative());
|
||||
return Some(Self(zero));
|
||||
};
|
||||
|
||||
// Round to this integer digit.
|
||||
num = num.round_dp_with_strategy(
|
||||
0,
|
||||
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
|
||||
))
|
||||
);
|
||||
|
||||
// Multiply by 10^digits again, which can overflow and fail.
|
||||
num.checked_mul(ten_to_digits).map(Self)
|
||||
}
|
||||
|
||||
/// Attempts to add two decimals.
|
||||
@ -426,4 +463,33 @@ mod tests {
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(hash128(&a), hash128(&b));
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_round(value: &str, digits: i32, expected: &str) {
|
||||
assert_eq!(
|
||||
Decimal::from_str(value).unwrap().round(digits),
|
||||
Some(Decimal::from_str(expected).unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal_positive_round() {
|
||||
test_round("312.55553", 0, "313.00000");
|
||||
test_round("312.55553", 3, "312.556");
|
||||
test_round("312.5555300000", 3, "312.556");
|
||||
test_round("-312.55553", 3, "-312.556");
|
||||
test_round("312.55553", 28, "312.55553");
|
||||
test_round("312.55553", 2341, "312.55553");
|
||||
test_round("-312.55553", 2341, "-312.55553");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal_negative_round() {
|
||||
test_round("4596.55553", -1, "4600");
|
||||
test_round("4596.555530000000", -1, "4600");
|
||||
test_round("-4596.55553", -3, "-5000");
|
||||
test_round("4596.55553", -28, "0");
|
||||
test_round("-4596.55553", -2341, "0");
|
||||
assert_eq!(Decimal::MAX.round(-1), None);
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ pub fn format_float(
|
||||
unit: &str,
|
||||
) -> EcoString {
|
||||
if let Some(p) = precision {
|
||||
value = round_with_precision(value, p);
|
||||
value = round_with_precision(value, p as i16);
|
||||
}
|
||||
// Debug for f64 always prints a decimal separator, while Display only does
|
||||
// when necessary.
|
||||
|
@ -4,18 +4,23 @@
|
||||
#test(type(calc.round(3.1415, digits: 2)), float)
|
||||
#test(type(calc.round(5, digits: 2)), int)
|
||||
#test(type(calc.round(decimal("3.1415"), digits: 2)), decimal)
|
||||
#test(type(calc.round(314.15, digits: -2)), float)
|
||||
#test(type(calc.round(523, digits: -2)), int)
|
||||
#test(type(calc.round(decimal("314.15"), digits: -2)), decimal)
|
||||
|
||||
--- calc-round-large-inputs ---
|
||||
#test(calc.round(31114, digits: 4000000000), 31114)
|
||||
#test(calc.round(9223372036854775807, digits: 12), 9223372036854775807)
|
||||
#test(calc.round(9223372036854775807, digits: -20), 0)
|
||||
#test(calc.round(238959235.129590203, digits: 4000000000), 238959235.129590203)
|
||||
#test(calc.round(1.7976931348623157e+308, digits: 12), 1.7976931348623157e+308)
|
||||
#test(calc.round(1.7976931348623157e+308, digits: -308), float.inf)
|
||||
#test(calc.round(-1.7976931348623157e+308, digits: -308), -float.inf)
|
||||
#test(calc.round(12.34, digits: -312), 0.0)
|
||||
#test(calc.round(decimal("238959235.129590203"), digits: 4000000000), decimal("238959235.129590203"))
|
||||
#test(calc.round(decimal("79228162514264337593543950335"), digits: 12), decimal("79228162514264337593543950335"))
|
||||
|
||||
--- calc-round-negative-digits ---
|
||||
// Error: 29-31 number must be at least zero
|
||||
#calc.round(243.32, digits: -2)
|
||||
#test(calc.round(decimal("79228162514264337593543950335"), digits: -50), decimal("0"))
|
||||
#test(calc.round(decimal("-79228162514264337593543950335"), digits: -2), decimal("-79228162514264337593543950300"))
|
||||
|
||||
--- calc-abs ---
|
||||
// Test the `abs` function.
|
||||
@ -331,6 +336,22 @@
|
||||
// Error: 2-47 the result is too large
|
||||
#calc.floor(decimal("-9223372036854775809.5"))
|
||||
|
||||
--- calc-round-int-too-large ---
|
||||
// Error: 2-45 the result is too large
|
||||
#calc.round(9223372036854775807, digits: -1)
|
||||
|
||||
--- calc-round-int-negative-too-large ---
|
||||
// Error: 2-46 the result is too large
|
||||
#calc.round(-9223372036854775807, digits: -1)
|
||||
|
||||
--- calc-round-decimal-too-large ---
|
||||
// Error: 2-66 the result is too large
|
||||
#calc.round(decimal("79228162514264337593543950335"), digits: -1)
|
||||
|
||||
--- calc-round-decimal-negative-too-large ---
|
||||
// Error: 2-67 the result is too large
|
||||
#calc.round(decimal("-79228162514264337593543950335"), digits: -1)
|
||||
|
||||
--- calc-min-nothing ---
|
||||
// Error: 2-12 expected at least one value
|
||||
#calc.min()
|
||||
|
Loading…
x
Reference in New Issue
Block a user