Skip to main content

forest/cli/
humantoken.rs

1// Copyright 2019-2026 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 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    /// Parse token amounts as floats with SI prefixed-units.
124    /// ```
125    /// # use forest::doctest_private::{TokenAmount, parse};
126    /// fn assert_attos(input: &str, attos: u64) {
127    ///     let expected = TokenAmount::from_atto(attos);
128    ///     let actual = parse(input).unwrap();
129    ///     assert_eq!(expected, actual);
130    /// }
131    /// assert_attos("1a", 1);
132    /// assert_attos("1aFIL", 1);
133    /// assert_attos("1 femtoFIL", 1000);
134    /// assert_attos("1.1 f", 1100);
135    /// assert_attos("1.0e3 attofil", 1000);
136    /// ```
137    ///
138    /// # Known bugs
139    /// - `1efil` will not parse as an exa (`10^18`), because we'll try and
140    ///   parse it as a exponent in the float. Instead use `1 efil`.
141    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        // Strip `fil` or `FIL` at most once from the end
169        let input = match (input.strip_suffix("FIL"), input.strip_suffix("fil")) {
170            // remove whitespace before the units if there was any
171            (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    /// Take an [si::Prefix] from the front of `input`
200    fn si_scale<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, si::Prefix, E> {
201        // Try the longest matches first, so we don't e.g match `a` instead of `atto`,
202        // leaving `tto`.
203
204        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    /// Take a float from the front of `input`
227    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            // plain
279            test_dec_scale("1", "1", None);
280
281            // include unit
282            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            // ENHANCE(aatifsyed): this should be parsed as 1 exa, but that
304            // would probably require an entirely custom float parser with
305            // lookahead - users will have to include a space for now
306
307            // do_test("1E", "1", exa);
308        }
309
310        #[test]
311        fn more_than_96_bits() {
312            use std::iter::once;
313
314            // The previous rust_decimal implementation had at most 96 bits of precision
315            // we should be able to exceed that
316            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                    // Need a space here because of the exa ambiguity
348                    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        /// Note the following format specifiers:
420        /// - `{:#}`: print number of FIL, not e.g `milliFIL`
421        /// - `{:.4}`: round to 4 significant figures
422        /// - `{:.#4}`: both
423        ///
424        /// ```
425        /// # use forest::doctest_private::{TokenAmountPretty as _, TokenAmount};
426        ///
427        /// let amount = TokenAmount::from_nano(1500);
428        ///
429        /// // Defaults to precise, with SI prefix
430        /// assert_eq!("1.5 microFIL", format!("{}", amount.pretty()));
431        ///
432        /// // Rounded to 1 s.f
433        /// assert_eq!("~2 microFIL", format!("{:.1}", amount.pretty()));
434        ///
435        /// // Show absolute FIL
436        /// assert_eq!("0.0000015 FIL", format!("{:#}", amount.pretty()));
437        ///
438        /// // Rounded absolute FIL
439        /// assert_eq!("~0.000002 FIL", format!("{:#.1}", amount.pretty()));
440        ///
441        /// // We only indicate lost precision when relevant
442        /// assert_eq!("1.5 microFIL", format!("{:.2}", amount.pretty()));
443        /// ```
444        ///
445        /// # Formatting
446        /// - We select the most diminutive SI prefix (or not!) that allows us
447        ///   to display an integer amount.
448        // RUST(aatifsyed): this should be -> impl fmt::Display
449        //
450        // Users shouldn't be able to name `Pretty` anyway
451        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            // rounding
461            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            // units or whole
475            let (print_me, prefix) = match f.alternate() {
476                true => (fil_for_printing, None),
477                false => scale(fil_for_printing),
478            };
479
480            // write the string
481            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            // Absolute works
572            assert_eq!("1 attoFIL", format!("{}", attos("1").pretty()));
573            assert_eq!(
574                "0.000000000000000001 FIL",
575                format!("{:#}", attos("1").pretty())
576            );
577
578            // We select the right suffix
579            assert_eq!("1 femtoFIL", format!("{}", attos("1000").pretty()));
580            assert_eq!("1.001 femtoFIL", format!("{}", attos("1001").pretty()));
581
582            // If you ask for 0 precision, you get it
583            assert_eq!("~0 FIL", format!("{:.0}", attos("1001").pretty()));
584
585            // Rounding without a prefix
586            assert_eq!("~10 FIL", format!("{:.1}", fils("11").pretty()));
587
588            // Rounding with absolute
589            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            // Small numbers with a gap then a trailing one are rounded down
603            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            // Small numbers with trailing numbers are rounded down
610            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            // Small numbers are rounded appropriately
617            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            // Big numbers with a gap then a trailing one are rounded down
622            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            // Big numbers with trailing numbers are rounded down
629            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            // Big numbers are rounded appropriately
636            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            // Default formatting
652            let actual = parse(&format!("{}", expected.pretty())).unwrap();
653            assert_eq!(expected, actual);
654
655            // Absolute formatting
656            let actual = parse(&format!("{:#}", expected.pretty())).unwrap();
657            assert_eq!(expected, actual);
658
659            // Don't test rounded formatting...
660        }
661    }
662
663    quickcheck! {
664        fn parser_no_panic(s: String) -> () {
665            let _ = parse(&s);
666        }
667    }
668}