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                    narrow_i256(self.trunc())
12                }
13
14                fn saturating_convert(self) -> $dst {
15                    if let Some(value) = narrow_i256(self.trunc()) {
16                        value
17                    } else if self.is_negative() {
18                        <$dst>::MIN
19                    } else {
20                        <$dst>::MAX
21                    }
22                }
23
24                fn wrapping_convert(self) -> $dst {
25                    self.saturating_convert()
26                }
27            }
28        )*
29    };
30}
31
32macro_rules! impl_safe_convert_decimal_to_float {
33    ($($dst:ty => $to:ident),*) => {
34        $(
35            impl SafeConvert<$dst> for Decimal {
36                fn checked_convert(self) -> Option<$dst> {
37                    let value = self.$to();
38                    value.is_finite().then_some(value)
39                }
40
41                fn saturating_convert(self) -> $dst {
42                    let value = self.$to();
43                    if value.is_finite() {
44                        value
45                    } else if value.is_sign_negative() {
46                        <$dst>::MIN
47                    } else {
48                        <$dst>::MAX
49                    }
50                }
51
52                fn wrapping_convert(self) -> $dst {
53                    self.saturating_convert()
54                }
55            }
56        )*
57    };
58}
59
60impl_safe_convert_decimal_to_int!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
61impl_safe_convert_decimal_to_float!(f32 => to_f32, f64 => to_f64);
62
63#[cfg(test)]
64pub mod tests {
65	mod i8 {
66		use super::*;
67		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
68
69		#[test]
70		fn test_checked_convert() {
71			let x = Decimal::from(127i64);
72			let y: Option<i8> = x.checked_convert();
73			assert_eq!(y, Some(127i8));
74		}
75
76		#[test]
77		fn test_checked_convert_overflow() {
78			let x = Decimal::from(128i64);
79			let y: Option<i8> = x.checked_convert();
80			assert_eq!(y, None);
81		}
82
83		#[test]
84		fn test_saturating_convert() {
85			let x = Decimal::from(200i64);
86			let y: i8 = x.saturating_convert();
87			assert_eq!(y, i8::MAX);
88		}
89
90		#[test]
91		fn test_wrapping_convert() {
92			let x = Decimal::from(-129i64);
93			let y: i8 = x.wrapping_convert();
94			assert_eq!(y, i8::MIN);
95		}
96	}
97
98	mod i32 {
99		use super::*;
100		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
101
102		#[test]
103		fn test_checked_convert() {
104			let x = Decimal::from(2147483647i64);
105			let y: Option<i32> = x.checked_convert();
106			assert_eq!(y, Some(2147483647i32));
107		}
108
109		#[test]
110		fn test_saturating_convert() {
111			let x = Decimal::from(-2147483648i64);
112			let y: i32 = x.saturating_convert();
113			assert_eq!(y, -2147483648i32);
114		}
115	}
116
117	mod u8 {
118		use super::*;
119		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
120
121		#[test]
122		fn test_checked_convert() {
123			let x = Decimal::from(255i64);
124			let y: Option<u8> = x.checked_convert();
125			assert_eq!(y, Some(255u8));
126		}
127
128		#[test]
129		fn test_checked_convert_overflow() {
130			let x = Decimal::from(256i64);
131			let y: Option<u8> = x.checked_convert();
132			assert_eq!(y, None);
133		}
134
135		#[test]
136		fn test_checked_convert_negative() {
137			let x = Decimal::from(-1i64);
138			let y: Option<u8> = x.checked_convert();
139			assert_eq!(y, None);
140		}
141
142		#[test]
143		fn test_saturating_convert() {
144			let x = Decimal::from(1000i64);
145			let y: u8 = x.saturating_convert();
146			assert_eq!(y, u8::MAX);
147		}
148	}
149
150	mod u32 {
151		use super::*;
152		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
153
154		#[test]
155		fn test_checked_convert() {
156			let x = Decimal::from(4294967295i64);
157			let y: Option<u32> = x.checked_convert();
158			assert_eq!(y, Some(4294967295u32));
159		}
160
161		#[test]
162		fn test_saturating_convert() {
163			let x = Decimal::from(-100i64);
164			let y: u32 = x.saturating_convert();
165			assert_eq!(y, 0u32);
166		}
167	}
168
169	mod f32 {
170		use std::str::FromStr;
171
172		use super::*;
173		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
174
175		#[test]
176		fn test_checked_convert() {
177			let x = Decimal::from(42i64);
178			let y: Option<f32> = x.checked_convert();
179			assert_eq!(y, Some(42.0f32));
180		}
181
182		#[test]
183		fn test_saturating_convert() {
184			let x = Decimal::from(-1000i64);
185			let y: f32 = x.saturating_convert();
186			assert_eq!(y, -1000.0f32);
187		}
188
189		#[test]
190		fn checked_convert_f32_max_exact_literal_roundtrips() {
191			// The exact decimal expansion of f32::MAX must convert back to it unchanged.
192			let dec = Decimal::from_str("3.4028234663852886e38").unwrap();
193			let out: Option<f32> = dec.checked_convert();
194			assert_eq!(out, Some(f32::MAX));
195		}
196
197		#[test]
198		fn checked_convert_neg_f32_max_exact_literal_roundtrips() {
199			let dec = Decimal::from_str("-3.4028234663852886e38").unwrap();
200			let out: Option<f32> = dec.checked_convert();
201			assert_eq!(out, Some(f32::MIN));
202		}
203
204		#[test]
205		fn checked_convert_rounds_value_just_above_f32_max_to_max() {
206			// This is the shortest decimal that prints f32::MAX, and as an exact value it is
207			// slightly above it. Conversion accepts anything that rounds into range and
208			// rejects only true overflow, so printing f32::MAX and reading it back works.
209			let dec = Decimal::from_str("3.4028235e38").unwrap();
210			let out: Option<f32> = dec.checked_convert();
211			assert_eq!(out, Some(f32::MAX));
212		}
213
214		#[test]
215		fn checked_convert_rejects_value_above_f32_max() {
216			let dec = Decimal::from_str("1e40").unwrap();
217			let out: Option<f32> = dec.checked_convert();
218			assert_eq!(out, None);
219		}
220
221		#[test]
222		fn saturating_convert_above_f32_max_returns_max() {
223			let dec = Decimal::from_str("1e40").unwrap();
224			let out: f32 = dec.saturating_convert();
225			assert_eq!(out, f32::MAX);
226		}
227
228		#[test]
229		fn saturating_convert_below_neg_f32_max_returns_min() {
230			let dec = Decimal::from_str("-1e40").unwrap();
231			let out: f32 = dec.saturating_convert();
232			assert_eq!(out, f32::MIN);
233		}
234
235		#[test]
236		fn checked_convert_f32_min_positive_roundtrips() {
237			// Subnormal boundary - must not flush to zero or fail.
238			let dec = Decimal::from_str("1.17549435e-38").unwrap();
239			let out: Option<f32> = dec.checked_convert();
240			assert_eq!(out, Some(f32::MIN_POSITIVE));
241		}
242	}
243
244	mod f64 {
245		use std::str::FromStr;
246
247		use super::*;
248		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
249
250		#[test]
251		fn test_checked_convert() {
252			let x = Decimal::from(42i64);
253			let y: Option<f64> = x.checked_convert();
254			assert_eq!(y, Some(42.0f64));
255		}
256
257		#[test]
258		fn test_saturating_convert() {
259			let x = Decimal::from(-1000i64);
260			let y: f64 = x.saturating_convert();
261			assert_eq!(y, -1000.0f64);
262		}
263
264		#[test]
265		fn checked_convert_widest_decimal_literal_roundtrips() {
266			// The conversion goes through the exact text, so 76 nines must land on the f64 that literal
267			// parses to.
268			let dec = Decimal::from_str(&"9".repeat(76)).unwrap();
269			let out: Option<f64> = dec.checked_convert();
270			assert_eq!(out, Some(1e76));
271		}
272
273		#[test]
274		fn checked_convert_neg_widest_decimal_literal_roundtrips() {
275			let dec = Decimal::from_str(&format!("-{}", "9".repeat(76))).unwrap();
276			let out: Option<f64> = dec.checked_convert();
277			assert_eq!(out, Some(-1e76));
278		}
279
280		#[test]
281		fn saturating_convert_of_the_widest_decimals_stays_finite() {
282			// Every decimal is below 1e76, so saturation to f64::MAX must never be reached.
283			let max = Decimal::from_str(&"9".repeat(76)).unwrap();
284			let out: f64 = max.clone().saturating_convert();
285			assert_eq!(out, 1e76);
286			let out: f64 = max.negate().saturating_convert();
287			assert_eq!(out, -1e76);
288		}
289
290		#[test]
291		fn checked_convert_smallest_positive_decimal_roundtrips() {
292			// The smallest decimal step must not flush to zero or fail.
293			let dec = Decimal::from_str(&format!("0.{}1", "0".repeat(75))).unwrap();
294			let out: Option<f64> = dec.checked_convert();
295			assert_eq!(out, Some(1e-76));
296		}
297
298		#[test]
299		fn checked_convert_rounds_at_seventeen_digits_like_the_literal() {
300			// Double rounding through a shorter form would disagree with parsing the same literal.
301			let text = "0.12345678901234567890123456789";
302			let dec = Decimal::from_str(text).unwrap();
303			let out: Option<f64> = dec.checked_convert();
304			assert_eq!(out, Some(text.parse::<f64>().unwrap()));
305		}
306	}
307
308	mod self_conversion {
309		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
310
311		#[test]
312		fn test_checked_convert() {
313			let x = Decimal::from(42i64);
314			let y: Option<Decimal> = x.clone().checked_convert();
315			assert_eq!(y, Some(x));
316		}
317
318		#[test]
319		fn test_saturating_convert() {
320			let x = Decimal::from(-100i64);
321			let y: Decimal = x.clone().saturating_convert();
322			assert_eq!(y, x);
323		}
324
325		#[test]
326		fn test_wrapping_convert() {
327			let x = Decimal::from(999i64);
328			let y: Decimal = x.clone().wrapping_convert();
329			assert_eq!(y, x);
330		}
331	}
332
333	mod range {
334		use std::str::FromStr;
335
336		use crate::value::{decimal::Decimal, number::safe::convert::SafeConvert};
337
338		#[test]
339		fn integer_targets_truncate_toward_zero() {
340			// Flooring would turn -1.9 into -2 where every integer conversion truncates.
341			let dec = Decimal::from_str("-1.9").unwrap();
342			assert_eq!(SafeConvert::<i8>::checked_convert(dec.clone()), Some(-1));
343		}
344
345		#[test]
346		fn wide_decimals_refuse_narrow_targets_and_clamp_with_their_sign() {
347			// A 76 digit decimal must never truncate to its low bits.
348			let max = Decimal::from_str(&"9".repeat(76)).unwrap();
349			assert_eq!(SafeConvert::<i128>::checked_convert(max.clone()), None);
350			assert_eq!(SafeConvert::<u128>::checked_convert(max.clone()), None);
351			assert_eq!(SafeConvert::<u128>::saturating_convert(max.clone()), u128::MAX);
352			assert_eq!(SafeConvert::<i64>::saturating_convert(max.negate()), i64::MIN);
353			let u128_max = Decimal::from_str(&u128::MAX.to_string()).unwrap();
354			assert_eq!(SafeConvert::<u128>::checked_convert(u128_max), Some(u128::MAX));
355		}
356	}
357}