Skip to main content

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

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