decimal_scaled/i64f64_compat.rs
1//! Compatibility layer for migrating call sites from `fixed::types::I64F64`
2//! to [`I128`].
3//!
4//! # Purpose
5//!
6//! Code bases that used `I64F64` typically construct and read back values
7//! through `from_num` / `to_num`:
8//!
9//! ```ignore
10//! let d = Decimal::from_num(some_i32);
11//! let f: f32 = d.to_num();
12//! ```
13//!
14//! When `Decimal` is aliased to [`I128`], those call sites can continue to
15//! compile without renaming every occurrence. New code should prefer the
16//! idiomatic [`From<T>`] / [`num_traits::FromPrimitive`] /
17//! [`num_traits::ToPrimitive`] surface instead.
18//!
19//! # Saturation policy
20//!
21//! Conversions never panic. Out-of-range inputs are saturated:
22//!
23//! - `NaN` maps to [`I128::ZERO`].
24//! - `+Infinity` maps to [`I128::MAX`].
25//! - `-Infinity` maps to [`I128::MIN`].
26//! - Finite values outside the representable range saturate to `MAX` or `MIN`
27//! by sign.
28//!
29//! `I128`'s storage range (~+/-1.7e26 model units at `SCALE = 12`) is wider
30//! than `I64F64`'s (~+/-9.2e18), so values that would have panicked in
31//! `I64F64::from_num` may succeed here. That is correct behaviour.
32//!
33//! # Examples
34//!
35//! ```
36//! use decimal_scaled::I128s12;
37//!
38//! // Constructor mirrors `I64F64::from_num`:
39//! let d = I128s12::from_num(42_i32);
40//! assert_eq!(d, I128s12::from(42_i32));
41//!
42//! // Reader mirrors `I64F64::to_num`:
43//! let f: f32 = d.to_num();
44//! assert_eq!(f, 42.0_f32);
45//!
46//! // Saturation: `f64::INFINITY` -> `I128::MAX` (not panic):
47//! assert_eq!(I128s12::from_num(f64::INFINITY), I128s12::MAX);
48//! ```
49
50use num_traits::{Bounded, NumCast, ToPrimitive};
51
52use crate::core_type::I128;
53
54impl<const SCALE: u32> I128<SCALE> {
55 /// Constructs a `I128<SCALE>` from any `T: ToPrimitive`.
56 ///
57 /// This is a compatibility alias for the idiomatic [`From<T>`] /
58 /// [`num_traits::FromPrimitive`] surface. Routes through
59 /// [`num_traits::NumCast::from`], which dispatches to the
60 /// [`num_traits::FromPrimitive`] impl on `I128`.
61 ///
62 /// # Precision
63 ///
64 /// Lossy: involves f32 or f64 at some point when `T` is a float type;
65 /// result may lose precision. For integer `T`, the conversion is Strict:
66 /// all arithmetic is integer-only; result is bit-exact.
67 ///
68 /// # Saturation policy
69 ///
70 /// - Float `NaN` maps to [`I128::ZERO`].
71 /// - `+Infinity` maps to [`I128::MAX`].
72 /// - `-Infinity` maps to [`I128::MIN`].
73 /// - Finite out-of-range positive maps to [`I128::MAX`].
74 /// - Finite out-of-range negative maps to [`I128::MIN`].
75 /// - Never panics.
76 ///
77 /// # Examples
78 ///
79 /// ```
80 /// use decimal_scaled::I128s12;
81 ///
82 /// assert_eq!(I128s12::from_num(42_i32), I128s12::from(42_i32));
83 /// assert_eq!(I128s12::from_num(f64::INFINITY), I128s12::MAX);
84 /// assert_eq!(I128s12::from_num(f64::NAN), I128s12::ZERO);
85 /// ```
86 pub fn from_num<T: ToPrimitive>(value: T) -> Self {
87 // Determine sign and NaN status before consuming `value` through
88 // NumCast. Integer signals (to_i128 / to_u128) are checked first so
89 // that integer-typed inputs never route through f64 -- I128's storage
90 // is wider than f64's mantissa and f64 sign-detection would lose
91 // precision at large integer values.
92 //
93 // Three cases cover all ToPrimitive implementors:
94 // - to_i128 returns Some(i): all signed primitives and unsigned
95 // primitives that fit in i128 (u8 through u64).
96 // - to_i128 returns None but to_u128 returns Some: unsigned values
97 // exceeding i128::MAX; sign is non-negative.
98 // - Both return None: input is f32 or f64; inspect to_f64 for NaN
99 // and sign classification.
100 let int_signal = value.to_i128();
101 let uint_signal = value.to_u128();
102 let float_signal = if int_signal.is_none() && uint_signal.is_none() {
103 value.to_f64()
104 } else {
105 None
106 };
107 // Early exit: NaN maps to ZERO. Only reachable on the float path.
108 if let Some(f) = float_signal {
109 if f.is_nan() {
110 return Self::ZERO;
111 }
112 }
113 if let Some(d) = <Self as NumCast>::from(value) {
114 return d;
115 }
116 // NumCast returned None -- saturate by sign of the original input.
117 // Prefer integer signals (lossless); fall back to float only for
118 // genuinely float-typed inputs.
119 if let Some(i) = int_signal {
120 return if i < 0 { Self::MIN } else { Self::MAX };
121 }
122 if uint_signal.is_some() {
123 // Unsigned-only representation cannot be negative.
124 return Self::MAX;
125 }
126 match float_signal {
127 Some(f) if f.is_sign_negative() => Self::MIN,
128 Some(_) => Self::MAX,
129 // No representation at all (exotic ToPrimitive impl). Default
130 // to ZERO rather than picking a sign.
131 None => Self::ZERO,
132 }
133 }
134
135 /// Converts `self` to any `T: NumCast + Bounded`.
136 ///
137 /// This is a compatibility alias for the idiomatic
138 /// [`num_traits::ToPrimitive`] / `to_X_lossy` surface. Routes through
139 /// [`num_traits::NumCast::from`], which dispatches to the
140 /// [`num_traits::ToPrimitive`] impl on `I128`.
141 ///
142 /// # Precision
143 ///
144 /// Lossy: involves f32 or f64 at some point when `T` is a float type;
145 /// result may lose precision. For integer `T`, the conversion is Strict:
146 /// all arithmetic is integer-only; result is bit-exact.
147 ///
148 /// # Saturation policy
149 ///
150 /// - In-range conversions return the cast value unchanged.
151 /// - Positive out-of-range maps to [`Bounded::max_value`] of `T`.
152 /// - Negative out-of-range maps to [`Bounded::min_value`] of `T`.
153 /// - Never panics.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// use decimal_scaled::I128s12;
159 ///
160 /// assert_eq!(I128s12::from(42_i32).to_num::<i32>(), 42_i32);
161 /// assert_eq!(I128s12::MAX.to_num::<i32>(), i32::MAX);
162 /// assert_eq!(I128s12::MIN.to_num::<i32>(), i32::MIN);
163 /// ```
164 pub fn to_num<T: NumCast + Bounded>(self) -> T {
165 match T::from(self) {
166 Some(t) => t,
167 None => {
168 // Saturate to T::MAX or T::MIN based on the sign of self.
169 // Read sign directly from the raw i128 field to avoid a
170 // Signed-trait dispatch round-trip.
171 if self.0 >= 0 {
172 T::max_value()
173 } else {
174 T::min_value()
175 }
176 }
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use crate::core_type::{I128, I128s12};
184
185 // from_num -- thin delegate over NumCast / FromPrimitive
186
187 /// `from_num(i32)` matches the idiomatic `From<i32>` impl.
188 #[test]
189 fn from_num_i32_round_trip() {
190 let d = I128s12::from_num(42_i32);
191 assert_eq!(d, I128s12::from(42_i32));
192 assert_eq!(d.to_num::<i32>(), 42_i32);
193 }
194
195 /// `from_num(i64)` matches `From<i64>`.
196 #[test]
197 fn from_num_i64_matches_from() {
198 let d = I128s12::from_num(1_000_i64);
199 assert_eq!(d, I128s12::from(1_000_i64));
200 }
201
202 /// `from_num(f64)` for an in-range value matches `from_f64_lossy`.
203 #[test]
204 fn from_num_f64_within_range() {
205 let d = I128s12::from_num(1.5_f64);
206 assert_eq!(d, I128s12::from_f64_lossy(1.5_f64));
207 }
208
209 /// `from_num(f64::INFINITY)` saturates to `MAX`.
210 #[test]
211 fn from_num_f64_inf_saturates_max() {
212 assert_eq!(I128s12::from_num(f64::INFINITY), I128s12::MAX);
213 }
214
215 /// `from_num(f64::NEG_INFINITY)` saturates to `MIN`.
216 #[test]
217 fn from_num_f64_neg_inf_saturates_min() {
218 assert_eq!(I128s12::from_num(f64::NEG_INFINITY), I128s12::MIN);
219 }
220
221 /// `from_num(f64::NAN)` returns `ZERO` (deterministic NaN policy).
222 #[test]
223 fn from_num_f64_nan_is_zero() {
224 assert_eq!(I128s12::from_num(f64::NAN), I128s12::ZERO);
225 }
226
227 /// Finite out-of-range f64 saturates by sign.
228 #[test]
229 fn from_num_f64_finite_oor_saturates() {
230 // 1e30 * 10^12 = 1e42 > i128::MAX ~1.7e38; positive -> MAX.
231 assert_eq!(I128s12::from_num(1e30_f64), I128s12::MAX);
232 // negative -> MIN.
233 assert_eq!(I128s12::from_num(-1e30_f64), I128s12::MIN);
234 }
235
236 /// `from_num(f32::INFINITY)` saturates (validates f32 path).
237 #[test]
238 fn from_num_f32_inf_saturates() {
239 assert_eq!(I128s12::from_num(f32::INFINITY), I128s12::MAX);
240 assert_eq!(I128s12::from_num(f32::NEG_INFINITY), I128s12::MIN);
241 assert_eq!(I128s12::from_num(f32::NAN), I128s12::ZERO);
242 }
243
244 // from_num -- wider range than I64F64
245
246 /// At `SCALE = 12`, `I128`'s integer range is ~+/-1.7e14 model units.
247 /// `I64F64`'s integer range is ~+/-9.2e9. A value of 1e10 is within
248 /// I128's range but exceeds I64F64's representable bound -- this call
249 /// must succeed without saturation.
250 #[test]
251 fn from_num_does_not_panic_on_wider_range_than_i64f64() {
252 let v: i64 = 10_000_000_000_i64;
253 let d = I128s12::from_num(v);
254 // Round-trip: to_int_lossy must return the original value.
255 assert_eq!(d.to_int_lossy(), v);
256 }
257
258 // to_num -- thin delegate over NumCast / ToPrimitive
259
260 /// `I128::ONE.to_num::<f64>() == 1.0`.
261 #[test]
262 fn to_num_f64_lossy() {
263 assert_eq!(I128s12::ONE.to_num::<f64>(), 1.0_f64);
264 assert_eq!((-I128s12::ONE).to_num::<f64>(), -1.0_f64);
265 assert_eq!(I128s12::ZERO.to_num::<f64>(), 0.0_f64);
266 }
267
268 /// `I128::ONE.to_num::<f32>() == 1.0`.
269 #[test]
270 fn to_num_f32_lossy() {
271 assert_eq!(I128s12::ONE.to_num::<f32>(), 1.0_f32);
272 assert_eq!((-I128s12::ONE).to_num::<f32>(), -1.0_f32);
273 }
274
275 /// `I128::from(42_i32).to_num::<i32>() == 42`.
276 #[test]
277 fn to_num_i32_in_range() {
278 let d = I128s12::from(42_i32);
279 assert_eq!(d.to_num::<i32>(), 42_i32);
280
281 let neg = I128s12::from(-42_i32);
282 assert_eq!(neg.to_num::<i32>(), -42_i32);
283 }
284
285 /// `I128::MAX.to_num::<i32>() == i32::MAX` (saturating positive).
286 #[test]
287 fn to_num_i32_out_of_range_saturates_max() {
288 assert_eq!(I128s12::MAX.to_num::<i32>(), i32::MAX);
289 }
290
291 /// `I128::MIN.to_num::<i32>() == i32::MIN` (saturating negative).
292 #[test]
293 fn to_num_i32_out_of_range_saturates_min() {
294 assert_eq!(I128s12::MIN.to_num::<i32>(), i32::MIN);
295 }
296
297 /// `to_num::<i64>()` saturates at i64 bounds.
298 #[test]
299 fn to_num_i64_saturates() {
300 assert_eq!(I128s12::MAX.to_num::<i64>(), i64::MAX);
301 assert_eq!(I128s12::MIN.to_num::<i64>(), i64::MIN);
302 assert_eq!(I128s12::from(42_i64).to_num::<i64>(), 42_i64);
303 }
304
305 /// `to_num::<u32>()` returns 0 for negative values (saturates to
306 /// u32::MIN = 0).
307 #[test]
308 fn to_num_u32_negative_saturates_to_zero() {
309 // u32::MIN is 0, so negative I128 values saturate to 0.
310 assert_eq!((-I128s12::ONE).to_num::<u32>(), u32::MIN);
311 assert_eq!(I128s12::MIN.to_num::<u32>(), u32::MIN);
312 // Positive out-of-range -> u32::MAX.
313 assert_eq!(I128s12::MAX.to_num::<u32>(), u32::MAX);
314 }
315
316 /// Round-trip via from_num / to_num for representative i32 values.
317 #[test]
318 fn from_num_to_num_round_trip_i32() {
319 for v in [0_i32, 1, -1, 42, -42, 1_000_000, -1_000_000] {
320 let d = I128s12::from_num(v);
321 assert_eq!(d.to_num::<i32>(), v);
322 }
323 }
324
325 // Cross-scale exercise -- non-default SCALE
326
327 /// Compat surface works at non-default SCALE.
328 #[test]
329 fn from_num_to_num_at_scale_6() {
330 type D6 = I128<6>;
331 let d = D6::from_num(7_i32);
332 assert_eq!(d, D6::from(7_i32));
333 assert_eq!(d.to_num::<i32>(), 7_i32);
334 }
335
336 // Integer-typed inputs must not route through f64 for sign detection.
337
338 /// `from_num(i128::MAX)` saturates to `I128::MAX` via the i128 sign
339 /// signal, not through a f64 round-trip. `i128::MAX * 10^12` overflows
340 /// i128 storage, so NumCast::from returns None; the saturation fallback
341 /// reads sign directly from i128.
342 #[test]
343 fn from_num_i128_max_saturates_via_int_signal() {
344 assert_eq!(I128s12::from_num(i128::MAX), I128s12::MAX);
345 }
346
347 /// `from_num(i128::MIN)` saturates to `I128::MIN` via the i128 sign signal.
348 #[test]
349 fn from_num_i128_min_saturates_via_int_signal() {
350 assert_eq!(I128s12::from_num(i128::MIN), I128s12::MIN);
351 }
352
353 /// `from_num(u128::MAX)` saturates to `I128::MAX` via the u128 sign
354 /// signal. `to_i128` returns None for u128 > i128::MAX, so the u128
355 /// fallback path is exercised here.
356 #[test]
357 fn from_num_u128_max_saturates_via_uint_signal() {
358 assert_eq!(I128s12::from_num(u128::MAX), I128s12::MAX);
359 }
360
361 /// `from_num(u64::MAX)` succeeds without saturation -- u64::MAX fits
362 /// in I128's storage at SCALE = 12.
363 #[test]
364 fn from_num_u64_max_succeeds_without_saturation() {
365 let d = I128s12::from_num(u64::MAX);
366 assert_eq!(d, I128s12::from(u64::MAX));
367 }
368}