1#[cfg(not(feature = "fixed-decimal"))]
14mod own;
15#[cfg(not(feature = "fixed-decimal"))]
16pub(crate) use own::Decimal;
17
18#[cfg(feature = "fixed-decimal")]
19mod fixed;
20#[cfg(feature = "fixed-decimal")]
21pub(crate) use fixed::Decimal;
22
23pub(crate) const INPUT_DIGITS: usize = 40;
26
27pub(crate) const MAX_EXPONENT: i32 = 9999;
30
31pub(crate) const MAX_MAGNITUDE: i32 = MAX_EXPONENT + 2 * 64;
33
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
36pub(crate) enum ParseError {
37 Syntax,
39 Limit,
42}
43
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
46#[non_exhaustive]
47pub enum RoundingMode {
48 Ceil,
50 Floor,
52 Expand,
54 Trunc,
56 HalfCeil,
58 HalfFloor,
60 HalfExpand,
62 HalfTrunc,
64 HalfEven,
66}
67
68#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
71pub(crate) enum Increment {
72 One,
73 Two,
74 Five,
75 TwentyFive,
76}
77
78impl Increment {
79 #[cfg_attr(feature = "fixed-decimal", allow(dead_code))]
80 pub(crate) const fn value(self) -> u8 {
81 match self {
82 Increment::One => 1,
83 Increment::Two => 2,
84 Increment::Five => 5,
85 Increment::TwentyFive => 25,
86 }
87 }
88}
89
90pub(crate) struct Literal<'s> {
92 pub(crate) neg: bool,
93 pub(crate) int: &'s [u8],
95 pub(crate) frac: &'s [u8],
97 pub(crate) exp: i32,
98}
99
100pub(crate) fn split_literal(b: &[u8]) -> Result<Literal<'_>, ParseError> {
102 let neg = b.first() == Some(&b'-');
103 let rest = if neg { b.get(1..).unwrap_or(&[]) } else { b };
104 let mut i = 0;
105 match rest.first() {
106 Some(b'0') => i = 1,
107 Some(b'1'..=b'9') => {
108 while rest.get(i).is_some_and(u8::is_ascii_digit) {
109 i += 1;
110 }
111 }
112 _ => return Err(ParseError::Syntax),
113 }
114 let int = rest.get(..i).unwrap_or(&[]);
115 let mut frac: &[u8] = &[];
116 if rest.get(i) == Some(&b'.') {
117 i += 1;
118 let start = i;
119 while rest.get(i).is_some_and(u8::is_ascii_digit) {
120 i += 1;
121 }
122 if i == start {
123 return Err(ParseError::Syntax);
124 }
125 frac = rest.get(start..i).unwrap_or(&[]);
126 }
127 let mut exp: i32 = 0;
128 let mut limit = false;
129 if matches!(rest.get(i), Some(b'e' | b'E')) {
130 i += 1;
131 let neg_exp = match rest.get(i) {
132 Some(b'-') => {
133 i += 1;
134 true
135 }
136 Some(b'+') => {
137 i += 1;
138 false
139 }
140 _ => false,
141 };
142 let start = i;
143 while let Some(&d) = rest.get(i).filter(|d| d.is_ascii_digit()) {
144 if !limit {
145 exp = exp * 10 + i32::from(d - b'0');
146 limit = exp > MAX_EXPONENT;
147 }
148 i += 1;
149 }
150 if i == start {
151 return Err(ParseError::Syntax);
152 }
153 if neg_exp {
154 exp = -exp;
155 }
156 }
157 if i != rest.len() {
158 return Err(ParseError::Syntax);
159 }
160 if limit {
161 return Err(ParseError::Limit);
162 }
163 Ok(Literal {
164 neg,
165 int,
166 frac,
167 exp,
168 })
169}
170
171#[cfg(test)]
172#[allow(clippy::panic)]
173mod tests {
174 use super::{Decimal, Increment, ParseError, RoundingMode};
175 use alloc::string::String;
176
177 fn plain(d: &Decimal) -> String {
178 let mut s = String::new();
179 d.write_plain(&mut s);
180 s
181 }
182
183 fn p(s: &str) -> Decimal {
184 match Decimal::parse(s.as_bytes()) {
185 Ok(d) => d,
186 Err(e) => panic!("{s}: {e:?}"),
187 }
188 }
189
190 #[test]
191 fn parse_and_print() {
192 for (src, want) in [
193 ("0", "0"),
194 ("-0", "-0"),
195 ("4.2", "4.2"),
196 ("-4.20", "-4.2"),
197 ("0.42e+1", "4.2"),
198 ("1E3", "1000"),
199 ("1e-2", "0.01"),
200 ("10", "10"),
201 ("0.000", "0"),
202 ("120.0500", "120.05"),
203 ("1e21", "1000000000000000000000"),
204 ("1.5e-7", "0.00000015"),
205 ] {
206 assert_eq!(plain(&p(src)), want, "{src}");
207 }
208 for bad in [
209 "00", "042", "1.", "1e", "1E", "1.e", "1.2e", "1.e3", "1e+", "1e-", "1.0e2.0", "foo",
210 ".1", "+1", "0x1", "", "-", "1 ", " 1",
211 ] {
212 assert_eq!(
213 Decimal::parse(bad.as_bytes()).err(),
214 Some(ParseError::Syntax),
215 "{bad}"
216 );
217 }
218 assert_eq!(Decimal::parse(b"1e10000").err(), Some(ParseError::Limit));
219 assert_eq!(plain(&p("1e0000000009")), "1000000000");
220 let long = "1234567890123456789012345678901234567890";
221 assert_eq!(plain(&p(long)), long);
222 let longer = "12345678901234567890123456789012345678901";
223 assert_eq!(
224 Decimal::parse(longer.as_bytes()).err(),
225 Some(ParseError::Limit)
226 );
227 assert_eq!(
229 plain(&p(
230 "0.000000000000000000001234567890123456789012345678901234567890000"
231 )),
232 "0.00000000000000000000123456789012345678901234567890123456789"
233 );
234 }
235
236 fn rounded(s: &str, pos: i16, mode: RoundingMode, inc: Increment) -> String {
237 let mut d = p(s);
238 d.round(pos, mode, inc);
239 plain(&d)
240 }
241
242 #[test]
243 fn rounding_modes() {
244 use RoundingMode::*;
245 let one = Increment::One;
246 let table: &[(&str, [&str; 9])] = &[
248 (
250 "-1.5",
251 ["-1", "-2", "-2", "-1", "-1", "-2", "-2", "-1", "-2"],
252 ),
253 (
254 "-0.5",
255 ["-0", "-1", "-1", "-0", "-0", "-1", "-1", "-0", "-0"],
256 ),
257 ("0.4", ["1", "0", "1", "0", "0", "0", "0", "0", "0"]),
258 ("0.5", ["1", "0", "1", "0", "1", "0", "1", "0", "0"]),
259 ("0.6", ["1", "0", "1", "0", "1", "1", "1", "1", "1"]),
260 ("1.5", ["2", "1", "2", "1", "2", "1", "2", "1", "2"]),
261 ("2.5", ["3", "2", "3", "2", "3", "2", "3", "2", "2"]),
262 ];
263 let modes = [
264 Ceil, Floor, Expand, Trunc, HalfCeil, HalfFloor, HalfExpand, HalfTrunc, HalfEven,
265 ];
266 for (v, wants) in table {
267 for (m, want) in modes.iter().zip(wants) {
268 assert_eq!(rounded(v, 0, *m, one), *want, "{v} {m:?}");
269 }
270 }
271 assert_eq!(rounded("9.995", -2, HalfExpand, one), "10");
272 assert_eq!(rounded("999.5", 0, HalfExpand, one), "1000");
273 assert_eq!(rounded("0.004", 0, Ceil, one), "1");
274 assert_eq!(rounded("0.004", 0, HalfExpand, one), "0");
275 assert_eq!(rounded("1234", 2, HalfExpand, one), "1200");
276 assert_eq!(rounded("1.25000000000000000001", -1, HalfEven, one), "1.3");
277 assert_eq!(rounded("1.25", -1, HalfEven, one), "1.2");
278 assert_eq!(rounded("1.35", -1, HalfEven, one), "1.4");
279 }
280
281 #[test]
282 fn increments() {
283 use RoundingMode::*;
284 assert_eq!(rounded("1.23", -2, HalfExpand, Increment::Five), "1.25");
286 assert_eq!(rounded("1.22", -2, HalfExpand, Increment::Five), "1.2");
287 assert_eq!(rounded("1.225", -2, HalfExpand, Increment::Five), "1.25");
288 assert_eq!(rounded("1.225", -2, HalfEven, Increment::Five), "1.2");
289 assert_eq!(rounded("1.275", -2, HalfEven, Increment::Five), "1.3");
290 assert_eq!(rounded("1.3", -1, HalfExpand, Increment::TwentyFive), "2.5");
291 assert_eq!(rounded("1.1", -1, HalfExpand, Increment::TwentyFive), "0");
292 assert_eq!(rounded("1.125", -2, HalfEven, Increment::TwentyFive), "1");
293 assert_eq!(rounded("1.375", -2, HalfEven, Increment::TwentyFive), "1.5");
294 assert_eq!(rounded("3", 0, HalfEven, Increment::Two), "4");
295 assert_eq!(rounded("5", 0, HalfEven, Increment::Two), "4");
296 assert_eq!(rounded("7", 0, HalfEven, Increment::Two), "8");
297 assert_eq!(rounded("7", 0, Trunc, Increment::Two), "6");
298 assert_eq!(rounded("-7", 0, Floor, Increment::Two), "-8");
299 assert_eq!(rounded("-7", 0, Ceil, Increment::Two), "-6");
300 assert_eq!(rounded("99.9", 1, HalfExpand, Increment::Five), "100");
301 assert_eq!(rounded("0.1", 2, Ceil, Increment::TwentyFive), "2500");
302 assert_eq!(rounded("12", -3, HalfExpand, Increment::Five), "12");
303 }
304
305 #[test]
306 fn addition() {
307 let add = |a: &str, b: &str| {
308 let d = p(a).add(&p(b)).map(|d| plain(&d));
309 d.unwrap_or_default()
310 };
311 assert_eq!(add("41", "1"), "42");
312 assert_eq!(add("52", "-10"), "42");
313 assert_eq!(add("1", "-3"), "-2");
314 assert_eq!(add("-1.5", "2"), "0.5");
315 assert_eq!(add("0.25", "-1"), "-0.75");
316 assert_eq!(add("999", "99"), "1098");
317 assert_eq!(add("-0", "0"), "0");
318 assert_eq!(add("1", "-1"), "0");
319 #[cfg(not(feature = "fixed-decimal"))]
322 assert_eq!(add("1e30", "1"), "1000000000000000000000000000001");
323 assert!(p("1e45").add(&p("1")).is_none());
324 }
325}