Skip to main content

cast/
base.rs

1//! Number base parsing and formatting for the `cast` conversion commands.
2
3use alloy_primitives::{I256, U256};
4use eyre::Result;
5use std::{
6    fmt::{Debug, Display, Formatter, LowerHex, Result as FmtResult},
7    num::IntErrorKind,
8    str::FromStr,
9};
10
11/// Represents a number's [radix] or base. Supports the same bases that [`std::fmt`] supports.
12///
13/// [radix]: https://en.wikipedia.org/wiki/Radix
14#[repr(u32)]
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum Base {
17    Binary = 2,
18    Octal = 8,
19    #[default]
20    Decimal = 10,
21    Hexadecimal = 16,
22}
23
24impl FromStr for Base {
25    type Err = eyre::Report;
26
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        match s.to_lowercase().as_str() {
29            "2" | "b" | "bin" | "binary" => Ok(Self::Binary),
30            "8" | "o" | "oct" | "octal" => Ok(Self::Octal),
31            "10" | "d" | "dec" | "decimal" => Ok(Self::Decimal),
32            "16" | "h" | "hex" | "hexadecimal" => Ok(Self::Hexadecimal),
33            s => Err(eyre::eyre!(
34                "\
35Invalid base \"{s}\". Possible values:
36 2, b, bin, binary
37 8, o, oct, octal
3810, d, dec, decimal
3916, h, hex, hexadecimal"
40            )),
41        }
42    }
43}
44
45impl Base {
46    /// Parses `base` when given, otherwise detects the base of `s` from its prefix.
47    pub fn unwrap_or_detect(base: Option<&str>, s: &str) -> Result<Self> {
48        match base {
49            Some(base) => base.parse(),
50            None => Self::detect(s),
51        }
52    }
53
54    /// Detects a number's base from its prefix, defaulting to decimal and then hexadecimal for
55    /// unprefixed values.
56    pub fn detect(s: &str) -> Result<Self> {
57        let s = s.strip_prefix(['+', '-']).unwrap_or(s);
58        let prefix = s.get(..2).map(str::to_ascii_lowercase);
59        match prefix.as_deref() {
60            Some("0b") => Self::detect_prefixed(s, Self::Binary, "binary"),
61            Some("0o") => Self::detect_prefixed(s, Self::Octal, "octal"),
62            Some("0x") => Self::detect_prefixed(s, Self::Hexadecimal, "hexadecimal"),
63            // Unprefixed digits are ambiguous; prefer decimal.
64            _ if U256::from_str_radix(s, 10).is_ok() => Ok(Self::Decimal),
65            _ => U256::from_str_radix(s, 16).map(|_| Self::Hexadecimal).map_err(|e| {
66                eyre::eyre!("could not autodetect base as neither decimal or hexadecimal: {e}")
67            }),
68        }
69    }
70
71    /// Validates the digits after a 2-char prefix. `PosOverflow` is accepted since the digits are
72    /// correct for the base; only the base is being detected here.
73    fn detect_prefixed(s: &str, base: Self, label: &str) -> Result<Self> {
74        match u64::from_str_radix(&s[2..], base as u32) {
75            Ok(_) => Ok(base),
76            Err(e) if *e.kind() == IntErrorKind::PosOverflow => Ok(base),
77            Err(e) => Err(eyre::eyre!("could not parse {label} value: {e}")),
78        }
79    }
80
81    /// Returns the Rust standard prefix for a base.
82    pub const fn prefix(self) -> &'static str {
83        match self {
84            Self::Binary => "0b",
85            Self::Octal => "0o",
86            Self::Decimal => "",
87            Self::Hexadecimal => "0x",
88        }
89    }
90}
91
92/// A parsed number together with the [`Base`] it is formatted in.
93///
94/// [`Debug`] formats the number in its base, [`Display`] in decimal and [`LowerHex`] in
95/// hexadecimal; the alternate flag (`#`) prepends the base prefix.
96///
97/// # Example
98///
99/// ```
100/// use alloy_primitives::U256;
101/// use cast::base::{Base, NumberWithBase};
102///
103/// let number = NumberWithBase::from(U256::from(12345));
104/// assert_eq!(format!("{number}"), "12345");
105/// assert_eq!(format!("{number:x}"), "3039");
106/// assert_eq!(format!("{number:#x}"), "0x3039");
107/// assert_eq!(format!("{:#?}", number.with_base(Base::Binary)), "0b11000000111001");
108/// assert_eq!(format!("{:#?}", number.with_base(Base::Octal)), "0o30071");
109/// ```
110#[derive(Clone, Copy)]
111pub struct NumberWithBase {
112    /// The number, as two's complement when negative.
113    number: U256,
114    /// Whether the number is positive or zero.
115    is_nonnegative: bool,
116    /// The base to format to.
117    base: Base,
118}
119
120impl Debug for NumberWithBase {
121    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
122        let prefix = self.base.prefix();
123        if self.number.is_zero() {
124            return f.pad_integral(true, prefix, "0");
125        }
126        // Only decimal output carries a sign; the other bases show the two's complement.
127        let is_nonnegative = self.base != Base::Decimal || self.is_nonnegative;
128        f.pad_integral(is_nonnegative, prefix, &self.format())
129    }
130}
131
132impl Display for NumberWithBase {
133    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
134        Debug::fmt(&self.with_base(Base::Decimal), f)
135    }
136}
137
138impl LowerHex for NumberWithBase {
139    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
140        Debug::fmt(&self.with_base(Base::Hexadecimal), f)
141    }
142}
143
144impl From<I256> for NumberWithBase {
145    fn from(number: I256) -> Self {
146        Self {
147            number: number.into_raw(),
148            is_nonnegative: !number.is_negative(),
149            base: Base::default(),
150        }
151    }
152}
153
154impl From<U256> for NumberWithBase {
155    fn from(number: U256) -> Self {
156        Self { number, is_nonnegative: true, base: Base::default() }
157    }
158}
159
160impl NumberWithBase {
161    /// Parses a signed integer, detecting the base from the prefix when `base` is `None`.
162    pub fn parse_int(s: &str, base: Option<&str>) -> Result<Self> {
163        Self::parse_int_in(s, Base::unwrap_or_detect(base, s)?)
164    }
165
166    /// Parses a signed integer in `base`.
167    pub fn parse_int_in(s: &str, base: Base) -> Result<Self> {
168        let (s, is_nonnegative) = match s.strip_prefix('-') {
169            Some(s) => (s, false),
170            None => (s.strip_prefix('+').unwrap_or(s), true),
171        };
172        let mut number = Self::parse_digits(s, base)?;
173        if !is_nonnegative {
174            number = number.wrapping_neg();
175        }
176        Ok(Self { number, is_nonnegative, base })
177    }
178
179    /// Parses an unsigned integer, detecting the base from the prefix when `base` is `None`.
180    pub fn parse_uint(s: &str, base: Option<&str>) -> Result<Self> {
181        let base = Base::unwrap_or_detect(base, s)?;
182        Ok(Self { number: Self::parse_digits(s, base)?, is_nonnegative: true, base })
183    }
184
185    /// Parses the digits of `s` in `base`, stripping only that base's prefix: a leading `0b` is a
186    /// prefix when parsing binary and a pair of hexadecimal digits otherwise.
187    fn parse_digits(s: &str, base: Base) -> Result<U256> {
188        let s = match s.get(..2) {
189            Some(p) if p.eq_ignore_ascii_case(base.prefix()) => &s[2..],
190            _ => s,
191        };
192        U256::from_str_radix(s, base as u64).map_err(Into::into)
193    }
194
195    /// Returns the number as an unsigned integer (two's complement when negative).
196    pub const fn number(&self) -> U256 {
197        self.number
198    }
199
200    /// Returns whether the number is positive or zero.
201    pub const fn is_nonnegative(&self) -> bool {
202        self.is_nonnegative
203    }
204
205    /// Returns a copy of the number formatted in `base`.
206    pub const fn with_base(self, base: Base) -> Self {
207        Self { base, ..self }
208    }
209
210    /// Formats the number's digits in its base, without any prefix, sign or padding.
211    fn format(&self) -> String {
212        match self.base {
213            Base::Binary => format!("{:b}", self.number),
214            Base::Octal => format!("{:o}", self.number),
215            Base::Decimal if self.is_nonnegative => self.number.to_string(),
216            Base::Decimal => I256::from_raw(self.number).to_string().trim_start_matches('-').into(),
217            Base::Hexadecimal => format!("{:x}", self.number),
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use Base::*;
226
227    const NUMS: [i128; 44] = [
228        1,
229        2,
230        3,
231        5,
232        7,
233        8,
234        10,
235        11,
236        13,
237        16,
238        17,
239        19,
240        23,
241        29,
242        31,
243        32,
244        37,
245        41,
246        43,
247        47,
248        53,
249        59,
250        61,
251        64,
252        67,
253        71,
254        73,
255        79,
256        83,
257        89,
258        97,
259        100,
260        128,
261        200,
262        333,
263        500,
264        666,
265        1000,
266        6666,
267        10000,
268        i16::MAX as i128,
269        i32::MAX as i128,
270        i64::MAX as i128,
271        i128::MAX,
272    ];
273
274    fn number(n: i128) -> NumberWithBase {
275        NumberWithBase::from(I256::try_from(n).unwrap())
276    }
277
278    #[test]
279    fn can_parse_base() {
280        for (aliases, base) in [
281            (["2", "b", "bin", "binary"], Binary),
282            (["8", "o", "oct", "octal"], Octal),
283            (["10", "d", "dec", "decimal"], Decimal),
284            (["16", "h", "hex", "hexadecimal"], Hexadecimal),
285        ] {
286            for alias in aliases {
287                assert_eq!(alias.parse::<Base>().unwrap(), base, "{alias}");
288                assert_eq!(alias.to_uppercase().parse::<Base>().unwrap(), base, "{alias}");
289            }
290        }
291        assert!("3".parse::<Base>().is_err());
292    }
293
294    #[test]
295    fn can_detect_base() {
296        assert_eq!(Base::detect("0b100").unwrap(), Binary);
297        assert_eq!(Base::detect("0o100").unwrap(), Octal);
298        assert_eq!(Base::detect("100").unwrap(), Decimal);
299        assert_eq!(Base::detect("0x100").unwrap(), Hexadecimal);
300
301        assert_eq!(Base::detect("0B100").unwrap(), Binary);
302        assert_eq!(Base::detect("0O100").unwrap(), Octal);
303        assert_eq!(Base::detect("0X100").unwrap(), Hexadecimal);
304
305        assert_eq!(Base::detect("-0B100").unwrap(), Binary);
306        assert_eq!(Base::detect("-0O100").unwrap(), Octal);
307        assert_eq!(Base::detect("-0X100").unwrap(), Hexadecimal);
308
309        assert_eq!(Base::detect("0123456789abcdef").unwrap(), Hexadecimal);
310
311        let _ = Base::detect("0b234abc").unwrap_err();
312        let _ = Base::detect("0o89cba").unwrap_err();
313        let _ = Base::detect("0123456789abcdefg").unwrap_err();
314        let _ = Base::detect("0x123abclpmk").unwrap_err();
315        let _ = Base::detect("hello world").unwrap_err();
316    }
317
318    #[test]
319    fn strips_only_the_prefix_of_the_requested_base() {
320        // `0b`/`0B` are hexadecimal digits when the base is explicitly hexadecimal.
321        for s in ["0b1010", "0B1010"] {
322            let number = NumberWithBase::parse_uint(s, Some("16")).unwrap();
323            assert_eq!(number.number(), U256::from(0xb1010));
324            assert_eq!(number.base, Hexadecimal);
325        }
326        let number = NumberWithBase::parse_int("-0b11", Some("hex")).unwrap();
327        assert_eq!(number.number(), U256::from(0xb11).wrapping_neg());
328        assert!(!number.is_nonnegative());
329
330        // Prefixes of the requested base are still accepted, in either case.
331        assert_eq!(
332            NumberWithBase::parse_uint("0x1f", Some("16")).unwrap().number(),
333            U256::from(31)
334        );
335        assert_eq!(
336            NumberWithBase::parse_uint("0X1F", Some("16")).unwrap().number(),
337            U256::from(31)
338        );
339        assert_eq!(NumberWithBase::parse_uint("0b11", Some("2")).unwrap().number(), U256::from(3));
340        assert_eq!(NumberWithBase::parse_uint("0o17", Some("8")).unwrap().number(), U256::from(15));
341        assert_eq!(NumberWithBase::parse_uint("42", Some("10")).unwrap().number(), U256::from(42));
342
343        // A prefix of another base is not a valid digit sequence in the requested base.
344        assert!(NumberWithBase::parse_uint("0x10", Some("2")).is_err());
345        assert!(NumberWithBase::parse_uint("0b10", Some("10")).is_err());
346
347        // Detection from the prefix is unaffected.
348        let number = NumberWithBase::parse_uint("0b1010", None).unwrap();
349        assert_eq!(number.number(), U256::from(10));
350        assert_eq!(number.base, Binary);
351    }
352
353    #[test]
354    fn formats_positive_numbers() {
355        for n in NUMS {
356            let num = number(n);
357            assert_eq!(num.with_base(Binary).format(), format!("{n:b}"));
358            assert_eq!(num.with_base(Octal).format(), format!("{n:o}"));
359            assert_eq!(num.with_base(Decimal).format(), n.to_string());
360            assert_eq!(num.with_base(Hexadecimal).format(), format!("{n:x}"));
361
362            assert_eq!(format!("{num}"), n.to_string());
363            assert_eq!(format!("{num:x}"), format!("{n:x}"));
364            assert_eq!(format!("{num:#x}"), format!("{n:#x}"));
365            assert_eq!(format!("{:#?}", num.with_base(Binary)), format!("{n:#b}"));
366            assert_eq!(format!("{:#?}", num.with_base(Octal)), format!("{n:#o}"));
367        }
368    }
369
370    #[test]
371    fn formats_negative_numbers() {
372        for n in NUMS.into_iter().map(|n| -n).chain([i128::MIN]) {
373            let num = number(n);
374            // The underlying number is 256 bits wide, so the two's complement is sign-extended.
375            assert_eq!(num.with_base(Binary).format(), format!("{n:1>256b}"));
376            assert_eq!(num.with_base(Hexadecimal).format(), format!("{n:f>64x}"));
377            // Decimal digits never carry the sign; `Display` adds it back.
378            assert_eq!(num.with_base(Decimal).format(), n.to_string().trim_start_matches('-'));
379            assert_eq!(format!("{num}"), n.to_string());
380        }
381    }
382}