forest/cli/
humantoken.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3//! This module defines a [parser](parse()) and
4//! [pretty-printer](TokenAmountPretty::pretty) for
5//! `TokenAmount`
6//!
7//! See the `si` module source for supported prefixes.
8
9pub use parse::parse;
10pub use print::TokenAmountPretty;
11
12/// SI prefix definitions
13mod si {
14    use bigdecimal::BigDecimal;
15
16    // Use a struct as a table row instead of an enum
17    // to make our code less macro-heavy
18    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19    pub struct Prefix {
20        /// `"micro"`
21        pub name: &'static str,
22        /// `[ "μ", "u" ]`
23        pub units: &'static [&'static str],
24        /// `-6`
25        pub exponent: i8,
26        /// `"0.000001"`
27        pub multiplier: &'static str,
28    }
29
30    impl Prefix {
31        // ENHANCE(aatifsyed): could memoize this if it's called in a hot loop
32        pub fn multiplier(&self) -> BigDecimal {
33            self.multiplier.parse().unwrap()
34        }
35    }
36
37    /// Biggest first
38    macro_rules! define_prefixes {
39        ($($name:ident $symbol:ident$(or $alt_symbol:ident)* $base_10:literal $decimal:literal),* $(,)?) =>
40            {
41                // Define constants
42                $(
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                /// Biggest first
53                // Define top level array
54                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        // Leave this out because
75        // - it simplifies our printing logic
76        // - these are not commonly used
77        // - it's more consistent with lotus
78        //
79        // hecto    h     2     100,
80        // deca     da    1     10,
81        // deci     d    -1     0.1,
82        // centi    c    -2     0.01,
83        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    // ENHANCE(aatifsyed): could accept pairs like "1 nano 1 atto"
106
107    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    /// Parse token amounts as floats with SI prefixed-units.
123    /// ```
124    /// # use forest::doctest_private::{TokenAmount, parse};
125    /// fn assert_attos(input: &str, attos: u64) {
126    ///     let expected = TokenAmount::from_atto(attos);
127    ///     let actual = parse(input).unwrap();
128    ///     assert_eq!(expected, actual);
129    /// }
130    /// assert_attos("1a", 1);
131    /// assert_attos("1aFIL", 1);
132    /// assert_attos("1 femtoFIL", 1000);
133    /// assert_attos("1.1 f", 1100);
134    /// assert_attos("1.0e3 attofil", 1000);
135    /// ```
136    ///
137    /// # Known bugs
138    /// - `1efil` will not parse as an exa (`10^18`), because we'll try and
139    ///   parse it as a exponent in the float. Instead use `1 efil`.
140    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        // Strip `fil` or `FIL` at most once from the end
168        let input = match (input.strip_suffix("FIL"), input.strip_suffix("fil")) {
169            // remove whitespace before the units if there was any
170            (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    /// Take an [si::Prefix] from the front of `input`
199    fn si_scale<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, si::Prefix, E> {
200        // Try the longest matches first, so we don't e.g match `a` instead of `atto`,
201        // leaving `tto`.
202
203        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    /// Take a float from the front of `input`
226    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            // plain
278            test_dec_scale("1", "1", None);
279
280            // include unit
281            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            // ENHANCE(aatifsyed): this should be parsed as 1 exa, but that
303            // would probably require an entirely custom float parser with
304            // lookahead - users will have to include a space for now
305
306            // do_test("1E", "1", exa);
307        }
308
309        #[test]
310        fn more_than_96_bits() {
311            use std::iter::once;
312
313            // The previous rust_decimal implementation had at most 96 bits of precision
314            // we should be able to exceed that
315            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                    // Need a space here because of the exa ambiguity
347                    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        /// Note the following format specifiers:
419        /// - `{:#}`: print number of FIL, not e.g `milliFIL`
420        /// - `{:.4}`: round to 4 significant figures
421        /// - `{:.#4}`: both
422        ///
423        /// ```
424        /// # use forest::doctest_private::{TokenAmountPretty as _, TokenAmount};
425        ///
426        /// let amount = TokenAmount::from_nano(1500);
427        ///
428        /// // Defaults to precise, with SI prefix
429        /// assert_eq!("1.5 microFIL", format!("{}", amount.pretty()));
430        ///
431        /// // Rounded to 1 s.f
432        /// assert_eq!("~2 microFIL", format!("{:.1}", amount.pretty()));
433        ///
434        /// // Show absolute FIL
435        /// assert_eq!("0.0000015 FIL", format!("{:#}", amount.pretty()));
436        ///
437        /// // Rounded absolute FIL
438        /// assert_eq!("~0.000002 FIL", format!("{:#.1}", amount.pretty()));
439        ///
440        /// // We only indicate lost precision when relevant
441        /// assert_eq!("1.5 microFIL", format!("{:.2}", amount.pretty()));
442        /// ```
443        ///
444        /// # Formatting
445        /// - We select the most diminutive SI prefix (or not!) that allows us
446        ///   to display an integer amount.
447        // RUST(aatifsyed): this should be -> impl fmt::Display
448        //
449        // Users shouldn't be able to name `Pretty` anyway
450        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            // rounding
460            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            // units or whole
474            let (print_me, prefix) = match f.alternate() {
475                true => (fil_for_printing, None),
476                false => scale(fil_for_printing),
477            };
478
479            // write the string
480            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            // Absolute works
571            assert_eq!("1 attoFIL", format!("{}", attos("1").pretty()));
572            assert_eq!(
573                "0.000000000000000001 FIL",
574                format!("{:#}", attos("1").pretty())
575            );
576
577            // We select the right suffix
578            assert_eq!("1 femtoFIL", format!("{}", attos("1000").pretty()));
579            assert_eq!("1.001 femtoFIL", format!("{}", attos("1001").pretty()));
580
581            // If you ask for 0 precision, you get it
582            assert_eq!("~0 FIL", format!("{:.0}", attos("1001").pretty()));
583
584            // Rounding without a prefix
585            assert_eq!("~10 FIL", format!("{:.1}", fils("11").pretty()));
586
587            // Rounding with absolute
588            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            // Small numbers with a gap then a trailing one are rounded down
602            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            // Small numbers with trailing numbers are rounded down
609            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            // Small numbers are rounded appropriately
616            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            // Big numbers with a gap then a trailing one are rounded down
621            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            // Big numbers with trailing numbers are rounded down
628            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            // Big numbers are rounded appropriately
635            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            // Default formatting
651            let actual = parse(&format!("{}", expected.pretty())).unwrap();
652            assert_eq!(expected, actual);
653
654            // Absolute formatting
655            let actual = parse(&format!("{:#}", expected.pretty())).unwrap();
656            assert_eq!(expected, actual);
657
658            // Don't test rounded formatting...
659        }
660    }
661
662    quickcheck! {
663        fn parser_no_panic(s: String) -> () {
664            let _ = parse(&s);
665        }
666    }
667}