Skip to main content

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}