Skip to main content

reifydb_value/value/number/safe/convert/
decimal.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use super::*;
5
6macro_rules! impl_safe_convert_decimal_to_int {
7    ($($dst:ty),*) => {
8        $(
9            impl SafeConvert<$dst> for Decimal {
10                fn checked_convert(self) -> Option<$dst> {
11                    if let Some(int_part) = self.inner().to_bigint() {
12                        <$dst>::try_from(int_part).ok()
13                    } else {
14                        None
15                    }
16                }
17
18                fn saturating_convert(self) -> $dst {
19                    if let Some(int_part) = self.inner().to_bigint() {
20                        if let Ok(val) = <$dst>::try_from(&int_part) {
21                            val
22                        } else if int_part < BigInt::from(0) {
23                            <$dst>::MIN
24                        } else {
25                            <$dst>::MAX
26                        }
27                    } else {
28                        0
29                    }
30                }
31
32                fn wrapping_convert(self) -> $dst {
33                    if let Some(int_part) = self.inner().to_bigint() {
34                        if let Ok(val) = <$dst>::try_from(&int_part) {
35                            val
36                        } else {
37                            self.saturating_convert()
38                        }
39                    } else {
40                        0
41                    }
42                }
43            }
44        )*
45    };
46}
47
48fn decimal_to_f64(decimal: &Decimal) -> f64 {
49	format!("{:e}", decimal.inner())
50		.parse::<f64>()
51		.expect("BigDecimal LowerExp always emits parseable f64 scientific notation")
52}
53
54macro_rules! impl_safe_convert_decimal_to_float {
55    ($($dst:ty),*) => {
56        $(
57            impl SafeConvert<$dst> for Decimal {
58                fn checked_convert(self) -> Option<$dst> {
59                    let f = decimal_to_f64(&self);
60                    if !f.is_finite() {
61                        return None;
62                    }
63                    let converted = f as $dst;
64                    if !converted.is_finite() {
65                        return None;
66                    }
67                    Some(converted)
68                }
69
70                fn saturating_convert(self) -> $dst {
71                    let f = decimal_to_f64(&self);
72                    if !f.is_finite() {
73                        return if f.is_sign_negative() { <$dst>::MIN } else { <$dst>::MAX };
74                    }
75                    if f < <$dst>::MIN as f64 {
76                        return <$dst>::MIN;
77                    }
78                    if f > <$dst>::MAX as f64 {
79                        return <$dst>::MAX;
80                    }
81                    f as $dst
82                }
83
84                fn wrapping_convert(self) -> $dst {
85                    self.saturating_convert()
86                }
87            }
88        )*
89    };
90}
91
92impl_safe_convert_decimal_to_int!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
93impl_safe_convert_decimal_to_float!(f32, f64);
94
95impl SafeConvert<Int> for Decimal {
96	fn checked_convert(self) -> Option<Int> {
97		self.inner().to_bigint().map(Int)
98	}
99
100	fn saturating_convert(self) -> Int {
101		self.checked_convert().unwrap_or(Int::zero())
102	}
103
104	fn wrapping_convert(self) -> Int {
105		self.saturating_convert()
106	}
107}
108
109impl SafeConvert<Uint> for Decimal {
110	fn checked_convert(self) -> Option<Uint> {
111		if let Some(big_int) = self.inner().to_bigint() {
112			if big_int >= BigInt::from(0) {
113				Some(Uint(big_int))
114			} else {
115				None
116			}
117		} else {
118			None
119		}
120	}
121
122	fn saturating_convert(self) -> Uint {
123		if let Some(big_int) = self.inner().to_bigint() {
124			if big_int >= BigInt::from(0) {
125				Uint(big_int)
126			} else {
127				Uint::zero()
128			}
129		} else {
130			Uint::zero()
131		}
132	}
133
134	fn wrapping_convert(self) -> Uint {
135		if let Some(big_int) = self.inner().to_bigint() {
136			Uint(big_int.abs())
137		} else {
138			Uint::zero()
139		}
140	}
141}
142
143#[cfg(test)]
144pub mod tests {
145	mod i8 {
146		use super::*;
147		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
148
149		#[test]
150		fn test_checked_convert() {
151			let x = Decimal::from(127i64);
152			let y: Option<i8> = x.checked_convert();
153			assert_eq!(y, Some(127i8));
154		}
155
156		#[test]
157		fn test_checked_convert_overflow() {
158			let x = Decimal::from(128i64);
159			let y: Option<i8> = x.checked_convert();
160			assert_eq!(y, None);
161		}
162
163		#[test]
164		fn test_saturating_convert() {
165			let x = Decimal::from(200i64);
166			let y: i8 = x.saturating_convert();
167			assert_eq!(y, i8::MAX);
168		}
169
170		#[test]
171		fn test_wrapping_convert() {
172			let x = Decimal::from(-129i64);
173			let y: i8 = x.wrapping_convert();
174			assert_eq!(y, i8::MIN);
175		}
176	}
177
178	mod i32 {
179		use super::*;
180		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
181
182		#[test]
183		fn test_checked_convert() {
184			let x = Decimal::from(2147483647i64);
185			let y: Option<i32> = x.checked_convert();
186			assert_eq!(y, Some(2147483647i32));
187		}
188
189		#[test]
190		fn test_saturating_convert() {
191			let x = Decimal::from(-2147483648i64);
192			let y: i32 = x.saturating_convert();
193			assert_eq!(y, -2147483648i32);
194		}
195	}
196
197	mod u8 {
198		use super::*;
199		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
200
201		#[test]
202		fn test_checked_convert() {
203			let x = Decimal::from(255i64);
204			let y: Option<u8> = x.checked_convert();
205			assert_eq!(y, Some(255u8));
206		}
207
208		#[test]
209		fn test_checked_convert_overflow() {
210			let x = Decimal::from(256i64);
211			let y: Option<u8> = x.checked_convert();
212			assert_eq!(y, None);
213		}
214
215		#[test]
216		fn test_checked_convert_negative() {
217			let x = Decimal::from(-1i64);
218			let y: Option<u8> = x.checked_convert();
219			assert_eq!(y, None);
220		}
221
222		#[test]
223		fn test_saturating_convert() {
224			let x = Decimal::from(1000i64);
225			let y: u8 = x.saturating_convert();
226			assert_eq!(y, u8::MAX);
227		}
228	}
229
230	mod u32 {
231		use super::*;
232		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
233
234		#[test]
235		fn test_checked_convert() {
236			let x = Decimal::from(4294967295i64);
237			let y: Option<u32> = x.checked_convert();
238			assert_eq!(y, Some(4294967295u32));
239		}
240
241		#[test]
242		fn test_saturating_convert() {
243			let x = Decimal::from(-100i64);
244			let y: u32 = x.saturating_convert();
245			assert_eq!(y, 0u32);
246		}
247	}
248
249	mod f32 {
250		use std::str::FromStr;
251
252		use bigdecimal::BigDecimal;
253
254		use super::*;
255		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
256
257		#[test]
258		fn test_checked_convert() {
259			let x = Decimal::from(42i64);
260			let y: Option<f32> = x.checked_convert();
261			assert_eq!(y, Some(42.0f32));
262		}
263
264		#[test]
265		fn test_saturating_convert() {
266			let x = Decimal::from(-1000i64);
267			let y: f32 = x.saturating_convert();
268			assert_eq!(y, -1000.0f32);
269		}
270
271		#[test]
272		fn checked_convert_f32_max_exact_literal_roundtrips() {
273			// The canonical f64 decimal of f32::MAX parses to exactly f32::MAX as f64,
274			// which is within range and must round-trip cleanly.
275			let bd = BigDecimal::from_str("3.4028234663852886e38").unwrap();
276			let dec = Decimal::new(bd);
277			let out: Option<f32> = dec.checked_convert();
278			assert_eq!(out, Some(f32::MAX));
279		}
280
281		#[test]
282		fn checked_convert_neg_f32_max_exact_literal_roundtrips() {
283			let bd = BigDecimal::from_str("-3.4028234663852886e38").unwrap();
284			let dec = Decimal::new(bd);
285			let out: Option<f32> = dec.checked_convert();
286			assert_eq!(out, Some(f32::MIN));
287		}
288
289		#[test]
290		fn checked_convert_rounds_value_just_above_f32_max_to_max() {
291			// 3.4028235e38 is the shortest decimal that rounds to f32::MAX; as
292			// an exact decimal it is slightly larger than f32::MAX, but IEEE
293			// round-to-nearest maps it back to f32::MAX. The printed form of
294			// f32::MAX must round-trip, so checked conversion accepts anything
295			// that rounds into range and rejects only true overflow (see
296			// checked_convert_rejects_value_above_f32_max). Same contract as
297			// parsing the equivalent string and the f64 -> f32 demote.
298			let bd = BigDecimal::from_str("3.4028235e38").unwrap();
299			let dec = Decimal::new(bd);
300			let out: Option<f32> = dec.checked_convert();
301			assert_eq!(out, Some(f32::MAX));
302		}
303
304		#[test]
305		fn checked_convert_rejects_value_above_f32_max() {
306			// 1e40 is well above f32::MAX; must be rejected.
307			let bd = BigDecimal::from_str("1e40").unwrap();
308			let dec = Decimal::new(bd);
309			let out: Option<f32> = dec.checked_convert();
310			assert_eq!(out, None);
311		}
312
313		#[test]
314		fn saturating_convert_above_f32_max_returns_max() {
315			let bd = BigDecimal::from_str("1e40").unwrap();
316			let dec = Decimal::new(bd);
317			let out: f32 = dec.saturating_convert();
318			assert_eq!(out, f32::MAX);
319		}
320
321		#[test]
322		fn saturating_convert_below_neg_f32_max_returns_min() {
323			let bd = BigDecimal::from_str("-1e40").unwrap();
324			let dec = Decimal::new(bd);
325			let out: f32 = dec.saturating_convert();
326			assert_eq!(out, f32::MIN);
327		}
328
329		#[test]
330		fn checked_convert_f32_min_positive_roundtrips() {
331			// Subnormal boundary - must not flush to zero or fail.
332			let bd = BigDecimal::from_str("1.17549435e-38").unwrap();
333			let dec = Decimal::new(bd);
334			let out: Option<f32> = dec.checked_convert();
335			assert_eq!(out, Some(f32::MIN_POSITIVE));
336		}
337	}
338
339	mod f64 {
340		use std::str::FromStr;
341
342		use bigdecimal::BigDecimal;
343
344		use super::*;
345		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
346
347		#[test]
348		fn test_checked_convert() {
349			let x = Decimal::from(42i64);
350			let y: Option<f64> = x.checked_convert();
351			assert_eq!(y, Some(42.0f64));
352		}
353
354		#[test]
355		fn test_saturating_convert() {
356			let x = Decimal::from(-1000i64);
357			let y: f64 = x.saturating_convert();
358			assert_eq!(y, -1000.0f64);
359		}
360
361		#[test]
362		fn checked_convert_f64_max_literal_roundtrips() {
363			// Regression: bigdecimal-0.4.10's to_f64 returns infinity for this
364			// representation (int=17976931348623157, scale=-292) via its lossy
365			// "simple integer" branch (int.to_f64() * powi(10, 292)). The string-based
366			// conversion must round-trip exactly to f64::MAX.
367			let bd = BigDecimal::from_str("1.7976931348623157e308").unwrap();
368			let dec = Decimal::new(bd);
369			let out: Option<f64> = dec.checked_convert();
370			assert_eq!(out, Some(f64::MAX));
371		}
372
373		#[test]
374		fn checked_convert_neg_f64_max_literal_roundtrips() {
375			let bd = BigDecimal::from_str("-1.7976931348623157e308").unwrap();
376			let dec = Decimal::new(bd);
377			let out: Option<f64> = dec.checked_convert();
378			assert_eq!(out, Some(f64::MIN));
379		}
380
381		#[test]
382		fn checked_convert_rejects_value_above_f64_max() {
383			// 1e400 exceeds f64::MAX (~1.8e308) so must parse as infinity and be rejected.
384			let bd = BigDecimal::from_str("1e400").unwrap();
385			let dec = Decimal::new(bd);
386			let out: Option<f64> = dec.checked_convert();
387			assert_eq!(out, None);
388		}
389
390		#[test]
391		fn saturating_convert_above_f64_max_returns_max() {
392			let bd = BigDecimal::from_str("1e400").unwrap();
393			let dec = Decimal::new(bd);
394			let out: f64 = dec.saturating_convert();
395			assert_eq!(out, f64::MAX);
396		}
397
398		#[test]
399		fn saturating_convert_below_neg_f64_max_returns_min() {
400			let bd = BigDecimal::from_str("-1e400").unwrap();
401			let dec = Decimal::new(bd);
402			let out: f64 = dec.saturating_convert();
403			assert_eq!(out, f64::MIN);
404		}
405
406		#[test]
407		fn checked_convert_f64_min_positive_roundtrips() {
408			let bd = BigDecimal::from_str("2.2250738585072014e-308").unwrap();
409			let dec = Decimal::new(bd);
410			let out: Option<f64> = dec.checked_convert();
411			assert_eq!(out, Some(f64::MIN_POSITIVE));
412		}
413	}
414
415	mod int {
416		use crate::value::{decimal::Decimal, int::Int, number::safe::convert::SafeConvert};
417
418		#[test]
419		fn test_checked_convert() {
420			let x = Decimal::from(12345i64);
421			let y: Option<Int> = x.checked_convert();
422			assert!(y.is_some());
423			assert_eq!(y.unwrap().to_string(), "12345");
424		}
425
426		#[test]
427		fn test_saturating_convert() {
428			let x = Decimal::from(-999999i64);
429			let y: Int = x.saturating_convert();
430			assert_eq!(y.to_string(), "-999999");
431		}
432
433		#[test]
434		fn test_wrapping_convert() {
435			let x = Decimal::from(0i64);
436			let y: Int = x.wrapping_convert();
437			assert_eq!(y.to_string(), "0");
438		}
439	}
440
441	mod uint {
442		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert, uint::Uint};
443
444		#[test]
445		fn test_checked_convert_positive() {
446			let x = Decimal::from(42i64);
447			let y: Option<Uint> = x.checked_convert();
448			assert!(y.is_some());
449			assert_eq!(y.unwrap().to_string(), "42");
450		}
451
452		#[test]
453		fn test_checked_convert_negative() {
454			let x = Decimal::from(-1i64);
455			let y: Option<Uint> = x.checked_convert();
456			assert!(y.is_none());
457		}
458
459		#[test]
460		fn test_saturating_convert() {
461			let x = Decimal::from(-100i64);
462			let y: Uint = x.saturating_convert();
463			assert_eq!(y.to_string(), "0");
464		}
465
466		#[test]
467		fn test_wrapping_convert() {
468			let x = Decimal::from(-1i64);
469			let y: Uint = x.wrapping_convert();
470			assert_eq!(y.to_string(), "1");
471		}
472	}
473
474	mod self_conversion {
475		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
476
477		#[test]
478		fn test_checked_convert() {
479			let x = Decimal::from(42i64);
480			let y: Option<Decimal> = x.clone().checked_convert();
481			assert_eq!(y, Some(x));
482		}
483
484		#[test]
485		fn test_saturating_convert() {
486			let x = Decimal::from(-100i64);
487			let y: Decimal = x.clone().saturating_convert();
488			assert_eq!(y, x);
489		}
490
491		#[test]
492		fn test_wrapping_convert() {
493			let x = Decimal::from(999i64);
494			let y: Decimal = x.clone().wrapping_convert();
495			assert_eq!(y, x);
496		}
497	}
498}