Skip to main content

icydb_schema/decimal/
arithmetic.rs

1use crate::decimal::{DEFAULT_DIVISION_SCALE, Decimal, MAX_SUPPORTED_SCALE};
2use ethnum::I256;
3use std::{
4    iter::{Product, Sum},
5    ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign},
6};
7
8impl Decimal {
9    fn checked_add_impl(self, rhs: Self) -> Option<Self> {
10        let target_scale = self.scale.max(rhs.scale);
11        let lhs = Self::align_to_scale(self.mantissa, self.scale, target_scale)?;
12        let rhs = Self::align_to_scale(rhs.mantissa, rhs.scale, target_scale)?;
13
14        Self::fit_wide_mantissa(lhs.checked_add(rhs)?, target_scale)
15    }
16
17    /// Checked addition rounds half away from zero to the greatest fitting
18    /// scale, starting at the greater operand scale. Returns `None` only when
19    /// the rounded magnitude cannot fit even at scale zero.
20    #[must_use]
21    pub fn checked_add(self, rhs: Self) -> Option<Self> {
22        self.checked_add_impl(rhs)
23    }
24
25    /// Checked subtraction uses the same fitting/rounding policy as addition.
26    /// Returns `None` when the rounded magnitude cannot fit at scale zero.
27    #[must_use]
28    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
29        let target_scale = self.scale.max(rhs.scale);
30        let lhs = Self::align_to_scale(self.mantissa, self.scale, target_scale)?;
31        let rhs = Self::align_to_scale(rhs.mantissa, rhs.scale, target_scale)?;
32        Self::fit_wide_mantissa(lhs.checked_sub(rhs)?, target_scale)
33    }
34
35    /// Checked multiplication normalizes operand padding and rounds half away
36    /// from zero to the greatest representable scale, at most 28. Returns `None`
37    /// when the rounded magnitude cannot fit even at scale zero.
38    #[must_use]
39    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
40        self.checked_mul_impl(rhs)
41    }
42
43    /// Checked division rounds half away from zero to the greatest fitting
44    /// scale, at most 18. Returns `None` when the divisor is zero or the rounded
45    /// magnitude cannot fit even at scale zero.
46    #[must_use]
47    pub fn checked_div(self, rhs: Self) -> Option<Self> {
48        self.checked_div_impl(rhs)
49    }
50
51    fn checked_mul_impl(self, rhs: Self) -> Option<Self> {
52        // Accepted fixed-scale fields retain padding that must not consume
53        // multiplication precision or cause avoidable intermediate overflow.
54        let lhs = self.normalize();
55        let rhs = rhs.normalize();
56        let scale = lhs.scale.checked_add(rhs.scale)?;
57        // Two i128 mantissas fit an exact I256 product. Keep that product until
58        // the final scale is known, so retries never double-round a value.
59        let product = I256::from(lhs.mantissa).checked_mul(I256::from(rhs.mantissa))?;
60        Self::fit_wide_mantissa(product, scale)
61    }
62
63    // Reduce precision from the original exact mantissa, never a rounded retry.
64    // Addition/subtraction and multiplication share this final fitting policy.
65    fn fit_wide_mantissa(mantissa: I256, scale: u32) -> Option<Self> {
66        let mut result_scale = scale.min(MAX_SUPPORTED_SCALE);
67        let mut divisor = I256::new(10).checked_pow(scale - result_scale)?;
68        loop {
69            if let Some(mantissa) = Self::div_round_half_away_from_zero(mantissa, divisor) {
70                return Some(Self {
71                    mantissa,
72                    scale: result_scale,
73                });
74            }
75            if result_scale == 0 {
76                return None;
77            }
78            result_scale -= 1;
79            divisor = divisor.checked_mul(I256::new(10))?;
80        }
81    }
82
83    fn checked_div_impl(self, rhs: Self) -> Option<Self> {
84        if rhs.is_zero() {
85            return None;
86        }
87
88        let lhs = self.normalize();
89        let rhs = rhs.normalize();
90        let mut target_scale = DEFAULT_DIVISION_SCALE;
91
92        // An overflowing wide numerator implies the quotient cannot fit at
93        // this scale: its unscaled denominator is at most an i128 magnitude.
94        // Retry from the original operands when scaling or rounding cannot fit.
95        loop {
96            if let Some((numerator, denominator)) = Self::division_operands(lhs, rhs, target_scale)
97                && let Some(mantissa) = Self::div_round_half_away_from_zero(numerator, denominator)
98            {
99                return Some(
100                    Self {
101                        mantissa,
102                        scale: target_scale,
103                    }
104                    .normalize(),
105                );
106            }
107
108            if target_scale == 0 {
109                return None;
110            }
111
112            target_scale -= 1;
113        }
114    }
115
116    fn checked_rem_impl(self, rhs: Self) -> Option<Self> {
117        if rhs.is_zero() {
118            return None;
119        }
120
121        let target_scale = self.scale.max(rhs.scale);
122        // Scale differences are at most 28, so both alignment products fit
123        // I256. One operand stays unscaled; the remainder magnitude is bounded
124        // by both operands and therefore still fits the i128 representation.
125        let lhs = Self::align_to_scale(self.mantissa, self.scale, target_scale)?;
126        let rhs = Self::align_to_scale(rhs.mantissa, rhs.scale, target_scale)?;
127
128        Some(Self {
129            mantissa: i128::try_from(lhs.checked_rem(rhs)?).ok()?,
130            scale: target_scale,
131        })
132    }
133
134    /// Round to a given number of decimal places.
135    #[must_use]
136    pub const fn round_dp(&self, dp: u32) -> Self {
137        if self.scale <= dp {
138            return *self;
139        }
140
141        let diff = self.scale - dp;
142        let Some(divisor) = Self::checked_pow10(diff) else {
143            return *self;
144        };
145        let quotient = self.mantissa / divisor;
146        let remainder = self.mantissa % divisor;
147
148        // `divisor` is 10^diff and always positive here.
149        let should_round = remainder.unsigned_abs() >= divisor.unsigned_abs() / 2;
150        let rounded = if should_round {
151            if self.mantissa.is_negative() {
152                quotient.saturating_sub(1)
153            } else {
154                quotient.saturating_add(1)
155            }
156        } else {
157            quotient
158        };
159
160        Self {
161            mantissa: rounded,
162            scale: dp,
163        }
164    }
165
166    /// Truncate toward zero to a given number of decimal places.
167    #[must_use]
168    pub const fn trunc_dp(&self, dp: u32) -> Self {
169        if self.scale <= dp {
170            return *self;
171        }
172
173        let diff = self.scale - dp;
174        let Some(divisor) = Self::checked_pow10(diff) else {
175            return *self;
176        };
177
178        Self {
179            mantissa: self.mantissa / divisor,
180            scale: dp,
181        }
182    }
183
184    /// Return the absolute value of the decimal.
185    #[must_use]
186    pub const fn abs(&self) -> Self {
187        Self {
188            mantissa: self.mantissa.saturating_abs(),
189            scale: self.scale,
190        }
191    }
192
193    /// Return the greatest integral decimal less than or equal to the value.
194    #[must_use]
195    pub const fn floor_dp0(&self) -> Self {
196        if self.scale == 0 {
197            return *self;
198        }
199
200        let Some(divisor) = Self::checked_pow10(self.scale) else {
201            return *self;
202        };
203        let quotient = self.mantissa / divisor;
204        let remainder = self.mantissa % divisor;
205        let integer = if self.mantissa.is_negative() && remainder != 0 {
206            quotient.saturating_sub(1)
207        } else {
208            quotient
209        };
210
211        Self {
212            mantissa: integer,
213            scale: 0,
214        }
215    }
216
217    /// Return the least integral decimal greater than or equal to the value.
218    #[must_use]
219    pub const fn ceil_dp0(&self) -> Self {
220        if self.scale == 0 {
221            return *self;
222        }
223
224        let Some(divisor) = Self::checked_pow10(self.scale) else {
225            return *self;
226        };
227        let quotient = self.mantissa / divisor;
228        let remainder = self.mantissa % divisor;
229        let integer = if self.mantissa.is_positive() && remainder != 0 {
230            quotient.saturating_add(1)
231        } else {
232            quotient
233        };
234
235        Self {
236            mantissa: integer,
237            scale: 0,
238        }
239    }
240
241    /// Saturating addition.
242    #[must_use]
243    pub fn saturating_add(self, rhs: Self) -> Self {
244        // Only true rounded magnitude overflow remains; overflowing addition
245        // has same-sign operands.
246        self.checked_add_impl(rhs)
247            .unwrap_or_else(|| Self::saturating_extreme(self.is_sign_negative()))
248    }
249
250    /// Saturating subtraction.
251    #[must_use]
252    pub fn saturating_sub(self, rhs: Self) -> Self {
253        // Operand ordering owns the difference's sign, including positive
254        // overflow near the asymmetric signed MIN boundary.
255        self.checked_sub(rhs)
256            .unwrap_or_else(|| Self::saturating_extreme(self < rhs))
257    }
258
259    /// Exact remainder with the dividend's sign, at the greater operand scale.
260    /// Returns `None` on division by zero; scale alignment cannot overflow.
261    #[must_use]
262    pub fn checked_rem(self, rhs: Self) -> Option<Self> {
263        self.checked_rem_impl(rhs)
264    }
265
266    /// Checked absolute value; returns `None` for the one `i128::MIN`
267    /// mantissa case that cannot be represented as positive `i128`.
268    #[must_use]
269    pub const fn checked_abs(&self) -> Option<Self> {
270        let Some(mantissa) = self.mantissa.checked_abs() else {
271            return None;
272        };
273
274        Some(Self {
275            mantissa,
276            scale: self.scale,
277        })
278    }
279
280    /// Integer exponentiation.
281    #[must_use]
282    pub fn powu(&self, exp: u64) -> Self {
283        if exp == 0 {
284            return Self::new(1, 0);
285        }
286
287        let mut base = *self;
288        let mut power = exp;
289        let mut acc = Self::new(1, 0);
290
291        while power > 0 {
292            if power & 1 == 1 {
293                acc *= base;
294            }
295
296            power >>= 1;
297
298            if power > 0 {
299                base = base * base;
300            }
301        }
302
303        acc
304    }
305
306    /// Checked integer exponentiation using the same exponentiation-by-squaring
307    /// shape as `powu`, but failing instead of saturating on intermediate
308    /// magnitude overflow. Each multiplication uses the same rounded
309    /// fixed-representation contract as `checked_mul`.
310    #[must_use]
311    pub fn checked_powu(&self, exp: u64) -> Option<Self> {
312        if exp == 0 {
313            return Some(Self::new(1, 0));
314        }
315
316        let mut base = *self;
317        let mut power = exp;
318        let mut acc = Self::new(1, 0);
319
320        while power > 0 {
321            if power & 1 == 1 {
322                acc = acc.checked_mul(base)?;
323            }
324
325            power >>= 1;
326
327            if power > 0 {
328                base = base.checked_mul(base)?;
329            }
330        }
331
332        Some(acc)
333    }
334
335    // Admitted scale alignment fits I256 even when the i128 intermediate does
336    // not. Addition, subtraction and exact remainder use this single owner.
337    fn align_to_scale(mantissa: i128, current_scale: u32, target_scale: u32) -> Option<I256> {
338        let factor = Self::checked_pow10(target_scale.checked_sub(current_scale)?)?;
339        I256::from(mantissa).checked_mul(I256::from(factor))
340    }
341
342    // Prepare integer operands for fixed-scale decimal division.
343    fn division_operands(lhs: Self, rhs: Self, target_scale: u32) -> Option<(I256, I256)> {
344        let exponent = i64::from(target_scale) + i64::from(rhs.scale) - i64::from(lhs.scale);
345        let factor = I256::new(10).checked_pow(u32::try_from(exponent.unsigned_abs()).ok()?)?;
346        let lhs = I256::from(lhs.mantissa);
347        let rhs = I256::from(rhs.mantissa);
348
349        if exponent >= 0 {
350            return Some((lhs.checked_mul(factor)?, rhs));
351        }
352
353        Some((lhs, rhs.checked_mul(factor)?))
354    }
355
356    // Divide with round-half-away-from-zero semantics.
357    fn div_round_half_away_from_zero(numerator: I256, denominator: I256) -> Option<i128> {
358        // Round before narrowing. An unrepresentable signed result, including
359        // i128::MIN / -1, follows the caller's maintained overflow contract.
360        let quotient = numerator.checked_div(denominator)?;
361        let remainder = numerator.checked_rem(denominator)?;
362
363        if remainder == 0 {
364            return i128::try_from(quotient).ok();
365        }
366
367        let twice_remainder = remainder.unsigned_abs().checked_mul(2_u8.into())?;
368        if twice_remainder < denominator.unsigned_abs() {
369            return i128::try_from(quotient).ok();
370        }
371
372        let rounded = if (numerator < 0) == (denominator < 0) {
373            quotient.checked_add(I256::new(1))?
374        } else {
375            quotient.checked_sub(I256::new(1))?
376        };
377        i128::try_from(rounded).ok()
378    }
379}
380
381impl Add for Decimal {
382    type Output = Self;
383
384    fn add(self, rhs: Self) -> Self::Output {
385        self.saturating_add(rhs)
386    }
387}
388
389impl AddAssign for Decimal {
390    fn add_assign(&mut self, rhs: Self) {
391        *self = *self + rhs;
392    }
393}
394
395impl Sub for Decimal {
396    type Output = Self;
397
398    fn sub(self, rhs: Self) -> Self::Output {
399        self.saturating_sub(rhs)
400    }
401}
402
403impl SubAssign for Decimal {
404    fn sub_assign(&mut self, rhs: Self) {
405        *self = *self - rhs;
406    }
407}
408
409impl Mul for Decimal {
410    type Output = Self;
411
412    fn mul(self, rhs: Self) -> Self::Output {
413        self.checked_mul_impl(rhs).unwrap_or_else(|| {
414            Self::saturating_extreme(self.is_sign_negative() != rhs.is_sign_negative())
415        })
416    }
417}
418
419impl MulAssign for Decimal {
420    fn mul_assign(&mut self, rhs: Self) {
421        *self = *self * rhs;
422    }
423}
424
425impl Neg for Decimal {
426    type Output = Self;
427
428    fn neg(self) -> Self::Output {
429        Self {
430            mantissa: self.mantissa.saturating_neg(),
431            scale: self.scale,
432        }
433    }
434}
435
436impl Product for Decimal {
437    fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
438        iter.fold(Self::new_unchecked(1, 0), |acc, value| acc * value)
439    }
440}
441
442impl Div for Decimal {
443    type Output = Self;
444
445    fn div(self, rhs: Self) -> Self::Output {
446        if rhs.is_zero() {
447            return Self::ZERO;
448        }
449
450        self.checked_div_impl(rhs).unwrap_or_else(|| {
451            let negative = self.is_sign_negative() != rhs.is_sign_negative();
452            Self::saturating_extreme(negative)
453        })
454    }
455}
456
457impl DivAssign for Decimal {
458    fn div_assign(&mut self, rhs: Self) {
459        *self = *self / rhs;
460    }
461}
462
463impl Rem for Decimal {
464    type Output = Self;
465
466    fn rem(self, rhs: Self) -> Self::Output {
467        self.checked_rem_impl(rhs).unwrap_or(Self::ZERO)
468    }
469}
470
471impl RemAssign for Decimal {
472    fn rem_assign(&mut self, rhs: Self) {
473        *self = *self % rhs;
474    }
475}
476
477impl Sum for Decimal {
478    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
479        iter.fold(Self::ZERO, |acc, value| acc + value)
480    }
481}