Skip to main content

steel_utils/
java.rs

1//! Java standard-library behavior used by vanilla parsing.
2
3/// Returns whether Java's `Character.isWhitespace` recognizes `character`.
4#[must_use]
5pub const fn is_whitespace(character: char) -> bool {
6    matches!(
7        character,
8        '\u{0009}'..='\u{000d}'
9            | '\u{001c}'..='\u{0020}'
10            | '\u{1680}'
11            | '\u{2000}'..='\u{2006}'
12            | '\u{2008}'..='\u{200a}'
13            | '\u{2028}'
14            | '\u{2029}'
15            | '\u{205f}'
16            | '\u{3000}'
17    )
18}
19
20/// Returns whether Java's `Character.isSpaceChar` recognizes `character`.
21#[must_use]
22pub const fn is_space_char(character: char) -> bool {
23    matches!(
24        character,
25        '\u{0020}' | '\u{00a0}' | '\u{1680}' | '\u{2000}'
26            ..='\u{200a}' | '\u{2028}' | '\u{2029}' | '\u{202f}' | '\u{205f}' | '\u{3000}'
27    )
28}
29
30/// Mirrors vanilla `StringUtil.isBlank`.
31#[must_use]
32pub fn is_blank(value: &str) -> bool {
33    value
34        .chars()
35        .all(|character| is_whitespace(character) || is_space_char(character))
36}
37
38/// Formats an `f32` like Java's `Float.toString`.
39#[must_use]
40pub fn float_to_string(value: f32) -> String {
41    floating_to_string(
42        value.is_sign_negative(),
43        value.is_nan(),
44        value.is_infinite(),
45        value == 0.0,
46        &format!("{:.8e}", value.abs()),
47        9,
48        |precision| format!("{:.*e}", precision, value.abs()),
49        |candidate| candidate.parse::<f32>().ok().map(f32::to_bits) == Some(value.abs().to_bits()),
50    )
51}
52
53pub(crate) fn double_to_string(value: f64) -> String {
54    floating_to_string(
55        value.is_sign_negative(),
56        value.is_nan(),
57        value.is_infinite(),
58        value == 0.0,
59        &format!("{:.16e}", value.abs()),
60        17,
61        |precision| format!("{:.*e}", precision, value.abs()),
62        |candidate| candidate.parse::<f64>().ok().map(f64::to_bits) == Some(value.abs().to_bits()),
63    )
64}
65
66#[expect(
67    clippy::too_many_arguments,
68    clippy::fn_params_excessive_bools,
69    reason = "shared Java float formatting parameters"
70)]
71fn floating_to_string(
72    negative: bool,
73    nan: bool,
74    infinite: bool,
75    zero: bool,
76    precise: &str,
77    max_digits: usize,
78    scientific: impl Fn(usize) -> String,
79    rounds_to_value: impl Fn(&str) -> bool,
80) -> String {
81    if nan {
82        return "NaN".to_owned();
83    }
84    if infinite {
85        return if negative { "-Infinity" } else { "Infinity" }.to_owned();
86    }
87    if zero {
88        return if negative { "-0.0" } else { "0.0" }.to_owned();
89    }
90
91    let Some((precise_mantissa, _)) = precise.split_once('e') else {
92        panic!("Rust scientific formatting omitted its exponent");
93    };
94    let precise_digits = precise_mantissa.replace('.', "");
95    let one_digit_is_exact = precise_digits[1..].bytes().all(|digit| digit == b'0');
96    let mut selected = None;
97    for length in 1..=max_digits {
98        let formatted = scientific(length - 1);
99        let Some((mantissa, exponent)) = formatted.split_once('e') else {
100            panic!("Rust scientific formatting omitted its exponent");
101        };
102        if !rounds_to_value(&formatted) {
103            continue;
104        }
105        let Ok(exponent) = exponent.parse::<i32>() else {
106            panic!("Rust scientific formatting emitted a non-decimal exponent");
107        };
108        let digits = mantissa.replace('.', "");
109        selected = Some((digits, exponent - length as i32 + 1));
110        if length >= 2 || one_digit_is_exact {
111            break;
112        }
113    }
114    let Some((mut digits, decimal_exponent)) = selected else {
115        panic!("full-precision Rust decimal did not round-trip");
116    };
117    while digits.ends_with('0') {
118        digits.pop();
119    }
120    let scientific_exponent = digits.len() as i32 + decimal_exponent - 1;
121    let mut output = if (-3..0).contains(&scientific_exponent) {
122        format!(
123            "0.{}{}",
124            "0".repeat((-scientific_exponent - 1) as usize),
125            digits
126        )
127    } else if (0..7).contains(&scientific_exponent) {
128        let decimal_position = (scientific_exponent + 1) as usize;
129        if decimal_position >= digits.len() {
130            format!(
131                "{}{}.0",
132                digits,
133                "0".repeat(decimal_position - digits.len())
134            )
135        } else {
136            format!(
137                "{}.{}",
138                &digits[..decimal_position],
139                &digits[decimal_position..]
140            )
141        }
142    } else {
143        let fraction = if digits.len() == 1 { "0" } else { &digits[1..] };
144        format!("{}.{}E{scientific_exponent}", &digits[..1], fraction)
145    };
146    if negative {
147        output.insert(0, '-');
148    }
149    output
150}
151
152#[cfg(test)]
153mod tests {
154    use super::{float_to_string, is_blank, is_space_char, is_whitespace};
155
156    #[test]
157    fn matches_java_whitespace_exclusions() {
158        assert!(is_whitespace(' '));
159        assert!(is_whitespace('\u{1680}'));
160        for non_breaking_space in ['\u{0085}', '\u{00a0}', '\u{2007}', '\u{202f}'] {
161            assert!(!is_whitespace(non_breaking_space));
162        }
163    }
164
165    #[test]
166    fn space_char_includes_unicode_space_separators() {
167        for space in [' ', '\u{00a0}', '\u{2007}', '\u{202f}'] {
168            assert!(is_space_char(space));
169        }
170        assert!(!is_space_char('\u{0085}'));
171    }
172
173    #[test]
174    fn blank_combines_java_whitespace_and_space_char() {
175        assert!(is_blank(""));
176        assert!(is_blank("\u{001c}\u{00a0}\u{202f}"));
177        assert!(!is_blank("\u{0085}"));
178        assert!(!is_blank(" text "));
179    }
180
181    #[test]
182    fn float_string_matches_java_integral_values() {
183        assert_eq!(float_to_string(0.0), "0.0");
184        assert_eq!(float_to_string(-0.0), "-0.0");
185        assert_eq!(float_to_string(90.0), "90.0");
186        assert_eq!(float_to_string(45.5), "45.5");
187    }
188}