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