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 itertools::Itertools as _;
111 use nom::{
112 IResult, Parser,
113 bytes::complete::tag,
114 character::complete::multispace0,
115 combinator::{map_res, opt},
116 error::{FromExternalError, ParseError},
117 number::complete::recognize_float,
118 sequence::terminated,
119 };
120
121 use super::si;
122
123 pub fn parse(input: &str) -> anyhow::Result<TokenAmount> {
142 let (mut big_decimal, scale) = parse_big_decimal_and_scale(input)?;
143
144 if let Some(scale) = scale {
145 big_decimal *= scale.multiplier();
146 }
147
148 let fil = big_decimal;
149 let attos = fil * si::atto.multiplier().inverse();
150
151 if !attos.is_integer() {
152 bail!("sub-atto amounts are not allowed");
153 }
154
155 let (attos, scale) = attos.with_scale(0).into_bigint_and_exponent();
156 assert_eq!(scale, 0, "we've just set the scale!");
157
158 Ok(TokenAmount::from_atto(attos))
159 }
160
161 fn nom2anyhow(e: nom::Err<nom::error::Error<&str>>) -> anyhow::Error {
162 anyhow!("parse error: {e}")
163 }
164
165 fn parse_big_decimal_and_scale(
166 input: &str,
167 ) -> anyhow::Result<(BigDecimal, Option<si::Prefix>)> {
168 let input = match (input.strip_suffix("FIL"), input.strip_suffix("fil")) {
170 (Some(stripped), _) => stripped.trim_end(),
172 (_, Some(stripped)) => stripped.trim_end(),
173 _ => input,
174 };
175
176 let (input, big_decimal) = permit_trailing_ws(bigdecimal)
177 .parse(input)
178 .map_err(nom2anyhow)?;
179 let (input, scale) = opt(permit_trailing_ws(si_scale))
180 .parse(input)
181 .map_err(nom2anyhow)?;
182
183 if !input.is_empty() {
184 bail!("Unexpected trailing input: {input}")
185 }
186
187 Ok((big_decimal, scale))
188 }
189
190 fn permit_trailing_ws<'a, I, O, E: ParseError<&'a str>>(
191 inner: I,
192 ) -> impl Parser<&'a str, Output = O, Error = E>
193 where
194 I: FnMut(&'a str) -> IResult<&'a str, O, E>,
195 {
196 terminated(inner, multispace0)
197 }
198
199 fn si_scale<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, si::Prefix, E> {
201 let mut scales = si::SUPPORTED_PREFIXES
205 .iter()
206 .flat_map(|scale| {
207 std::iter::once(&scale.name)
208 .chain(scale.units)
209 .map(move |prefix| (*prefix, scale))
210 })
211 .collect_vec();
212 scales.sort_by_key(|(prefix, _)| std::cmp::Reverse(*prefix));
213
214 for (prefix, scale) in scales {
215 if let Ok((rem, _prefix)) = tag::<_, _, E>(prefix)(input) {
216 return Ok((rem, *scale));
217 }
218 }
219
220 Err(nom::Err::Error(E::from_error_kind(
221 input,
222 nom::error::ErrorKind::Alt,
223 )))
224 }
225
226 fn bigdecimal<'a, E>(input: &'a str) -> IResult<&'a str, BigDecimal, E>
228 where
229 E: ParseError<&'a str> + FromExternalError<&'a str, ParseBigDecimalError>,
230 {
231 map_res(recognize_float, str::parse).parse(input)
232 }
233
234 #[cfg(test)]
235 mod tests {
236 use std::str::FromStr as _;
237
238 use num::{BigInt, One as _};
239
240 use super::*;
241
242 #[test]
243 fn cover_scales() {
244 for scale in si::SUPPORTED_PREFIXES {
245 let _did_not_panic = scale.multiplier();
246 }
247 }
248
249 #[test]
250 fn parse_bigdecimal() {
251 fn do_test(input: &str, expected: &str) {
252 let expected = BigDecimal::from_str(expected).unwrap();
253 let (rem, actual) = bigdecimal::<nom::error::Error<_>>(input).unwrap();
254 assert_eq!(expected, actual);
255 assert!(rem.is_empty());
256 }
257 do_test("1", "1");
258 do_test("0.1", "0.1");
259 do_test(".1", ".1");
260 do_test("1e1", "10");
261 do_test("1.", "1");
262 }
263
264 fn test_dec_scale(
265 input: &str,
266 expected_amount: &str,
267 expected_scale: impl Into<Option<si::Prefix>>,
268 ) {
269 let expected_amount = BigDecimal::from_str(expected_amount).unwrap();
270 let expected_scale = expected_scale.into();
271 let (actual_amount, actual_scale) = parse_big_decimal_and_scale(input).unwrap();
272 assert_eq!(expected_amount, actual_amount, "{input}");
273 assert_eq!(expected_scale, actual_scale, "{input}");
274 }
275
276 #[test]
277 fn basic_bigdecimal_and_scale() {
278 test_dec_scale("1", "1", None);
280
281 test_dec_scale("1 FIL", "1", None);
283 test_dec_scale("1FIL", "1", None);
284 test_dec_scale("1 fil", "1", None);
285 test_dec_scale("1fil", "1", None);
286
287 let possible_units = ["", "fil", "FIL", " fil", " FIL"];
288 let possible_prefixes = ["atto", "a", " atto", " a"];
289
290 for unit in possible_units {
291 for prefix in possible_prefixes {
292 let input = format!("1{prefix}{unit}");
293 test_dec_scale(&input, "1", si::atto)
294 }
295 }
296 }
297
298 #[test]
299 fn parse_exa_and_exponent() {
300 test_dec_scale("1 E", "1", si::exa);
301 test_dec_scale("1e0E", "1", si::exa);
302
303 }
309
310 #[test]
311 fn more_than_96_bits() {
312 use std::iter::once;
313
314 let test_str = once('1')
317 .chain(std::iter::repeat_n('0', 98))
318 .chain(['1'])
319 .collect::<String>();
320 test_dec_scale(&test_str, &test_str, None);
321 }
322
323 #[test]
324 fn disallow_too_small() {
325 parse("1 atto").unwrap();
326 assert_eq!(
327 parse("0.1 atto").unwrap_err().to_string(),
328 "sub-atto amounts are not allowed"
329 )
330 }
331
332 #[test]
333 fn some_values() {
334 let one_atto = TokenAmount::from_atto(BigInt::one());
335 let one_nano = TokenAmount::from_nano(BigInt::one());
336
337 assert_eq!(one_atto, parse("1 atto").unwrap());
338 assert_eq!(one_atto, parse("1000 zepto").unwrap());
339
340 assert_eq!(one_nano, parse("1 nano").unwrap());
341 }
342
343 #[test]
344 fn all_possible_prefixes() {
345 for scale in si::SUPPORTED_PREFIXES {
346 for prefix in scale.units.iter().chain([&scale.name]) {
347 test_dec_scale(&format!("1 {prefix}"), "1", *scale);
349 }
350 }
351 }
352
353 #[test]
354 fn wrong_unit() {
355 parse("1 attoo").unwrap_err();
356 parse("1 atto filecoin").unwrap_err();
357 }
358 }
359}
360
361mod print {
362 use std::fmt;
363
364 use crate::shim::econ::TokenAmount;
365 use bigdecimal::BigDecimal;
366 use num::{BigInt, One as _, Zero as _};
367
368 use super::si;
369
370 fn scale(n: BigDecimal) -> (BigDecimal, Option<si::Prefix>) {
371 for prefix in si::SUPPORTED_PREFIXES
372 .iter()
373 .filter(|prefix| prefix.exponent > 0)
374 {
375 let scaled = (n.clone() * prefix.multiplier().inverse()).normalized();
376 if scaled >= BigDecimal::one() {
377 return (scaled, Some(*prefix));
378 }
379 }
380
381 if n >= BigDecimal::one() {
382 return (n, None);
383 }
384
385 for prefix in si::SUPPORTED_PREFIXES
386 .iter()
387 .filter(|prefix| prefix.exponent < 0)
388 {
389 let scaled = (n.clone() * prefix.multiplier().inverse()).normalized();
390 if scaled >= BigDecimal::one() {
391 return (scaled, Some(*prefix));
392 }
393 }
394
395 let smallest_prefix = si::SUPPORTED_PREFIXES.last().unwrap();
396 (
397 n * smallest_prefix.multiplier().inverse(),
398 Some(*smallest_prefix),
399 )
400 }
401
402 pub struct Pretty {
403 attos: BigInt,
404 }
405
406 impl From<&TokenAmount> for Pretty {
407 fn from(value: &TokenAmount) -> Self {
408 Self {
409 attos: value.atto().clone(),
410 }
411 }
412 }
413
414 pub trait TokenAmountPretty {
415 fn pretty(&self) -> Pretty;
416 }
417
418 impl TokenAmountPretty for TokenAmount {
419 fn pretty(&self) -> Pretty {
452 Pretty::from(self)
453 }
454 }
455
456 impl fmt::Display for Pretty {
457 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458 let actual_fil = &self.attos * si::atto.multiplier();
459
460 let fil_for_printing = match f.precision() {
462 None => actual_fil.normalized(),
463 Some(prec) => actual_fil
464 .with_prec(u64::try_from(prec).expect("requested precision is absurd"))
465 .normalized(),
466 };
467
468 let precision_was_lost = fil_for_printing != actual_fil;
469
470 if precision_was_lost {
471 f.write_str("~")?;
472 }
473
474 let (print_me, prefix) = match f.alternate() {
476 true => (fil_for_printing, None),
477 false => scale(fil_for_printing),
478 };
479
480 match print_me.is_zero() {
482 true => f.write_str("0 FIL"),
483 false => match prefix {
484 Some(prefix) => f.write_fmt(format_args!("{print_me} {}FIL", prefix.name)),
485 None => f.write_fmt(format_args!("{print_me} FIL")),
486 },
487 }
488 }
489 }
490
491 #[cfg(test)]
492 mod tests {
493 use std::str::FromStr as _;
494
495 use num::One as _;
496 use pretty_assertions::assert_eq;
497
498 use super::*;
499
500 #[test]
501 fn prefixes_represent_themselves() {
502 for prefix in si::SUPPORTED_PREFIXES {
503 let input = BigDecimal::from_str(prefix.multiplier).unwrap();
504 assert_eq!((BigDecimal::one(), Some(*prefix)), scale(input));
505 }
506 }
507
508 #[test]
509 fn very_large() {
510 let mut one_thousand_quettas = String::from(si::quetta.multiplier);
511 one_thousand_quettas.push_str("000");
512
513 test_scale(&one_thousand_quettas, "1000", si::quetta);
514 }
515
516 #[test]
517 fn very_small() {
518 let mut one_thousanth_of_a_quecto = String::from(si::quecto.multiplier);
519 one_thousanth_of_a_quecto.pop();
520 one_thousanth_of_a_quecto.push_str("0001");
521
522 test_scale(&one_thousanth_of_a_quecto, "0.001", si::quecto);
523 }
524
525 #[track_caller]
526 fn test_scale(
527 input: &str,
528 expected_value: &str,
529 expected_prefix: impl Into<Option<si::Prefix>>,
530 ) {
531 let input = BigDecimal::from_str(input).unwrap();
532 let expected_value = BigDecimal::from_str(expected_value).unwrap();
533 let expected_prefix = expected_prefix.into();
534
535 assert_eq!((expected_value, expected_prefix), scale(input))
536 }
537
538 #[test]
539 fn simple() {
540 test_scale("1000000", "1", si::mega);
541 test_scale("100000", "100", si::kilo);
542 test_scale("10000", "10", si::kilo);
543 test_scale("1000", "1", si::kilo);
544 test_scale("100", "100", None);
545 test_scale("10", "10", None);
546 test_scale("1", "1", None);
547 test_scale("0.1", "100", si::milli);
548 test_scale("0.01", "10", si::milli);
549 test_scale("0.001", "1", si::milli);
550 test_scale("0.0001", "100", si::micro);
551 }
552 #[test]
553 fn trailing_one() {
554 test_scale("10001000", "10.001", si::mega);
555 test_scale("10001", "10.001", si::kilo);
556 test_scale("1000.1", "1.0001", si::kilo);
557 }
558
559 fn attos(input: &str) -> TokenAmount {
560 TokenAmount::from_atto(BigInt::from_str(input).unwrap())
561 }
562
563 fn fils(input: &str) -> TokenAmount {
564 TokenAmount::from_whole(BigInt::from_str(input).unwrap())
565 }
566
567 #[test]
568 fn test_display() {
569 assert_eq!("0 FIL", format!("{}", attos("0").pretty()));
570
571 assert_eq!("1 attoFIL", format!("{}", attos("1").pretty()));
573 assert_eq!(
574 "0.000000000000000001 FIL",
575 format!("{:#}", attos("1").pretty())
576 );
577
578 assert_eq!("1 femtoFIL", format!("{}", attos("1000").pretty()));
580 assert_eq!("1.001 femtoFIL", format!("{}", attos("1001").pretty()));
581
582 assert_eq!("~0 FIL", format!("{:.0}", attos("1001").pretty()));
584
585 assert_eq!("~10 FIL", format!("{:.1}", fils("11").pretty()));
587
588 assert_eq!(
590 "~0.000000000000002 FIL",
591 format!("{:#.1}", attos("1940").pretty())
592 );
593 assert_eq!(
594 "~0.0000000000000019 FIL",
595 format!("{:#.2}", attos("1940").pretty())
596 );
597 assert_eq!(
598 "0.00000000000000194 FIL",
599 format!("{:#.3}", attos("1940").pretty())
600 );
601
602 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1001").pretty()));
604 assert_eq!("~1 femtoFIL", format!("{:.2}", attos("1001").pretty()));
605 assert_eq!("~1 femtoFIL", format!("{:.3}", attos("1001").pretty()));
606 assert_eq!("1.001 femtoFIL", format!("{:.4}", attos("1001").pretty()));
607 assert_eq!("1.001 femtoFIL", format!("{:.5}", attos("1001").pretty()));
608
609 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1234").pretty()));
611 assert_eq!("~1.2 femtoFIL", format!("{:.2}", attos("1234").pretty()));
612 assert_eq!("~1.23 femtoFIL", format!("{:.3}", attos("1234").pretty()));
613 assert_eq!("1.234 femtoFIL", format!("{:.4}", attos("1234").pretty()));
614 assert_eq!("1.234 femtoFIL", format!("{:.5}", attos("1234").pretty()));
615
616 assert_eq!("~2 femtoFIL", format!("{:.1}", attos("1900").pretty()));
618 assert_eq!("~2 femtoFIL", format!("{:.1}", attos("1500").pretty()));
619 assert_eq!("~1 femtoFIL", format!("{:.1}", attos("1400").pretty()));
620
621 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1001").pretty()));
623 assert_eq!("~1 kiloFIL", format!("{:.2}", fils("1001").pretty()));
624 assert_eq!("~1 kiloFIL", format!("{:.3}", fils("1001").pretty()));
625 assert_eq!("1.001 kiloFIL", format!("{:.4}", fils("1001").pretty()));
626 assert_eq!("1.001 kiloFIL", format!("{:.5}", fils("1001").pretty()));
627
628 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1234").pretty()));
630 assert_eq!("~1.2 kiloFIL", format!("{:.2}", fils("1234").pretty()));
631 assert_eq!("~1.23 kiloFIL", format!("{:.3}", fils("1234").pretty()));
632 assert_eq!("1.234 kiloFIL", format!("{:.4}", fils("1234").pretty()));
633 assert_eq!("1.234 kiloFIL", format!("{:.5}", fils("1234").pretty()));
634
635 assert_eq!("~2 kiloFIL", format!("{:.1}", fils("1900").pretty()));
637 assert_eq!("~2 kiloFIL", format!("{:.1}", fils("1500").pretty()));
638 assert_eq!("~1 kiloFIL", format!("{:.1}", fils("1400").pretty()));
639 }
640 }
641}
642
643#[cfg(test)]
644mod fuzz {
645 use quickcheck::quickcheck;
646
647 use super::*;
648
649 quickcheck! {
650 fn roundtrip(expected: crate::shim::econ::TokenAmount) -> () {
651 let actual = parse(&format!("{}", expected.pretty())).unwrap();
653 assert_eq!(expected, actual);
654
655 let actual = parse(&format!("{:#}", expected.pretty())).unwrap();
657 assert_eq!(expected, actual);
658
659 }
661 }
662
663 quickcheck! {
664 fn parser_no_panic(s: String) -> () {
665 let _ = parse(&s);
666 }
667 }
668}