claudiofsr_lib/
rounded.rs

1/// Round floating numbers (f32 or f64)
2pub trait RoundFloat<T> {
3    /**
4    Round floating-point numbers to a specified number of decimal places.
5
6    Two Rounding method for floating-point operations:
7
8    1. Round to nearest value, ties to even:
9
10        if the number falls midway, it is rounded to the nearest value with an even least significant digit.
11
12    2. Round to nearest value, ties away from zero (or ties to away):
13
14        if the number falls midway, it is rounded to the nearest value above (for positive numbers) or below (for negative numbers).
15
16    Python takes the first approach and Rust takes the second.
17
18    Neither is contradicting the IEEE-754 standard, which defines and allows for both.
19
20    Examples:
21    ```
22        use claudiofsr_lib::RoundFloat;
23
24        let decimal_places: u32 = 2;
25        let number: f64 = 1.454999;
26        let result: f64 = number.round_float(decimal_places);
27        assert_eq!(result, 1.45);
28
29        let decimal_places: usize = 2;
30        let result = 1.455000.round_float(decimal_places);
31        assert_eq!(result, 1.46);
32
33        let decimal_places: u128 = 6;
34        let number: f64 = 3.455000500;
35        let result: f64 = number.round_float(decimal_places);
36        assert_eq!(result, 3.455001);
37
38        let result = 1.455050.round_float(4); // 4i32
39        assert_eq!(result, 1.4551);
40
41        let result = 1.455000.round_float(0);
42        assert_eq!(result, 1.0);
43
44        let number: f32 = -2.0 / 3.0;
45        let result: f32 = number.round_float(5);
46        assert_eq!(result, -0.66667);
47
48        let number: f32 = 5.99997;
49        let result: f32 = number.round_float(4);
50        assert_eq!(result, 6.0); // 6.0000
51
52        let decimal_places: isize = 3;
53        let number: f32 = 5.99997;
54        let result: f32 = number.round_float(decimal_places);
55        assert_eq!(result, 6.0); // 6.000
56
57        let decimal_places: u8 = 4;
58        let result: f32 = 5.00007.round_float(decimal_places);
59        assert_eq!(result, 5.0001);
60    ```
61    <https://floating-point-gui.de/languages/rust>
62
63    <https://doc.rust-lang.org/std/primitive.f64.html#method.powi>
64
65    <https://doc.rust-lang.org/std/convert/trait.TryFrom.html>
66    */
67    fn round_float(self, decimal_places: T) -> Self
68    where
69        Self: std::marker::Sized; // This trait is object safe
70}
71
72impl<T> RoundFloat<T> for f64
73where
74    i32: TryFrom<T>,
75    <i32 as TryFrom<T>>::Error: std::fmt::Display,
76{
77    fn round_float(self, decimal_places: T) -> f64 {
78        match i32::try_from(decimal_places) {
79            Ok(dec) => {
80                if dec <= 0 || self == 0.0 {
81                    self.round()
82                } else {
83                    let multiplier: f64 = 10.0_f64.powi(dec);
84                    (self * multiplier).round() / multiplier
85                }
86            }
87            Err(why) => {
88                let t = std::any::type_name::<T>();
89                eprintln!("fn round_float() for f64: {self}");
90                eprintln!("Error converting decimal places from type {t} to i32.");
91                panic!("Invalid Decimal Places: {why}")
92            }
93        }
94    }
95}
96
97impl<T> RoundFloat<T> for f32
98where
99    i32: TryFrom<T>,
100    <i32 as TryFrom<T>>::Error: std::fmt::Display,
101{
102    fn round_float(self, decimal_places: T) -> f32 {
103        match i32::try_from(decimal_places) {
104            Ok(dec) => {
105                if dec <= 0 || self == 0.0 {
106                    self.round()
107                } else {
108                    let multiplier: f64 = 10.0_f64.powi(dec);
109                    (((self as f64) * multiplier).round() / multiplier) as f32
110                }
111            }
112            Err(why) => {
113                let t = std::any::type_name::<T>();
114                eprintln!("fn round_float() for f32: {self}");
115                eprintln!("Error converting decimal places from type {t} to i32.");
116                panic!("Invalid Decimal Places: {why}")
117            }
118        }
119    }
120}
121
122/// Try Convert Extension
123pub trait TryConvertExtension<T> {
124    /**
125    Try converting type T to type U
126
127    "Simple and safe type conversions that may fail
128    in a controlled way under some circumstances.""
129
130    Example:
131    ```
132        use claudiofsr_lib::TryConvertExtension;
133
134        let type_u8: u8 = 5;
135        let type_i16: i16 = 5;
136        let type_u32: u32 = 5;
137        let type_f64: f64 = 5.0;
138
139        let value_f64: f64 = type_u8.try_convert();
140        assert_eq!(type_f64, value_f64);
141
142        let value_u32: u32 = type_i16.try_convert();
143        assert_eq!(type_u32, value_u32);
144
145        let value_usize: usize = 7_i32.try_convert();
146        assert_eq!(7_usize, value_usize);
147
148        let value_f64: f64 = 9_u16.try_convert();
149        assert_eq!(9.0_f64, value_f64);
150
151        // With TurboFish
152
153        let value_u32 = type_i16.try_convert::<u32>();
154        assert_eq!(type_u32, value_u32);
155
156        let value_f64 = 2_i32.try_convert::<f64>();
157        assert_eq!(2.0_f64, value_f64);
158    ```
159    */
160    fn try_convert<U>(self) -> U
161    where
162        U: TryFrom<T>,
163        <U as TryFrom<T>>::Error: std::fmt::Display;
164}
165
166impl<T> TryConvertExtension<T> for T {
167    fn try_convert<U>(self) -> U
168    where
169        U: TryFrom<T>,
170        <U as TryFrom<T>>::Error: std::fmt::Display,
171    {
172        match U::try_from(self) {
173            Ok(type_u) => type_u,
174            Err(why) => {
175                let t = std::any::type_name::<T>();
176                let u = std::any::type_name::<U>();
177                panic!("Error converting from {t} to {u}: {why}")
178            }
179        }
180    }
181}
182
183#[cfg(test)]
184mod round_numbers {
185    use super::*;
186
187    // cargo test -- --help
188    // cargo test -- --nocapture
189    // cargo test -- --show-output
190
191    #[test]
192    /// `cargo test -- --show-output round_float_f64`
193    fn round_float_f64() {
194        let decimal_places: u32 = 2;
195        let number: f64 = 1.454999;
196        let result: f64 = number.round_float(decimal_places);
197        assert_eq!(result, 1.45);
198
199        let decimal_places: usize = 3;
200        let result = 1.455500.round_float(decimal_places);
201        assert_eq!(result, 1.456);
202
203        let decimal_places: u128 = 6;
204        let number: f64 = 3.455000500;
205        let result: f64 = number.round_float(decimal_places);
206        assert_eq!(result, 3.455001);
207
208        let result = 1.455000.round_float(1); // 1i32
209        assert_eq!(result, 1.5);
210
211        let result = 1.455000.round_float(0);
212        assert_eq!(result, 1.0);
213
214        let number: f64 = 5.99997000 + 4.0e-8;
215        let decimal_places: u8 = 255;
216        let result: f64 = number.round_float(decimal_places);
217        assert_eq!(result, 5.99997004);
218
219        let decimal_places: i32 = -1;
220        let result = 1.455000.round_float(decimal_places);
221        assert_eq!(result, 1.0);
222
223        let decimal_places: u8 = 255; // type `u8` has range `0..=255`
224        let number: f64 = 5.99997000 + 4.0e-8;
225        let result: f64 = number.round_float(decimal_places);
226
227        println!("decimal_places: {decimal_places}");
228        println!("number: {number:.30}");
229        println!("result: {result:.30}");
230
231        assert_eq!(result, 5.99997004);
232    }
233
234    #[test]
235    fn round_float_f64_negative() {
236        let number: f64 = -2.0 / 3.0;
237        let result: f64 = number.round_float(5);
238        assert_eq!(result, -0.66667);
239
240        let decimal_places: usize = 3;
241        let result = (-1.455000).round_float(decimal_places);
242        assert_eq!(result, -1.455);
243    }
244
245    #[test]
246    fn round_float_f64_zero() {
247        let number: f64 = 0.0;
248        let decimal_places: u64 = 2;
249        let result: f64 = number.round_float(decimal_places);
250        assert_eq!(result, 0.0);
251
252        let decimal_places: usize = 3;
253        let result = 0.000.round_float(decimal_places);
254        assert_eq!(result, 0.0);
255    }
256
257    #[test]
258    fn round_float_f64_nan() {
259        let number: f64 = f64::NAN;
260        let decimal_places: u32 = 2;
261        let result: f64 = number.round_float(decimal_places);
262        assert!(result.is_nan());
263
264        let decimal_places: isize = 3;
265        let result = f64::NAN.round_float(decimal_places);
266        assert!(result.is_nan());
267    }
268
269    #[test]
270    fn round_float_f64_inf() {
271        let number: f64 = f64::INFINITY;
272        let decimal_places: u32 = 2;
273        let result: f64 = number.round_float(decimal_places);
274        assert!(result.is_infinite());
275
276        let decimal_places: usize = 3;
277        let result = f64::INFINITY.round_float(decimal_places);
278        assert!(result.is_infinite());
279    }
280}