1use 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
37pub fn carried(ir: Places, dmax: u32, arith: Arith) -> Places {
40 let n = arith.intermediate_digits();
41 if ir.total() <= n {
42 ir
43 } else if ir.dec <= dmax {
44 Places::new(n.saturating_sub(ir.dec), ir.dec)
45 } else if ir.int + dmax <= n {
46 Places::new(ir.int, n - ir.int)
47 } else {
48 Places::new(n.saturating_sub(dmax), dmax)
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum ArithError {
54 DivideByZero,
55 BeyondModel,
56}
57
58impl fmt::Display for ArithError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::DivideByZero => write!(f, "division by zero"),
62 Self::BeyondModel => write!(f, "an intermediate wider than 256 bits"),
63 }
64 }
65}
66
67impl std::error::Error for ArithError {}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Fixed {
72 pub negative: bool,
73 pub magnitude: U256,
74 pub places: Places,
75}
76
77fn pow10(n: u32) -> U256 {
78 U256::pow10(n)
79}
80
81impl Fixed {
82 pub fn new(value: i128, places: Places) -> Self {
83 Self::signed(value < 0, U256::from_u128(value.unsigned_abs()), places)
84 }
85
86 fn signed(negative: bool, magnitude: U256, places: Places) -> Self {
87 Self { negative: negative && !magnitude.is_zero(), magnitude, places }
88 }
89
90 pub fn to_i128(self) -> Option<i128> {
91 let m = i128::try_from(self.magnitude.to_u128()?).ok()?;
92 Some(if self.negative { -m } else { m })
93 }
94
95 pub fn fit(self, to: Places) -> Self {
98 let from = self.places.dec;
99 let magnitude = if to.dec < from {
100 (self.magnitude.div_rem(pow10(from - to.dec)).0).div_rem(pow10(to.total())).1
101 } else {
102 self.magnitude.div_rem(pow10(to.int + from)).1.checked_mul(pow10(to.dec - from)).expect("fits by construction")
103 };
104 Self::signed(self.negative, magnitude, to)
105 }
106
107 fn aligned(self, dec: u32) -> Result<U256, ArithError> {
108 self.magnitude.checked_mul(pow10(dec - self.places.dec)).ok_or(ArithError::BeyondModel)
109 }
110
111 fn negated(self) -> Self {
112 Self::signed(!self.negative, self.magnitude, self.places)
113 }
114
115 pub fn add(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
116 let dec = self.places.dec.max(other.places.dec);
117 let (a, b) = (self.aligned(dec)?, other.aligned(dec)?);
118 let (negative, magnitude) = match (self.negative == other.negative, a >= b) {
119 (true, _) => (self.negative, a.checked_add(b).ok_or(ArithError::BeyondModel)?),
120 (false, true) => (self.negative, a - b),
121 (false, false) => (other.negative, b - a),
122 };
123 let ir = sum_places(self.places, other.places);
124 Ok(Self::signed(negative, magnitude, Places::new(ir.int, dec)).fit(carried(ir, dmax, arith)))
125 }
126
127 pub fn sub(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
128 self.add(other.negated(), dmax, arith)
129 }
130
131 pub fn mul(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
132 let ir = product_places(self.places, other.places);
133 let magnitude = self.magnitude.checked_mul(other.magnitude).ok_or(ArithError::BeyondModel)?;
134 Ok(Self::signed(self.negative != other.negative, magnitude, ir).fit(carried(ir, dmax, arith)))
135 }
136
137 pub fn div(self, divisor: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
138 if divisor.magnitude.is_zero() {
139 return Err(ArithError::DivideByZero);
140 }
141 let to = carried(quotient_places(self.places, divisor.places, dmax), dmax, arith);
142 let shift = (divisor.places.dec + to.dec) as i64 - self.places.dec as i64;
143 let (numerator, denominator) = if shift >= 0 {
144 (self.magnitude.checked_mul(pow10(shift as u32)), Some(divisor.magnitude))
145 } else {
146 (Some(self.magnitude), divisor.magnitude.checked_mul(pow10(shift.unsigned_abs() as u32)))
147 };
148 let (numerator, denominator) = (numerator.ok_or(ArithError::BeyondModel)?, denominator.ok_or(ArithError::BeyondModel)?);
149 let quotient = numerator.div_rem(denominator).0;
150 let exact = Self::signed(self.negative != divisor.negative, quotient, Places::new(u32::MAX / 2, to.dec));
151 Ok(exact.fit(to))
152 }
153
154 pub fn to_receiver(self, places: Places, rounded: bool) -> (Self, bool) {
157 let from = self.places.dec;
158 let magnitude = if places.dec >= from {
159 self.magnitude.checked_mul(pow10(places.dec - from))
160 } else {
161 let (kept, dropped) = self.magnitude.div_rem(pow10(from - places.dec));
162 let half = pow10(from - places.dec - 1).checked_mul(U256::from_u128(5)).unwrap();
163 Some(if rounded && dropped >= half { kept + U256::from_u128(1) } else { kept })
164 };
165 let cap = pow10(places.total());
166 match magnitude {
167 Some(m) if m < cap => (Self::signed(self.negative, m, places), false),
168 Some(m) => (Self::signed(self.negative, m.div_rem(cap).1, places), true),
169 None => (Self::signed(self.negative, U256::ZERO, places), true),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 const S18: Places = Places::new(18, 0);
179
180 #[test]
181 fn places_for_each_operation() {
182 assert_eq!(sum_places(Places::new(5, 2), Places::new(3, 4)), Places::new(6, 4));
183 assert_eq!(product_places(Places::new(5, 2), Places::new(3, 4)), Places::new(8, 6));
184 assert_eq!(quotient_places(Places::new(5, 2), Places::new(3, 4), 3), Places::new(9, 3));
185 }
186
187 #[test]
188 fn the_carried_places_table() {
189 assert_eq!(carried(Places::new(20, 5), 5, Arith::Compat), Places::new(20, 5));
190 assert_eq!(carried(Places::new(28, 6), 6, Arith::Compat), Places::new(24, 6));
191 assert_eq!(carried(Places::new(20, 12), 4, Arith::Compat), Places::new(20, 10));
192 assert_eq!(carried(Places::new(28, 12), 4, Arith::Compat), Places::new(26, 4));
193 assert_eq!(carried(Places::new(28, 6), 6, Arith::Extend), Places::new(25, 6));
194 }
195
196 #[test]
197 fn an_18_by_18_digit_product_loses_high_order_digits_under_compat_but_fewer_under_extend() {
198 let big = Fixed::new(999_999_999_999_999_999, S18);
199 let compat = big.mul(big, 0, Arith::Compat).unwrap();
200 let extend = big.mul(big, 0, Arith::Extend).unwrap();
201 assert_eq!(compat.places, Places::new(30, 0));
202 assert_eq!(extend.places, Places::new(31, 0));
203 let exact = U256::widening_mul(999_999_999_999_999_999, 999_999_999_999_999_999);
204 assert_eq!(compat.magnitude, exact.div_rem(U256::pow10(30)).1);
205 assert_eq!(extend.magnitude, exact.div_rem(U256::pow10(31)).1);
206 }
207
208 #[test]
209 fn division_carries_dmax_decimal_places_and_truncates() {
210 let q = Fixed::new(10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
211 assert_eq!((q.to_i128(), q.places.dec), (Some(333), 2));
212 let n = Fixed::new(-10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
213 assert_eq!(n.to_i128(), Some(-333));
214 assert_eq!(Fixed::new(1, S18).div(Fixed::new(0, S18), 0, Arith::Compat), Err(ArithError::DivideByZero));
215 }
216
217 #[test]
218 fn addition_aligns_decimal_points() {
219 let s = Fixed::new(125, Places::new(1, 2)).add(Fixed::new(-3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
220 assert_eq!((s.to_i128(), s.places), (Some(-175), Places::new(2, 2)));
221 }
222
223 #[test]
224 fn receivers_truncate_or_round_half_away_and_flag_size_errors() {
225 let v = Fixed::new(-12345, Places::new(3, 2));
226 assert_eq!(v.to_receiver(Places::new(3, 1), false).0.to_i128(), Some(-1234));
227 assert_eq!(v.to_receiver(Places::new(3, 1), true).0.to_i128(), Some(-1235));
228 let (wrapped, size_error) = v.to_receiver(Places::new(2, 0), false);
229 assert_eq!((wrapped.to_i128(), size_error), (Some(-23), true));
230 let (rounded_over, size_error) = Fixed::new(999, Places::new(1, 2)).to_receiver(Places::new(1, 1), true);
231 assert_eq!((rounded_over.to_i128(), size_error), (Some(0), true));
232 }
233
234 #[test]
235 fn negative_zero_is_normalized() {
236 let z = Fixed::new(5, Places::new(1, 0)).sub(Fixed::new(5, Places::new(1, 0)), 0, Arith::Compat).unwrap();
237 assert!(!z.negative);
238 }
239}