1use crate::options::{Arith, ExtraPlace};
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 #[inline]
17 pub const fn new(int: u32, dec: u32) -> Self {
18 Self { int, dec }
19 }
20
21 #[inline]
22 pub const fn total(self) -> u32 {
23 self.int + self.dec
24 }
25}
26
27#[inline]
28pub fn sum_places(a: Places, b: Places) -> Places {
29 Places::new(a.int.max(b.int) + 1, a.dec.max(b.dec))
30}
31
32#[inline]
33pub fn product_places(a: Places, b: Places) -> Places {
34 Places::new(a.int + b.int, a.dec + b.dec)
35}
36
37pub fn quotient_places(dividend: Places, divisor: Places, dmax: u32) -> Places {
38 Places::new(dividend.int + divisor.dec, dividend.dec.saturating_sub(divisor.dec).max(dmax))
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct Dmax {
45 pub last: u32,
46 pub inner: u32,
47}
48
49impl Dmax {
50 pub const fn receiver(scale: u32, rounded: bool, place: ExtraPlace) -> Self {
54 let last = scale + rounded as u32;
55 match place {
56 ExtraPlace::Every => Self { last, inner: last },
57 ExtraPlace::Last => Self { last, inner: scale },
58 ExtraPlace::Off => Self { last: scale, inner: scale },
59 }
60 }
61
62 pub fn max(self, other: Self) -> Self {
63 Self { last: self.last.max(other.last), inner: self.inner.max(other.inner) }
64 }
65
66 pub fn with(self, places: u32) -> Self {
68 Self { last: self.last.max(places), inner: self.inner.max(places) }
69 }
70}
71
72pub fn carried(ir: Places, dmax: u32, arith: Arith) -> Places {
75 let n = arith.intermediate_digits();
76 if ir.total() <= n {
77 ir
78 } else if ir.dec <= dmax {
79 Places::new(n.saturating_sub(ir.dec), ir.dec)
80 } else if ir.int + dmax <= n {
81 Places::new(ir.int, n - ir.int)
82 } else {
83 Places::new(n.saturating_sub(dmax), dmax)
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum ArithError {
89 DivideByZero,
90 BeyondModel,
91}
92
93impl fmt::Display for ArithError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::DivideByZero => write!(f, "division by zero"),
97 Self::BeyondModel => write!(f, "an intermediate wider than 256 bits"),
98 }
99 }
100}
101
102impl std::error::Error for ArithError {}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct Fixed {
107 pub negative: bool,
108 pub magnitude: U256,
109 pub places: Places,
110}
111
112#[inline]
113fn pow10(n: u32) -> U256 {
114 U256::pow10(n)
115}
116
117impl Fixed {
118 #[inline]
119 pub fn new(value: i128, places: Places) -> Self {
120 Self::signed(value < 0, U256::from_u128(value.unsigned_abs()), places)
121 }
122
123 #[inline]
124 fn signed(negative: bool, magnitude: U256, places: Places) -> Self {
125 Self { negative: negative && !magnitude.is_zero(), magnitude, places }
126 }
127
128 pub fn to_i128(self) -> Option<i128> {
129 zarch::wide::signed_i128(self.negative, self.magnitude)
130 }
131
132 pub fn fit(self, to: Places) -> Self {
135 let from = self.places.dec;
136 let magnitude = if to.dec < from {
137 (self.magnitude.div_rem(pow10(from - to.dec)).0).div_rem(pow10(to.total())).1
138 } else {
139 self.magnitude.div_rem(pow10(to.int + from)).1.checked_mul(pow10(to.dec - from)).expect("fits by construction")
140 };
141 Self::signed(self.negative, magnitude, to)
142 }
143
144 fn aligned(self, dec: u32) -> Result<U256, ArithError> {
145 self.magnitude.checked_mul(pow10(dec - self.places.dec)).ok_or(ArithError::BeyondModel)
146 }
147
148 fn negated(self) -> Self {
149 Self::signed(!self.negative, self.magnitude, self.places)
150 }
151
152 pub fn add(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
153 let dec = self.places.dec.max(other.places.dec);
154 let (a, b) = (self.aligned(dec)?, other.aligned(dec)?);
155 let (negative, magnitude) = match (self.negative == other.negative, a >= b) {
156 (true, _) => (self.negative, a.checked_add(b).ok_or(ArithError::BeyondModel)?),
157 (false, true) => (self.negative, a - b),
158 (false, false) => (other.negative, b - a),
159 };
160 let ir = sum_places(self.places, other.places);
161 Ok(Self::signed(negative, magnitude, Places::new(ir.int, dec)).fit(carried(ir, dmax, arith)))
162 }
163
164 pub fn sub(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
165 self.add(other.negated(), dmax, arith)
166 }
167
168 pub fn mul(self, other: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
169 let ir = product_places(self.places, other.places);
170 let magnitude = self.magnitude.checked_mul(other.magnitude).ok_or(ArithError::BeyondModel)?;
171 Ok(Self::signed(self.negative != other.negative, magnitude, ir).fit(carried(ir, dmax, arith)))
172 }
173
174 pub fn div(self, divisor: Self, dmax: u32, arith: Arith) -> Result<Self, ArithError> {
175 if divisor.magnitude.is_zero() {
176 return Err(ArithError::DivideByZero);
177 }
178 let to = carried(quotient_places(self.places, divisor.places, dmax), dmax, arith);
179 let shift = (divisor.places.dec + to.dec) as i64 - self.places.dec as i64;
180 let (numerator, denominator) = if shift >= 0 {
181 (self.magnitude.checked_mul(pow10(shift as u32)), Some(divisor.magnitude))
182 } else {
183 (Some(self.magnitude), divisor.magnitude.checked_mul(pow10(shift.unsigned_abs() as u32)))
184 };
185 let (numerator, denominator) = (numerator.ok_or(ArithError::BeyondModel)?, denominator.ok_or(ArithError::BeyondModel)?);
186 let quotient = numerator.div_rem(denominator).0;
187 let exact = Self::signed(self.negative != divisor.negative, quotient, Places::new(u32::MAX / 2, to.dec));
188 Ok(exact.fit(to))
189 }
190
191 pub fn to_receiver(self, places: Places, rounded: bool) -> (Self, bool) {
194 let from = self.places.dec;
195 let magnitude = if places.dec >= from {
196 self.magnitude.checked_mul(pow10(places.dec - from))
197 } else {
198 let (kept, dropped) = self.magnitude.div_rem(pow10(from - places.dec));
199 let half = pow10(from - places.dec - 1).checked_mul(U256::from_u128(5)).unwrap();
200 Some(if rounded && dropped >= half { kept + U256::from_u128(1) } else { kept })
201 };
202 let cap = pow10(places.total());
203 match magnitude {
204 Some(m) if m < cap => (Self::signed(self.negative, m, places), false),
205 Some(m) => (Self::signed(self.negative, m.div_rem(cap).1, places), true),
206 None => (Self::signed(self.negative, U256::ZERO, places), true),
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 const S18: Places = Places::new(18, 0);
216
217 #[test]
218 fn places_for_each_operation() {
219 assert_eq!(sum_places(Places::new(5, 2), Places::new(3, 4)), Places::new(6, 4));
220 assert_eq!(product_places(Places::new(5, 2), Places::new(3, 4)), Places::new(8, 6));
221 assert_eq!(quotient_places(Places::new(5, 2), Places::new(3, 4), 3), Places::new(9, 3));
222 }
223
224 #[test]
227 fn a_quotient_carries_the_dividend_s_places_less_the_divisor_s_or_dmax() {
228 assert_eq!(quotient_places(Places::new(2, 4), Places::new(1, 1), 2), Places::new(3, 3));
229 assert_eq!(quotient_places(Places::new(2, 4), Places::new(1, 3), 2), Places::new(5, 2));
230 let product = Fixed::new(12321, Places::new(2, 4));
231 let q = product.div(Fixed::new(7, Places::new(1, 1)), 2, Arith::Compat).unwrap();
232 assert_eq!((q.to_i128(), q.places.dec), (Some(1760), 3));
233 }
234
235 #[test]
236 fn the_carried_places_table() {
237 assert_eq!(carried(Places::new(20, 5), 5, Arith::Compat), Places::new(20, 5));
238 assert_eq!(carried(Places::new(28, 6), 6, Arith::Compat), Places::new(24, 6));
239 assert_eq!(carried(Places::new(20, 12), 4, Arith::Compat), Places::new(20, 10));
240 assert_eq!(carried(Places::new(28, 12), 4, Arith::Compat), Places::new(26, 4));
241 assert_eq!(carried(Places::new(28, 6), 6, Arith::Extend), Places::new(25, 6));
242 }
243
244 #[test]
245 fn an_18_by_18_digit_product_loses_high_order_digits_under_compat_but_fewer_under_extend() {
246 let big = Fixed::new(999_999_999_999_999_999, S18);
247 let compat = big.mul(big, 0, Arith::Compat).unwrap();
248 let extend = big.mul(big, 0, Arith::Extend).unwrap();
249 assert_eq!(compat.places, Places::new(30, 0));
250 assert_eq!(extend.places, Places::new(31, 0));
251 let exact = U256::widening_mul(999_999_999_999_999_999, 999_999_999_999_999_999);
252 assert_eq!(compat.magnitude, exact.div_rem(U256::pow10(30)).1);
253 assert_eq!(extend.magnitude, exact.div_rem(U256::pow10(31)).1);
254 }
255
256 #[test]
257 fn division_carries_dmax_decimal_places_and_truncates() {
258 let q = Fixed::new(10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
259 assert_eq!((q.to_i128(), q.places.dec), (Some(333), 2));
260 let n = Fixed::new(-10, Places::new(2, 0)).div(Fixed::new(3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
261 assert_eq!(n.to_i128(), Some(-333));
262 assert_eq!(Fixed::new(1, S18).div(Fixed::new(0, S18), 0, Arith::Compat), Err(ArithError::DivideByZero));
263 }
264
265 #[test]
266 fn a_rounded_quotient_carries_one_place_more_than_its_receiver() {
267 let (dividend, divisor) = (Fixed::new(16617, Places::new(4, 1)), Fixed::new(441, Places::new(2, 1)));
268 let receiver = Places::new(4, 1);
269 let truncated = dividend.div(divisor, Dmax::receiver(receiver.dec, false, ExtraPlace::Every).last, Arith::Compat).unwrap();
270 assert_eq!(truncated.to_receiver(receiver, true).0.to_i128(), Some(376));
271 let rounded = dividend.div(divisor, Dmax::receiver(receiver.dec, true, ExtraPlace::Every).last, Arith::Compat).unwrap();
272 assert_eq!((rounded.to_i128(), rounded.places.dec), (Some(3768), 2));
273 assert_eq!(rounded.to_receiver(receiver, true).0.to_i128(), Some(377));
274 }
275
276 #[test]
277 fn gnucobol_counts_the_rounded_place_in_the_last_operation_alone() {
278 assert_eq!(Dmax::receiver(2, true, ExtraPlace::Every), Dmax { last: 3, inner: 3 });
279 assert_eq!(Dmax::receiver(2, true, ExtraPlace::Last), Dmax { last: 3, inner: 2 });
280 assert_eq!(Dmax::receiver(2, false, ExtraPlace::Last), Dmax { last: 2, inner: 2 });
281 let statement = Dmax::receiver(2, true, ExtraPlace::Last).max(Dmax::receiver(1, false, ExtraPlace::Last)).with(1);
282 assert_eq!(statement, Dmax { last: 3, inner: 2 });
283 assert_eq!(statement.with(4), Dmax { last: 4, inner: 4 });
284 }
285
286 #[test]
287 fn off_counts_no_rounded_place() {
288 assert_eq!(Dmax::receiver(2, true, ExtraPlace::Off), Dmax { last: 2, inner: 2 });
289 assert_eq!(Dmax::receiver(2, true, ExtraPlace::Off).with(3), Dmax { last: 3, inner: 3 });
290 }
291
292 #[test]
293 fn addition_aligns_decimal_points() {
294 let s = Fixed::new(125, Places::new(1, 2)).add(Fixed::new(-3, Places::new(1, 0)), 2, Arith::Compat).unwrap();
295 assert_eq!((s.to_i128(), s.places), (Some(-175), Places::new(2, 2)));
296 }
297
298 #[test]
299 fn receivers_truncate_or_round_half_away_and_flag_size_errors() {
300 let v = Fixed::new(-12345, Places::new(3, 2));
301 assert_eq!(v.to_receiver(Places::new(3, 1), false).0.to_i128(), Some(-1234));
302 assert_eq!(v.to_receiver(Places::new(3, 1), true).0.to_i128(), Some(-1235));
303 let (wrapped, size_error) = v.to_receiver(Places::new(2, 0), false);
304 assert_eq!((wrapped.to_i128(), size_error), (Some(-23), true));
305 let (rounded_over, size_error) = Fixed::new(999, Places::new(1, 2)).to_receiver(Places::new(1, 1), true);
306 assert_eq!((rounded_over.to_i128(), size_error), (Some(0), true));
307 }
308
309 #[test]
310 fn negative_zero_is_normalized() {
311 let z = Fixed::new(5, Places::new(1, 0)).sub(Fixed::new(5, Places::new(1, 0)), 0, Arith::Compat).unwrap();
312 assert!(!z.negative);
313 }
314}