Skip to main content

decimal_scaled/
num_traits.rs

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