Skip to main content

ironwork_rt/
json.rs

1//! The text JSON GENERATE writes for elementary data (Language Reference SC27-8713-03, pp. 381-382).
2
3use crate::intrinsic::real::Real;
4use std::ops::{Div, Mul};
5use zarch::hfp::Hfp;
6use zarch::wide::U256;
7
8pub mod parse;
9
10/// A fixed-point value as if moved to a numeric-edited item of `integers` integer positions (at
11/// least one), `scale` decimal places after an actual period, and a leading minus sign; then
12/// trimmed of leading zeros up to the digit before the point, and of the sign's space.
13pub fn fixed_number(negative: bool, magnitude: U256, scale: u32, integers: u32) -> String {
14    let digits = magnitude.to_u128().map_or_else(|| decimal(magnitude), |m| m.to_string());
15    let digits = format!("{digits:0>width$}", width = scale as usize + 1);
16    let (whole, fraction) = digits.split_at(digits.len() - scale as usize);
17    let kept = &whole[whole.len().saturating_sub(integers.max(1) as usize)..];
18    let trimmed = kept.trim_start_matches('0');
19    let whole = if trimmed.is_empty() { "0" } else { trimmed };
20    let zero = magnitude.is_zero() || (whole == "0" && fraction.bytes().all(|b| b == b'0'));
21    let sign = if negative && !zero { "-" } else { "" };
22    if scale == 0 { format!("{sign}{whole}") } else { format!("{sign}{whole}.{fraction}") }
23}
24
25fn decimal(mut m: U256) -> String {
26    let mut out = Vec::new();
27    let ten = U256::from_u128(10);
28    while !m.is_zero() {
29        let (q, r) = m.div_rem(ten);
30        out.push(b'0' + r.lo as u8);
31        m = q;
32    }
33    out.reverse();
34    String::from_utf8(out).unwrap_or_default()
35}
36
37/// COMP-1 as if moved to PICTURE -9.9(8)E+99 and COMP-2 to -9.9(17)E+99, rounded, with the
38/// sign's space trimmed.
39pub fn float_number(value: Hfp, decimals: u32) -> String {
40    let v = Real::from_hfp(value);
41    if v.is_zero() {
42        return format!("0.{}E+00", "0".repeat(decimals as usize));
43    }
44    let magnitude = v.abs();
45    let mut exponent = magnitude.to_f64().log10().floor() as i32;
46    let significant = |e: i32| -> u128 {
47        let shift = decimals as i32 - e;
48        let scaled = if shift >= 0 { magnitude.mul(power_of_ten(shift as u32)) } else { magnitude.div(power_of_ten(shift.unsigned_abs())) };
49        scaled.round_to_integer().unwrap_or(0).unsigned_abs()
50    };
51    let low = 10u128.pow(decimals);
52    let mut digits = significant(exponent);
53    if digits >= low * 10 {
54        exponent += 1;
55        digits = significant(exponent);
56    } else if digits < low {
57        exponent -= 1;
58        digits = significant(exponent);
59    }
60    let text = digits.to_string();
61    let sign = if v.is_negative() { "-" } else { "" };
62    let exponent_sign = if exponent < 0 { '-' } else { '+' };
63    format!("{sign}{}.{}E{exponent_sign}{:02}", &text[..1], &text[1..], exponent.unsigned_abs())
64}
65
66fn power_of_ten(mut n: u32) -> Real {
67    let mut scale = Real::ONE;
68    while n > 0 {
69        let step = n.min(38);
70        scale = scale.mul(Real::from_u128(10u128.pow(step)));
71        n -= step;
72    }
73    scale
74}
75
76/// Character data trimmed of trailing spaces, or of leading ones for a JUSTIFIED item; all
77/// spaces leave one.
78pub fn trimmed(text: &str, justified: bool) -> &str {
79    let t = if justified { text.trim_start_matches(' ') } else { text.trim_end_matches(' ') };
80    if t.is_empty() && !text.is_empty() { &text[..1] } else { t }
81}
82
83/// A JSON string, with IBM's escapes: \" \\ \b \t \n \f \r, \x for NEXT LINE (U+0085), and \uhhhh
84/// for the other control characters below U+0020.
85pub fn string(text: &str) -> String {
86    let mut out = String::with_capacity(text.len() + 2);
87    out.push('"');
88    for c in text.chars() {
89        match c {
90            '"' => out.push_str("\\\""),
91            '\\' => out.push_str("\\\\"),
92            '\u{8}' => out.push_str("\\b"),
93            '\t' => out.push_str("\\t"),
94            '\n' => out.push_str("\\n"),
95            '\u{c}' => out.push_str("\\f"),
96            '\r' => out.push_str("\\r"),
97            '\u{85}' => out.push_str("\\x"),
98            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04X}", c as u32)),
99            c => out.push(c),
100        }
101    }
102    out.push('"');
103    out
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use zarch::hfp::Precision;
110
111    fn u(n: u128) -> U256 {
112        U256::from_u128(n)
113    }
114
115    #[test]
116    fn fixed_point_values_trim_as_the_language_reference_shows() {
117        assert_eq!(fixed_number(false, u(78), 1, 2), "7.8");
118        assert_eq!(fixed_number(true, u(90), 1, 2), "-9.0");
119        assert_eq!(fixed_number(true, u(12340), 3, 3), "-12.340");
120        assert_eq!(fixed_number(false, u(45), 2, 4), "0.45");
121        assert_eq!(fixed_number(false, u(13), 0, 4), "13");
122        assert_eq!(fixed_number(false, u(0), 0, 4), "0");
123        assert_eq!(fixed_number(true, u(0), 2, 3), "0.00");
124        assert_eq!(fixed_number(false, u(123_456), 0, 5), "23456");
125    }
126
127    #[test]
128    fn floating_point_values_take_the_external_float_picture() {
129        let long = |n: i128| Hfp::from_integer(n, Precision::Long);
130        assert_eq!(float_number(long(78).div(long(10), Default::default()).unwrap(), 17), "7.79999999999999982E+00");
131        assert_eq!(float_number(long(-1234), 8), "-1.23400000E+03");
132        assert_eq!(float_number(long(1).div(long(1000), Default::default()).unwrap(), 8), "1.00000000E-03");
133        assert_eq!(float_number(Hfp::zero(Precision::Short), 8), "0.00000000E+00");
134    }
135
136    #[test]
137    fn strings_are_escaped_as_ibm_escapes_them() {
138        assert_eq!(string("a\"b\\c\td\u{85}\u{1}"), "\"a\\\"b\\\\c\\td\\x\\u0001\"");
139        assert_eq!(trimmed("SX1234  ", false), "SX1234");
140        assert_eq!(trimmed("   ", false), " ");
141        assert_eq!(trimmed("  AB", true), "AB");
142    }
143}