Skip to main content

ion_rs/types/decimal/
mod.rs

1//! Types related to [`Decimal`], the in-memory representation of an Ion decimal value.
2
3use std::cmp::Ordering;
4
5use crate::ion_data::{IonDataHash, IonDataOrd, IonEq};
6use crate::result::{IonError, IonFailure};
7use crate::types::integer::{IntData, UIntData};
8use crate::{Int, IonResult};
9use num_bigint::{BigInt, BigUint};
10use num_traits::Zero;
11use std::convert::{TryFrom, TryInto};
12use std::fmt::{Display, Formatter};
13use std::hash::{Hash, Hasher};
14use std::ops::Neg;
15
16pub(crate) mod coefficient;
17pub use coefficient::{Coefficient, Sign};
18
19/// An arbitrary-precision Decimal type with a distinct representation of negative zero (`-0`).
20///
21/// A `Decimal` can be thought of as a `(coefficient, exponent)` pair, and its value can be
22/// calculated using the formula `coefficient * 10^exponent`.
23///
24/// ```
25/// # use ion_rs::IonResult;
26/// # fn main() -> IonResult<()> {
27/// use ion_rs::{Int, Decimal, UInt};
28/// use ion_rs::decimal::Sign;
29/// // Equivalent to: 1225 * 10^-2, or 12.25
30/// let decimal = Decimal::new(1225, -2);
31/// // The coefficient can be viewed as a sign/magnitude pair...
32/// assert_eq!(decimal.coefficient().sign(), Sign::Positive);
33/// assert_eq!(decimal.coefficient().magnitude(), UInt::from(1225u64));
34/// // ...or, if it is not negative zero, by converting it into an Int.
35/// let coefficient: Int = decimal.coefficient().try_into().expect("`decimal` is not negative zero");
36/// assert_eq!(coefficient, Int::from(1225));
37///
38/// assert_eq!(decimal.exponent(), -2);
39/// # Ok(())
40/// # }
41/// ```
42#[derive(Clone, Debug)]
43pub struct Decimal {
44    // ===== A note on layout =====
45    // A `Coefficient` is a `(Sign, Int)` pair. The `Sign` is a one-byte enum that allows the
46    // `Coefficient` to distinguish between positive and negative zero. If the value is not
47    // negative zero, then the `Sign` will agree with the sign of the `Int` value.
48    //
49    // `Decimal` previously contained a `Coefficient`, but this resulted in large amounts of wasted
50    // memory. Because of alignment requirements, the `Coefficient` was stored like this:
51    //
52    //              Int      Sign    Padding
53    //       ┌───────┴───────┐╰╮┌───────┴──────┐
54    //       IIIIIIII IIIIIIII S_______ ________
55    //
56    //
57    // When the `Coefficient` was stored inside the `Decimal`, the situation was compounded.
58    // The `Decimal` required an alignment of 16 bytes, causing even more padding to be added:
59    //
60    //              Int      Sign    Padding     Exponent  Padding
61    //       ┌───────┴───────┐╰╮┌───────┴──────┐ ┌──┴───┐ ┌──┴───┐
62    //       IIIIIIII IIIIIIII S_______ ________ EEEEEEEE ________
63    //       └──────────────┬──────────────────┘
64    //                   Decimal
65    // Of the `Decimal`'s 48 bytes, 23 were padding—a huge waste. Because many types contain a
66    // Decimal (however indirectly), this meant that lots of data types had 23 bytes of dead space.
67    //
68    // `Decimal` now stores the value and sign fields itself and uses them to construct a `Coefficient`
69    // on demand. As a result, `Decimal` is now only 32 bytes, 7 of which are padding.
70    //
71    //              Int        Exponent  Padding
72    //       ┌───────┴───────┐ ┌───┴──┐  ┌──┴──┐
73    //       IIIIIIII IIIIIIII EEEEEEEE S_______
74    //                                  ╰╮
75    //                                  Sign
76    //
77    // While the compiler is free to rearrange the layout of a type in each compile, the layouts
78    // described above have remained steady over several versions of Rust.
79    pub(crate) coefficient_value: Int,
80    pub(crate) coefficient_sign: Sign,
81    pub(crate) exponent: i64,
82}
83
84impl Decimal {
85    pub const ZERO: Decimal = Decimal {
86        coefficient_value: Int::ZERO,
87        coefficient_sign: Sign::Positive,
88        exponent: 0,
89    };
90
91    pub const NEGATIVE_ZERO: Decimal = Decimal {
92        coefficient_value: Int::ZERO,
93        coefficient_sign: Sign::Negative,
94        exponent: 0,
95    };
96
97    /// Constructs a new Decimal with the provided components. The value of the decimal is:
98    ///    `coefficient * 10^exponent`
99    pub fn new<C: Into<Coefficient>, E: Into<i64>>(coefficient: C, exponent: E) -> Decimal {
100        let coefficient = coefficient.into();
101        let exponent = exponent.into();
102        if coefficient.is_negative_zero() {
103            return Decimal::negative_zero_with_exponent(exponent);
104        }
105        Decimal {
106            coefficient_value: coefficient.as_int().unwrap(), // Only fails for -0, checked above.
107            coefficient_sign: coefficient.sign(),
108            exponent,
109        }
110    }
111
112    /// Returns this `Decimal`'s coefficient.
113    pub fn coefficient(&self) -> Coefficient {
114        Coefficient::from_sign_and_value(self.coefficient_sign, self.coefficient_value.clone())
115    }
116
117    /// Returns this `Decimal`'s exponent.
118    pub fn exponent(&self) -> i64 {
119        self.exponent
120    }
121
122    /// Returns scale of the Decimal value
123    /// If zero or positive, a scale indicates the number of digits to the right of the decimal point.
124    /// If negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale.
125    /// For example, a scale of -3 means the unscaled value is multiplied by 1000.
126    pub fn scale(&self) -> i64 {
127        self.exponent.neg()
128    }
129
130    /// Returns the number of digits in the non-scaled integer representation of the decimal.
131    pub fn precision(&self) -> u64 {
132        self.coefficient().number_of_decimal_digits() as u64
133    }
134
135    /// Constructs a Decimal with the value `-0d0`. This is provided as a convenience method
136    /// because Rust will ignore a unary minus when it is applied to an zero literal (`-0`).
137    pub fn negative_zero() -> Decimal {
138        Decimal::negative_zero_with_exponent(0)
139    }
140
141    /// Constructs a Decimal with a coefficient of `-0` and the specified exponent. This function
142    /// is provided as a convenience method because Rust will ignore a unary minus when it is
143    /// applied to a zero literal (`-0`).
144    pub fn negative_zero_with_exponent(exponent: i64) -> Decimal {
145        Decimal {
146            coefficient_value: Int::ZERO,
147            coefficient_sign: Sign::Negative,
148            exponent,
149        }
150    }
151
152    /// Returns `true` if this Decimal is a zero of any sign or exponent.
153    pub fn is_zero(&self) -> bool {
154        self.coefficient().is_zero()
155    }
156
157    /// Returns true if this Decimal's coefficient has a negative sign AND a magnitude greater than
158    /// zero. Otherwise, returns false. (Negative zero returns false.)
159    pub fn is_less_than_zero(&self) -> bool {
160        let coefficient = self.coefficient();
161        coefficient.sign() == Sign::Negative && !coefficient.magnitude().is_zero()
162    }
163
164    /// Semantically identical to `self >= Decimal::new(1, 0)`, but much cheaper to compute.
165    pub(crate) fn is_greater_than_or_equal_to_one(&self) -> bool {
166        // If the coefficient has a magnitude of zero, the Decimal is a zero of some precision
167        // and so is not >= 1.
168        if self.coefficient().is_zero() {
169            return false;
170        }
171
172        // If the coefficient is non-zero, look at the exponent. A non-negative exponent means the
173        // value has to be >= 1.
174        if self.exponent >= 0 {
175            return true;
176        }
177
178        // If the exponent is negative, we have to see whether if its magnitude outweighs the
179        // magnitude of the coefficient.
180        let num_coefficient_decimal_digits = self.coefficient().number_of_decimal_digits() as u64;
181        num_coefficient_decimal_digits > self.exponent.unsigned_abs()
182    }
183
184    // Determines whether the first decimal value is greater than, equal to, or less than
185    // the second decimal value.
186    fn compare(d1: &Decimal, d2: &Decimal) -> Ordering {
187        if d1.is_zero() && d2.is_zero() {
188            // Ignore the sign/exponent if they're both some flavor of zero.
189            return Ordering::Equal;
190        }
191        // Even if the exponents are wildly different, disagreement in the coefficient's signs
192        // still tells us which value is bigger.
193        let sign_cmp = d1.coefficient().sign().cmp(&d2.coefficient().sign());
194        if sign_cmp != Ordering::Equal {
195            return sign_cmp;
196        }
197
198        // If the signs are the same, compare their magnitudes.
199        let ordering = Decimal::compare_magnitudes(d1, d2);
200
201        if d1.coefficient().sign() == Sign::Positive {
202            // If the values are both positive, use the magnitudes' ordering.
203            ordering
204        } else {
205            // If the values are both negative, reverse the magnitudes' ordering.
206            // For example: -100 has a greater magnitude (i.e. absolute value) than -99,
207            //              but -99 is the larger number.
208            ordering.reverse()
209        }
210    }
211
212    // Compare the magnitudes (absolute values) of the provided decimal values.
213    fn compare_magnitudes(d1: &Decimal, d2: &Decimal) -> Ordering {
214        // If the exponents match, we can compare the two coefficients directly.
215        if d1.exponent == d2.exponent {
216            return d1
217                .coefficient()
218                .magnitude()
219                .cmp(&d2.coefficient().magnitude());
220        }
221
222        // If the exponents don't match, we need to scale one of the magnitudes to match the other
223        // for comparison. For example, when comparing 16e3 and 1600e1, we can't compare the
224        // magnitudes (16 and 1600) directly. Instead, we need to multiply 16 by 10^2 to compensate
225        // for the difference in their exponents (3-1). Then we'll be comparing 1600 to 1600,
226        // and can safely conclude that they are equal.
227        if d1.exponent > d2.exponent {
228            Self::compare_scaled_coefficients(d1, d2)
229        } else {
230            Self::compare_scaled_coefficients(d2, d1).reverse()
231        }
232    }
233
234    // Scales up the coefficient associated with a greater exponent and compares it with the
235    // other coefficient. `d1` must have a larger exponent than `d2`.
236    fn compare_scaled_coefficients(d1: &Decimal, d2: &Decimal) -> Ordering {
237        let exponent_delta = d1.exponent - d2.exponent;
238        // d1 has a larger exponent, so scale up its coefficient to match d2's exponent.
239        // For example, when comparing these values of d1 and d2:
240        //     d1 =  8 * 10^3
241        //     d2 = 80 * 10^2
242        // d1 has the larger exponent (3). We need to scale its coefficient up to d2's 10^2 scale.
243        // We do this by multiplying it times 10^exponent_delta, which is 1 in this case.
244        // This lets us compare 80 and 80, determining that the decimals are equal.
245        let d1_mag = d1.coefficient().magnitude();
246        let d2_mag = d2.coefficient().magnitude();
247        // Fast path: both fit in u128
248        if let (Some(m1), Some(m2)) = (d1_mag.as_u128(), d2_mag.as_u128()) {
249            if let Some(multiplicand) = 10u128.checked_pow(exponent_delta as u32) {
250                if let Some(scaled) = m1.checked_mul(multiplicand) {
251                    return scaled.cmp(&m2);
252                }
253            }
254        }
255        // Slow path: use BigUint for arbitrary-precision scaling and comparison
256        let d1_big = d1_mag.data;
257        let scaled = d1_big * UIntData::from_big(BigUint::from(10u32).pow(exponent_delta as u32));
258        let other = d2_mag.data;
259        scaled.cmp(&other)
260    }
261
262    /// Returns the integer part of `self`. This means that non-integer numbers are always
263    /// truncated towards zero, maintaining the sign of the original coefficient.
264    pub fn trunc(&self) -> Decimal {
265        if self.exponent >= 0 {
266            self.clone()
267        } else {
268            // Extract the coefficient, we'll lose the sign if it's ZERO, but we add it back at the
269            // end.
270            let mut coeff = self.coefficient().as_int().unwrap_or(Int::ZERO).data;
271            let scaling_factor =
272                IntData::from_big(BigInt::from(10).pow(self.exponent.unsigned_abs() as u32));
273            coeff = coeff / scaling_factor;
274            Decimal::new(
275                Coefficient::from_sign_and_value(self.coefficient_sign, coeff),
276                0,
277            )
278        }
279    }
280
281    /// Returns the fractional part of `self`. Values with no fractional component will return
282    /// zero maintaining the sign of the original coefficient.
283    pub fn fract(&self) -> Decimal {
284        if self.exponent >= 0 {
285            Decimal::new(
286                Coefficient::from_sign_and_value(self.coefficient_sign, 0),
287                0,
288            )
289        } else {
290            let mut coeff = self.coefficient().as_int().unwrap_or(Int::ZERO).data;
291            let scaling_factor =
292                IntData::from_big(BigInt::from(10).pow(self.exponent.unsigned_abs() as u32));
293            coeff = coeff % scaling_factor;
294            Decimal::new(
295                Coefficient::from_sign_and_value(self.coefficient_sign, coeff),
296                self.exponent,
297            )
298        }
299    }
300}
301
302impl PartialEq for Decimal {
303    fn eq(&self, other: &Self) -> bool {
304        self.cmp(other) == Ordering::Equal
305    }
306}
307
308impl Eq for Decimal {}
309
310impl IonEq for Decimal {
311    fn ion_eq(&self, other: &Self) -> bool {
312        self.exponent == other.exponent && self.coefficient() == other.coefficient()
313    }
314}
315
316impl IonDataOrd for Decimal {
317    // Numerical order (least to greatest) and then by number of significant figures (least to greatest)
318    fn ion_cmp(&self, other: &Self) -> Ordering {
319        let sign_cmp = self.coefficient().sign().cmp(&other.coefficient().sign());
320        if sign_cmp != Ordering::Equal {
321            return sign_cmp;
322        }
323
324        // If the signs are the same, compare their magnitudes.
325        let ordering = Decimal::compare_magnitudes(self, other);
326        if ordering != Ordering::Equal {
327            return match self.coefficient().sign() {
328                Sign::Negative => ordering.reverse(),
329                Sign::Positive => ordering,
330            };
331        };
332        // Finally, compare the number of significant figures.
333        // Since we know the numeric value is the same, we only need to look at the exponents here.
334        self.exponent.cmp(&other.exponent).reverse()
335    }
336}
337
338impl IonDataHash for Decimal {
339    fn ion_data_hash<H: Hasher>(&self, state: &mut H) {
340        state.write_i8(self.coefficient().sign() as i8);
341        self.coefficient().magnitude().hash(state);
342        state.write_i64(self.exponent);
343    }
344}
345
346impl PartialOrd for Decimal {
347    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
348        Some(self.cmp(other))
349    }
350}
351
352impl Ord for Decimal {
353    fn cmp(&self, other: &Self) -> Ordering {
354        Decimal::compare(self, other)
355    }
356}
357
358macro_rules! impl_decimal_from_unsigned_primitive_integer {
359    ($($t:ty),*) => ($(
360        impl From<$t> for Decimal {
361            fn from(value: $t) -> Self {
362                Decimal::new(value as u64, 0)
363            }
364        }
365    )*)
366}
367impl_decimal_from_unsigned_primitive_integer!(u8, u16, u32, u64, usize);
368
369macro_rules! impl_decimal_from_signed_primitive_integer {
370    ($($t:ty),*) => ($(
371        impl From<$t> for Decimal {
372            fn from(value: $t) -> Self {
373                Decimal::new(Coefficient::new(value), 0)
374            }
375        }
376    )*)
377}
378impl_decimal_from_signed_primitive_integer!(i8, i16, i32, i64, isize);
379
380impl From<Int> for Decimal {
381    fn from(value: Int) -> Self {
382        Decimal::new(value, 0)
383    }
384}
385
386impl TryFrom<f32> for Decimal {
387    type Error = IonError;
388
389    fn try_from(value: f32) -> Result<Self, Self::Error> {
390        // Defer to the f64 implementation of `TryInto`
391        (value as f64).try_into()
392    }
393}
394
395impl TryFrom<f64> for Decimal {
396    type Error = IonError;
397    /// Attempts to create a Decimal from an f64. Returns an Error if the f64 being
398    /// converted is a special value, including:
399    ///   * Infinity
400    ///   * Negative infinity
401    ///   * NaN (not-a-number)
402    ///
403    /// Otherwise, returns Ok.
404    ///
405    /// Because Decimal can represent negative zero, f64::neg_zero() IS supported.
406    ///
407    /// NOTE: While the resulting decimal will be a very close approximation of the original f64's
408    ///       value, this is an inherently lossy operation. Floating point values do not encode a
409    ///       precision. When converting an f64 to a Decimal, a precision for the new Decimal must
410    ///       be chosen somewhat arbitrarily. Do NOT rely on the precision of the resulting Decimal.
411    ///       This implementation may change without notice.
412    fn try_from(value: f64) -> Result<Self, Self::Error> {
413        // === Special `f64` values ===
414
415        if value.is_infinite() {
416            if value.is_sign_negative() {
417                return IonResult::illegal_operation(
418                    "Cannot convert f64 negative infinity to Decimal.",
419                );
420            } else {
421                return IonResult::illegal_operation("Cannot convert f64 infinity to Decimal.");
422            }
423        } else if value.is_nan() {
424            return IonResult::illegal_operation(
425                "Cannot convert f64 NaN (not-a-number) to Decimal.",
426            );
427        }
428
429        // === The signed and unsigned zero cases ===
430
431        // You can't use the `log10` operation on a zero value, so check for these cases explicitly.
432        if value == 0f64 {
433            //    ^- Positive and negative zero are mathematically equivalent,
434            //       so we can use `==` here to check for both.
435            if value.is_sign_negative() {
436                return Ok(Decimal::NEGATIVE_ZERO);
437            }
438            return Ok(Decimal::ZERO);
439        }
440
441        // === Split the value into its int and fraction components ===
442
443        // Isolate the integral portion of the f64. (e.g. 3.14 -> 3.0)
444        let f64_int = value.trunc();
445        //                  ^^^^^^
446        // The `trunc()` method discards the fractional part of the value, returning its integer
447        // component as an `f64`.
448
449        // Isolate the fractional portion of the value. (e.g. 3.14 -> 0.14)
450        let f64_fract = value.fract();
451        //                    ^^^^^^
452        // The `fract()` method returns the fractional part of the value as an f64.
453
454        // === The general case ===
455
456        // Due to the nature of IEEE-754 `f64` encoding, the value's integral component can be an
457        // integer outside the integer range supported by `i128`. Starting at (2^53 + 1), an `f64`
458        // will start skipping over one or more integers as its representational precision breaks down.
459        // An `i128` can precisely represent more integers than an `f64` can, but because `f64` can
460        // "skip" regions of large numbers, the `f64` has a wider range of integers it can represent.
461        //
462        // Our coefficient will be stored in an `Int` with 128 bits, allowing us to store up to 38
463        // decimal places of precision.
464        const MAX_DECIMAL_DIGITS: u32 = 38;
465        // If the total number of digits in the f64 exceed 38, we'll need to truncate the coefficient
466        // at 38 digits and increase the exponent (i.e. the number of trailing zeros) to approximate
467        // what was lost.
468        //
469        // For example, this 40 digit number:
470        //
471        //    1234567890_1234567890_1234567890_1234567890
472        //
473        // would be turned into a decimal whose coefficient was:
474        //
475        //    1234567890_1234567890_1234567890_12345678XX
476        //                               discarded ----^^
477        //
478        // and its exponent would be increased by 2 to retain the scale of discarded digits.
479
480        // Store a copy of the value as an i128
481        let integral_value = f64_int as i128;
482        // Determine how many decimal digits comprise the integral portion
483        let num_integral_decimal_digits = f64_int.abs().log10().floor() as u32 + 1;
484
485        // Check to see if the fractional part of the value is zero; if so, the value is an integer.
486        if f64_fract.is_zero() {
487            // If the f64 is an integer value, we can convert it to a decimal trivially.
488            // For very large integer values, we need to set the exponent to capture any scale
489            // that the coefficient alone was not able to store.
490            let exponent = (num_integral_decimal_digits as i64 - MAX_DECIMAL_DIGITS as i64).max(0);
491            return Ok(Decimal::new(integral_value, exponent));
492        }
493
494        // The number of fractional digits we'll retain is the smaller of:
495        //   * the number of digits _not_ occupied by the integral portion of the number
496        //     OR
497        //   * the number of fractional digits an f64 can represent
498        let num_fractional_digits =
499            (MAX_DECIMAL_DIGITS - num_integral_decimal_digits).min(f64::DIGITS);
500        //                                                         ^^^^^^^^^^^
501        // `f64::DIGITS` is the number of base 10 digits of fractional precision in an `f64`: 15
502
503        // Shift `num_fractional_digits` fractional digits into the integer portion of the f64.
504        let coefficient_f64 = value * 10f64.powi(num_fractional_digits as i32);
505        let coefficient = coefficient_f64 as i128;
506
507        let exponent = -(num_fractional_digits as i64);
508        Ok(Decimal::new(coefficient, exponent))
509    }
510}
511
512impl Display for Decimal {
513    #[rustfmt::skip] // https://github.com/rust-lang/rustfmt/issues/3255
514    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
515        // Inspired by the formatting conventions of Java's BigDecimal.toString()
516        const WIDE_NUMBER: usize = 6; // if you think about it, six is a lot 🙃
517
518        let digits = &*self.coefficient().magnitude().to_string();
519        let len = digits.len();
520        // The index of the decimal point, relative to the magnitude representation
521        //       0123                                                       01234
522        // Given ABCDd-2, the decimal gets inserted at position 2, yielding AB.CD
523        let dot_index = len as i64 + self.exponent;
524
525        if self.coefficient().sign() == Sign::Negative {
526            write!(f, "-").unwrap();
527        };
528
529        if self.exponent == 0 && len > WIDE_NUMBER { // e.g. A.BCDEFGd6
530            write!(f, "{}.{}d{}", &digits[0..1], &digits[1..len], (dot_index - 1))
531        } else if self.exponent == 0 { // e.g. ABC.
532            write!(f, "{}.", &digits)
533        } else if self.exponent >= 0 { // e.g. ABCd1
534            write!(f, "{}d{}", &digits, self.exponent)
535        } else { // exponent < 0, there is a fractional component
536            if dot_index > 0 { // e.g. A.BC or AB.C
537                let dot_index = dot_index as usize;
538                write!(f, "{}.{}", &digits[0..dot_index], &digits[dot_index..len])
539            } else if dot_index > -(WIDE_NUMBER as i64) { // e.g. 0.ABC or 0.000ABC
540                let width = dot_index.unsigned_abs() as usize + len;
541                write!(f, "0.{digits:0>width$}")
542            } else { // e.g. A.BCd-12
543                write!(f, "{}.{}d{}", &digits[0..1], &digits[1..len], (dot_index - 1))
544            }
545        }
546    }
547}
548
549#[cfg(feature = "bigdecimal")]
550mod bigdecimal {
551    use crate::result::IonFailure;
552    use crate::{Decimal, IonError, IonResult};
553    use bigdecimal::BigDecimal;
554    use num_traits::ToPrimitive;
555
556    impl TryInto<BigDecimal> for Decimal {
557        type Error = IonError;
558
559        /// Attempts to create a BigDecimal from a Decimal. Returns an Error if the Decimal being
560        /// converted is a special value (negative zero) or has a magnitude no representable as u128.
561        fn try_into(self) -> Result<BigDecimal, Self::Error> {
562            if self.coefficient().is_negative_zero() {
563                return IonResult::illegal_operation("Cannot convert negative zero to BigDecimal.");
564            }
565            let bigint = self.coefficient_value.to_bigint();
566            Ok(BigDecimal::new(bigint, self.scale()))
567        }
568    }
569
570    impl TryFrom<BigDecimal> for Decimal {
571        type Error = IonError;
572
573        /// Attempts to create a Decimal from a BigDecimal. Returns an Error if the BigDecimal cannot be
574        /// represented as a Decimal in this library.
575        fn try_from(value: BigDecimal) -> Result<Self, Self::Error> {
576            let (coeff, exponent) = value.into_bigint_and_exponent();
577            let Some(data) = coeff.to_i128() else {
578                return IonResult::illegal_operation("Cannot represent coefficient as i128.");
579            };
580
581            Ok(Decimal::new(data, -exponent))
582        }
583    }
584
585    #[cfg(test)]
586    mod tests {
587        use crate::Decimal;
588        use bigdecimal::BigDecimal;
589        use rstest::*;
590
591        #[fixture]
592        /// We use this function to represent when we don't expect to have a value to interact with.
593        /// In a less safe language this would be a "null object" or "pebble object" but here we'll just
594        /// document the expectation.
595        fn no_such_decimal() -> Decimal {
596            Decimal::NEGATIVE_ZERO
597        }
598
599        #[rstest]
600        #[case("123e1", Decimal::new(123, 1))]
601        #[case("123.", Decimal::new(123, 0))]
602        #[case("-123.", Decimal::new(-123,  0))]
603        #[case("12.3", Decimal::new( 123, -1))]
604        #[case("0.123", Decimal::new( 123, -3))]
605        #[case("-0.00123", Decimal::new(-123, -5))]
606        #[case("0.00123", Decimal::new( 123, -5))]
607        #[case("1.23e-8", Decimal::new( 123, -10))]
608        #[case("-1.23e-8", Decimal::new(-123, -10))]
609        #[case::out_of_double("9_007_199_254_740_993", Decimal::new(9_007_199_254_740_993i128, 0))]
610        #[should_panic]
611        #[case::coeff_too_large(
612            "1427247692705959881058285969449495136382746624",
613            no_such_decimal()
614        )]
615        /// Effectively tests TryFrom<BigDecimal> for Decimal. The only failure cases should be when
616        /// the coefficient is larger i128
617        fn try_from_bigdecimal_for_decimal(#[case] input: BigDecimal, #[case] expected: Decimal) {
618            let actual = Decimal::try_from(input).unwrap();
619            assert_eq!(actual, expected);
620        }
621
622        #[fixture]
623        /// We use this function to represent when we don't expect to have a value to interact with.
624        /// In a less safe language this would be a "null object" or "pebble object" but here we'll just
625        /// document the expectation.
626        fn no_such_bigdecimal() -> BigDecimal {
627            0.into()
628        }
629
630        #[rstest]
631        #[case(Decimal::new(123, 1), "123e1")]
632        #[case(Decimal::new(123, 0), "123.")]
633        #[case(Decimal::new(-123,  0),"-123.")]
634        #[case(Decimal::new( 123, -1),  "12.3")]
635        #[case(Decimal::new( 123, -3),   "0.123")]
636        #[case(Decimal::new(-123, -5),  "-0.00123")]
637        #[case(Decimal::new( 123, -5),   "0.00123")]
638        #[case(Decimal::new( 123, -10),  "1.23e-8")]
639        #[case(Decimal::new(-123, -10), "-1.23e-8")]
640        #[should_panic]
641        #[case::negative_zero(Decimal::NEGATIVE_ZERO, no_such_bigdecimal())]
642        /// Effectively tests TryFrom<Decimal> for BigDecimal. The only failure cases should be when
643        /// the coefficient is larger i128
644        fn try_into_bigdecimal_for_decimal(#[case] input: Decimal, #[case] expected: BigDecimal) {
645            let actual: BigDecimal = Decimal::try_into(input).unwrap();
646            assert_eq!(actual, expected);
647        }
648
649        #[test]
650        fn convert_large_coefficient_decimal_to_bigdecimal() {
651            use crate::decimal::Coefficient;
652            use crate::Int;
653            // 2^128 + 1 is a valid Ion decimal coefficient that exceeds u128::MAX.
654            let mut bytes = vec![1u8];
655            bytes.extend(vec![0u8; 16]);
656            bytes[16] = 1;
657            let big_int = Int::from_le_signed_bytes(&bytes);
658            let d = Decimal::new(Coefficient::new(big_int), 0i64);
659            let result: Result<BigDecimal, _> = d.try_into();
660            assert!(result.is_ok());
661        }
662    }
663}
664
665#[cfg(test)]
666mod decimal_tests {
667    use crate::decimal::{Coefficient, Sign};
668    use crate::result::IonResult;
669    use crate::{Decimal, Int};
670
671    use num_traits::Float;
672    use std::cmp::Ordering;
673    use std::convert::TryInto;
674    use std::fmt::Write;
675
676    use crate::ion_data::IonEq;
677
678    use rstest::*;
679
680    #[rstest]
681    #[case(Decimal::new(123, 1), "123d1")]
682    #[case(Decimal::new(123, 0), "123.")]
683    #[case(Decimal::new(-123,  0),"-123.")]
684    #[case(Decimal::new( 123, -1),  "12.3")]
685    #[case(Decimal::new( 123, -3),   "0.123")]
686    #[case(Decimal::new(-123, -5),  "-0.00123")]
687    #[case(Decimal::new( 123, -5),   "0.00123")]
688    #[case(Decimal::new( 123, -10),  "1.23d-8")]
689    #[case(Decimal::new(-123, -10), "-1.23d-8")]
690    fn test_display(#[case] decimal: Decimal, #[case] expected: &str) {
691        let mut buffer = String::new();
692        write!(buffer, "{decimal}").unwrap();
693        assert_eq!(buffer.as_str(), expected);
694    }
695
696    #[test]
697    fn test_decimal_eq_negative_zeros() {
698        // Decimal zeros of any sign/exponent are mathematically equal.
699        assert_eq!(Decimal::negative_zero(), Decimal::negative_zero());
700        assert_eq!(
701            Decimal::negative_zero_with_exponent(2),
702            Decimal::negative_zero_with_exponent(7)
703        );
704        assert_eq!(
705            Decimal::new(0, 0),
706            Decimal::new(Coefficient::negative_zero(), 0)
707        );
708    }
709
710    #[test]
711    fn test_decimal_ion_eq_negative_zeros() {
712        // To be IonEq, decimal zeros must have the same sign and exponent.
713        assert!(Decimal::negative_zero().ion_eq(&Decimal::negative_zero()));
714        assert!(!Decimal::negative_zero_with_exponent(2)
715            .ion_eq(&Decimal::negative_zero_with_exponent(7)));
716        assert!(!Decimal::new(0, 0).ion_eq(&Decimal::new(Coefficient::negative_zero(), 0)));
717    }
718
719    #[rstest]
720    // Each tuple is a coefficient/exponent pair that will be used to construct a Decimal.
721    // The boolean indicates whether the two Decimals are expected to be equal.
722    #[case((80, 2), (80, 2), true)]
723    #[case((124, -2), (1240, -3), true)]
724    #[case((0, 0), (0, 0), true)]
725    #[case((0, -2), (0, 3), true)]
726    #[case((0, 2), (0, 5), true)]
727    fn test_decimal_eq<I: Into<Coefficient>>(
728        #[case] components1: (I, i64),
729        #[case] components2: (I, i64),
730        #[case] is_equal: bool,
731    ) {
732        let decimal1 = Decimal::new(components1.0.into(), components1.1);
733        let decimal2 = Decimal::new(components2.0.into(), components2.1);
734        assert_eq!(decimal1 == decimal2, is_equal);
735    }
736
737    #[rstest]
738    // Each tuple is a coefficient/exponent pair that will be used to construct a Decimal.
739    // The boolean indicates whether the two Decimals are expected to be Ion-equal.
740    #[case((80, 2), (80, 2), true)]
741    #[case((124, -2), (124, -2), true)]
742    #[case((-124, -2), (-124, -2), true)]
743    #[case((124, -2), (1240, -3), false)]
744    #[case((0, 0), (0, 0), true)]
745    #[case((0, -2), (0, -3), false)]
746    #[case((0, -2), (0, 3), false)]
747    #[case((0, -2), (0, -2), true)]
748    #[case((0, 2), (0, 5), false)]
749    fn test_decimal_ion_eq<I: Into<Coefficient>>(
750        #[case] components1: (I, i64),
751        #[case] components2: (I, i64),
752        #[case] ion_eq_expected: bool,
753    ) {
754        let decimal1 = Decimal::new(components1.0.into(), components1.1);
755        let decimal2 = Decimal::new(components2.0.into(), components2.1);
756        assert_eq!(decimal1.ion_eq(&decimal2), ion_eq_expected);
757    }
758
759    #[rstest]
760    // Each tuple is a coefficient/exponent pair that will be used to construct a Decimal
761    // Positive numbers
762    #[case((80, 3), Ordering::Equal,   (80, 3))]
763    #[case((80, 3), Ordering::Greater, (79, 3))]
764    #[case((80, 3), Ordering::Less,    (81, 3))]
765    #[case((80, 3), Ordering::Greater, (80, 2))]
766    #[case((80, 3), Ordering::Less,    (80, 4))]
767    #[case((80, 3), Ordering::Equal,   (8, 4))]
768    #[case((80, 3), Ordering::Equal,   (800, 2))]
769    // Negative numbers
770    #[case((-80, 3), Ordering::Equal,   (-80, 3))]
771    #[case((-80, 3), Ordering::Less,    (-79, 3))]
772    #[case((-80, 3), Ordering::Greater, (-81, 3))]
773    #[case((-80, 3), Ordering::Less,    (-80, 2))]
774    #[case((-80, 3), Ordering::Greater, (-80, 4))]
775    #[case((-80, 3), Ordering::Equal,   (-8, 4))]
776    #[case((-80, 3), Ordering::Equal,   (-800, 2))]
777    // Positive zeros
778    #[case((0, 3), Ordering::Equal,   (0, 3))]
779    #[case((0, 3), Ordering::Greater, (-1, 3))]
780    #[case((0, 3), Ordering::Less,    (1, 3))]
781    #[case((0, 3), Ordering::Equal,   (0, -2))]
782    #[case((0, 3), Ordering::Equal,   (0, -1))]
783    #[case((0, 3), Ordering::Equal,   (0, 0))]
784    #[case((0, 3), Ordering::Equal,   (0, 1))]
785    #[case((0, 3), Ordering::Equal,   (0, 2))]
786    // Negative zeros
787    #[case((0, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, -1))]
788    #[case((0, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, 0))]
789    #[case((0, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, 1))]
790    #[case((Coefficient::NEGATIVE_ZERO, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, -1))]
791    #[case((Coefficient::NEGATIVE_ZERO, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, 0))]
792    #[case((Coefficient::NEGATIVE_ZERO, 3), Ordering::Equal,   (Coefficient::NEGATIVE_ZERO, 1))]
793    // Other interesting numbers
794    #[case((-1000, -1), Ordering::Less, (-99_999_999_999i64, -9))]
795    #[case((1000, -1), Ordering::Greater, (99_999_999_999i64, -9))]
796    fn test_decimal_ord<A: Into<Coefficient>, B: Into<Coefficient>>(
797        #[case] components1: (A, i64),
798        #[case] ordering: Ordering,
799        #[case] components2: (B, i64),
800    ) {
801        let decimal1 = Decimal::new(components1.0.into(), components1.1);
802        let decimal2 = Decimal::new(components2.0.into(), components2.1);
803        assert_eq!(decimal1.cmp(&decimal2), ordering);
804        // Make sure the inverse relationship holds
805        assert_eq!(decimal2.cmp(&decimal1), ordering.reverse());
806    }
807
808    #[rstest]
809    // Positive integers
810    #[case(i32::MIN as f64, Decimal::from(i32::MIN))]
811    #[case(10.0, Decimal::from(10))]
812    #[case(1.0, Decimal::from(1))]
813    #[case(0.0, Decimal::ZERO)]
814    // The largest positive integer an f64 can precisely represent
815    #[case((2i64.pow(53) - 1) as f64, Decimal::new(2i64.pow(53) - 1, 0))]
816    // Negative integers
817    #[case(-0.0, Decimal::NEGATIVE_ZERO)]
818    #[case(-1.0, Decimal::from(-1))]
819    #[case(-10.0, Decimal::from(-10))]
820    #[case(i32::MAX as f64, Decimal::from(i32::MAX))]
821    // The largest negative integer an f64 can precisely represent
822    #[case((-(2i64.pow(53)) + 1) as f64, Decimal::new(-(2i64.pow(53)) + 1, 0))]
823    // Positive floats
824    #[case(8.67, Decimal::new(867, -2))]
825    #[case(8.6753, Decimal::new(86753, -4))]
826    #[case(8.675309, Decimal::new(8675309, -6))]
827    // Negative float
828    #[case(-8.67, Decimal::new(-867, -2))]
829    #[case(-8.6753, Decimal::new(-86753, -4))]
830    #[case(-8.675309, Decimal::new(-8675309, -6))]
831    // Positive zero-with-fraction
832    #[case(0.2, Decimal::new(2, -1))]
833    #[case(0.24, Decimal::new(24, -2))]
834    #[case(0.246, Decimal::new(246, -3))]
835    #[case(0.24601, Decimal::new(24601, -5))]
836    // Negative zero-with-fraction
837    #[case(-0.2, Decimal::new(-2, -1))]
838    #[case(-0.24, Decimal::new(-24, -2))]
839    #[case(-0.246, Decimal::new(-246, -3))]
840    #[case(-0.24601, Decimal::new(-24601, -5))]
841    // Values with very small magnitudes
842    #[case(0.000_000_000_000_001, Decimal::new(1, -15))]
843    #[case(-0.000_000_000_000_001, Decimal::new(-1, -15))]
844    fn test_decimal_try_from_f64_ok(#[case] value: f64, #[case] expected: Decimal) {
845        let actual: Decimal = value.try_into().unwrap();
846        assert_eq!(
847            actual, expected,
848            "float {value}: actual {actual} != expected {expected}"
849        );
850    }
851
852    #[rstest]
853    #[case::positive_infinity(f64::infinity())]
854    #[case::negative_infinity(f64::neg_infinity())]
855    #[case::nan(f64::nan())]
856    fn test_decimal_try_from_f64_err(#[case] value: f64) {
857        let conversion_result: IonResult<Decimal> = value.try_into();
858        assert!(conversion_result.is_err());
859    }
860
861    #[rstest]
862    #[case(Decimal::new(23, -3), 3)]
863    #[case(Decimal::new(23, -2), 2)]
864    #[case(Decimal::new(23, -1), 1)]
865    #[case(Decimal::new(23, 0), 0)]
866    #[case(Decimal::new(23, 1), -1)]
867    #[case(Decimal::new(23, 2), -2)]
868    #[case(Decimal::new(23, 3), -3)]
869    #[case(Decimal::new(4, 3), -3)]
870    #[case(Decimal::new(40, 3), -3)]
871    #[case(Decimal::new(400, 3), -3)]
872    #[case(Decimal::new(5, -4), 4)]
873    #[case(Decimal::new(50, -4), 4)]
874    #[case(Decimal::new(500, -4), 4)]
875    #[case(Decimal::new(0, 0), 0)]
876    #[case(Decimal::negative_zero(), 0)]
877    #[case(Decimal::negative_zero_with_exponent(1), -1)]
878    #[case(Decimal::negative_zero_with_exponent(2), -2)]
879    #[case(Decimal::new(u64::MAX, -5), 5)]
880    #[case(Decimal::new(u64::MAX, 0), 0)]
881    fn test_scale(#[case] value: Decimal, #[case] expected: i64) {
882        assert_eq!(value.scale(), expected)
883    }
884
885    #[rstest]
886    #[case(Decimal::new(-24600, -3), 5)]
887    #[case(Decimal::new(-24600, -2), 5)]
888    #[case(Decimal::new(-24600, -1), 5)]
889    #[case(Decimal::new(-24600, 0), 5)]
890    #[case(Decimal::new(-24600, 1), 5)]
891    #[case(Decimal::new(-24600, 2), 5)]
892    #[case(Decimal::new(-24600, 3), 5)]
893    #[case(Decimal::new(5, -3), 1)]
894    #[case(Decimal::new(50, -3), 2)]
895    #[case(Decimal::new(500, -3), 3)]
896    #[case(Decimal::new(6, 3), 1)]
897    #[case(Decimal::new(60, 3), 2)]
898    #[case(Decimal::new(600, 3), 3)]
899    #[case(Decimal::new(0, -2), 1)]
900    #[case(Decimal::new(0, -1), 1)]
901    #[case(Decimal::new(0, 0), 1)]
902    #[case(Decimal::new(0, 1), 1)]
903    #[case(Decimal::new(0, 2), 1)]
904    #[case(Decimal::negative_zero_with_exponent(-2), 1)]
905    #[case(Decimal::negative_zero_with_exponent(-1), 1)]
906    #[case(Decimal::negative_zero(), 1)]
907    #[case(Decimal::negative_zero_with_exponent(1), 1)]
908    #[case(Decimal::negative_zero_with_exponent(2), 1)]
909    #[case(Decimal::new(u64::MAX, 3), 20)]
910    #[case(Decimal::new(i128::MAX, -2), 39)]
911    fn test_precision(#[case] value: Decimal, #[case] expected: u64) {
912        assert_eq!(value.precision(), expected);
913    }
914
915    #[rstest]
916    #[case(0, Decimal::new(0, 0))]
917    #[case(1, Decimal::new(1, 0))]
918    #[case(-1, Decimal::new(-1, 0))]
919    #[case(-8675309i64, Decimal::new(-8675309i64, 0))]
920    #[case(8675309u32, Decimal::new(8675309u32, 0))]
921    // mixed coefficient representations
922    #[case(8675309i64, Decimal::new(8675309u32, 0))]
923    #[case(Int::from(-8675309i64), Decimal::new(-8675309i64, 0))]
924    #[case(Int::from(-8675309i128), Decimal::new(-8675309i64, 0))]
925    fn decimal_from_integers(
926        #[case] coefficient: impl Into<Coefficient>,
927        #[case] expected: Decimal,
928    ) {
929        assert_eq!(Decimal::new(coefficient, 0), expected);
930    }
931
932    #[rstest]
933    #[case(Decimal::new(1, 0), Decimal::new(1, 0))]
934    #[case(Decimal::new(15, -1), Decimal::new(1, 0))]
935    #[case(Decimal::new(105, -1), Decimal::new(10, 0))]
936    #[case(Decimal::new(-5, -1), Decimal::new(0, 0))]
937    #[case(Decimal::new(-5, -1), Decimal::NEGATIVE_ZERO)]
938    #[case(Decimal::NEGATIVE_ZERO, Decimal::NEGATIVE_ZERO)]
939    #[case(Decimal::new(0, -5), Decimal::new(0, 0))]
940    #[case(Decimal::new(Coefficient::from_sign_and_value(Sign::Negative, 0), -5), Decimal::new(0, 0))]
941    fn decimal_trunc(#[case] value: Decimal, #[case] expected: Decimal) {
942        assert_eq!(value.trunc(), expected);
943    }
944
945    #[rstest]
946    #[case(Decimal::new(1, 0), Decimal::new(0, 0))]
947    #[case(Decimal::new(15, -1), Decimal::new(5, -1))]
948    #[case(Decimal::new(105, -1), Decimal::new(5, -1))]
949    fn decimal_fract(#[case] value: Decimal, #[case] expected: Decimal) {
950        assert_eq!(value.fract(), expected);
951    }
952
953    #[test]
954    fn decimal_cmp_arbitrary_precision() {
955        use crate::ion_data::IonDataHash;
956        use std::collections::hash_map::DefaultHasher;
957        use std::hash::Hasher;
958
959        // A value that exceeds i128: 2^128 + 42
960        let mut bytes = vec![0u8; 18];
961        bytes[0] = 42;
962        bytes[16] = 1;
963        let big_value = Int::from_le_signed_bytes(&bytes);
964
965        let mut bytes2 = vec![0u8; 18];
966        bytes2[16] = 2;
967        let bigger_value = Int::from_le_signed_bytes(&bytes2);
968
969        let d1 = Decimal::new(Coefficient::new(big_value.clone()), 0i64);
970        let d2 = Decimal::new(Coefficient::new(big_value), 0i64);
971        let small = Decimal::new(1i64, 0i64);
972        let zero = Decimal::new(0i64, 0i64);
973
974        // Equality
975        assert_eq!(d1, d2);
976
977        // Ordering: big > small
978        assert_eq!(d1.cmp(&small), Ordering::Greater);
979        assert_eq!(small.cmp(&d1), Ordering::Less);
980
981        // Ordering: big > zero
982        assert_eq!(d1.cmp(&zero), Ordering::Greater);
983
984        // is_zero
985        assert!(!d1.is_zero());
986        assert!(zero.is_zero());
987
988        // Hash consistency
989        let hash = |d: &Decimal| {
990            let mut h = DefaultHasher::new();
991            d.ion_data_hash(&mut h);
992            h.finish()
993        };
994        assert_eq!(hash(&d1), hash(&d2));
995
996        // Different big magnitudes
997        let d3 = Decimal::new(Coefficient::new(bigger_value), 0i64);
998        assert_eq!(d1.cmp(&d3), Ordering::Less);
999        assert_eq!(d3.cmp(&d1), Ordering::Greater);
1000    }
1001
1002    #[test]
1003    fn compare_decimals_with_large_exponent_difference() {
1004        // 1e40 and 1e0 are both valid Ion decimals. Comparing them requires scaling
1005        // one coefficient by 10^40, which exceeds i128::MAX.
1006        let d1 = Decimal::new(1i64, 40i64);
1007        let d2 = Decimal::new(1i64, 0i64);
1008        assert_eq!(d1.cmp(&d2), Ordering::Greater);
1009    }
1010
1011    #[test]
1012    fn trunc_decimal_with_large_negative_exponent() {
1013        // 1e-40 is a valid Ion decimal. trunc() should return 0.
1014        let d = Decimal::new(1i64, -40i64);
1015        assert_eq!(d.trunc(), Decimal::new(0i64, 0i64));
1016    }
1017
1018    #[test]
1019    fn fract_decimal_with_large_negative_exponent() {
1020        // 1e-39 is a valid Ion decimal. fract() should return the value itself
1021        // since the integer part is zero.
1022        let d = Decimal::new(1i64, -39i64);
1023        assert_eq!(d.fract(), Decimal::new(1i64, -39i64));
1024    }
1025}