fixed_decimal/
compact.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::ParseError;
10
11/// A struct containing a [`Decimal`] significand together with an exponent, representing a
12/// number written in compact notation (such as 1.2M).
13/// This represents a _source number_, as defined
14/// [in UTS #35](https://www.unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).
15/// The value exponent=0 represents a number in non-compact
16/// notation (such as 1 200 000).
17///
18/// This is distinct from [`crate::ScientificDecimal`] because it does not represent leading 0s
19/// nor a sign in the exponent, and behaves differently in pluralization.
20#[derive(Debug, Clone, PartialEq)]
21pub struct CompactDecimal {
22    significand: Decimal,
23    exponent: u8,
24}
25
26impl CompactDecimal {
27    /// Constructs a [`CompactDecimal`] from its significand and exponent.
28    pub fn from_significand_and_exponent(significand: Decimal, exponent: u8) -> Self {
29        Self {
30            significand,
31            exponent,
32        }
33    }
34
35    /// Returns a reference to the significand of `self`.
36    /// ```
37    /// # use fixed_decimal::CompactDecimal;
38    /// # use fixed_decimal::Decimal;
39    /// # use std::str::FromStr;
40    /// #
41    /// assert_eq!(
42    ///     CompactDecimal::from_str("+1.20c6").unwrap().significand(),
43    ///     &Decimal::from_str("+1.20").unwrap()
44    /// );
45    /// ```
46    pub fn significand(&self) -> &Decimal {
47        &self.significand
48    }
49
50    /// Returns the significand of `self`.
51    /// ```
52    /// # use fixed_decimal::CompactDecimal;
53    /// # use fixed_decimal::Decimal;
54    /// # use std::str::FromStr;
55    /// #
56    /// assert_eq!(
57    ///     CompactDecimal::from_str("+1.20c6")
58    ///         .unwrap()
59    ///         .into_significand(),
60    ///     Decimal::from_str("+1.20").unwrap()
61    /// );
62    /// ```
63    pub fn into_significand(self) -> Decimal {
64        self.significand
65    }
66
67    /// Returns the exponent of `self`.
68    /// ```
69    /// # use fixed_decimal::CompactDecimal;
70    /// # use std::str::FromStr;
71    /// #
72    /// assert_eq!(CompactDecimal::from_str("+1.20c6").unwrap().exponent(), 6);
73    /// assert_eq!(CompactDecimal::from_str("1729").unwrap().exponent(), 0);
74    /// ```
75    pub fn exponent(&self) -> u8 {
76        self.exponent
77    }
78}
79
80/// Render the [`CompactDecimal`] in sampleValue syntax.
81/// The letter c is used, rather than the deprecated e.
82///
83/// # Examples
84///
85/// ```
86/// # use fixed_decimal::CompactDecimal;
87/// # use std::str::FromStr;
88/// # use writeable::assert_writeable_eq;
89/// #
90/// assert_writeable_eq!(
91///     CompactDecimal::from_str("+1.20c6").unwrap(),
92///     "+1.20c6"
93/// );
94/// assert_writeable_eq!(CompactDecimal::from_str("+1729").unwrap(), "+1729");
95/// ```
96impl writeable::Writeable for CompactDecimal {
97    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
98        self.significand.write_to(sink)?;
99        if self.exponent != 0 {
100            sink.write_char('c')?;
101            self.exponent.write_to(sink)?;
102        }
103        Ok(())
104    }
105
106    fn writeable_length_hint(&self) -> writeable::LengthHint {
107        let mut result = self.significand.writeable_length_hint();
108        if self.exponent != 0 {
109            result += self.exponent.writeable_length_hint() + 1;
110        }
111        result
112    }
113}
114
115writeable::impl_display_with_writeable!(CompactDecimal);
116
117impl CompactDecimal {
118    #[inline]
119    /// Parses a [`CompactDecimal`].
120    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
121        Self::try_from_utf8(s.as_bytes())
122    }
123
124    /// The deprecated letter e is not accepted as a synonym for c.
125    fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
126        if code_units.iter().any(|&c| c == b'e' || c == b'E') {
127            return Err(ParseError::Syntax);
128        }
129        let mut parts = code_units.split(|&c| c == b'c');
130        let significand = Decimal::try_from_utf8(parts.next().ok_or(ParseError::Syntax)?)?;
131        match parts.next() {
132            None => Ok(CompactDecimal {
133                significand,
134                exponent: 0,
135            }),
136            Some(exponent_str) => {
137                let exponent_str =
138                    core::str::from_utf8(exponent_str).map_err(|_| ParseError::Syntax)?;
139                if parts.next().is_some() {
140                    return Err(ParseError::Syntax);
141                }
142                if exponent_str.is_empty()
143                    || exponent_str.bytes().next() == Some(b'0')
144                    || !exponent_str.bytes().all(|c| c.is_ascii_digit())
145                {
146                    return Err(ParseError::Syntax);
147                }
148                let exponent = exponent_str.parse().map_err(|_| ParseError::Limit)?;
149                Ok(CompactDecimal {
150                    significand,
151                    exponent,
152                })
153            }
154        }
155    }
156}
157
158impl FromStr for CompactDecimal {
159    type Err = ParseError;
160    fn from_str(s: &str) -> Result<Self, Self::Err> {
161        Self::try_from_str(s)
162    }
163}
164
165#[test]
166fn test_compact_syntax_error() {
167    #[derive(Debug)]
168    struct TestCase {
169        pub input_str: &'static str,
170        pub expected_err: Option<ParseError>,
171    }
172    let cases = [
173        TestCase {
174            input_str: "-123e4",
175            expected_err: Some(ParseError::Syntax),
176        },
177        TestCase {
178            input_str: "-123c",
179            expected_err: Some(ParseError::Syntax),
180        },
181        TestCase {
182            input_str: "1c10",
183            expected_err: None,
184        },
185        TestCase {
186            input_str: "1E1c1",
187            expected_err: Some(ParseError::Syntax),
188        },
189        TestCase {
190            input_str: "1e1c1",
191            expected_err: Some(ParseError::Syntax),
192        },
193        TestCase {
194            input_str: "1c1e1",
195            expected_err: Some(ParseError::Syntax),
196        },
197        TestCase {
198            input_str: "1c1E1",
199            expected_err: Some(ParseError::Syntax),
200        },
201        TestCase {
202            input_str: "-1c01",
203            expected_err: Some(ParseError::Syntax),
204        },
205        TestCase {
206            input_str: "-1c-1",
207            expected_err: Some(ParseError::Syntax),
208        },
209        TestCase {
210            input_str: "-1c1",
211            expected_err: None,
212        },
213    ];
214    for cas in &cases {
215        match CompactDecimal::from_str(cas.input_str) {
216            Ok(dec) => {
217                assert_eq!(cas.expected_err, None, "{cas:?}");
218                assert_eq!(cas.input_str, dec.to_string(), "{cas:?}");
219            }
220            Err(err) => {
221                assert_eq!(cas.expected_err, Some(err), "{cas:?}");
222            }
223        }
224    }
225}