Skip to main content

chronix_core/
decimal.rs

1//! Exact fixed-point decimals.
2//!
3//! [`Decimal`] is `mantissa × 10⁻ˢᶜᵃˡᵉ` — an `i128` mantissa and a `u8`
4//! scale, which is `rust_decimal`'s representation and PostgreSQL
5//! `NUMERIC`'s, so a round trip through either is lossless and needs no
6//! dependency on either.
7//!
8//! # Why a database of metrics needs one
9//!
10//! Every other field type in Chronix answers *how much*, approximately: a
11//! temperature, a load average, a byte count. [`FieldValue::F64`](crate::FieldValue::F64) is the
12//! right shape for those, and the compression stack is built around the
13//! observation that most of them started life as decimals.
14//!
15//! A quarter-hour meter register is a different kind of quantity. It is a
16//! *legal* one: a German household settlement under BK 618-25-02 (MiSpeL) is
17//! computed from `Z1NB¼`, `Z2V¼` and their siblings, and a settlement that
18//! went through a `double` is a settlement nobody can reproduce. The same is
19//! true of a price, an invoice line, a tariff step and a regulatory report.
20//! For those, "close enough" is not a weaker answer — it is the wrong one.
21//!
22//! So the exactness is structural, not best-effort:
23//!
24//! - **No `f64` anywhere on the path.** There is no `From<f64>`. The only
25//!   way in is [`Decimal::new`] from an integer mantissa or [`FromStr`] from
26//!   the digits themselves; the only lossy way out is
27//!   [`to_f64_lossy`](Decimal::to_f64_lossy), which says so in its name.
28//! - **Rescaling is lossless or refused.** [`rescale`](Decimal::rescale)
29//!   widens (`1.5` → `1.500`) and narrows only when the digits it drops are
30//!   zeros; otherwise it is an error, never a rounding.
31//! - **One scale per column.** A decimal field's scale is fixed when the
32//!   column is created and every later write is rescaled to it — see
33//!   [`ColumnType::Decimal`](crate::ColumnType::Decimal). A register's scale
34//!   is a property of the series, not of a point.
35//!
36//! # Limits
37//!
38//! Scale ≤ [`MAX_DECIMAL_SCALE`] and |mantissa| ≤ [`MAX_DECIMAL_MANTISSA`],
39//! i.e. 38 significant digits. Both come from Arrow `Decimal128(38, s)`,
40//! which is what a decimal column becomes in a query result and therefore
41//! what DataFusion aggregates exactly. A value that does not fit is rejected
42//! at ingest rather than truncated.
43
44use std::cmp::Ordering;
45use std::fmt;
46use std::str::FromStr;
47
48use serde::{Deserialize, Deserializer, Serialize, Serializer};
49
50/// Largest scale a [`Decimal`] may carry.
51///
52/// 38 is Arrow `Decimal128`'s maximum precision: a column is read back as
53/// `Decimal128(38, scale)`, and a scale above the precision cannot be
54/// represented there.
55pub const MAX_DECIMAL_SCALE: u8 = 38;
56
57/// The precision every decimal column declares in Arrow: `Decimal128(38, s)`.
58///
59/// Fixed rather than derived from the data, because the precision is part of
60/// the Arrow schema and two segments of the same column must produce the
61/// same schema for their batches to concatenate.
62pub const DECIMAL_PRECISION: u8 = 38;
63
64/// Largest absolute mantissa a [`Decimal`] may carry: `10³⁸ − 1`.
65///
66/// One less than `10^DECIMAL_PRECISION`, i.e. 38 significant digits. Arrow
67/// rejects a `Decimal128(38, s)` value outside this range, so accepting one
68/// at ingest would produce a point that can be written and never read.
69pub const MAX_DECIMAL_MANTISSA: i128 = 99_999_999_999_999_999_999_999_999_999_999_999_999;
70
71/// Powers of ten up to `10³⁸`, for scaling without repeated multiplication.
72const POW10: [i128; 39] = {
73    let mut table = [1i128; 39];
74    let mut i = 1;
75    while i < 39 {
76        table[i] = table[i - 1] * 10;
77        i += 1;
78    }
79    table
80};
81
82/// Ten raised to `exp`, or `None` when `exp > 38`.
83///
84/// Public because rescaling a mantissa is not only this module's job: the
85/// query layer aligns two decimal columns, and the segment writer aligns a
86/// value to its column. One table, so they cannot disagree.
87#[inline]
88#[must_use]
89pub fn pow10(exp: u32) -> Option<i128> {
90    POW10.get(exp as usize).copied()
91}
92
93/// Why a [`Decimal`] could not be constructed, parsed or rescaled.
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95pub enum DecimalError {
96    /// The scale exceeds [`MAX_DECIMAL_SCALE`].
97    #[error("decimal scale {scale} exceeds the maximum of {max}")]
98    ScaleTooLarge {
99        /// The requested scale.
100        scale: u32,
101        /// [`MAX_DECIMAL_SCALE`].
102        max: u8,
103    },
104
105    /// The mantissa needs more than [`DECIMAL_PRECISION`] significant digits.
106    #[error("decimal mantissa {mantissa} exceeds {digits} significant digits")]
107    PrecisionOverflow {
108        /// The offending mantissa.
109        mantissa: i128,
110        /// [`DECIMAL_PRECISION`].
111        digits: u8,
112    },
113
114    /// Narrowing the scale would have dropped a non-zero digit.
115    #[error("cannot rescale {value} from scale {from} to scale {to} without losing digits")]
116    InexactRescale {
117        /// The value that could not be rescaled, rendered exactly.
118        value: String,
119        /// Its current scale.
120        from: u8,
121        /// The requested scale.
122        to: u8,
123    },
124
125    /// An arithmetic result does not fit in 38 significant digits.
126    #[error("decimal arithmetic overflowed 38 significant digits")]
127    Overflow,
128
129    /// The text is not a decimal literal.
130    #[error("invalid decimal literal \"{input}\": {reason}")]
131    Parse {
132        /// The text that failed to parse.
133        input: String,
134        /// What was wrong with it.
135        reason: &'static str,
136    },
137}
138
139/// An exact fixed-point decimal: `mantissa × 10⁻ˢᶜᵃˡᵉ`.
140///
141/// See the [module documentation](self) for why the type exists and what it
142/// guarantees. Construct with [`Decimal::new`] or by parsing the digits:
143///
144/// ```
145/// use chronix_core::Decimal;
146///
147/// let register: Decimal = "1234.5678".parse().unwrap();
148/// assert_eq!(register.mantissa(), 12_345_678);
149/// assert_eq!(register.scale(), 4);
150/// assert_eq!(register.to_string(), "1234.5678");
151///
152/// // Widening is exact; narrowing past a non-zero digit is refused.
153/// assert_eq!(register.rescale(6).unwrap().to_string(), "1234.567800");
154/// assert!(register.rescale(2).is_err());
155/// ```
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157pub struct Decimal {
158    /// The unscaled integer value.
159    mantissa: i128,
160    /// The number of digits after the decimal point.
161    scale: u8,
162}
163
164impl Decimal {
165    /// Construct from an unscaled mantissa and a scale.
166    ///
167    /// # Errors
168    ///
169    /// [`DecimalError::ScaleTooLarge`] if `scale > MAX_DECIMAL_SCALE`, and
170    /// [`DecimalError::PrecisionOverflow`] if the mantissa needs more than
171    /// 38 significant digits.
172    pub const fn new(mantissa: i128, scale: u8) -> Result<Self, DecimalError> {
173        if scale > MAX_DECIMAL_SCALE {
174            return Err(DecimalError::ScaleTooLarge {
175                scale: scale as u32,
176                max: MAX_DECIMAL_SCALE,
177            });
178        }
179        if mantissa > MAX_DECIMAL_MANTISSA || mantissa < -MAX_DECIMAL_MANTISSA {
180            return Err(DecimalError::PrecisionOverflow {
181                mantissa,
182                digits: DECIMAL_PRECISION,
183            });
184        }
185        Ok(Self { mantissa, scale })
186    }
187
188    /// Zero at the given scale.
189    ///
190    /// # Errors
191    ///
192    /// [`DecimalError::ScaleTooLarge`] if `scale > MAX_DECIMAL_SCALE`.
193    pub const fn zero(scale: u8) -> Result<Self, DecimalError> {
194        Self::new(0, scale)
195    }
196
197    /// The unscaled integer value.
198    #[inline]
199    #[must_use]
200    pub const fn mantissa(&self) -> i128 {
201        self.mantissa
202    }
203
204    /// The number of digits after the decimal point.
205    #[inline]
206    #[must_use]
207    pub const fn scale(&self) -> u8 {
208        self.scale
209    }
210
211    /// `true` when the value is exactly zero, at any scale.
212    #[inline]
213    #[must_use]
214    pub const fn is_zero(&self) -> bool {
215        self.mantissa == 0
216    }
217
218    /// `-1`, `0` or `1` — the sign of the value.
219    #[inline]
220    #[must_use]
221    pub const fn signum(&self) -> i8 {
222        if self.mantissa > 0 {
223            1
224        } else if self.mantissa < 0 {
225            -1
226        } else {
227            0
228        }
229    }
230
231    /// The absolute value, at the same scale.
232    ///
233    /// Total: `|mantissa|` cannot overflow because the mantissa is bounded
234    /// by [`MAX_DECIMAL_MANTISSA`], well inside `i128::MIN`.
235    #[inline]
236    #[must_use]
237    pub const fn abs(&self) -> Self {
238        Self {
239            mantissa: self.mantissa.abs(),
240            scale: self.scale,
241        }
242    }
243
244    /// The negation, at the same scale.
245    #[inline]
246    #[must_use]
247    pub const fn neg(&self) -> Self {
248        Self {
249            mantissa: -self.mantissa,
250            scale: self.scale,
251        }
252    }
253
254    /// Return the same value expressed at `target` scale.
255    ///
256    /// Widening multiplies by a power of ten and is always exact. Narrowing
257    /// succeeds only when every digit it would drop is a zero — `1.500` to
258    /// scale 1 is `1.5`, `1.55` to scale 1 is an error, never `1.6` and
259    /// never `1.5`.
260    ///
261    /// # Errors
262    ///
263    /// - [`DecimalError::ScaleTooLarge`] if `target > MAX_DECIMAL_SCALE`.
264    /// - [`DecimalError::InexactRescale`] if narrowing would drop a
265    ///   non-zero digit.
266    /// - [`DecimalError::PrecisionOverflow`] if widening pushes the mantissa
267    ///   past 38 significant digits.
268    pub fn rescale(&self, target: u8) -> Result<Self, DecimalError> {
269        if target > MAX_DECIMAL_SCALE {
270            return Err(DecimalError::ScaleTooLarge {
271                scale: u32::from(target),
272                max: MAX_DECIMAL_SCALE,
273            });
274        }
275        match target.cmp(&self.scale) {
276            Ordering::Equal => Ok(*self),
277            Ordering::Greater => {
278                let factor = pow10(u32::from(target - self.scale)).ok_or(DecimalError::Overflow)?;
279                let mantissa = self
280                    .mantissa
281                    .checked_mul(factor)
282                    .ok_or(DecimalError::Overflow)?;
283                Self::new(mantissa, target)
284            }
285            Ordering::Less => {
286                let factor = pow10(u32::from(self.scale - target)).ok_or(DecimalError::Overflow)?;
287                if self.mantissa % factor != 0 {
288                    return Err(DecimalError::InexactRescale {
289                        value: self.to_string(),
290                        from: self.scale,
291                        to: target,
292                    });
293                }
294                Self::new(self.mantissa / factor, target)
295            }
296        }
297    }
298
299    /// Drop trailing fractional zeros, down to `min_scale`.
300    ///
301    /// `1.2500` normalised to a minimum scale of 0 is `1.25`; to a minimum
302    /// of 3 it is `1.250`. Never changes the value.
303    #[must_use]
304    pub fn normalized(&self, min_scale: u8) -> Self {
305        let mut out = *self;
306        while out.scale > min_scale && out.mantissa % 10 == 0 && out.mantissa != 0 {
307            out.mantissa /= 10;
308            out.scale -= 1;
309        }
310        // A zero mantissa carries no digits to strip; take it straight down.
311        if out.mantissa == 0 && out.scale > min_scale {
312            out.scale = min_scale;
313        }
314        out
315    }
316
317    /// Exact sum, at the wider of the two scales.
318    ///
319    /// # Errors
320    ///
321    /// [`DecimalError::Overflow`] if the result needs more than 38
322    /// significant digits.
323    pub fn checked_add(&self, other: &Self) -> Result<Self, DecimalError> {
324        let scale = self.scale.max(other.scale);
325        let a = self.rescale(scale)?;
326        let b = other.rescale(scale)?;
327        let mantissa = a
328            .mantissa
329            .checked_add(b.mantissa)
330            .ok_or(DecimalError::Overflow)?;
331        Self::new(mantissa, scale).map_err(|_| DecimalError::Overflow)
332    }
333
334    /// Exact difference, at the wider of the two scales.
335    ///
336    /// # Errors
337    ///
338    /// [`DecimalError::Overflow`] if the result needs more than 38
339    /// significant digits.
340    pub fn checked_sub(&self, other: &Self) -> Result<Self, DecimalError> {
341        self.checked_add(&other.neg())
342    }
343
344    /// The value as an `f64` — **lossy**, and named so at every call site.
345    ///
346    /// The only conversion to binary floating point this type offers. A
347    /// settlement quantity must not travel this way; a chart of one may.
348    #[inline]
349    #[must_use]
350    #[allow(clippy::cast_precision_loss)]
351    pub fn to_f64_lossy(&self) -> f64 {
352        self.mantissa as f64 / pow10(u32::from(self.scale)).unwrap_or(1) as f64
353    }
354
355    /// Compare against another decimal of any scale, exactly.
356    ///
357    /// Aligning two mantissas at different scales can overflow `i128` —
358    /// `10³⁷` at scale 0 against anything at scale 38 does. An overflow is
359    /// not an error here but an answer: the side that cannot be represented
360    /// at the common scale is the side with the larger magnitude, because
361    /// the other one *is* representable there.
362    #[must_use]
363    pub fn cmp_exact(&self, other: &Self) -> Ordering {
364        if self.scale == other.scale {
365            return self.mantissa.cmp(&other.mantissa);
366        }
367        // Different signs (or a zero) decide it without any scaling.
368        let sign = self.signum().cmp(&other.signum());
369        if sign != Ordering::Equal {
370            return sign;
371        }
372        let scale = self.scale.max(other.scale);
373        match (self.rescale(scale), other.rescale(scale)) {
374            (Ok(a), Ok(b)) => a.mantissa.cmp(&b.mantissa),
375            // `self` overflowed the common scale and `other` did not, so
376            // |self| > |other|; the sign then decides the direction.
377            (Err(_), Ok(_)) => {
378                if self.signum() >= 0 {
379                    Ordering::Greater
380                } else {
381                    Ordering::Less
382                }
383            }
384            (Ok(_), Err(_)) => {
385                if self.signum() >= 0 {
386                    Ordering::Less
387                } else {
388                    Ordering::Greater
389                }
390            }
391            // Both overflow: compare at the narrower scale instead, where at
392            // least one of them is exact by construction.
393            (Err(_), Err(_)) => {
394                let narrow = self.scale.min(other.scale);
395                let a = self.mantissa / pow10(u32::from(self.scale - narrow)).unwrap_or(1);
396                let b = other.mantissa / pow10(u32::from(other.scale - narrow)).unwrap_or(1);
397                a.cmp(&b)
398            }
399        }
400    }
401}
402
403impl PartialOrd for Decimal {
404    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
405        Some(self.cmp(other))
406    }
407}
408
409impl Ord for Decimal {
410    fn cmp(&self, other: &Self) -> Ordering {
411        self.cmp_exact(other)
412    }
413}
414
415impl fmt::Display for Decimal {
416    /// Render the exact digits: no exponent, no rounding, no `f64`.
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        if self.scale == 0 {
419            return write!(f, "{}", self.mantissa);
420        }
421        let negative = self.mantissa < 0;
422        // `unsigned_abs` rather than `abs`: the mantissa is bounded well
423        // inside `i128::MIN`, but the unsigned form makes that irrelevant.
424        let digits = self.mantissa.unsigned_abs().to_string();
425        let scale = usize::from(self.scale);
426        let (int_part, frac_part) = if digits.len() > scale {
427            let split = digits.len() - scale;
428            (digits[..split].to_string(), digits[split..].to_string())
429        } else {
430            ("0".to_string(), format!("{:0>scale$}", digits))
431        };
432        if negative {
433            write!(f, "-")?;
434        }
435        write!(f, "{int_part}.{frac_part}")
436    }
437}
438
439impl FromStr for Decimal {
440    type Err = DecimalError;
441
442    /// Parse the digits themselves — never via `f64`.
443    ///
444    /// Accepts an optional sign, digits with an optional fractional part,
445    /// and an optional decimal exponent (`1.5e3`). The scale is the number
446    /// of fractional digits less the exponent; a literal that lands on a
447    /// scale above [`MAX_DECIMAL_SCALE`] is accepted only if the excess
448    /// digits are trailing zeros that can be dropped without changing the
449    /// value.
450    fn from_str(s: &str) -> Result<Self, Self::Err> {
451        let err = |reason: &'static str| DecimalError::Parse {
452            input: s.to_string(),
453            reason,
454        };
455        if s.is_empty() {
456            return Err(err("empty"));
457        }
458
459        let (mantissa_str, exponent) = match s.find(['e', 'E']) {
460            Some(idx) => {
461                let exp = s[idx + 1..]
462                    .parse::<i32>()
463                    .map_err(|_| err("malformed exponent"))?;
464                if !(-1_000..=1_000).contains(&exp) {
465                    return Err(err("exponent out of range"));
466                }
467                (&s[..idx], exp)
468            }
469            None => (s, 0),
470        };
471
472        let (negative, digits) = match mantissa_str.as_bytes().first() {
473            Some(b'-') => (true, &mantissa_str[1..]),
474            Some(b'+') => (false, &mantissa_str[1..]),
475            _ => (false, mantissa_str),
476        };
477        if digits.is_empty() {
478            return Err(err("no digits"));
479        }
480
481        let (int_digits, mut frac_digits) = match digits.find('.') {
482            Some(idx) => {
483                let frac = &digits[idx + 1..];
484                if frac.contains('.') {
485                    return Err(err("more than one decimal point"));
486                }
487                (&digits[..idx], frac)
488            }
489            None => (digits, ""),
490        };
491        if int_digits.is_empty() && frac_digits.is_empty() {
492            return Err(err("no digits"));
493        }
494
495        // A literal with more fractional digits than the maximum scale is
496        // not automatically out of range: `0.10000…0` carries no information
497        // in its tail. Drop trailing zeros — and only zeros, and only as far
498        // as the maximum scale — before the mantissa is accumulated, so a
499        // representable value is not refused for an overflow it never had.
500        while i64::try_from(frac_digits.len()).map_err(|_| err("too many fractional digits"))?
501            - i64::from(exponent)
502            > i64::from(MAX_DECIMAL_SCALE)
503            && frac_digits.ends_with('0')
504        {
505            frac_digits = &frac_digits[..frac_digits.len() - 1];
506        }
507
508        let mut mantissa: i128 = 0;
509        for part in [int_digits, frac_digits] {
510            for byte in part.bytes() {
511                let digit = match byte {
512                    b'0'..=b'9' => i128::from(byte - b'0'),
513                    _ => return Err(err("non-digit character")),
514                };
515                mantissa = mantissa
516                    .checked_mul(10)
517                    .and_then(|m| m.checked_add(digit))
518                    .ok_or(DecimalError::PrecisionOverflow {
519                        mantissa: i128::MAX,
520                        digits: DECIMAL_PRECISION,
521                    })?;
522            }
523        }
524        if negative {
525            mantissa = -mantissa;
526        }
527
528        // The literal's scale before any normalisation. `frac_digits.len()`
529        // fits an i32 for any input a caller can hold in memory, but the
530        // subtraction is checked anyway so a pathological literal is a
531        // parse error rather than a panic.
532        let raw_scale = i64::try_from(frac_digits.len())
533            .map_err(|_| err("too many fractional digits"))?
534            - i64::from(exponent);
535
536        if raw_scale < 0 {
537            // A negative scale means the value is an integer with trailing
538            // zeros: fold them into the mantissa and land on scale 0.
539            let shift = u32::try_from(-raw_scale).map_err(|_| err("exponent out of range"))?;
540            let factor = pow10(shift).ok_or(DecimalError::Overflow)?;
541            let mantissa = mantissa.checked_mul(factor).ok_or(DecimalError::Overflow)?;
542            return Self::new(mantissa, 0);
543        }
544
545        if raw_scale > i64::from(MAX_DECIMAL_SCALE) {
546            // Every trailing zero that could have been dropped already was,
547            // so what is left is a digit that would have to be rounded away.
548            return Err(DecimalError::ScaleTooLarge {
549                scale: u32::try_from(raw_scale).unwrap_or(u32::MAX),
550                max: MAX_DECIMAL_SCALE,
551            });
552        }
553
554        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
555        Self::new(mantissa, raw_scale as u8)
556    }
557}
558
559impl Serialize for Decimal {
560    /// Exact in both directions: the digits as a string for JSON and any
561    /// other human-readable format, the `(mantissa, scale)` pair for
562    /// postcard and the rest of the binary ones.
563    ///
564    /// A decimal must never reach JSON as a JSON number: `serde_json`
565    /// parses one back through `f64`, which is the loss this type exists to
566    /// prevent.
567    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
568        if serializer.is_human_readable() {
569            serializer.serialize_str(&self.to_string())
570        } else {
571            let repr = DecimalRepr {
572                mantissa: self.mantissa,
573                scale: self.scale,
574            };
575            repr.serialize(serializer)
576        }
577    }
578}
579
580impl<'de> Deserialize<'de> for Decimal {
581    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
582        if deserializer.is_human_readable() {
583            let text = String::deserialize(deserializer)?;
584            text.parse().map_err(serde::de::Error::custom)
585        } else {
586            let repr = DecimalRepr::deserialize(deserializer)?;
587            Self::new(repr.mantissa, repr.scale).map_err(serde::de::Error::custom)
588        }
589    }
590}
591
592/// The binary serde shape: exactly the two fields, nothing derived.
593#[derive(Serialize, Deserialize)]
594struct DecimalRepr {
595    mantissa: i128,
596    scale: u8,
597}
598
599/// Conversions to and from `rust_decimal::Decimal`.
600///
601/// Enabled by the `rust_decimal` feature. Both directions are exact
602/// wherever they are possible at all:
603///
604/// - **In**: `rust_decimal` is a 96-bit mantissa with scale `0..=28`, which
605///   fits inside this type's 128 bits and scale `0..=38` with room to
606///   spare. It cannot fail, and the `TryFrom` bound is there only because
607///   the two ranges are not the same type.
608/// - **Out**: a value with more than 96 bits of mantissa, or a scale above
609///   28, has no `rust_decimal` representation. That is a genuine failure and
610///   is reported rather than rounded.
611///
612/// Without the feature the conversion is two lines, because the
613/// representation is the same on both sides:
614///
615/// ```ignore
616/// let chronix = chronix_core::Decimal::new(d.mantissa(), d.scale() as u8)?;
617/// let back = rust_decimal::Decimal::try_from_i128_with_scale(
618///     chronix.mantissa(), u32::from(chronix.scale()))?;
619/// ```
620#[cfg(feature = "rust_decimal")]
621mod rust_decimal_interop {
622    use super::{Decimal, DecimalError, DECIMAL_PRECISION, MAX_DECIMAL_SCALE};
623
624    impl TryFrom<rust_decimal::Decimal> for Decimal {
625        type Error = DecimalError;
626
627        fn try_from(value: rust_decimal::Decimal) -> Result<Self, Self::Error> {
628            let scale = u8::try_from(value.scale()).map_err(|_| DecimalError::ScaleTooLarge {
629                scale: value.scale(),
630                max: MAX_DECIMAL_SCALE,
631            })?;
632            Self::new(value.mantissa(), scale)
633        }
634    }
635
636    impl TryFrom<Decimal> for rust_decimal::Decimal {
637        type Error = DecimalError;
638
639        fn try_from(value: Decimal) -> Result<Self, Self::Error> {
640            Self::try_from_i128_with_scale(value.mantissa(), u32::from(value.scale())).map_err(
641                |_| DecimalError::PrecisionOverflow {
642                    mantissa: value.mantissa(),
643                    digits: DECIMAL_PRECISION,
644                },
645            )
646        }
647    }
648
649    #[cfg(test)]
650    mod tests {
651        use super::*;
652        use std::str::FromStr;
653
654        #[test]
655        fn round_trip_through_rust_decimal() {
656            for text in ["0", "231.45", "-231.45", "1234.5678", "0.0000000001"] {
657                let theirs = rust_decimal::Decimal::from_str(text).unwrap();
658                let ours = Decimal::try_from(theirs).unwrap();
659                assert_eq!(ours.to_string(), text);
660                let back = rust_decimal::Decimal::try_from(ours).unwrap();
661                assert_eq!(back, theirs);
662            }
663        }
664
665        #[test]
666        fn a_value_too_wide_for_rust_decimal_is_refused_not_rounded() {
667            // 38 digits: representable here, not in a 96-bit mantissa.
668            let ours: Decimal = "99999999999999999999999999999999999999".parse().unwrap();
669            assert!(rust_decimal::Decimal::try_from(ours).is_err());
670        }
671
672        #[test]
673        fn a_scale_above_28_is_refused() {
674            let ours = Decimal::new(1, 30).unwrap();
675            assert!(rust_decimal::Decimal::try_from(ours).is_err());
676        }
677    }
678}
679
680impl TryFrom<i64> for Decimal {
681    type Error = DecimalError;
682
683    /// An integer is a decimal of scale 0.
684    fn try_from(value: i64) -> Result<Self, Self::Error> {
685        Self::new(i128::from(value), 0)
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692
693    #[test]
694    fn parse_and_render_round_trip() {
695        for text in [
696            "0",
697            "0.0",
698            "231.45",
699            "-231.45",
700            "1234.5678",
701            "-0.001",
702            "0.00000000000000000001",
703            "99999999999999999999999999999999999999",
704        ] {
705            let d: Decimal = text.parse().unwrap();
706            assert_eq!(d.to_string(), text, "round trip of {text}");
707        }
708    }
709
710    #[test]
711    fn parse_keeps_every_digit_an_f64_would_lose() {
712        // 0.1 + 0.2 in binary floating point is 0.30000000000000004; the
713        // whole point of this type is that these digits survive.
714        let d: Decimal = "0.30000000000000004".parse().unwrap();
715        assert_eq!(d.mantissa(), 30_000_000_000_000_004);
716        assert_eq!(d.scale(), 17);
717    }
718
719    #[test]
720    fn parse_scale_and_sign() {
721        let d: Decimal = "231.45".parse().unwrap();
722        assert_eq!(d.mantissa(), 23_145);
723        assert_eq!(d.scale(), 2);
724        let d: Decimal = "-0.05".parse().unwrap();
725        assert_eq!(d.mantissa(), -5);
726        assert_eq!(d.scale(), 2);
727        let d: Decimal = "+7".parse().unwrap();
728        assert_eq!(d.mantissa(), 7);
729        assert_eq!(d.scale(), 0);
730    }
731
732    #[test]
733    fn parse_exponent() {
734        assert_eq!("1.5e3".parse::<Decimal>().unwrap().to_string(), "1500");
735        assert_eq!("15e-3".parse::<Decimal>().unwrap().to_string(), "0.015");
736        assert_eq!("1E2".parse::<Decimal>().unwrap().to_string(), "100");
737    }
738
739    #[test]
740    fn parse_rejects_junk() {
741        for bad in [
742            "", "-", ".", "1.2.3", "1,5", "abc", "1.2x", "1e", "1e999999",
743        ] {
744            assert!(bad.parse::<Decimal>().is_err(), "{bad} should not parse");
745        }
746    }
747
748    #[test]
749    fn parse_bare_fraction() {
750        let d: Decimal = ".5".parse().unwrap();
751        assert_eq!(d.to_string(), "0.5");
752    }
753
754    #[test]
755    fn parse_overflow_is_rejected_not_wrapped() {
756        let too_big = "1".repeat(40);
757        assert!(too_big.parse::<Decimal>().is_err());
758    }
759
760    #[test]
761    fn parse_strips_representable_trailing_zeros_beyond_max_scale() {
762        // 40 fractional digits whose tail is zeros: representable at 38.
763        let text = format!("0.{}", "1".to_string() + &"0".repeat(39));
764        let d: Decimal = text.parse().unwrap();
765        assert_eq!(d.scale(), MAX_DECIMAL_SCALE);
766        assert_eq!(d.mantissa(), 10_i128.pow(37));
767        // 41 fractional digits with a non-zero tail: not representable.
768        let text = format!("0.{}1", "0".repeat(40));
769        assert!(text.parse::<Decimal>().is_err());
770    }
771
772    #[test]
773    fn rescale_widens_exactly_and_refuses_to_round() {
774        let d: Decimal = "1.5".parse().unwrap();
775        assert_eq!(d.rescale(4).unwrap().to_string(), "1.5000");
776        assert_eq!(d.rescale(4).unwrap().rescale(1).unwrap(), d);
777        let d: Decimal = "1.55".parse().unwrap();
778        assert!(matches!(
779            d.rescale(1),
780            Err(DecimalError::InexactRescale { .. })
781        ));
782    }
783
784    #[test]
785    fn rescale_rejects_scale_above_max() {
786        let d: Decimal = "1".parse().unwrap();
787        assert!(matches!(
788            d.rescale(MAX_DECIMAL_SCALE + 1),
789            Err(DecimalError::ScaleTooLarge { .. })
790        ));
791    }
792
793    #[test]
794    fn new_rejects_out_of_range() {
795        assert!(matches!(
796            Decimal::new(1, MAX_DECIMAL_SCALE + 1),
797            Err(DecimalError::ScaleTooLarge { .. })
798        ));
799        assert!(matches!(
800            Decimal::new(MAX_DECIMAL_MANTISSA + 1, 0),
801            Err(DecimalError::PrecisionOverflow { .. })
802        ));
803        assert!(matches!(
804            Decimal::new(-MAX_DECIMAL_MANTISSA - 1, 0),
805            Err(DecimalError::PrecisionOverflow { .. })
806        ));
807    }
808
809    #[test]
810    fn arithmetic_is_exact() {
811        let a: Decimal = "0.1".parse().unwrap();
812        let b: Decimal = "0.2".parse().unwrap();
813        assert_eq!(a.checked_add(&b).unwrap().to_string(), "0.3");
814        let a: Decimal = "1.5".parse().unwrap();
815        let b: Decimal = "0.250".parse().unwrap();
816        assert_eq!(a.checked_add(&b).unwrap().to_string(), "1.750");
817        assert_eq!(a.checked_sub(&b).unwrap().to_string(), "1.250");
818    }
819
820    #[test]
821    fn addition_overflow_is_an_error() {
822        let a = Decimal::new(MAX_DECIMAL_MANTISSA, 0).unwrap();
823        assert!(matches!(a.checked_add(&a), Err(DecimalError::Overflow)));
824    }
825
826    #[test]
827    fn ordering_across_scales() {
828        let a: Decimal = "1.5".parse().unwrap();
829        let b: Decimal = "1.50".parse().unwrap();
830        assert_eq!(a.cmp_exact(&b), Ordering::Equal);
831        let c: Decimal = "1.51".parse().unwrap();
832        assert_eq!(a.cmp_exact(&c), Ordering::Less);
833        let neg: Decimal = "-1.5".parse().unwrap();
834        assert_eq!(neg.cmp_exact(&a), Ordering::Less);
835        assert_eq!(neg.cmp_exact(&neg), Ordering::Equal);
836    }
837
838    #[test]
839    fn ordering_when_alignment_overflows() {
840        // 10³⁷ at scale 0 cannot be expressed at scale 38, but it is still
841        // the larger of the two.
842        let huge = Decimal::new(POW10[37], 0).unwrap();
843        let tiny = Decimal::new(1, MAX_DECIMAL_SCALE).unwrap();
844        assert_eq!(huge.cmp_exact(&tiny), Ordering::Greater);
845        assert_eq!(tiny.cmp_exact(&huge), Ordering::Less);
846        let huge_neg = huge.neg();
847        assert_eq!(huge_neg.cmp_exact(&tiny), Ordering::Less);
848        assert_eq!(tiny.cmp_exact(&huge_neg), Ordering::Greater);
849    }
850
851    #[test]
852    fn ordering_is_a_total_order_on_a_mixed_scale_sample() {
853        let mut values: Vec<Decimal> = [
854            "-2", "-1.5", "-1.50", "0", "0.000", "0.5", "1", "1.0000", "1.5", "2",
855        ]
856        .iter()
857        .map(|s| s.parse().unwrap())
858        .collect();
859        values.sort();
860        let rendered: Vec<String> = values.iter().map(ToString::to_string).collect();
861        assert_eq!(
862            rendered,
863            ["-2", "-1.5", "-1.50", "0", "0.000", "0.5", "1", "1.0000", "1.5", "2"]
864                .iter()
865                .map(ToString::to_string)
866                .collect::<Vec<_>>()
867        );
868    }
869
870    #[test]
871    fn normalized_strips_only_trailing_zeros() {
872        let d: Decimal = "1.2500".parse().unwrap();
873        assert_eq!(d.normalized(0).to_string(), "1.25");
874        assert_eq!(d.normalized(3).to_string(), "1.250");
875        let z: Decimal = "0.0000".parse().unwrap();
876        assert_eq!(z.normalized(0).to_string(), "0");
877    }
878
879    #[test]
880    fn json_is_a_string_and_survives_the_round_trip() {
881        let d: Decimal = "1234.5678".parse().unwrap();
882        let json = serde_json::to_string(&d).unwrap();
883        assert_eq!(json, "\"1234.5678\"");
884        let back: Decimal = serde_json::from_str(&json).unwrap();
885        assert_eq!(back, d);
886    }
887
888    #[test]
889    fn postcard_round_trip_is_the_mantissa_and_scale() {
890        let d: Decimal = "1234.5678".parse().unwrap();
891        let bytes = postcard::to_stdvec(&d).unwrap();
892        let back: Decimal = postcard::from_bytes(&bytes).unwrap();
893        assert_eq!(back, d);
894        assert_eq!(back.scale(), 4);
895    }
896
897    #[test]
898    fn to_f64_lossy_is_the_only_way_out() {
899        let d: Decimal = "231.45".parse().unwrap();
900        assert!((d.to_f64_lossy() - 231.45).abs() < 1e-9);
901    }
902
903    #[test]
904    fn signum_and_abs() {
905        let d: Decimal = "-1.5".parse().unwrap();
906        assert_eq!(d.signum(), -1);
907        assert_eq!(d.abs().to_string(), "1.5");
908        let z: Decimal = "0.00".parse().unwrap();
909        assert_eq!(z.signum(), 0);
910        assert!(z.is_zero());
911    }
912}