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}