fixed_decimal/
scientific.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use core::fmt;
6use core::str::FromStr;
7
8use crate::Decimal;
9use crate::FixedInteger;
10use crate::ParseError;
11
12/// A struct containing a [`Decimal`] significand together with an exponent, representing a
13/// number written in scientific notation, such as 1.729×10³.
14///
15/// This structure represents any 0s shown in the significand and exponent,
16/// and an optional sign for both the significand and the exponent.
17#[derive(Debug, Clone, PartialEq)]
18pub struct ScientificDecimal {
19    significand: Decimal,
20    exponent: FixedInteger,
21}
22
23impl ScientificDecimal {
24    pub fn from(significand: Decimal, exponent: FixedInteger) -> Self {
25        ScientificDecimal {
26            significand,
27            exponent,
28        }
29    }
30}
31
32/// Render the [`ScientificDecimal`] as a string of ASCII digits with a possible decimal point,
33/// followed by the letter 'e', and the exponent.
34///
35/// # Examples
36///
37/// ```
38/// # use fixed_decimal::Decimal;
39/// # use fixed_decimal::FixedInteger;
40/// # use fixed_decimal::ScientificDecimal;
41/// # use std::str::FromStr;
42/// # use writeable::assert_writeable_eq;
43/// #
44/// assert_writeable_eq!(
45///     ScientificDecimal::from(
46///         {
47///             let mut dec = Decimal::from(1729u32);
48///             dec.multiply_pow10(-3);
49///             dec
50///         },
51///         FixedInteger::from(3)
52///     ),
53///     "1.729e3"
54/// );
55/// assert_writeable_eq!(
56///     ScientificDecimal::from(
57///         Decimal::from_str("+1.729").unwrap(),
58///         FixedInteger::from_str("+03").unwrap()
59///     ),
60///     "+1.729e+03"
61/// );
62/// ```
63impl writeable::Writeable for ScientificDecimal {
64    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
65        self.significand.write_to(sink)?;
66        sink.write_char('e')?;
67        self.exponent.write_to(sink)
68    }
69
70    fn writeable_length_hint(&self) -> writeable::LengthHint {
71        self.significand.writeable_length_hint() + 1 + self.exponent.writeable_length_hint()
72    }
73}
74
75writeable::impl_display_with_writeable!(ScientificDecimal);
76
77impl ScientificDecimal {
78    #[inline]
79    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
80        Self::try_from_utf8(s.as_bytes())
81    }
82
83    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
84        // Fixed_Decimal::try_from supports scientific notation; ensure that
85        // we don’t accept something like 1e1E1.  Splitting on 'e' ensures that
86        // we disallow 1e1e1.
87        if code_units.contains(&b'E') {
88            return Err(ParseError::Syntax);
89        }
90        let mut parts = code_units.split(|&c| c == b'e');
91        let significand = parts.next().ok_or(ParseError::Syntax)?;
92        let exponent = parts.next().ok_or(ParseError::Syntax)?;
93        if parts.next().is_some() {
94            return Err(ParseError::Syntax);
95        }
96        Ok(ScientificDecimal::from(
97            Decimal::try_from_utf8(significand)?,
98            FixedInteger::try_from_utf8(exponent)?,
99        ))
100    }
101}
102
103impl FromStr for ScientificDecimal {
104    type Err = ParseError;
105    #[inline]
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        Self::try_from_str(s)
108    }
109}
110
111#[test]
112fn test_scientific_syntax_error() {
113    #[derive(Debug)]
114    struct TestCase {
115        pub input_str: &'static str,
116        pub expected_err: Option<ParseError>,
117    }
118    let cases = [
119        TestCase {
120            input_str: "5",
121            expected_err: Some(ParseError::Syntax),
122        },
123        TestCase {
124            input_str: "-123c4",
125            expected_err: Some(ParseError::Syntax),
126        },
127        TestCase {
128            input_str: "-123e",
129            expected_err: Some(ParseError::Syntax),
130        },
131        TestCase {
132            input_str: "1e10",
133            expected_err: None,
134        },
135        TestCase {
136            input_str: "1e1e1",
137            expected_err: Some(ParseError::Syntax),
138        },
139        TestCase {
140            input_str: "1e1E1",
141            expected_err: Some(ParseError::Syntax),
142        },
143        TestCase {
144            input_str: "1E1e1",
145            expected_err: Some(ParseError::Syntax),
146        },
147        TestCase {
148            input_str: "-1e+01",
149            expected_err: None,
150        },
151        TestCase {
152            input_str: "-1e+1.0",
153            expected_err: Some(ParseError::Limit),
154        },
155        TestCase {
156            input_str: "-1e+-1",
157            expected_err: Some(ParseError::Syntax),
158        },
159        TestCase {
160            input_str: "123E4",
161            expected_err: Some(ParseError::Syntax),
162        },
163    ];
164    for cas in &cases {
165        match ScientificDecimal::from_str(cas.input_str) {
166            Ok(dec) => {
167                assert_eq!(cas.expected_err, None, "{cas:?}");
168                assert_eq!(cas.input_str, dec.to_string(), "{cas:?}");
169            }
170            Err(err) => {
171                assert_eq!(cas.expected_err, Some(err), "{cas:?}");
172            }
173        }
174    }
175}