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 exact decimal expansion of f32::MAX must convert back to it unchanged.
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_rounds_value_just_above_f32_max_to_max() {
290			// This is the shortest decimal that prints f32::MAX, and as an exact value it is
291			// slightly above it. Conversion accepts anything that rounds into range and
292			// rejects only true overflow, so printing f32::MAX and reading it back works.
293			let bd = BigDecimal::from_str("3.4028235e38").unwrap();
294			let dec = Decimal::new(bd);
295			let out: Option<f32> = dec.checked_convert();
296			assert_eq!(out, Some(f32::MAX));
297		}
298
299		#[test]
300		fn checked_convert_rejects_value_above_f32_max() {
301			let bd = BigDecimal::from_str("1e40").unwrap();
302			let dec = Decimal::new(bd);
303			let out: Option<f32> = dec.checked_convert();
304			assert_eq!(out, None);
305		}
306
307		#[test]
308		fn saturating_convert_above_f32_max_returns_max() {
309			let bd = BigDecimal::from_str("1e40").unwrap();
310			let dec = Decimal::new(bd);
311			let out: f32 = dec.saturating_convert();
312			assert_eq!(out, f32::MAX);
313		}
314
315		#[test]
316		fn saturating_convert_below_neg_f32_max_returns_min() {
317			let bd = BigDecimal::from_str("-1e40").unwrap();
318			let dec = Decimal::new(bd);
319			let out: f32 = dec.saturating_convert();
320			assert_eq!(out, f32::MIN);
321		}
322
323		#[test]
324		fn checked_convert_f32_min_positive_roundtrips() {
325			// Subnormal boundary - must not flush to zero or fail.
326			let bd = BigDecimal::from_str("1.17549435e-38").unwrap();
327			let dec = Decimal::new(bd);
328			let out: Option<f32> = dec.checked_convert();
329			assert_eq!(out, Some(f32::MIN_POSITIVE));
330		}
331	}
332
333	mod f64 {
334		use std::str::FromStr;
335
336		use bigdecimal::BigDecimal;
337
338		use super::*;
339		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
340
341		#[test]
342		fn test_checked_convert() {
343			let x = Decimal::from(42i64);
344			let y: Option<f64> = x.checked_convert();
345			assert_eq!(y, Some(42.0f64));
346		}
347
348		#[test]
349		fn test_saturating_convert() {
350			let x = Decimal::from(-1000i64);
351			let y: f64 = x.saturating_convert();
352			assert_eq!(y, -1000.0f64);
353		}
354
355		#[test]
356		fn checked_convert_f64_max_literal_roundtrips() {
357			// bigdecimal 0.4.10's to_f64 returns infinity for this representation through its
358			// lossy "simple integer" branch, so the conversion has to go via the string form.
359			let bd = BigDecimal::from_str("1.7976931348623157e308").unwrap();
360			let dec = Decimal::new(bd);
361			let out: Option<f64> = dec.checked_convert();
362			assert_eq!(out, Some(f64::MAX));
363		}
364
365		#[test]
366		fn checked_convert_neg_f64_max_literal_roundtrips() {
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::MIN));
371		}
372
373		#[test]
374		fn checked_convert_rejects_value_above_f64_max() {
375			// Past f64::MAX the parse yields infinity, which must be rejected, not stored.
376			let bd = BigDecimal::from_str("1e400").unwrap();
377			let dec = Decimal::new(bd);
378			let out: Option<f64> = dec.checked_convert();
379			assert_eq!(out, None);
380		}
381
382		#[test]
383		fn saturating_convert_above_f64_max_returns_max() {
384			let bd = BigDecimal::from_str("1e400").unwrap();
385			let dec = Decimal::new(bd);
386			let out: f64 = dec.saturating_convert();
387			assert_eq!(out, f64::MAX);
388		}
389
390		#[test]
391		fn saturating_convert_below_neg_f64_max_returns_min() {
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::MIN);
396		}
397
398		#[test]
399		fn checked_convert_f64_min_positive_roundtrips() {
400			let bd = BigDecimal::from_str("2.2250738585072014e-308").unwrap();
401			let dec = Decimal::new(bd);
402			let out: Option<f64> = dec.checked_convert();
403			assert_eq!(out, Some(f64::MIN_POSITIVE));
404		}
405	}
406
407	mod int {
408		use crate::value::{decimal::Decimal, int::Int, number::safe::convert::SafeConvert};
409
410		#[test]
411		fn test_checked_convert() {
412			let x = Decimal::from(12345i64);
413			let y: Option<Int> = x.checked_convert();
414			assert!(y.is_some());
415			assert_eq!(y.unwrap().to_string(), "12345");
416		}
417
418		#[test]
419		fn test_saturating_convert() {
420			let x = Decimal::from(-999999i64);
421			let y: Int = x.saturating_convert();
422			assert_eq!(y.to_string(), "-999999");
423		}
424
425		#[test]
426		fn test_wrapping_convert() {
427			let x = Decimal::from(0i64);
428			let y: Int = x.wrapping_convert();
429			assert_eq!(y.to_string(), "0");
430		}
431	}
432
433	mod uint {
434		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert, uint::Uint};
435
436		#[test]
437		fn test_checked_convert_positive() {
438			let x = Decimal::from(42i64);
439			let y: Option<Uint> = x.checked_convert();
440			assert!(y.is_some());
441			assert_eq!(y.unwrap().to_string(), "42");
442		}
443
444		#[test]
445		fn test_checked_convert_negative() {
446			let x = Decimal::from(-1i64);
447			let y: Option<Uint> = x.checked_convert();
448			assert!(y.is_none());
449		}
450
451		#[test]
452		fn test_saturating_convert() {
453			let x = Decimal::from(-100i64);
454			let y: Uint = x.saturating_convert();
455			assert_eq!(y.to_string(), "0");
456		}
457
458		#[test]
459		fn test_wrapping_convert() {
460			let x = Decimal::from(-1i64);
461			let y: Uint = x.wrapping_convert();
462			assert_eq!(y.to_string(), "1");
463		}
464	}
465
466	mod self_conversion {
467		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
468
469		#[test]
470		fn test_checked_convert() {
471			let x = Decimal::from(42i64);
472			let y: Option<Decimal> = x.clone().checked_convert();
473			assert_eq!(y, Some(x));
474		}
475
476		#[test]
477		fn test_saturating_convert() {
478			let x = Decimal::from(-100i64);
479			let y: Decimal = x.clone().saturating_convert();
480			assert_eq!(y, x);
481		}
482
483		#[test]
484		fn test_wrapping_convert() {
485			let x = Decimal::from(999i64);
486			let y: Decimal = x.clone().wrapping_convert();
487			assert_eq!(y, x);
488		}
489	}
490}