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) ]
37120mod 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