Skip to main content

ironwork_numeric/
float.rs

1//! Moves between fixed-point decimal and COMP-1/COMP-2, and floating-point intermediates. Each
2//! conversion here is an assumption: see [`crate::assumptions::FLOAT_FROM_DECIMAL`],
3//! [`crate::assumptions::FLOAT_TO_DECIMAL`] and [`crate::assumptions::FLOAT_NARROWING_ROUNDS`].
4
5use crate::precision::{Fixed, Places};
6use zarch::check::{ProgramCheck, ProgramMask};
7use zarch::hfp::{Hfp, Precision, Rounding};
8use zarch::wide::U256;
9
10pub fn from_fixed(value: Fixed, precision: Precision, mask: ProgramMask) -> Result<Hfp, ProgramCheck> {
11    let magnitude = value.magnitude.to_u128().and_then(|m| i128::try_from(m).ok()).expect("a fixed-point operand fits 31 digits");
12    let integer = Hfp::from_integer(if value.negative { -magnitude } else { magnitude }, precision);
13    if value.places.dec == 0 {
14        return Ok(integer);
15    }
16    let scale = U256::pow10(value.places.dec).to_u128().expect("31 decimal places at most") as i128;
17    integer.div(Hfp::from_integer(scale, precision), mask)
18}
19
20/// The fixed-point value a receiver of `places` gets, and whether it overflowed (ON SIZE ERROR).
21pub fn to_fixed(value: Hfp, places: Places, rounded: bool) -> (Fixed, bool) {
22    let rounding = if rounded { Rounding::HalfAwayFromZero } else { Rounding::TowardZero };
23    match value.to_scaled_integer(places.dec, rounding) {
24        Some((negative, magnitude)) => {
25            let exact = Fixed { negative, magnitude, places: Places::new(u32::MAX / 2, places.dec) };
26            exact.to_receiver(places, false)
27        }
28        None => (Fixed::new(0, places), true),
29    }
30}
31
32pub fn narrow(value: Hfp, target: Precision) -> Hfp {
33    value.truncate(target)
34}
35
36/// A floating-point value moved or stored into a fixed-point receiver of `places`, and whether it
37/// overflowed (ON SIZE ERROR): rounded in the receiver's low-order position, short precision giving
38/// at most 9 significant digits and long at most 18, the rest zero (Programming Guide SC27-8714-03,
39/// p. 52).
40pub fn to_receiver(value: Hfp, places: Places) -> (Fixed, bool) {
41    let Some((negative, mut magnitude)) = value.to_scaled_integer(places.dec, Rounding::HalfAwayFromZero) else {
42        return (Fixed::new(0, places), true);
43    };
44    let significant = match value.precision {
45        Precision::Short => Some(9),
46        Precision::Long => Some(18),
47        Precision::Extended => None,
48    };
49    if let Some(limit) = significant
50        && let Some(drop) = decimal_digits(magnitude).checked_sub(limit).filter(|&d| d > 0)
51        && let Some((_, truncated)) = value.to_scaled_integer(places.dec, Rounding::TowardZero)
52    {
53        let unit = U256::pow10(drop);
54        let (kept, rest) = truncated.div_rem(unit);
55        let up = rest.checked_add(rest).is_some_and(|twice| twice >= unit);
56        magnitude = (if up { kept + U256::from_u128(1) } else { kept }).checked_mul(unit).unwrap_or(magnitude);
57    }
58    let exact = Fixed { negative: negative && !magnitude.is_zero(), magnitude, places: Places::new(u32::MAX / 2, places.dec) };
59    exact.to_receiver(places, false)
60}
61
62/// A floating-point value stored into a narrower COMP-1 or COMP-2: LOAD ROUNDED (p. 52).
63pub fn narrow_rounded(value: Hfp, target: Precision) -> Result<Hfp, ProgramCheck> {
64    value.round(target)
65}
66
67fn decimal_digits(n: U256) -> u32 {
68    (1..=77).find(|&d| n < U256::pow10(d)).unwrap_or(78)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn a_decimal_fraction_becomes_a_truncated_quotient() {
77        let tenth = from_fixed(Fixed::new(1, Places::new(0, 1)), Precision::Long, ProgramMask::default()).unwrap();
78        assert_eq!(tenth.to_bytes(), 0x4019_9999_9999_9999u64.to_be_bytes());
79    }
80
81    #[test]
82    fn a_truncated_tenth_moves_back_as_zero_point_zero_nine() {
83        let tenth = Hfp::from_bytes(Precision::Long, &0x4019_9999_9999_9999u64.to_be_bytes());
84        assert_eq!(to_fixed(tenth, Places::new(1, 2), false).0.to_i128(), Some(9));
85        assert_eq!(to_fixed(tenth, Places::new(1, 2), true).0.to_i128(), Some(10));
86    }
87
88    #[test]
89    fn a_receiver_gets_the_value_rounded_and_at_most_eighteen_digits_from_long_precision() {
90        let tenth = Hfp::from_bytes(Precision::Long, &0x4019_9999_9999_9999u64.to_be_bytes());
91        assert_eq!(to_receiver(tenth, Places::new(1, 2)).0.to_i128(), Some(10));
92        let third = Hfp::from_integer(1, Precision::Long).div(Hfp::from_integer(3, Precision::Long), ProgramMask::default()).unwrap();
93        assert_eq!(to_receiver(third, Places::new(0, 25)).0.to_i128(), Some(3_333_333_333_333_333_290_000_000));
94        assert_eq!(to_receiver(third.round(Precision::Short).unwrap(), Places::new(0, 12)).0.to_i128(), Some(333_333_313_000));
95    }
96
97    #[test]
98    fn overflow_is_a_size_error() {
99        let big = Hfp::from_integer(123_456, Precision::Long);
100        assert!(to_fixed(big, Places::new(3, 0), false).1);
101    }
102}