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