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}