typst_utils/
round.rs

1/// Returns value with `n` digits after floating point where `n` is `precision`.
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).
8///
9/// If rounding the `value` will have no effect (e.g., it's infinite or NaN),
10/// returns `value` unchanged.
11///
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///
16/// # Examples
17///
18/// ```
19/// # use typst_utils::round_with_precision;
20/// let rounded = round_with_precision(-0.56553, 2);
21/// assert_eq!(-0.57, rounded);
22///
23/// let rounded_negative = round_with_precision(823543.0, -3);
24/// assert_eq!(824000.0, rounded_negative);
25/// ```
26pub fn round_with_precision(value: f64, precision: i16) -> f64 {
27    // Don't attempt to round the float if that wouldn't have any effect.
28    // This includes infinite or NaN values, as well as integer values
29    // with a filled mantissa (which can't have a fractional part).
30    // Rounding with a precision larger than the amount of digits that can be
31    // effectively represented would also be a no-op. Given that, the check
32    // below ensures we won't proceed if `|value| >= 2^53` or if
33    // `precision >= 15`, which also ensures the multiplication by `offset`
34    // won't return `inf`, since `2^53 * 10^15` (larger than any possible
35    // `value * offset` multiplication) does not.
36    if value.is_infinite()
37        || value.is_nan()
38        || precision >= 0 && value.abs() >= (1_i64 << f64::MANTISSA_DIGITS) as f64
39        || precision >= f64::DIGITS as i16
40    {
41        return value;
42    }
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/// ```
82pub 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)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::{round_int_with_precision as rip, round_with_precision as rp};
122
123    #[test]
124    fn test_round_with_precision_0() {
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);
132    }
133
134    #[test]
135    fn test_round_with_precision_1() {
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);
147    }
148
149    #[test]
150    fn test_round_with_precision_2() {
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);
164    }
165
166    #[test]
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    }
191
192    #[test]
193    fn test_round_with_precision_fuzzy() {
194        let max_int = (1_i64 << f64::MANTISSA_DIGITS) as f64;
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));
303    }
304}