Skip to content

Commit b206dfd

Browse files
PgBiellaurmaedje
andcommitted
(Re-)implement rounding with negative digits (#5198)
Co-authored-by: Laurenz <[email protected]>
1 parent fe43e27 commit b206dfd

File tree

6 files changed

+383
-75
lines changed

6 files changed

+383
-75
lines changed

crates/typst-utils/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub use self::deferred::Deferred;
1717
pub use self::duration::format_duration;
1818
pub use self::hash::LazyHash;
1919
pub use self::pico::PicoStr;
20-
pub use self::round::round_with_precision;
20+
pub use self::round::{round_int_with_precision, round_with_precision};
2121
pub use self::scalar::Scalar;
2222

2323
use std::fmt::{Debug, Formatter};

crates/typst-utils/src/round.rs

Lines changed: 256 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
/// Returns value with `n` digits after floating point where `n` is `precision`.
2-
/// Standard rounding rules apply (if `n+1`th digit >= 5, round up).
2+
/// Standard rounding rules apply (if `n+1`th digit >= 5, round away from zero).
3+
///
4+
/// If `precision` is negative, returns value with `n` less significant integer
5+
/// digits before floating point where `n` is `-precision`. Standard rounding
6+
/// rules apply to the first remaining significant digit (if `n`th digit from
7+
/// the floating point >= 5, round away from zero).
38
///
49
/// If rounding the `value` will have no effect (e.g., it's infinite or NaN),
510
/// returns `value` unchanged.
611
///
12+
/// Note that rounding with negative precision may return plus or minus
13+
/// infinity if the result would overflow or underflow (respectively) the range
14+
/// of floating-point numbers.
15+
///
716
/// # Examples
817
///
918
/// ```
1019
/// # use typst_utils::round_with_precision;
1120
/// let rounded = round_with_precision(-0.56553, 2);
1221
/// assert_eq!(-0.57, rounded);
22+
///
23+
/// let rounded_negative = round_with_precision(823543.0, -3);
24+
/// assert_eq!(824000.0, rounded_negative);
1325
/// ```
14-
pub fn round_with_precision(value: f64, precision: u8) -> f64 {
26+
pub fn round_with_precision(value: f64, precision: i16) -> f64 {
1527
// Don't attempt to round the float if that wouldn't have any effect.
1628
// This includes infinite or NaN values, as well as integer values
1729
// 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 {
2335
// `value * offset` multiplication) does not.
2436
if value.is_infinite()
2537
|| value.is_nan()
26-
|| value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
27-
|| precision as u32 >= f64::DIGITS
38+
|| precision >= 0 && value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
39+
|| precision >= f64::DIGITS as i16
2840
{
2941
return value;
3042
}
31-
let offset = 10_f64.powi(precision.into());
32-
assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
33-
(value * offset).round() / offset
43+
// Floats cannot have more than this amount of base-10 integer digits.
44+
if precision < -(f64::MAX_10_EXP as i16) {
45+
// Multiply by zero to ensure sign is kept.
46+
return value * 0.0;
47+
}
48+
if precision > 0 {
49+
let offset = 10_f64.powi(precision.into());
50+
assert!((value * offset).is_finite(), "{value} * {offset} is not finite!");
51+
(value * offset).round() / offset
52+
} else {
53+
// Divide instead of multiplying by a negative exponent given that
54+
// `f64::MAX_10_EXP` is larger than `f64::MIN_10_EXP` in absolute value
55+
// (|308| > |-307|), allowing for the precision of -308 to be used.
56+
let offset = 10_f64.powi((-precision).into());
57+
(value / offset).round() * offset
58+
}
59+
}
60+
61+
/// This is used for rounding into integer digits, and is a no-op for positive
62+
/// `precision`.
63+
///
64+
/// If `precision` is negative, returns value with `n` less significant integer
65+
/// digits from the first digit where `n` is `-precision`. Standard rounding
66+
/// rules apply to the first remaining significant digit (if `n`th digit from
67+
/// the first digit >= 5, round away from zero).
68+
///
69+
/// Note that this may return `None` for negative precision when rounding
70+
/// beyond [`i64::MAX`] or [`i64::MIN`].
71+
///
72+
/// # Examples
73+
///
74+
/// ```
75+
/// # use typst_utils::round_int_with_precision;
76+
/// let rounded = round_int_with_precision(-154, -2);
77+
/// assert_eq!(Some(-200), rounded);
78+
///
79+
/// let rounded = round_int_with_precision(823543, -3);
80+
/// assert_eq!(Some(824000), rounded);
81+
/// ```
82+
pub fn round_int_with_precision(value: i64, precision: i16) -> Option<i64> {
83+
if precision >= 0 {
84+
return Some(value);
85+
}
86+
87+
let digits = -precision as u32;
88+
let Some(ten_to_digits) = 10i64.checked_pow(digits - 1) else {
89+
// Larger than any possible amount of integer digits.
90+
return Some(0);
91+
};
92+
93+
// Divide by 10^(digits - 1).
94+
//
95+
// We keep the last digit we want to remove as the first digit of this
96+
// number, so we can check it with mod 10 for rounding purposes.
97+
let truncated = value / ten_to_digits;
98+
if truncated == 0 {
99+
return Some(0);
100+
}
101+
102+
let rounded = if (truncated % 10).abs() >= 5 {
103+
// Round away from zero (towards the next multiple of 10).
104+
//
105+
// This may overflow in the particular case of rounding MAX/MIN
106+
// with -1.
107+
truncated.checked_add(truncated.signum() * (10 - (truncated % 10).abs()))?
108+
} else {
109+
// Just replace the last digit with zero, since it's < 5.
110+
truncated - (truncated % 10)
111+
};
112+
113+
// Multiply back by 10^(digits - 1).
114+
//
115+
// May overflow / underflow, in which case we fail.
116+
rounded.checked_mul(ten_to_digits)
34117
}
35118

36119
#[cfg(test)]
37120
mod tests {
38-
use super::*;
121+
use super::{round_int_with_precision as rip, round_with_precision as rp};
39122

40123
#[test]
41124
fn test_round_with_precision_0() {
42-
let round = |value| round_with_precision(value, 0);
43-
assert_eq!(0.0, round(0.0));
44-
assert_eq!(-0.0, round(-0.0));
45-
assert_eq!(0.0, round(0.4));
46-
assert_eq!(-0.0, round(-0.4));
47-
assert_eq!(1.0, round(0.56453));
48-
assert_eq!(-1.0, round(-0.56453));
125+
let round = |value| rp(value, 0);
126+
assert_eq!(round(0.0), 0.0);
127+
assert_eq!(round(-0.0), -0.0);
128+
assert_eq!(round(0.4), 0.0);
129+
assert_eq!(round(-0.4), -0.0);
130+
assert_eq!(round(0.56453), 1.0);
131+
assert_eq!(round(-0.56453), -1.0);
49132
}
50133

51134
#[test]
52135
fn test_round_with_precision_1() {
53-
let round = |value| round_with_precision(value, 1);
54-
assert_eq!(0.0, round(0.0));
55-
assert_eq!(-0.0, round(-0.0));
56-
assert_eq!(0.4, round(0.4));
57-
assert_eq!(-0.4, round(-0.4));
58-
assert_eq!(0.4, round(0.44));
59-
assert_eq!(-0.4, round(-0.44));
60-
assert_eq!(0.6, round(0.56453));
61-
assert_eq!(-0.6, round(-0.56453));
62-
assert_eq!(1.0, round(0.96453));
63-
assert_eq!(-1.0, round(-0.96453));
136+
let round = |value| rp(value, 1);
137+
assert_eq!(round(0.0), 0.0);
138+
assert_eq!(round(-0.0), -0.0);
139+
assert_eq!(round(0.4), 0.4);
140+
assert_eq!(round(-0.4), -0.4);
141+
assert_eq!(round(0.44), 0.4);
142+
assert_eq!(round(-0.44), -0.4);
143+
assert_eq!(round(0.56453), 0.6);
144+
assert_eq!(round(-0.56453), -0.6);
145+
assert_eq!(round(0.96453), 1.0);
146+
assert_eq!(round(-0.96453), -1.0);
64147
}
65148

66149
#[test]
67150
fn test_round_with_precision_2() {
68-
let round = |value| round_with_precision(value, 2);
69-
assert_eq!(0.0, round(0.0));
70-
assert_eq!(-0.0, round(-0.0));
71-
assert_eq!(0.4, round(0.4));
72-
assert_eq!(-0.4, round(-0.4));
73-
assert_eq!(0.44, round(0.44));
74-
assert_eq!(-0.44, round(-0.44));
75-
assert_eq!(0.44, round(0.444));
76-
assert_eq!(-0.44, round(-0.444));
77-
assert_eq!(0.57, round(0.56553));
78-
assert_eq!(-0.57, round(-0.56553));
79-
assert_eq!(1.0, round(0.99553));
80-
assert_eq!(-1.0, round(-0.99553));
151+
let round = |value| rp(value, 2);
152+
assert_eq!(round(0.0), 0.0);
153+
assert_eq!(round(-0.0), -0.0);
154+
assert_eq!(round(0.4), 0.4);
155+
assert_eq!(round(-0.4), -0.4);
156+
assert_eq!(round(0.44), 0.44);
157+
assert_eq!(round(-0.44), -0.44);
158+
assert_eq!(round(0.444), 0.44);
159+
assert_eq!(round(-0.444), -0.44);
160+
assert_eq!(round(0.56553), 0.57);
161+
assert_eq!(round(-0.56553), -0.57);
162+
assert_eq!(round(0.99553), 1.0);
163+
assert_eq!(round(-0.99553), -1.0);
81164
}
82165

83166
#[test]
84-
fn test_round_with_precision_fuzzy() {
85-
let round = |value| round_with_precision(value, 0);
86-
assert_eq!(f64::INFINITY, round(f64::INFINITY));
87-
assert_eq!(f64::NEG_INFINITY, round(f64::NEG_INFINITY));
88-
assert!(round(f64::NAN).is_nan());
167+
fn test_round_with_precision_negative_1() {
168+
let round = |value| rp(value, -1);
169+
assert_eq!(round(0.0), 0.0);
170+
assert_eq!(round(-0.0), -0.0);
171+
assert_eq!(round(0.4), 0.0);
172+
assert_eq!(round(-0.4), -0.0);
173+
assert_eq!(round(1234.5), 1230.0);
174+
assert_eq!(round(-1234.5), -1230.0);
175+
assert_eq!(round(1245.232), 1250.0);
176+
assert_eq!(round(-1245.232), -1250.0);
177+
}
178+
179+
#[test]
180+
fn test_round_with_precision_negative_2() {
181+
let round = |value| rp(value, -2);
182+
assert_eq!(round(0.0), 0.0);
183+
assert_eq!(round(-0.0), -0.0);
184+
assert_eq!(round(0.4), 0.0);
185+
assert_eq!(round(-0.4), -0.0);
186+
assert_eq!(round(1243.232), 1200.0);
187+
assert_eq!(round(-1243.232), -1200.0);
188+
assert_eq!(round(1253.232), 1300.0);
189+
assert_eq!(round(-1253.232), -1300.0);
190+
}
89191

192+
#[test]
193+
fn test_round_with_precision_fuzzy() {
90194
let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
91-
let f64_digits = f64::DIGITS as u8;
92-
93-
// max
94-
assert_eq!(max_int, round(max_int));
95-
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits));
96-
assert_eq!(max_int, round_with_precision(max_int, f64_digits));
97-
98-
// max - 1
99-
assert_eq!(max_int - 1f64, round(max_int - 1f64));
100-
assert_eq!(0.123456, round_with_precision(0.123456, f64_digits - 1));
101-
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits));
102-
assert_eq!(max_int, round_with_precision(max_int, f64_digits - 1));
103-
assert_eq!(max_int - 1f64, round_with_precision(max_int - 1f64, f64_digits - 1));
195+
let max_digits = f64::DIGITS as i16;
196+
197+
// Special cases.
198+
assert_eq!(rp(f64::INFINITY, 0), f64::INFINITY);
199+
assert_eq!(rp(f64::NEG_INFINITY, 0), f64::NEG_INFINITY);
200+
assert!(rp(f64::NAN, 0).is_nan());
201+
202+
// Max
203+
assert_eq!(rp(max_int, 0), max_int);
204+
assert_eq!(rp(0.123456, max_digits), 0.123456);
205+
assert_eq!(rp(max_int, max_digits), max_int);
206+
207+
// Max - 1
208+
assert_eq!(rp(max_int - 1.0, 0), max_int - 1.0);
209+
assert_eq!(rp(0.123456, max_digits - 1), 0.123456);
210+
assert_eq!(rp(max_int - 1.0, max_digits), max_int - 1.0);
211+
assert_eq!(rp(max_int, max_digits - 1), max_int);
212+
assert_eq!(rp(max_int - 1.0, max_digits - 1), max_int - 1.0);
213+
}
214+
215+
#[test]
216+
fn test_round_with_precision_fuzzy_negative() {
217+
let exp10 = |exponent: i16| 10_f64.powi(exponent.into());
218+
let max_digits = f64::MAX_10_EXP as i16;
219+
let max_up = max_digits + 1;
220+
let max_down = max_digits - 1;
221+
222+
// Special cases.
223+
assert_eq!(rp(f64::INFINITY, -1), f64::INFINITY);
224+
assert_eq!(rp(f64::NEG_INFINITY, -1), f64::NEG_INFINITY);
225+
assert!(rp(f64::NAN, -1).is_nan());
226+
227+
// Max
228+
assert_eq!(rp(f64::MAX, -max_digits), f64::INFINITY);
229+
assert_eq!(rp(f64::MIN, -max_digits), f64::NEG_INFINITY);
230+
assert_eq!(rp(1.66 * exp10(max_digits), -max_digits), f64::INFINITY);
231+
assert_eq!(rp(-1.66 * exp10(max_digits), -max_digits), f64::NEG_INFINITY);
232+
assert_eq!(rp(1.66 * exp10(max_down), -max_digits), 0.0);
233+
assert_eq!(rp(-1.66 * exp10(max_down), -max_digits), -0.0);
234+
assert_eq!(rp(1234.5678, -max_digits), 0.0);
235+
assert_eq!(rp(-1234.5678, -max_digits), -0.0);
236+
237+
// Max + 1
238+
assert_eq!(rp(f64::MAX, -max_up), 0.0);
239+
assert_eq!(rp(f64::MIN, -max_up), -0.0);
240+
assert_eq!(rp(1.66 * exp10(max_digits), -max_up), 0.0);
241+
assert_eq!(rp(-1.66 * exp10(max_digits), -max_up), -0.0);
242+
assert_eq!(rp(1.66 * exp10(max_down), -max_up), 0.0);
243+
assert_eq!(rp(-1.66 * exp10(max_down), -max_up), -0.0);
244+
assert_eq!(rp(1234.5678, -max_up), 0.0);
245+
assert_eq!(rp(-1234.5678, -max_up), -0.0);
246+
247+
// Max - 1
248+
assert_eq!(rp(f64::MAX, -max_down), f64::INFINITY);
249+
assert_eq!(rp(f64::MIN, -max_down), f64::NEG_INFINITY);
250+
assert_eq!(rp(1.66 * exp10(max_down), -max_down), 2.0 * exp10(max_down));
251+
assert_eq!(rp(-1.66 * exp10(max_down), -max_down), -2.0 * exp10(max_down));
252+
assert_eq!(rp(1234.5678, -max_down), 0.0);
253+
assert_eq!(rp(-1234.5678, -max_down), -0.0);
254+
255+
// Must be approx equal to 1.7e308. Using some division and flooring
256+
// to avoid weird results due to imprecision.
257+
assert_eq!(
258+
(rp(1.66 * exp10(max_digits), -max_down) / exp10(max_down)).floor(),
259+
17.0,
260+
);
261+
assert_eq!(
262+
(rp(-1.66 * exp10(max_digits), -max_down) / exp10(max_down)).floor(),
263+
-17.0,
264+
);
265+
}
266+
267+
#[test]
268+
fn test_round_int_with_precision_positive() {
269+
assert_eq!(rip(0, 0), Some(0));
270+
assert_eq!(rip(10, 0), Some(10));
271+
assert_eq!(rip(23, 235), Some(23));
272+
assert_eq!(rip(i64::MAX, 235), Some(i64::MAX));
273+
}
274+
275+
#[test]
276+
fn test_round_int_with_precision_negative_1() {
277+
let round = |value| rip(value, -1);
278+
assert_eq!(round(0), Some(0));
279+
assert_eq!(round(3), Some(0));
280+
assert_eq!(round(5), Some(10));
281+
assert_eq!(round(13), Some(10));
282+
assert_eq!(round(1234), Some(1230));
283+
assert_eq!(round(-1234), Some(-1230));
284+
assert_eq!(round(1245), Some(1250));
285+
assert_eq!(round(-1245), Some(-1250));
286+
assert_eq!(round(i64::MAX), None);
287+
assert_eq!(round(i64::MIN), None);
288+
}
289+
290+
#[test]
291+
fn test_round_int_with_precision_negative_2() {
292+
let round = |value| rip(value, -2);
293+
assert_eq!(round(0), Some(0));
294+
assert_eq!(round(3), Some(0));
295+
assert_eq!(round(5), Some(0));
296+
assert_eq!(round(13), Some(0));
297+
assert_eq!(round(1245), Some(1200));
298+
assert_eq!(round(-1245), Some(-1200));
299+
assert_eq!(round(1253), Some(1300));
300+
assert_eq!(round(-1253), Some(-1300));
301+
assert_eq!(round(i64::MAX), Some(i64::MAX - 7));
302+
assert_eq!(round(i64::MIN), Some(i64::MIN + 8));
104303
}
105304
}

0 commit comments

Comments
 (0)