rusty_common/
decimal_utils.rs1use rust_decimal::Decimal;
8use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum PrecisionValidation {
14 Lossless,
16 AcceptableLoss {
18 relative_error: f64,
21 },
22 SignificantLoss {
24 relative_error: f64,
27 },
28 ConversionFailed,
30}
31
32impl fmt::Display for PrecisionValidation {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::Lossless => write!(f, "Lossless conversion"),
36 Self::AcceptableLoss { relative_error } => {
37 write!(
38 f,
39 "Acceptable precision loss: {relative_error:.2e} relative error"
40 )
41 }
42 Self::SignificantLoss { relative_error } => {
43 write!(
44 f,
45 "Significant precision loss: {relative_error:.2e} relative error"
46 )
47 }
48 Self::ConversionFailed => write!(f, "Conversion failed"),
49 }
50 }
51}
52
53#[must_use]
82pub fn validate_decimal_to_f64_precision(
83 decimal_value: Decimal,
84 tolerance: f64,
85) -> PrecisionValidation {
86 let f64_value = match decimal_value.to_f64() {
88 Some(val) if val.is_finite() => val,
89 _ => return PrecisionValidation::ConversionFailed,
90 };
91
92 let round_trip_decimal = match Decimal::from_f64(f64_value) {
94 Some(val) => val,
95 None => return PrecisionValidation::ConversionFailed,
96 };
97
98 if decimal_value.is_zero() && round_trip_decimal.is_zero() {
100 return PrecisionValidation::Lossless;
101 }
102
103 if decimal_value.is_zero() || round_trip_decimal.is_zero() {
104 return PrecisionValidation::SignificantLoss {
106 relative_error: 1.0,
107 };
108 }
109
110 let difference = (decimal_value - round_trip_decimal).abs();
111 let relative_error = difference / decimal_value.abs();
112
113 let relative_error_f64 = relative_error.to_f64().unwrap_or(1.0);
115
116 if relative_error_f64 == 0.0 {
117 PrecisionValidation::Lossless
118 } else if relative_error_f64 <= tolerance {
119 PrecisionValidation::AcceptableLoss {
120 relative_error: relative_error_f64,
121 }
122 } else {
123 PrecisionValidation::SignificantLoss {
124 relative_error: relative_error_f64,
125 }
126 }
127}
128
129#[must_use]
153pub fn decimal_to_f64_with_validation(
154 decimal_value: Decimal,
155 tolerance: f64,
156) -> (f64, PrecisionValidation) {
157 let validation = validate_decimal_to_f64_precision(decimal_value, tolerance);
158 let f64_value = match validation {
159 PrecisionValidation::ConversionFailed => f64::NAN,
160 _ => decimal_value.to_f64().unwrap_or(f64::NAN),
161 };
162 (f64_value, validation)
163}
164
165#[inline]
185#[must_use]
186pub fn decimal_to_f64_or_nan(d: Decimal) -> f64 {
187 d.to_f64().unwrap_or_else(|| {
188 #[cfg(debug_assertions)]
189 eprintln!("Warning: Decimal to f64 conversion failed for value: {d}");
190 f64::NAN
191 })
192}
193
194#[must_use]
216pub fn decimal_to_f64_safe(decimal_value: Decimal, tolerance: f64) -> f64 {
217 let (f64_value, validation) = decimal_to_f64_with_validation(decimal_value, tolerance);
218
219 #[cfg(debug_assertions)]
220 match validation {
221 PrecisionValidation::SignificantLoss { relative_error } => {
222 eprintln!(
223 "Warning: Significant precision loss in Decimal to f64 conversion: {decimal_value} -> {f64_value}, relative error: {relative_error:.2e}"
224 );
225 }
226 PrecisionValidation::ConversionFailed => {
227 eprintln!("Warning: Decimal to f64 conversion failed for value: {decimal_value}");
228 }
229 _ => {}
230 }
231
232 f64_value
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use rust_decimal_macros::dec;
239
240 #[test]
241 fn test_normal_conversion() {
242 assert_eq!(decimal_to_f64_or_nan(dec!(123.45)), 123.45);
243 assert_eq!(decimal_to_f64_or_nan(dec!(0)), 0.0);
244 assert_eq!(decimal_to_f64_or_nan(dec!(-99.99)), -99.99);
245 }
246
247 #[test]
248 fn test_large_decimal() {
249 let large = Decimal::MAX;
251 let result = decimal_to_f64_or_nan(large);
252 assert!(result.is_finite() && result > 0.0);
255 assert!(result < f64::MAX);
256 }
257
258 #[test]
259 fn test_precision_validation_lossless() {
260 assert_eq!(
262 validate_decimal_to_f64_precision(dec!(123.45), 1e-15),
263 PrecisionValidation::Lossless
264 );
265 assert_eq!(
266 validate_decimal_to_f64_precision(dec!(0), 1e-15),
267 PrecisionValidation::Lossless
268 );
269 assert_eq!(
270 validate_decimal_to_f64_precision(dec!(-99.99), 1e-15),
271 PrecisionValidation::Lossless
272 );
273 assert_eq!(
274 validate_decimal_to_f64_precision(dec!(1.0), 1e-15),
275 PrecisionValidation::Lossless
276 );
277 }
278
279 #[test]
280 fn test_precision_validation_high_precision_loss() {
281 let high_precision = dec!(0.1111111111111111111111111111);
283 let validation = validate_decimal_to_f64_precision(high_precision, 1e-15);
284
285 println!("High precision validation result: {validation:?}");
287
288 assert!(matches!(
291 validation,
292 PrecisionValidation::SignificantLoss { .. }
293 | PrecisionValidation::AcceptableLoss { .. }
294 ));
295
296 let very_high_precision = Decimal::new(111111111111111111, 18); let validation2 = validate_decimal_to_f64_precision(very_high_precision, 1e-15);
299 println!("Very high precision validation result: {validation2:?}");
300
301 assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
303 assert!(!matches!(
304 validation2,
305 PrecisionValidation::ConversionFailed
306 ));
307 }
308
309 #[test]
310 fn test_precision_validation_acceptable_loss() {
311 let small_loss = dec!(0.1); let validation = validate_decimal_to_f64_precision(small_loss, 1e-10); assert!(matches!(
315 validation,
316 PrecisionValidation::Lossless | PrecisionValidation::AcceptableLoss { .. }
317 ));
318 }
319
320 #[test]
321 fn test_precision_validation_conversion_failed() {
322 let validation = validate_decimal_to_f64_precision(Decimal::MAX, 1e-15);
324 println!("Decimal::MAX validation result: {validation:?}");
325
326 assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
329
330 let large_precise = Decimal::new(999999999999999999, 10); let validation2 = validate_decimal_to_f64_precision(large_precise, 1e-15);
333 println!("Large precise validation result: {validation2:?}");
334 assert!(!matches!(
335 validation2,
336 PrecisionValidation::ConversionFailed
337 ));
338 }
339
340 #[test]
341 fn test_decimal_to_f64_with_validation() {
342 let price = dec!(123.45);
343 let (f64_price, validation) = decimal_to_f64_with_validation(price, 1e-15);
344 assert_eq!(f64_price, 123.45);
345 assert_eq!(validation, PrecisionValidation::Lossless);
346
347 let high_precision = dec!(0.1111111111111111111111111111);
349 let (f64_val, validation) = decimal_to_f64_with_validation(high_precision, 1e-15);
350 println!("High precision conversion validation: {validation:?}");
351 assert!(f64_val.is_finite());
352
353 assert!(matches!(
355 validation,
356 PrecisionValidation::Lossless
357 | PrecisionValidation::AcceptableLoss { .. }
358 | PrecisionValidation::SignificantLoss { .. }
359 ));
360 }
361
362 #[test]
363 fn test_decimal_to_f64_safe() {
364 let price = dec!(123.45);
366 let f64_price = decimal_to_f64_safe(price, 1e-15);
367 assert_eq!(f64_price, 123.45);
368
369 let high_precision = dec!(0.1111111111111111111111111111);
371 let f64_val = decimal_to_f64_safe(high_precision, 1e-15);
372 assert!(f64_val.is_finite());
373 }
374
375 #[test]
376 fn test_precision_validation_edge_cases() {
377 assert_eq!(
379 validate_decimal_to_f64_precision(dec!(0), 1e-15),
380 PrecisionValidation::Lossless
381 );
382
383 let tiny = Decimal::new(1, 28); let validation = validate_decimal_to_f64_precision(tiny, 1e-15);
386 println!("Tiny value validation: {validation:?}");
387 assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
389
390 let negative_precise = dec!(-0.1111111111111111111111111111);
392 let validation = validate_decimal_to_f64_precision(negative_precise, 1e-15);
393 println!("Negative precise validation: {validation:?}");
394
395 assert!(matches!(
397 validation,
398 PrecisionValidation::Lossless
399 | PrecisionValidation::AcceptableLoss { .. }
400 | PrecisionValidation::SignificantLoss { .. }
401 ));
402 }
403
404 #[test]
405 fn test_relative_error_calculation() {
406 let test_value = dec!(1.0000000000000001); let validation = validate_decimal_to_f64_precision(test_value, 1e-15);
409
410 match validation {
411 PrecisionValidation::Lossless => {
412 }
414 PrecisionValidation::AcceptableLoss { relative_error }
415 | PrecisionValidation::SignificantLoss { relative_error } => {
416 assert!(relative_error >= 0.0);
417 assert!(relative_error <= 1.0); }
419 PrecisionValidation::ConversionFailed => {
420 panic!("Conversion should not fail for this value");
421 }
422 }
423 }
424
425 #[test]
426 fn test_financial_precision_scenarios() {
427 let btc_price = dec!(67234.56789123); let validation = validate_decimal_to_f64_precision(btc_price, 1e-12); assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
433
434 let micro_quantity = dec!(0.000000123456789); let validation = validate_decimal_to_f64_precision(micro_quantity, 1e-12);
437 assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
438
439 let market_cap = dec!(1234567890123.45); let validation = validate_decimal_to_f64_precision(market_cap, 1e-12);
442 assert!(!matches!(validation, PrecisionValidation::ConversionFailed));
443 }
444}