1pub use parse::parse;
10pub use print::TokenAmountPretty;
11
12mod si {
14 use bigdecimal::BigDecimal;
15
16 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19 pub struct Prefix {
20 pub name: &'static str,
22 pub units: &'static [&'static str],
24 pub exponent: i8,
26 pub multiplier: &'static str,
28 }
29
30 impl Prefix {
31 pub fn multiplier(&self) -> BigDecimal {
33 self.multiplier.parse().unwrap()
34 }
35 }
36
37 macro_rules! define_prefixes {
39 ($($name:ident $symbol:ident$(or $alt_symbol:ident)* $base_10:literal $decimal:literal),* $(,)?) =>
40 {
41 $(
43 #[allow(non_upper_case_globals)]
44 pub const $name: Prefix = Prefix {
45 name: stringify!($name),
46 units: &[stringify!($symbol) $(, stringify!($alt_symbol))* ],
47 exponent: $base_10,
48 multiplier: stringify!($decimal),
49 };
50 )*
51
52 pub const SUPPORTED_PREFIXES: &[Prefix] =
55 &[
56 $(
57 $name
58 ,)*
59 ];
60 };
61 }
62
63 define_prefixes! {
64 quetta Q 30 1000000000000000000000000000000,
65 ronna R 27 1000000000000000000000000000,
66 yotta Y 24 1000000000000000000000000,
67 zetta Z 21 1000000000000000000000,
68 exa E 18 1000000000000000000,
69 peta P 15 1000000000000000,
70 tera T 12 1000000000000,
71 giga G 9 1000000000,
72 mega M 6 1000000,
73 kilo k 3 1000,
74 milli m -3 0.001,
84 micro μ or u -6 0.000001,
85 nano n -9 0.000000001,
86 pico p -12 0.000000000001,
87 femto f -15 0.000000000000001,
88 atto a -18 0.000000000000000001,
89 zepto z -21 0.000000000000000000001,
90 yocto y -24 0.000000000000000000000001,
91 ronto r -27 0.000000000000000000000000001,
92 quecto q -30 0.000000000000000000000000000001,
93 }
94
95 #[test]
96 fn sorted() {
97 let is_sorted_biggest_first = SUPPORTED_PREFIXES
98 .windows(2)
99 .all(|pair| pair[0].multiplier() > pair[1].multiplier());
100 assert!(is_sorted_biggest_first)
101 }
102}
103
104mod parse {
105 use crate::shim::econ::TokenAmount;
108 use anyhow::{anyhow, bail};
109 use bigdecimal::{BigDecimal, ParseBigDecimalError};
110 use nom::{
111 IResult, Parser,
112 bytes::complete::tag,
113 character::complete::multispace0,
114 combinator::{map_res, opt},
115 error::{FromExternalError, ParseError},
116 number::complete::recognize_float,
117 sequence::terminated,
118 };
119
120 use super::si;
121
122 pub fn parse(input: &str) -> anyhow::Result<TokenAmount> {
141 let (mut big_decimal, scale) = parse_big_decimal_and_scale(input)?;
142
143 if let Some(scale) = scale {
144 big_decimal *= scale.multiplier();
145 }
146
147 let fil = big_decimal;
148 let attos = fil * si::atto.multiplier().inverse();
149
150 if !attos.is_integer() {
151 bail!("sub-atto amounts are not allowed");
152 }
153
154 let (attos, scale) = attos.with_scale(0).into_bigint_and_exponent();
155 assert_eq!(scale, 0, "we've just set the scale!");
156
157 Ok(TokenAmount::from_atto(attos))
158 }
159
160 fn nom2anyhow(e: nom::Err<nom::error::Error<&str>>) -> anyhow::Error {
161 anyhow!("parse error: {e}")
162 }
163
164 fn parse_big_decimal_and_scale(
165 input: &str,
166 ) -> anyhow::Result<(BigDecimal, Option<si::Prefix>)> {
167 let input = match (input.strip_suffix("FIL"), input.strip_suffix("fil")) {
169 (Some(stripped), _) => stripped.trim_end(),
171 (_, Some(stripped)) => stripped.trim_end(),
172 _ => input,
173 };
174
175 let (input, big_decimal) = permit_trailing_ws(bigdecimal)
176 .parse(input)
177 .map_err(nom2anyhow)?;
178 let (input, scale) = opt(permit_trailing_ws(si_scale))
179 .parse(input)
180 .map_err(nom2anyhow)?;
181
182 if !input.is_empty() {
183 bail!("Unexpected trailing input: {input}")
184 }
185
186 Ok((big_decimal, scale))
187 }
188
189 fn permit_trailing_ws<'a, I, O, E: ParseError<&'a str>>(
190 inner: I,
191 ) -> impl Parser<&'a str, Output = O, Error = E>
192 where
193 I: FnMut(&'a str) -> IResult<&'a str, O, E>,
194 {
195 terminated(inner, multispace0)
196 }
197
198 fn si_scale<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, si::Prefix, E> {
200 let mut scales = si::SUPPORTED_PREFIXES
204 .iter()
205 .flat_map(|scale| {
206 std::iter::once(&scale.name)
207 .chain(scale.units)
208 .map(move |prefix| (*prefix, scale))
209 })
210 .collect::<Vec<_>>();
211 scales.sort_by_key(|(prefix, _)| std::cmp::Reverse(*prefix));
212
213 for (prefix, scale) in scales {
214 if let Ok((rem, _prefix)) = tag::<_, _, E>(prefix)(input) {
215 return Ok((rem, *scale));
216 }
217 }
218
219 Err(nom::Err::Error(E::from_error_kind(
220 input,
221 nom::error::ErrorKind::Alt,
222 )))
223 }
224
225 fn bigdecimal<'a, E>(input: &'a str) -> IResult<&'a str, BigDecimal, E>
227 where
228 E: ParseError<&'a str> + FromExternalError<&'a str, ParseBigDecimalError>,
229 {
230 map_res(recognize_float, str::parse).parse(input)
231 }
232
233 #[cfg(test)]
234 mod tests {
235 use std::str::FromStr as _;
236
237 use num::{BigInt, One as _};
238
239 use super::*;
240
241 #[test]
242 fn cover_scales() {
243 for scale in si::SUPPORTED_PREFIXES {
244 let _did_not_panic = scale.multiplier();
245 }
246 }
247
248 #[test]
249 fn parse_bigdecimal() {
250 fn do_test(input: &str, expected: &str) {
251 let expected = BigDecimal::from_str(expected).unwrap();
252 let (rem, actual) = bigdecimal::<nom::error::Error<_>>(input).unwrap();
253 assert_eq!(expected, actual);
254 assert!(rem.is_empty());
255 }
256 do_test("1", "1");
257 do_test("0.1", "0.1");
258 do_test(".1", ".1");
259 do_test("1e1", "10");
260 do_test("1.", "1");
261 }
262
263 fn test_dec_scale(
264 input: &str,
265 expected_amount: &str,
266 expected_scale: impl Into<Option<si::Prefix>>,
267 ) {
268 let expected_amount = BigDecimal::from_str(expected_amount).unwrap();
269 let expected_scale = expected_scale.into();
270 let (actual_amount, actual_scale) = parse_big_decimal_and_scale(input).unwrap();
271 assert_eq!(expected_amount, actual_amount, "{input}");
272 assert_eq!(expected_scale, actual_scale, "{input}");
273 }
274
275 #[test]
276 fn basic_bigdecimal_and_scale() {
277 test_dec_scale("1", "1", None);
279
280 test_dec_scale("1 FIL", "1", None);
282 test_dec_scale("1FIL", "1", None);
283 test_dec_scale("1 fil", "1", None);
284 test_dec_scale("1fil", "1", None);
285
286 let possible_units = ["", "fil", "FIL", " fil", " FIL"];
287 let possible_prefixes = ["atto", "a", " atto", " a"];
288
289 for unit in possible_units {
290 for prefix in possible_prefixes {
291 let input = format!("1{prefix}{unit}");
292 test_dec_scale(&input, "1", si::atto)
293 }
294 }
295 }
296
297 #[test]
298 fn parse_exa_and_exponent() {
299 test_dec_scale("1 E", "1", si::exa);
300 test_dec_scale("1e0E", "1", si::exa);
301
302 }
308
309 #[test]
310 fn more_than_96_bits() {
311 use std::iter::once;
312
313 let test_str = once('1')
316 .chain(std::iter::repeat_n('0', 98))
317 .chain(['1'])
318 .collect::<String>();
319 test_dec_scale(&test_str, &test_str, None);
320 }
321
322 #[test]
323 fn disallow_too_small() {
324 parse("1 atto").unwrap();
325 assert_eq!(
326 parse("0.1 atto").unwrap_err().to_string(),
327 "sub-atto amounts are not allowed"
328 )
329 }
330
331 #[test]
332 fn some_values() {
333 let one_atto = TokenAmount::from_atto(BigInt::one());
334 let one_nano = TokenAmount::from_nano(BigInt::one());
335
336 assert_eq!(one_atto, parse("1 atto").unwrap());
337 assert_eq!(one_atto, parse("1000 zepto").unwrap());
338
339 assert_eq!(one_nano, parse("1 nano").unwrap());
340 }
341
342 #[test]
343 fn all_possible_prefixes() {
344 for scale in si::SUPPORTED_PREFIXES {
345 for prefix in scale.units.iter().chain([&scale.name]) {
346 test_dec_scale(&format!("1 {prefix}"), "1", *scale);
348 }
349 }
350 }
351
352 #[test]
353 fn wrong_unit() {
354 parse("1 attoo").unwrap_err();
355 parse("1 atto filecoin").unwrap_err();
356 }
357 }
358}
359
360mod print {
361 use std::fmt;
362
363 use crate::shim::econ::TokenAmount;
364 use bigdecimal::BigDecimal;
365 use num::{BigInt, One as _, Zero as _};
366
367 use super::si;
368
369 fn scale(n: BigDecimal) -> (BigDecimal, Option<si::Prefix>) {
370 for prefix in si::SUPPORTED_PREFIXES
371 .iter()
372 .filter(|prefix| prefix.exponent > 0)
373 {
374 let scaled = (n.clone() * prefix.multiplier().inverse()).normalized();
375 if scaled >= BigDecimal::one() {
376 return (scaled, Some(*prefix));
377 }
378 }
379
380 if n >= BigDecimal::one() {
381 return (n, None);
382 }
383
384 for prefix in si::SUPPORTED_PREFIXES
385 .iter()
386 .filter(|prefix| prefix.exponent < 0)
387 {
388 let scaled = (n.clone() * prefix.multiplier().inverse()).normalized();
389 if scaled >= BigDecimal::one() {
390 return (scaled, Some(*prefix));
391 }
392 }
393
394 let smallest_prefix = si::SUPPORTED_PREFIXES.last().unwrap();
395 (
396 n * smallest_prefix.multiplier().inverse(),
397 Some(*smallest_prefix),
398 )
399 }
400
401 pub struct Pretty {
402 attos: BigInt,
403 }
404
405 impl From<&TokenAmount> for Pretty {
406 fn from(value: &TokenAmount) -> Self {
407 Self {
408 attos: value.atto().clone(),
409 }
410 }
411 }
412
413 pub trait TokenAmountPretty {
414 fn pretty(&self) -> Pretty;
415 }
416
417 impl TokenAmountPretty for TokenAmount {
418 fn pretty(&self) -> Pretty {
451 Pretty::from(self)
452 }
453 }
454
455 impl fmt::Display for Pretty {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 let actual_fil = &self.attos * si::atto.multiplier();
458
459 let fil_for_printing = match f.precision() {
461 None => actual_fil.normalized(),
462 Some(prec) => actual_fil
463 .with_prec(u64::try_from(prec).expect("requested precision is absurd"))
464 .normalized(),
465 };
466
467 let precision_was_lost = fil_for_printing != actual_fil;
468
469 if precision_was_lost {
470 f.write_str("~")?;
471 }
472
473 let (print_me, prefix) = match f.alternate() {
475 true => (fil_for_printing, None),
476 false => scale(fil_for_printing),
477 };
478
479 match print_me.is_zero() {
481 true => f.write_str("0 FIL"),
482 false => match prefix {
483 Some(prefix) => f.write_fmt(format_args!("{print_me} {}FIL", prefix.name)),
484 None => f.write_fmt(format_args!("{print_me} FIL")),
485 },
486 }
487 }
488 }
489
490 #[cfg(test)]
491 mod tests {
492 use std::str::FromStr as _;
493
494 use num::One as _;
495 use pretty_assertions::assert_eq;
496
497 use super::*;
498
499 #[test]
500 fn prefixes_represent_themselves() {
501 for prefix in si::SUPPORTED_PREFIXES {
502 let input = BigDecimal::from_str(prefix.multiplier).unwrap();
503 assert_eq!((BigDecimal::one(), Some(*prefix)), scale(input));
504 }
505 }
506
507 #[test]
508 fn very_large() {
509 let mut one_thousand_quettas = String::from(si::quetta.multiplier);
510 one_thousand_quettas.push_str("000");
511
512 test_scale(&one_thousand_quettas, "1000", si::quetta);
513 }
514
515 #[test]
516 fn very_small() {
517 let mut one_thousanth_of_a_quecto = String::from(si::quecto.multiplier);
518 one_thousanth_of_a_quecto.pop();
519 one_thousanth_of_a_quecto.push_str("0001");
520
521 test_scale(&one_thousanth_of_a_quecto, "0.001", si::quecto);
522 }
523
524 #[track_caller]
525 fn test_scale(
526 input: &str,
527 expected_value: &str,
528 expected_prefix: impl Into<Option<si::Prefix>>,
529 ) {
530 let input = BigDecimal::from_str(input).unwrap();
531 let expected_value = BigDecimal::from_str(expected_value).unwrap();
532 let expected_prefix = expected_prefix.into();
533
534 assert_eq!((expected_value, expected_prefix), scale(input))
535 }
536
537 #[test]
538 fn simple() {
539 test_scale("1000000", "1", si::mega);
540 test_scale("100000", "100", si::kilo);
541 test_scale("10000", "10", si::kilo);
542 test_scale("1000", "1", si::kilo);
543 test_scale("100", "100", None);
544 test_scale("10", "10", None);
545 test_scale("1", "1", None);
546 test_scale("0.1", "100", si::milli);
547 test_scale("0.01", "10", si::milli);
548 test_scale("0.001", "1", si::milli);
549 test_scale("0.0001", "100", si::micro);
550 }
551 #[test]
552 fn trailing_one() {
553 test_scale("10001000", "10.001", si::mega);
554 test_scale("10001", "10.001", si::kilo);
555 test_scale("1000.1", "1.0001", si::kilo);
556 }
557
558 fn attos(input: &str) -> TokenAmount {
559 TokenAmount::from_atto(BigInt::from_str(input).unwrap())
560 }
561
562 fn fils(input: &str) -> TokenAmount {
563 TokenAmount::from_whole(BigInt::from_str(input).unwrap())
564 }
565
566 #[test]
567 fn test_display() {
568 assert_eq!("0 FIL", format!("{}", attos("0").pretty()));
569
570 assert_eq!("1 attoFIL", format!("{}", attos("1").pretty()));
572 assert_eq!(
573 "0.000000000000000001 FIL",
574 format!("{:#}", attos("1").pretty())
575 );
576
577 assert_eq!("1 femtoFIL", format!("{}", attos("1000").pretty()));
579 assert_eq!("1.001 femtoFIL", format!("{}", attos("1001").pretty()));
580
581 assert_eq!("~0 FIL", format!("{:.0}", attos("1001").pretty()));
583
584 assert_eq!("~10 FIL", format!("{:.1}", fils("11").pretty()));
586
587 assert_eq!(
589 "~0.000000000000002 FIL",
590 format!("{:#.1}", attos("1940").pretty())
591 );
592 assert_eq!(
593 "~0.0000000000000019 FIL",
594 format!("{:#.2}", attos("1940").pretty())
595 );
596 assert_eq!(
597 "0.00000000000000194 FIL",
598 format!("{:#.3}", attos("1940").pretty())
599 );
600
601 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1001").pretty()));
603 assert_eq!("~1 femtoFIL", format!("{:.2}", attos("1001").pretty()));
604 assert_eq!("~1 femtoFIL", format!("{:.3}", attos("1001").pretty()));
605 assert_eq!("1.001 femtoFIL", format!("{:.4}", attos("1001").pretty()));
606 assert_eq!("1.001 femtoFIL", format!("{:.5}", attos("1001").pretty()));
607
608 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1234").pretty()));
610 assert_eq!("~1.2 femtoFIL", format!("{:.2}", attos("1234").pretty()));
611 assert_eq!("~1.23 femtoFIL", format!("{:.3}", attos("1234").pretty()));
612 assert_eq!("1.234 femtoFIL", format!("{:.4}", attos("1234").pretty()));
613 assert_eq!("1.234 femtoFIL", format!("{:.5}", attos("1234").pretty()));
614
615 assert_eq!("~2 femtoFIL", format!("{:.1}", attos("1900").pretty()));
617 assert_eq!("~2 femtoFIL", format!("{:.1}", attos("1500").pretty()));
618 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1400").pretty()));
619
620 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1001").pretty()));
622 assert_eq!("~1 kiloFIL", format!("{:.2}", fils("1001").pretty()));
623 assert_eq!("~1 kiloFIL", format!("{:.3}", fils("1001").pretty()));
624 assert_eq!("1.001 kiloFIL", format!("{:.4}", fils("1001").pretty()));
625 assert_eq!("1.001 kiloFIL", format!("{:.5}", fils("1001").pretty()));
626
627 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1234").pretty()));
629 assert_eq!("~1.2 kiloFIL", format!("{:.2}", fils("1234").pretty()));
630 assert_eq!("~1.23 kiloFIL", format!("{:.3}", fils("1234").pretty()));
631 assert_eq!("1.234 kiloFIL", format!("{:.4}", fils("1234").pretty()));
632 assert_eq!("1.234 kiloFIL", format!("{:.5}", fils("1234").pretty()));
633
634 assert_eq!("~2 kiloFIL", format!("{:.1}", fils("1900").pretty()));
636 assert_eq!("~2 kiloFIL", format!("{:.1}", fils("1500").pretty()));
637 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1400").pretty()));
638 }
639 }
640}
641
642#[cfg(test)]
643mod fuzz {
644 use quickcheck::quickcheck;
645
646 use super::*;
647
648 quickcheck! {
649 fn roundtrip(expected: crate::shim::econ::TokenAmount) -> () {
650 let actual = parse(&format!("{}", expected.pretty())).unwrap();
652 assert_eq!(expected, actual);
653
654 let actual = parse(&format!("{:#}", expected.pretty())).unwrap();
656 assert_eq!(expected, actual);
657
658 }
660 }
661
662 quickcheck! {
663 fn parser_no_panic(s: String) -> () {
664 let _ = parse(&s);
665 }
666 }
667}