ion_rs/text/parsers/
float.rs

1use crate::text::parse_result::{IonParseResult, OrFatalParseError, UpgradeIResult};
2use crate::text::parsers::numeric_support::{
3    digits_before_dot, exponent_digits, floating_point_number,
4};
5use crate::text::parsers::stop_character;
6use crate::text::text_value::TextValue;
7use nom::branch::alt;
8use nom::bytes::streaming::tag;
9use nom::character::streaming::one_of;
10use nom::combinator::{map, opt, recognize};
11use nom::sequence::{pair, preceded, terminated, tuple};
12use nom::Parser;
13use std::str::FromStr;
14
15/// Matches the text representation of a float value and returns the resulting [f64]
16/// as a [TextValue::Float].
17pub(crate) fn parse_float(input: &str) -> IonParseResult<TextValue> {
18    terminated(
19        alt((float_special_value, float_numeric_value)),
20        stop_character,
21    )(input)
22}
23
24/// Matches special IEEE-754 floating point values, including +/- infinity and NaN.
25fn float_special_value(input: &str) -> IonParseResult<TextValue> {
26    map(tag("nan"), |_| TextValue::Float(f64::NAN))
27        .or(map(tag("+inf"), |_| TextValue::Float(f64::INFINITY)))
28        .or(map(tag("-inf"), |_| TextValue::Float(f64::NEG_INFINITY)))
29        .parse(input)
30        .upgrade()
31}
32
33/// Matches numeric floating point values. (e.g. `7e0`, `7.1e0` or `71e-1`)
34fn float_numeric_value(input: &str) -> IonParseResult<TextValue> {
35    let (remaining, text) = recognize(tuple((
36        alt((
37            floating_point_number,
38            recognize(pair(opt(tag("-")), digits_before_dot)),
39        )),
40        recognize(float_exponent_marker_followed_by_digits),
41    )))(input)?;
42    // TODO: Reusable buffer for sanitization
43    let mut sanitized = text.replace('_', "");
44    if sanitized.ends_with('e') || sanitized.ends_with('E') {
45        sanitized.push('0');
46    }
47    let float = f64::from_str(&sanitized)
48        .or_fatal_parse_error(input, "could not parse float as f64")?
49        .1;
50    Ok((remaining, TextValue::Float(float)))
51}
52
53fn float_exponent_marker_followed_by_digits(input: &str) -> IonParseResult<&str> {
54    preceded(one_of("eE"), exponent_digits)(input)
55}
56
57#[cfg(test)]
58mod float_parsing_tests {
59    use crate::text::parsers::float::parse_float;
60    use crate::text::parsers::unit_test_support::{parse_test_err, parse_test_ok, parse_unwrap};
61    use crate::text::text_value::TextValue;
62    use std::str::FromStr;
63
64    fn parse_equals(text: &str, expected: f64) {
65        parse_test_ok(parse_float, text, TextValue::Float(expected))
66    }
67
68    fn parse_fails(text: &str) {
69        parse_test_err(parse_float, text)
70    }
71
72    #[test]
73    fn test_parse_float_special_values() {
74        parse_equals("+inf ", f64::INFINITY);
75        parse_equals("-inf ", f64::NEG_INFINITY);
76
77        // Can't test two NaNs for equality with assert_eq
78        let value = parse_unwrap(parse_float, "nan ");
79        if let TextValue::Float(f) = value {
80            assert!(f.is_nan());
81        } else {
82            panic!("Expected NaN, but got: {value:?}");
83        }
84
85        // -0 keeps its negative sign
86        let value = parse_unwrap(parse_float, "-0e0 ");
87        if let TextValue::Float(f) = value {
88            assert!(f == 0.0f64);
89            assert!(f.is_sign_negative())
90        } else {
91            panic!("Expected -0e0, but got: {value:?}");
92        }
93    }
94
95    #[test]
96    fn test_parse_float_numeric_values() {
97        parse_equals("0.0e0 ", 0.0);
98        parse_equals("0E0 ", 0.0);
99        parse_equals("0e0 ", 0e0);
100        parse_equals("305e1 ", 3050.0);
101        parse_equals("305.0e1 ", 3050.0);
102        parse_equals("-0.279e3 ", -279.0);
103        parse_equals("-279e0 ", -279.0);
104        parse_equals("-279.5e0 ", -279.5);
105
106        // Missing exponent (would be parsed as an integer)
107        parse_fails("305 ");
108        // Has exponent delimiter but missing exponent
109        parse_fails("305e ");
110        // No digits before the decimal point
111        parse_fails(".305e ");
112        // Fractional exponent
113        parse_fails("305e0.5");
114        // Negative fractional exponent
115        parse_fails("305e-0.5");
116        // Doesn't consume leading whitespace
117        parse_fails(" 305e1 ");
118        // Doesn't accept leading zeros
119        parse_fails("0305e1 ");
120        // Doesn't accept leading plus sign
121        parse_fails("+305e1 ");
122        // Doesn't accept multiple negative signs
123        parse_fails("--305e1 ");
124        // Doesn't accept a number if it's the last thing in the input (might be incomplete stream)
125        parse_fails("305e1");
126    }
127
128    #[test]
129    fn test_parse_float_numeric_values_with_underscores() {
130        parse_equals("111_111e222 ", 111111.0 * 10f64.powf(222f64));
131        parse_equals("111_111.667e222 ", 111111.667 * 10f64.powf(222f64));
132        parse_equals("111_111e222_222 ", 111111.0 * 10f64.powf(222222f64));
133        parse_equals("-999_9e9_9 ", f64::from_str("-9999e99").unwrap());
134
135        // Doesn't accept leading underscores
136        parse_fails("_305e1 ");
137        // Doesn't accept trailing underscores
138        parse_fails("305e1_ ");
139        // Doesn't accept multiple consecutive underscores
140        parse_fails("30__5e1 ");
141    }
142}