Skip to main content

ironwork_numeric/
precision.rs

1//! Fixed-point intermediate results: how many integer and decimal places IBM carries for each
2//! operation, and exact arithmetic that drops exactly the digits it drops. See
3//! [`crate::assumptions::INTERMEDIATE_TABLE`].
4
5use crate::options::Arith;
6use std::fmt;
7use zarch::wide::U256;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Places {
11    pub int: u32,
12    pub dec: u32,
13}
14
15impl Places {
16    pub const fn new(int: u32, dec: u32) -> Self {
17        Self { int, dec }
18    }
19
20    pub const fn total(self) -> u32 {
21        self.int + self.dec
22    }
23}
24
25pub fn sum_places(a: Places, b: Places) -> Places {
26    Places::new(a.int.max(b.int) + 1, a.dec.max(b.dec))
27}
28
29pub fn product_places(a: Places, b: Places) -> Places {
30    Places::new(a.int + b.int, a.dec + b.dec)
31}
32
33pub fn quotient_places(dividend: Places, divisor: Places, dmax: u32) -> Places {
34    Places::new(dividend.int + divisor.dec, dividend.dec.max(dmax))
35}
36
37/// The decimal places a receiver of `scale` counts for in dmax: under ROUNDED one more, the digit
38/// rounding reads. See [`crate::assumptions::ROUNDED_EXTRA_PLACE`].
39pub const fn receiver_dec(scale: u32, rounded: bool) -> u32 {
40    scale + rounded as u32
41}
42
43/// The places carried for an intermediate result `ir`. `dmax` is the most decimal places among
44/// the statement's receivers and its operands other than divisors and exponents.
45pub fn carried(ir: Places, dmax: u32, arith: Arith) -> Places {
46    let n = arith.intermediate_digits();
47    if ir.total() <= n {
48        ir
49    } else if ir.dec <= dmax {
50        Places::new(n.saturating_sub(ir.dec), ir.dec)
51    } else if ir.int + dmax <= n {
52        Places::new(ir.int, n - ir.int)
53    } else {
54        Places::new(n.saturating_sub(dmax), dmax)
55    }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum ArithError {
60    DivideByZero,
61    BeyondModel,
62}
63
64impl fmt::Display for ArithError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::DivideByZero => write!(f, "division by zero"),
68            Self::BeyondModel => write!(f, "an intermediate wider than 256 bits"),
69        }
70    }
71}
72
73impl std::error::Error for ArithError {}
74
75/// A fixed-point value: magnitude × 10^-places.dec, with the places it is declared to hold.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub struct Fixed {
78    pub negative: bool,
79    pub magnitude: U256,
80    pub places: Places,
81}
82
83fn pow10(n: u32) -> U256 {
84    U256::pow10(n)
85}
86
87impl Fixed {
88    pub fn new(value: i128, places: Places) -> Self {
89        Self::signed(value < 0, U256::from_u128(value.unsigned_abs()), places)
90    }
91
92    fn signed(negative: bool, magnitude: U256, places: Places) -> Self {
93        Self { negative: negative && !magnitude.is_zero(), magnitude, places }
94    }
95
96    pub fn to_i128(self) -> Option<i128> {
97        zarch::wide::signed_i128(self.negative, self.magnitude)
98    }
99
100    /// Keeps `to.dec` decimal places, truncating the rest, and `to.int` integer places, dropping
101    /// high-order digits.
102    pub fn fit(self, to: Places) -> Self {
103        let from = self.places.dec;
104        let magnitude = if to.dec < from {
105            (self.magnitude.div_rem(pow10(from - to.dec)).0).div_rem(pow10(to.total())).1
106        } else {
107            self.magnitude.div_rem(pow10(to.int + from)).1.checked_mul(pow10(to.dec - from)).expect("fits by construction")
108        };
109        Self::signed(self.negative, magnitude, to)
110    }
111
112    fn aligned(self, dec: u32) -> Result<U256, ArithError> {
113        self.magnitude.checked_mul(pow10(dec - self.places.dec)).ok_or(ArithError::BeyondModel)
114    }
115
116    fn negated(self) -> Self {
117        Self::signed(!self.negative, self.magnitude, self.places)
118    }
119
120    pub fn add(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
121        let dec = self.places.dec.max(other.places.dec);
122        let (a, b) = (self.aligned(dec)?, other.aligned(dec)?);
123        let (negative, magnitude) = match (self.negative == other.negative, a >= b) {
124            (true, _) => (self.negative, a.checked_add(b).ok_or(ArithError::BeyondModel)?),
125            (false, true) => (self.negative, a - b),
126            (false, false) => (other.negative, b - a),
127        };
128        let ir = sum_places(self.places, other.places);
129        Ok(Self::signed(negative, magnitude, Places::new(ir.int, dec)).fit(carried(ir, dmax, arith)))
130    }
131
132    pub fn sub(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
133        self.add(other.negated(), dmax, arith)
134    }
135
136    pub fn mul(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
137        let ir = product_places(self.places, other.places);
138        let magnitude = self.magnitude.checked_mul(other.magnitude).ok_or(ArithError::BeyondModel)?;
139        Ok(Self::signed(self.negative != other.negative, magnitude, ir).fit(carried(ir, dmax, arith)))
140    }
141
142    pub fn div(self, divisor: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
143        if divisor.magnitude.is_zero() {
144            return Err(ArithError::DivideByZero);
145        }
146        let to = carried(quotient_places(self.places, divisor.places, dmax), dmax, arith);
147        let shift = (divisor.places.dec + to.dec) as i64 - self.places.dec as i64;
148        let (numerator, denominator) = if shift >= 0 {
149            (self.magnitude.checked_mul(pow10(shift as u32)), Some(divisor.magnitude))
150        } else {
151            (Some(self.magnitude), divisor.magnitude.checked_mul(pow10(shift.unsigned_abs() as u32)))
152        };
153        let (numerator, denominator) = (numerator.ok_or(ArithError::BeyondModel)?, denominator.ok_or(ArithError::BeyondModel)?);
154        let quotient = numerator.div_rem(denominator).0;
155        let exact = Self::signed(self.negative != divisor.negative, quotient, Places::new(u32::MAX / 2, to.dec));
156        Ok(exact.fit(to))
157    }
158
159    /// The value a receiver of `places` gets: truncated, or rounded half away from zero, and whether
160    /// the integer part overflowed it (ON SIZE ERROR).
161    pub fn to_receiver(self, places: Places, rounded: bool) -> (Self, bool) {
162        let from = self.places.dec;
163        let magnitude = if places.dec >= from {
164            self.magnitude.checked_mul(pow10(places.dec - from))
165        } else {
166            let (kept, dropped) = self.magnitude.div_rem(pow10(from - places.dec));
167            let half = pow10(from - places.dec - 1).checked_mul(U256::from_u128(5)).unwrap();
168            Some(if rounded && dropped >= half { kept + U256::from_u128(1) } else { kept })
169        };
170        let cap = pow10(places.total());
171        match magnitude {
172            Some(m) if m < cap => (Self::signed(self.negative, m, places), false),
173            Some(m) => (Self::signed(self.negative, m.div_rem(cap).1, places), true),
174            None => (Self::signed(self.negative, U256::ZERO, places), true),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    const S18: Places = Places::new(18, 0);
184
185    #[test]
186    fn places_for_each_operation() {
187        assert_eq!(sum_places(Places::new(5, 2), Places::new(3, 4)), Places::new(6, 4));
188        assert_eq!(product_places(Places::new(5, 2), Places::new(3, 4)), Places::new(8, 6));
189        assert_eq!(quotient_places(Places::new(5, 2), Places::new(3, 4), 3), Places::new(9, 3));
190    }
191
192    #[test]
193    fn the_carried_places_table() {
194        assert_eq!(carried(Places::new(20, 5), 5, Arith::Compat), Places::new(20, 5));
195        assert_eq!(carried(Places::new(28, 6), 6, Arith::Compat), Places::new(24, 6));
196        assert_eq!(carried(Places::new(20, 12), 4, Arith::Compat), Places::new(20, 10));
197        assert_eq!(carried(Places::new(28, 12), 4, Arith::Compat), Places::new(26, 4));
198        assert_eq!(carried(Places::new(28, 6), 6, Arith::Extend), Places::new(25, 6));
199    }
200
201    #[test]
202    fn an_18_by_18_digit_product_loses_high_order_digits_under_compat_but_fewer_under_extend() {
203        let big = Fixed::new(999_999_999_999_999_999, S18);
204        let compat = big.mul(big, 0, Arith::Compat).unwrap();
205        let extend = big.mul(big, 0, Arith::Extend).unwrap();
206        assert_eq!(compat.places, Places::new(30, 0));
207        assert_eq!(extend.places, Places::new(31, 0));
208        let exact = U256::widening_mul(999_999_999_999_999_999, 999_999_999_999_999_999);
209        assert_eq!(compat.magnitude, exact.div_rem(U256::pow10(30)).1);
210        assert_eq!(extend.magnitude, exact.div_rem(U256::pow10(31)).1);
211    }
212
213    #[test]
214    fn division_carries_dmax_decimal_places_and_truncates() {
215        let q = Fixed::new(10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
216        assert_eq!((q.to_i128(), q.places.dec), (Some(333), 2));
217        let n = Fixed::new(-10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
218        assert_eq!(n.to_i128(), Some(-333));
219        assert_eq!(Fixed::new(1, S18).div(Fixed::new(0, S18), 0, Arith::Compat), Err(ArithError::DivideByZero));
220    }
221
222    #[test]
223    fn a_rounded_quotient_carries_one_place_more_than_its_receiver() {
224        let (dividend, divisor) = (Fixed::new(16617, Places::new(4, 1)), Fixed::new(441, Places::new(2, 1)));
225        let receiver = Places::new(4, 1);
226        let truncated = dividend.div(divisor, receiver_dec(receiver.dec, false), Arith::Compat).unwrap();
227        assert_eq!(truncated.to_receiver(receiver, true).0.to_i128(), Some(376));
228        let rounded = dividend.div(divisor, receiver_dec(receiver.dec, true), Arith::Compat).unwrap();
229        assert_eq!((rounded.to_i128(), rounded.places.dec), (Some(3768), 2));
230        assert_eq!(rounded.to_receiver(receiver, true).0.to_i128(), Some(377));
231    }
232
233    #[test]
234    fn addition_aligns_decimal_points() {
235        let s = Fixed::new(125, Places::new(1, 2)).add(Fixed::new(-3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
236        assert_eq!((s.to_i128(), s.places), (Some(-175), Places::new(2, 2)));
237    }
238
239    #[test]
240    fn receivers_truncate_or_round_half_away_and_flag_size_errors() {
241        let v = Fixed::new(-12345, Places::new(3, 2));
242        assert_eq!(v.to_receiver(Places::new(3, 1), false).0.to_i128(), Some(-1234));
243        assert_eq!(v.to_receiver(Places::new(3, 1), true).0.to_i128(), Some(-1235));
244        let (wrapped, size_error) = v.to_receiver(Places::new(2, 0), false);
245        assert_eq!((wrapped.to_i128(), size_error), (Some(-23), true));
246        let (rounded_over, size_error) = Fixed::new(999, Places::new(1, 2)).to_receiver(Places::new(1, 1), true);
247        assert_eq!((rounded_over.to_i128(), size_error), (Some(0), true));
248    }
249
250    #[test]
251    fn negative_zero_is_normalized() {
252        let z = Fixed::new(5, Places::new(1, 0)).sub(Fixed::new(5, Places::new(1, 0)), 0, Arith::Compat).unwrap();
253        assert!(!z.negative);
254    }
255}