Skip to main content

decimal_scaled/
conversions.rs

1//! Conversions between [`I128`] and primitive numeric types.
2//!
3//! # Naming convention
4//!
5//! - `from_int(i64)` / `from_i32(i32)` -- exact named constructors; thin
6//!   wrappers around the `From<iN>` impls.
7//! - `from_f64_lossy(f64)` -- explicitly lossy; multiplies the float by
8//!   `10^SCALE`, truncates to `i128`, and saturates on out-of-range or
9//!   non-finite inputs.
10//! - `to_int_lossy() -> i64` -- truncates the fractional part toward zero;
11//!   saturates to `i64::MAX` / `i64::MIN` when the integer magnitude exceeds
12//!   `i64`'s range.
13//! - `to_f64_lossy() -> f64` -- divides the raw `i128` storage by the
14//!   multiplier in `f64`; f64's 53-bit mantissa cannot represent every `I128`
15//!   value exactly.
16//! - `to_f32_lossy() -> f32` -- converts via `f64` first, then narrows to
17//!   `f32`; lossier than `to_f64_lossy`.
18//!
19//! # Lossless `From` impls
20//!
21//! Eight `From` impls cover integer types whose values fit losslessly into
22//! `i128` after scaling by `10^SCALE` at practical scales: `i8`, `i16`,
23//! `i32`, `i64`, `u8`, `u16`, `u32`, `u64`. Each multiplies the input by
24//! `multiplier()` (= `10^SCALE`).
25//!
26//! At pathological scales (for example `SCALE >= 20` for `u64`) the
27//! multiplication can overflow `i128`; the result follows the standard
28//! Rust panic-in-debug / wrap-in-release behaviour.
29//!
30//! # Fallible `TryFrom` impls
31//!
32//! Four `TryFrom` impls cover types where lossless conversion is not always
33//! possible: `i128`, `u128`, `f32`, `f64`. They return [`DecimalConvertError`]
34//! with two variants:
35//!
36//! - `Overflow` -- the magnitude exceeds `I128::MAX` / `I128::MIN` after
37//!   scaling by `10^SCALE`.
38//! - `NotFinite` -- the float input is `NaN` or an infinity.
39//!
40//! # Saturation-versus-error policy
41//!
42//! - Lossy methods saturate: `from_f64_lossy(f64::INFINITY)` returns
43//!   `I128::MAX`; `from_f64_lossy(f64::NAN)` returns `I128::ZERO`.
44//! - `TryFrom` variants return `Err`: `Err(NotFinite)` for `NaN`/`inf`,
45//!   `Err(Overflow)` for finite out-of-range inputs.
46
47use crate::core_type::I128;
48
49// ──────────────────────────────────────────────────────────────────────
50// Error type
51// ──────────────────────────────────────────────────────────────────────
52
53/// Error returned by the fallible [`TryFrom`] impls on [`I128`].
54///
55/// Covers the two distinct failure modes:
56/// - [`DecimalConvertError::Overflow`] -- the input, after scaling by
57///   `10^SCALE`, exceeds the range `[I128::MIN, I128::MAX]`.
58/// - [`DecimalConvertError::NotFinite`] -- the float input is `NaN`,
59///   `+inf`, or `-inf`.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum DecimalConvertError {
62    /// Input magnitude is outside `[I128::MIN, I128::MAX]` after scaling.
63    Overflow,
64    /// Input is `NaN`, `+inf`, or `-inf` (only reachable from the
65    /// `TryFrom<f32>` / `TryFrom<f64>` impls).
66    NotFinite,
67}
68
69impl core::fmt::Display for DecimalConvertError {
70    /// Formats the error as a short human-readable message.
71    ///
72    /// # Precision
73    ///
74    /// Strict: all arithmetic is integer-only; result is bit-exact.
75    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76        match self {
77            Self::Overflow => f.write_str("decimal conversion overflow"),
78            Self::NotFinite => f.write_str("decimal conversion from non-finite float"),
79        }
80    }
81}
82
83#[cfg(feature = "std")]
84impl std::error::Error for DecimalConvertError {}
85
86// ──────────────────────────────────────────────────────────────────────
87// Lossless From<integer> impls
88// ──────────────────────────────────────────────────────────────────────
89//
90// Each impl multiplies the input by `multiplier()` (= 10^SCALE).
91// At SCALE = 12, the worst-case `u64` product is ~1.8e31, well under
92// i128::MAX ~1.7e38, so all eight impls are infallible in practice.
93
94impl<const SCALE: u32> From<i8> for I128<SCALE> {
95    /// Converts `value` by scaling it to `value * 10^SCALE`.
96    ///
97    /// Lossless for all `SCALE < 36` (since `i8::MAX * 10^36 < i128::MAX`).
98    ///
99    /// # Precision
100    ///
101    /// Strict: all arithmetic is integer-only; result is bit-exact.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use decimal_scaled::I128s12;
107    ///
108    /// assert_eq!(I128s12::from(1_i8).to_bits(), 1_000_000_000_000);
109    /// assert_eq!(I128s12::from(-1_i8).to_bits(), -1_000_000_000_000);
110    /// ```
111    #[inline]
112    fn from(value: i8) -> Self {
113        Self((value as i128) * Self::multiplier())
114    }
115}
116
117impl<const SCALE: u32> From<i16> for I128<SCALE> {
118    /// Converts `value` by scaling it to `value * 10^SCALE`.
119    ///
120    /// Lossless for all `SCALE < 33`.
121    ///
122    /// # Precision
123    ///
124    /// Strict: all arithmetic is integer-only; result is bit-exact.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use decimal_scaled::I128s12;
130    ///
131    /// assert_eq!(I128s12::from(1_i16).to_bits(), 1_000_000_000_000);
132    /// ```
133    #[inline]
134    fn from(value: i16) -> Self {
135        Self((value as i128) * Self::multiplier())
136    }
137}
138
139impl<const SCALE: u32> From<i32> for I128<SCALE> {
140    /// Converts `value` by scaling it to `value * 10^SCALE`.
141    ///
142    /// Lossless for all `SCALE < 28`.
143    ///
144    /// # Precision
145    ///
146    /// Strict: all arithmetic is integer-only; result is bit-exact.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use decimal_scaled::I128s12;
152    ///
153    /// assert_eq!(I128s12::from(1_i32).to_bits(), 1_000_000_000_000);
154    /// ```
155    #[inline]
156    fn from(value: i32) -> Self {
157        Self((value as i128) * Self::multiplier())
158    }
159}
160
161impl<const SCALE: u32> From<i64> for I128<SCALE> {
162    /// Converts `value` by scaling it to `value * 10^SCALE`.
163    ///
164    /// Lossless for all `SCALE < 19`. At `SCALE = 12` all `i64` values
165    /// fit with roughly six orders of magnitude of headroom before
166    /// `i128::MAX`.
167    ///
168    /// # Precision
169    ///
170    /// Strict: all arithmetic is integer-only; result is bit-exact.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use decimal_scaled::I128s12;
176    ///
177    /// assert_eq!(I128s12::from(1_i64).to_bits(), 1_000_000_000_000);
178    /// assert_eq!(I128s12::from(-1_i64).to_bits(), -1_000_000_000_000);
179    /// ```
180    #[inline]
181    fn from(value: i64) -> Self {
182        Self((value as i128) * Self::multiplier())
183    }
184}
185
186impl<const SCALE: u32> From<u8> for I128<SCALE> {
187    /// Converts `value` by scaling it to `value * 10^SCALE`.
188    ///
189    /// Lossless for all `SCALE < 36`.
190    ///
191    /// # Precision
192    ///
193    /// Strict: all arithmetic is integer-only; result is bit-exact.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use decimal_scaled::I128s12;
199    ///
200    /// assert_eq!(I128s12::from(1_u8).to_bits(), 1_000_000_000_000);
201    /// ```
202    #[inline]
203    fn from(value: u8) -> Self {
204        Self((value as i128) * Self::multiplier())
205    }
206}
207
208impl<const SCALE: u32> From<u16> for I128<SCALE> {
209    /// Converts `value` by scaling it to `value * 10^SCALE`.
210    ///
211    /// Lossless for all `SCALE < 33`.
212    ///
213    /// # Precision
214    ///
215    /// Strict: all arithmetic is integer-only; result is bit-exact.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use decimal_scaled::I128s12;
221    ///
222    /// assert_eq!(I128s12::from(1_u16).to_bits(), 1_000_000_000_000);
223    /// ```
224    #[inline]
225    fn from(value: u16) -> Self {
226        Self((value as i128) * Self::multiplier())
227    }
228}
229
230impl<const SCALE: u32> From<u32> for I128<SCALE> {
231    /// Converts `value` by scaling it to `value * 10^SCALE`.
232    ///
233    /// Lossless for all `SCALE < 28`.
234    ///
235    /// # Precision
236    ///
237    /// Strict: all arithmetic is integer-only; result is bit-exact.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use decimal_scaled::I128s12;
243    ///
244    /// assert_eq!(I128s12::from(1_u32).to_bits(), 1_000_000_000_000);
245    /// ```
246    #[inline]
247    fn from(value: u32) -> Self {
248        Self((value as i128) * Self::multiplier())
249    }
250}
251
252impl<const SCALE: u32> From<u64> for I128<SCALE> {
253    /// Converts `value` by scaling it to `value * 10^SCALE`.
254    ///
255    /// Lossless for all `SCALE < 19`. At `SCALE = 12` the worst-case
256    /// product is `u64::MAX * 10^12` (~1.8e31), well under `i128::MAX`
257    /// (~1.7e38).
258    ///
259    /// # Precision
260    ///
261    /// Strict: all arithmetic is integer-only; result is bit-exact.
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use decimal_scaled::I128s12;
267    ///
268    /// assert_eq!(I128s12::from(1_u64).to_bits(), 1_000_000_000_000);
269    /// ```
270    #[inline]
271    fn from(value: u64) -> Self {
272        Self((value as i128) * Self::multiplier())
273    }
274}
275
276// ──────────────────────────────────────────────────────────────────────
277// Fallible TryFrom impls
278// ──────────────────────────────────────────────────────────────────────
279//
280// `TryFrom<i128>` and `TryFrom<u128>` use `checked_mul` to detect
281// overflow when scaling by `multiplier()`. `TryFrom<f32>` delegates to
282// `TryFrom<f64>`. `TryFrom<f64>` multiplies in f64, compares against
283// the f64 representations of i128::MIN / i128::MAX, and casts to i128.
284
285impl<const SCALE: u32> TryFrom<i128> for I128<SCALE> {
286    type Error = DecimalConvertError;
287
288    /// Scales `value` by `10^SCALE` using checked multiplication.
289    ///
290    /// Returns `Err(Overflow)` if the product exceeds `i128::MAX` or
291    /// falls below `i128::MIN`.
292    ///
293    /// # Precision
294    ///
295    /// Strict: all arithmetic is integer-only; result is bit-exact.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use decimal_scaled::{I128s12, DecimalConvertError};
301    ///
302    /// let v: I128s12 = 1_i128.try_into().unwrap();
303    /// assert_eq!(v, I128s12::ONE);
304    ///
305    /// let overflow: Result<I128s12, _> = i128::MAX.try_into();
306    /// assert_eq!(overflow, Err(DecimalConvertError::Overflow));
307    /// ```
308    #[inline]
309    fn try_from(value: i128) -> Result<Self, Self::Error> {
310        value
311            .checked_mul(Self::multiplier())
312            .map(Self)
313            .ok_or(DecimalConvertError::Overflow)
314    }
315}
316
317impl<const SCALE: u32> TryFrom<u128> for I128<SCALE> {
318    type Error = DecimalConvertError;
319
320    /// Converts `value` to `i128`, then scales by `10^SCALE`.
321    ///
322    /// Returns `Err(Overflow)` if `value > i128::MAX` or if the scaled
323    /// product overflows `i128`.
324    ///
325    /// # Precision
326    ///
327    /// Strict: all arithmetic is integer-only; result is bit-exact.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use decimal_scaled::{I128s12, DecimalConvertError};
333    ///
334    /// let v: I128s12 = 42_u128.try_into().unwrap();
335    /// assert_eq!(v.to_bits(), 42_000_000_000_000);
336    ///
337    /// let overflow: Result<I128s12, _> = u128::MAX.try_into();
338    /// assert_eq!(overflow, Err(DecimalConvertError::Overflow));
339    /// ```
340    #[inline]
341    fn try_from(value: u128) -> Result<Self, Self::Error> {
342        // Step 1: u128 -> i128 (overflows when value > i128::MAX).
343        let as_i128: i128 = i128::try_from(value).map_err(|_| DecimalConvertError::Overflow)?;
344        // Step 2: scale using the existing checked path.
345        as_i128
346            .checked_mul(Self::multiplier())
347            .map(Self)
348            .ok_or(DecimalConvertError::Overflow)
349    }
350}
351
352impl<const SCALE: u32> TryFrom<f32> for I128<SCALE> {
353    type Error = DecimalConvertError;
354
355    /// Widens `value` to `f64` and delegates to [`TryFrom<f64>`].
356    ///
357    /// Returns `Err(NotFinite)` for `NaN` and the infinities; `Err(Overflow)`
358    /// for finite values whose magnitude exceeds `I128::MAX` after scaling.
359    ///
360    /// # Precision
361    ///
362    /// Lossy: involves f32 or f64 at some point; result may lose precision.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// use decimal_scaled::{I128s12, DecimalConvertError};
368    ///
369    /// let v: I128s12 = 1.0_f32.try_into().unwrap();
370    /// assert_eq!(v, I128s12::ONE);
371    ///
372    /// let nan: Result<I128s12, _> = f32::NAN.try_into();
373    /// assert_eq!(nan, Err(DecimalConvertError::NotFinite));
374    /// ```
375    #[inline]
376    fn try_from(value: f32) -> Result<Self, Self::Error> {
377        Self::try_from(value as f64)
378    }
379}
380
381impl<const SCALE: u32> TryFrom<f64> for I128<SCALE> {
382    type Error = DecimalConvertError;
383
384    /// Multiplies `value` by `10^SCALE` in `f64` and truncates to `i128`.
385    ///
386    /// Returns `Err(NotFinite)` for `NaN`/`inf`, and `Err(Overflow)` for
387    /// finite inputs whose scaled value falls outside `[i128::MIN, i128::MAX)`.
388    ///
389    /// Note: `i128::MAX as f64` rounds up to `2^127` because `i128::MAX`
390    /// (`2^127 - 1`) is not exactly representable in f64. The range check
391    /// uses a strict `<` on the upper bound to reject `2^127` itself.
392    ///
393    /// # Precision
394    ///
395    /// Lossy: involves f32 or f64 at some point; result may lose precision.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use decimal_scaled::{I128s12, DecimalConvertError};
401    ///
402    /// let v: I128s12 = 1.0_f64.try_into().unwrap();
403    /// assert_eq!(v, I128s12::ONE);
404    ///
405    /// let nan: Result<I128s12, _> = f64::NAN.try_into();
406    /// assert_eq!(nan, Err(DecimalConvertError::NotFinite));
407    ///
408    /// let overflow: Result<I128s12, _> = 1e30_f64.try_into();
409    /// assert_eq!(overflow, Err(DecimalConvertError::Overflow));
410    /// ```
411    #[inline]
412    fn try_from(value: f64) -> Result<Self, Self::Error> {
413        if !value.is_finite() {
414            return Err(DecimalConvertError::NotFinite);
415        }
416        let scaled = value * (Self::multiplier() as f64);
417        // i128::MAX as f64 rounds up to 2^127; use strict `<` so 2^127 is rejected.
418        const I128_MAX_F64: f64 = i128::MAX as f64;
419        const I128_MIN_F64: f64 = i128::MIN as f64;
420        if !(I128_MIN_F64..I128_MAX_F64).contains(&scaled) {
421            return Err(DecimalConvertError::Overflow);
422        }
423        Ok(Self(scaled as i128))
424    }
425}
426
427// ──────────────────────────────────────────────────────────────────────
428// I128 inherent conversion methods
429// ──────────────────────────────────────────────────────────────────────
430
431impl<const SCALE: u32> I128<SCALE> {
432    /// Constructs a `I128` from an `i64` integer value.
433    ///
434    /// Named constructor that wraps `From<i64>`. Prefer this over
435    /// `I128::from(value)` when the intent of converting from an integer
436    /// should be explicit at the call site.
437    ///
438    /// At `SCALE = 12` every `i64` value fits with roughly six orders of
439    /// magnitude of headroom before `i128::MAX`.
440    ///
441    /// # Precision
442    ///
443    /// Strict: all arithmetic is integer-only; result is bit-exact.
444    ///
445    /// # Examples
446    ///
447    /// ```
448    /// use decimal_scaled::I128s12;
449    ///
450    /// assert_eq!(I128s12::from_int(1), I128s12::ONE);
451    /// assert_eq!(I128s12::from_int(-42).to_bits(), -42_000_000_000_000_i128);
452    /// ```
453    #[inline]
454    pub fn from_int(value: i64) -> Self {
455        Self::from(value)
456    }
457
458    /// Constructs a `I128` from an `i32` integer value.
459    ///
460    /// Named constructor that wraps `From<i32>`. Lossless at any
461    /// practical `SCALE` (safe up to `SCALE < 28`).
462    ///
463    /// # Precision
464    ///
465    /// Strict: all arithmetic is integer-only; result is bit-exact.
466    ///
467    /// # Examples
468    ///
469    /// ```
470    /// use decimal_scaled::I128s12;
471    ///
472    /// assert_eq!(I128s12::from_i32(1), I128s12::ONE);
473    /// assert_eq!(I128s12::from_i32(0), I128s12::ZERO);
474    /// ```
475    #[inline]
476    pub fn from_i32(value: i32) -> Self {
477        Self::from(value)
478    }
479
480    /// Constructs a `I128` from an `f64`, saturating on non-finite or
481    /// out-of-range inputs.
482    ///
483    /// Multiplies `value` by `10^SCALE` and truncates to `i128`. Non-finite
484    /// and out-of-range inputs are handled as follows:
485    ///
486    /// - `NaN` returns `I128::ZERO` (deterministic, no panic).
487    /// - `+inf` or any finite value above the representable range returns `I128::MAX`.
488    /// - `-inf` or any finite value below the representable range returns `I128::MIN`.
489    ///
490    /// Use [`TryFrom<f64>`] when you want an error instead of saturation.
491    ///
492    /// # Precision
493    ///
494    /// Lossy: involves f32 or f64 at some point; result may lose precision.
495    ///
496    /// # Examples
497    ///
498    /// ```
499    /// use decimal_scaled::I128s12;
500    ///
501    /// assert_eq!(I128s12::from_f64_lossy(1.0), I128s12::ONE);
502    /// assert_eq!(I128s12::from_f64_lossy(f64::NAN), I128s12::ZERO);
503    /// assert_eq!(I128s12::from_f64_lossy(f64::INFINITY), I128s12::MAX);
504    /// assert_eq!(I128s12::from_f64_lossy(f64::NEG_INFINITY), I128s12::MIN);
505    /// ```
506    pub fn from_f64_lossy(value: f64) -> Self {
507        if value.is_nan() {
508            return Self::ZERO;
509        }
510        if value.is_infinite() {
511            return if value > 0.0 { Self::MAX } else { Self::MIN };
512        }
513        let scaled = value * (Self::multiplier() as f64);
514        const I128_MAX_F64: f64 = i128::MAX as f64;
515        const I128_MIN_F64: f64 = i128::MIN as f64;
516        if scaled >= I128_MAX_F64 {
517            return Self::MAX;
518        }
519        if scaled < I128_MIN_F64 {
520            return Self::MIN;
521        }
522        Self(scaled as i128)
523    }
524
525    /// Converts to `i64` by truncating the fractional part toward zero.
526    ///
527    /// The integer part is `self.0 / 10^SCALE`. If that value exceeds
528    /// `i64::MAX` or falls below `i64::MIN`, the result saturates to
529    /// `i64::MAX` or `i64::MIN` respectively. At `SCALE = 12` the saturation
530    /// threshold is approximately 9.2e18 (the `i64` limit), which is well
531    /// below the `I128` maximum of ~1.7e26.
532    ///
533    /// # Precision
534    ///
535    /// Lossy: involves f32 or f64 at some point; result may lose precision.
536    ///
537    /// # Examples
538    ///
539    /// ```
540    /// use decimal_scaled::I128s12;
541    ///
542    /// // Truncates toward zero.
543    /// assert_eq!(I128s12::from_bits(2_500_000_000_000).to_int_lossy(), 2);
544    /// assert_eq!(I128s12::from_bits(-2_500_000_000_000).to_int_lossy(), -2);
545    ///
546    /// // Saturates when the integer part exceeds i64 range.
547    /// assert_eq!(I128s12::MAX.to_int_lossy(), i64::MAX);
548    /// assert_eq!(I128s12::MIN.to_int_lossy(), i64::MIN);
549    /// ```
550    #[inline]
551    pub fn to_int_lossy(self) -> i64 {
552        let int_part: i128 = self.0 / Self::multiplier();
553        if int_part > i64::MAX as i128 {
554            i64::MAX
555        } else if int_part < i64::MIN as i128 {
556            i64::MIN
557        } else {
558            int_part as i64
559        }
560    }
561
562    /// Converts to `f64` by dividing the raw storage by `10^SCALE`.
563    ///
564    /// f64 has a 53-bit mantissa, so large or precision-dense `I128` values
565    /// will round. The division is performed as `(self.0 as f64) / multiplier`
566    /// to keep as much precision as f64 allows.
567    ///
568    /// # Precision
569    ///
570    /// Lossy: involves f32 or f64 at some point; result may lose precision.
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// use decimal_scaled::I128s12;
576    ///
577    /// assert_eq!(I128s12::ZERO.to_f64_lossy(), 0.0);
578    /// assert_eq!(I128s12::ONE.to_f64_lossy(), 1.0);
579    /// ```
580    #[inline]
581    pub fn to_f64_lossy(self) -> f64 {
582        (self.0 as f64) / (Self::multiplier() as f64)
583    }
584
585    /// Converts to `f32` via `f64`, then narrows to `f32`.
586    ///
587    /// f32 has only a 24-bit mantissa, making this lossier than
588    /// [`Self::to_f64_lossy`]. The `f64` intermediate step retains the
589    /// best precision available before the final narrowing cast.
590    ///
591    /// # Precision
592    ///
593    /// Lossy: involves f32 or f64 at some point; result may lose precision.
594    ///
595    /// # Examples
596    ///
597    /// ```
598    /// use decimal_scaled::I128s12;
599    ///
600    /// assert_eq!(I128s12::ZERO.to_f32_lossy(), 0.0_f32);
601    /// assert_eq!(I128s12::ONE.to_f32_lossy(), 1.0_f32);
602    /// ```
603    #[inline]
604    pub fn to_f32_lossy(self) -> f32 {
605        self.to_f64_lossy() as f32
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::DecimalConvertError;
612    use crate::core_type::{I128, I128s12};
613
614    // ──────────────────────────────────────────────────────────────────
615    // from_int / from_i32 -- foundation wrappers around From<iN>
616    // ──────────────────────────────────────────────────────────────────
617
618    #[test]
619    fn from_int_zero_is_zero() {
620        assert_eq!(I128s12::from_int(0), I128s12::ZERO);
621    }
622
623    #[test]
624    fn from_i32_zero_is_zero() {
625        assert_eq!(I128s12::from_i32(0), I128s12::ZERO);
626    }
627
628    #[test]
629    fn from_int_one_is_one() {
630        assert_eq!(I128s12::from_int(1), I128s12::ONE);
631    }
632
633    #[test]
634    fn from_i32_one_is_one() {
635        assert_eq!(I128s12::from_i32(1), I128s12::ONE);
636    }
637
638    #[test]
639    fn from_int_negative() {
640        assert_eq!(I128s12::from_int(-1), -I128s12::ONE);
641        assert_eq!(I128s12::from_int(-42).to_bits(), -42_000_000_000_000_i128);
642    }
643
644    // ──────────────────────────────────────────────────────────────────
645    // Lossless From<iN> / From<uN> -- bit-exact scaling
646    // ──────────────────────────────────────────────────────────────────
647
648    #[test]
649    fn from_i8_scales_correctly() {
650        assert_eq!(I128s12::from(0_i8).to_bits(), 0);
651        assert_eq!(I128s12::from(1_i8).to_bits(), 1_000_000_000_000);
652        assert_eq!(I128s12::from(-1_i8).to_bits(), -1_000_000_000_000);
653        assert_eq!(I128s12::from(i8::MAX).to_bits(), 127_000_000_000_000);
654        assert_eq!(I128s12::from(i8::MIN).to_bits(), -128_000_000_000_000);
655    }
656
657    #[test]
658    fn from_i16_scales_correctly() {
659        assert_eq!(I128s12::from(0_i16).to_bits(), 0);
660        assert_eq!(I128s12::from(1_i16).to_bits(), 1_000_000_000_000);
661        assert_eq!(I128s12::from(i16::MAX).to_bits(), 32_767_000_000_000_000);
662        assert_eq!(I128s12::from(i16::MIN).to_bits(), -32_768_000_000_000_000);
663    }
664
665    #[test]
666    fn from_i32_scales_correctly() {
667        assert_eq!(I128s12::from(0_i32).to_bits(), 0);
668        assert_eq!(I128s12::from(i32::MAX).to_bits(), (i32::MAX as i128) * 1_000_000_000_000);
669        assert_eq!(I128s12::from(i32::MIN).to_bits(), (i32::MIN as i128) * 1_000_000_000_000);
670    }
671
672    #[test]
673    fn from_i64_scales_correctly() {
674        assert_eq!(I128s12::from(0_i64).to_bits(), 0);
675        assert_eq!(I128s12::from(i64::MAX).to_bits(), (i64::MAX as i128) * 1_000_000_000_000);
676        assert_eq!(I128s12::from(i64::MIN).to_bits(), (i64::MIN as i128) * 1_000_000_000_000);
677    }
678
679    #[test]
680    fn from_u8_scales_correctly() {
681        assert_eq!(I128s12::from(0_u8).to_bits(), 0);
682        assert_eq!(I128s12::from(u8::MAX).to_bits(), 255_000_000_000_000);
683    }
684
685    #[test]
686    fn from_u16_scales_correctly() {
687        assert_eq!(I128s12::from(0_u16).to_bits(), 0);
688        assert_eq!(I128s12::from(u16::MAX).to_bits(), 65_535_000_000_000_000);
689    }
690
691    #[test]
692    fn from_u32_scales_correctly() {
693        assert_eq!(I128s12::from(0_u32).to_bits(), 0);
694        assert_eq!(I128s12::from(u32::MAX).to_bits(), (u32::MAX as i128) * 1_000_000_000_000);
695    }
696
697    /// `From<u64>` at the boundary -- u64::MAX times multiplier is
698    /// ~1.8e31, well under i128::MAX ~1.7e38, so this is lossless
699    /// at SCALE=12.
700    #[test]
701    fn from_u64_at_boundary_is_lossless() {
702        let v = I128s12::from(u64::MAX);
703        // u64::MAX = 2^64 - 1 = 18_446_744_073_709_551_615
704        assert_eq!(v.to_bits(), (u64::MAX as i128) * 1_000_000_000_000);
705    }
706
707    /// Sanity: round-trip `I128::from(int).to_int_lossy() == int as i64`
708    /// across representative integer types.
709    #[test]
710    fn integer_round_trip_via_lossy_to_int() {
711        for v in [0_i32, 1, -1, 42, -42, i32::MAX, i32::MIN] {
712            assert_eq!(I128s12::from(v).to_int_lossy(), v as i64);
713        }
714        for v in [0_i64, 1, -1, 1_000_000_000, -1_000_000_000] {
715            assert_eq!(I128s12::from(v).to_int_lossy(), v);
716        }
717    }
718
719    // ──────────────────────────────────────────────────────────────────
720    // from_f64_lossy + to_f64_lossy + to_f32_lossy
721    // ──────────────────────────────────────────────────────────────────
722
723    #[test]
724    fn from_f64_lossy_zero_is_zero() {
725        assert_eq!(I128s12::from_f64_lossy(0.0), I128s12::ZERO);
726    }
727
728    #[test]
729    fn zero_to_int_lossy_is_zero() {
730        assert_eq!(I128s12::ZERO.to_int_lossy(), 0);
731    }
732
733    #[test]
734    fn zero_to_f64_lossy_is_zero() {
735        assert_eq!(I128s12::ZERO.to_f64_lossy(), 0.0);
736    }
737
738    #[test]
739    fn zero_to_f32_lossy_is_zero() {
740        assert_eq!(I128s12::ZERO.to_f32_lossy(), 0.0);
741    }
742
743    #[test]
744    fn from_f64_lossy_one_is_one() {
745        let v = I128s12::from_f64_lossy(1.0);
746        assert_eq!(v, I128s12::ONE);
747    }
748
749    #[test]
750    fn from_f64_lossy_negative() {
751        let v = I128s12::from_f64_lossy(-1.0);
752        assert_eq!(v, -I128s12::ONE);
753    }
754
755    /// Property test: `(from_f64_lossy(x).to_f64_lossy() - x).abs()`
756    /// is within 1 LSB (= 10^-SCALE) for representative x in
757    /// [-1e10, 1e10]. The 1-LSB tolerance covers the integer
758    /// truncation in `from_f64_lossy`.
759    #[test]
760    fn from_f64_to_f64_round_trip_within_1_lsb() {
761        let lsb = 1.0 / (I128s12::multiplier() as f64);
762        let cases = [
763            0.0_f64,
764            1.0,
765            -1.0,
766            0.5,
767            -0.5,
768            1.5,
769            -1.5,
770            // Pick a value that's not close to any well-known math
771            // constant so clippy's `approx_constant` lint stays quiet.
772            1.234567890123_f64,
773            -1.234567890123_f64,
774            1e6,
775            -1e6,
776            1e10,
777            -1e10,
778            // Headline: 1.1, which f64 cannot represent exactly.
779            1.1,
780            2.2,
781            3.3,
782            // Sub-LSB; will round to ZERO at SCALE=12 (LSB = 1e-12).
783            // Skip values smaller than the LSB.
784        ];
785        for x in cases {
786            let v = I128s12::from_f64_lossy(x);
787            let back = v.to_f64_lossy();
788            let err = (back - x).abs();
789            assert!(
790                err <= lsb * 2.0, // allow up to 2 LSB to absorb f64 round-trip rounding
791                "round-trip exceeded 2 LSB for x = {x}: back = {back}, err = {err}, lsb = {lsb}"
792            );
793        }
794    }
795
796    /// `to_f32_lossy` matches `to_f64_lossy as f32` (defines its
797    /// implementation contract).
798    #[test]
799    fn to_f32_lossy_matches_f64_path() {
800        let cases = [
801            I128s12::ZERO,
802            I128s12::ONE,
803            -I128s12::ONE,
804            I128s12::from_bits(1_500_000_000_000),
805            I128s12::from_bits(-7_321_654_987_000),
806        ];
807        for v in cases {
808            let via_f64 = v.to_f64_lossy() as f32;
809            assert_eq!(v.to_f32_lossy(), via_f64);
810        }
811    }
812
813    /// Saturation: `from_f64_lossy(f64::INFINITY) == I128::MAX`.
814    #[test]
815    fn from_f64_lossy_infinity_saturates_max() {
816        assert_eq!(I128s12::from_f64_lossy(f64::INFINITY), I128s12::MAX);
817    }
818
819    /// Saturation: `from_f64_lossy(f64::NEG_INFINITY) == I128::MIN`.
820    #[test]
821    fn from_f64_lossy_neg_infinity_saturates_min() {
822        assert_eq!(I128s12::from_f64_lossy(f64::NEG_INFINITY), I128s12::MIN);
823    }
824
825    /// NaN handling (locked policy): `from_f64_lossy(NaN) == ZERO`.
826    #[test]
827    fn from_f64_lossy_nan_is_zero() {
828        assert_eq!(I128s12::from_f64_lossy(f64::NAN), I128s12::ZERO);
829    }
830
831    /// Saturation: finite out-of-range inputs clamp to MAX/MIN.
832    #[test]
833    fn from_f64_lossy_finite_out_of_range_saturates() {
834        // 1e30 * 10^12 = 1e42 > i128::MAX ~1.7e38
835        assert_eq!(I128s12::from_f64_lossy(1e30), I128s12::MAX);
836        assert_eq!(I128s12::from_f64_lossy(-1e30), I128s12::MIN);
837    }
838
839    /// `to_int_lossy` truncates toward zero (drops fractional part).
840    #[test]
841    fn to_int_lossy_truncates_toward_zero() {
842        // 2.5 -> 2
843        assert_eq!(I128s12::from_bits(2_500_000_000_000).to_int_lossy(), 2);
844        // -2.5 -> -2 (toward zero, not toward neg-infinity)
845        assert_eq!(I128s12::from_bits(-2_500_000_000_000).to_int_lossy(), -2);
846        // 0.999... -> 0
847        assert_eq!(I128s12::from_bits(999_999_999_999).to_int_lossy(), 0);
848        // -0.999... -> 0
849        assert_eq!(I128s12::from_bits(-999_999_999_999).to_int_lossy(), 0);
850    }
851
852    /// `to_int_lossy` saturates beyond i64's range.
853    #[test]
854    fn to_int_lossy_saturates() {
855        // I128s12::MAX is i128::MAX bits; integer part = i128::MAX / 10^12
856        // ~= 1.7e26, way above i64::MAX. Saturates to i64::MAX.
857        assert_eq!(I128s12::MAX.to_int_lossy(), i64::MAX);
858        // I128s12::MIN is i128::MIN bits; integer part way below i64::MIN.
859        // Saturates to i64::MIN.
860        assert_eq!(I128s12::MIN.to_int_lossy(), i64::MIN);
861    }
862
863    // ──────────────────────────────────────────────────────────────────
864    // TryFrom<i128> / TryFrom<u128>
865    // ──────────────────────────────────────────────────────────────────
866
867    #[test]
868    fn try_from_i128_zero_succeeds() {
869        let v: I128s12 = 0_i128.try_into().expect("zero fits");
870        assert_eq!(v, I128s12::ZERO);
871    }
872
873    #[test]
874    fn try_from_i128_in_range_succeeds() {
875        // 1_000_000 model units -> 1e6 * 10^12 = 1e18, well under i128::MAX
876        let v: I128s12 = 1_000_000_i128.try_into().expect("in-range fits");
877        assert_eq!(v.to_bits(), 1_000_000 * 1_000_000_000_000);
878    }
879
880    #[test]
881    fn try_from_i128_overflow_returns_err() {
882        // i128::MAX cannot be scaled by 10^12.
883        let result: Result<I128s12, _> = i128::MAX.try_into();
884        assert_eq!(result, Err(DecimalConvertError::Overflow));
885
886        let result_neg: Result<I128s12, _> = i128::MIN.try_into();
887        assert_eq!(result_neg, Err(DecimalConvertError::Overflow));
888    }
889
890    #[test]
891    fn try_from_u128_zero_succeeds() {
892        let v: I128s12 = 0_u128.try_into().expect("zero fits");
893        assert_eq!(v, I128s12::ZERO);
894    }
895
896    #[test]
897    fn try_from_u128_in_range_succeeds() {
898        let v: I128s12 = 42_u128.try_into().expect("in-range fits");
899        assert_eq!(v.to_bits(), 42 * 1_000_000_000_000);
900    }
901
902    #[test]
903    fn try_from_u128_above_i128_max_returns_err() {
904        // Any u128 > i128::MAX is unrepresentable.
905        let above: u128 = (i128::MAX as u128) + 1;
906        let result: Result<I128s12, _> = above.try_into();
907        assert_eq!(result, Err(DecimalConvertError::Overflow));
908    }
909
910    #[test]
911    fn try_from_u128_max_returns_err() {
912        let result: Result<I128s12, _> = u128::MAX.try_into();
913        assert_eq!(result, Err(DecimalConvertError::Overflow));
914    }
915
916    // ──────────────────────────────────────────────────────────────────
917    // TryFrom<f64> / TryFrom<f32>
918    // ──────────────────────────────────────────────────────────────────
919
920    #[test]
921    fn try_from_f64_zero_succeeds() {
922        let v: I128s12 = 0.0_f64.try_into().expect("zero fits");
923        assert_eq!(v, I128s12::ZERO);
924    }
925
926    #[test]
927    fn try_from_f64_one_succeeds() {
928        let v: I128s12 = 1.0_f64.try_into().expect("one fits");
929        assert_eq!(v, I128s12::ONE);
930    }
931
932    #[test]
933    fn try_from_f64_nan_returns_err() {
934        let result: Result<I128s12, _> = f64::NAN.try_into();
935        assert_eq!(result, Err(DecimalConvertError::NotFinite));
936    }
937
938    #[test]
939    fn try_from_f64_pos_infinity_returns_err() {
940        let result: Result<I128s12, _> = f64::INFINITY.try_into();
941        assert_eq!(result, Err(DecimalConvertError::NotFinite));
942    }
943
944    #[test]
945    fn try_from_f64_neg_infinity_returns_err() {
946        let result: Result<I128s12, _> = f64::NEG_INFINITY.try_into();
947        assert_eq!(result, Err(DecimalConvertError::NotFinite));
948    }
949
950    #[test]
951    fn try_from_f64_out_of_range_returns_err() {
952        // 1e30 * 10^12 = 1e42 > i128::MAX
953        let result: Result<I128s12, _> = 1e30_f64.try_into();
954        assert_eq!(result, Err(DecimalConvertError::Overflow));
955
956        let result_neg: Result<I128s12, _> = (-1e30_f64).try_into();
957        assert_eq!(result_neg, Err(DecimalConvertError::Overflow));
958    }
959
960    #[test]
961    fn try_from_f32_zero_succeeds() {
962        let v: I128s12 = 0.0_f32.try_into().expect("zero fits");
963        assert_eq!(v, I128s12::ZERO);
964    }
965
966    #[test]
967    fn try_from_f32_nan_returns_err() {
968        let result: Result<I128s12, _> = f32::NAN.try_into();
969        assert_eq!(result, Err(DecimalConvertError::NotFinite));
970    }
971
972    #[test]
973    fn try_from_f32_infinity_returns_err() {
974        let result: Result<I128s12, _> = f32::INFINITY.try_into();
975        assert_eq!(result, Err(DecimalConvertError::NotFinite));
976    }
977
978    #[test]
979    fn try_from_f32_neg_infinity_returns_err() {
980        let result: Result<I128s12, _> = f32::NEG_INFINITY.try_into();
981        assert_eq!(result, Err(DecimalConvertError::NotFinite));
982    }
983
984    // ──────────────────────────────────────────────────────────────────
985    // DecimalConvertError -- Display + Debug shape
986    // ──────────────────────────────────────────────────────────────────
987
988    /// Display impl produces stable strings for both variants.
989    #[cfg(feature = "alloc")]
990    #[test]
991    fn convert_error_display() {
992        extern crate alloc;
993        use alloc::string::ToString;
994        assert_eq!(
995            DecimalConvertError::Overflow.to_string(),
996            "decimal conversion overflow"
997        );
998        assert_eq!(
999            DecimalConvertError::NotFinite.to_string(),
1000            "decimal conversion from non-finite float"
1001        );
1002    }
1003
1004    /// `DecimalConvertError` is `Debug + Clone + Copy + Eq + Hash`
1005    /// (basic suite expected of any leaf error type).
1006    #[test]
1007    fn convert_error_traits_compile() {
1008        // Compile-time check: Copy + Clone + Eq + Hash bounds.
1009        fn assert_traits<T: core::fmt::Debug + Copy + Eq + core::hash::Hash>() {}
1010        assert_traits::<DecimalConvertError>();
1011    }
1012
1013    // ──────────────────────────────────────────────────────────────────
1014    // Cross-scale exercise -- non-default SCALE
1015    // ──────────────────────────────────────────────────────────────────
1016
1017    /// At SCALE = 6 (microseconds-style) the `From<i64>` impl still
1018    /// works and the round-trip via `to_int_lossy` is exact.
1019    #[test]
1020    fn from_int_works_at_scale_6() {
1021        type D6 = I128<6>;
1022        let v: D6 = D6::from(1_000_i64);
1023        assert_eq!(v.to_bits(), 1_000_000_000); // 10^9
1024        assert_eq!(v.to_int_lossy(), 1_000);
1025    }
1026
1027    /// At SCALE = 0 the multiplier is 1 -- conversions are trivial.
1028    #[test]
1029    fn from_int_works_at_scale_0() {
1030        type D0 = I128<0>;
1031        let v: D0 = D0::from(42_i64);
1032        assert_eq!(v.to_bits(), 42);
1033        assert_eq!(v.to_int_lossy(), 42);
1034    }
1035}