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            eyre::ensure!(
175                number <= I256::MIN.unsigned_abs(),
176                "value out of range for a signed 256-bit integer"
177            );
178            number = number.wrapping_neg();
179        }
180        Ok(Self { number, is_nonnegative, base })
181    }
182
183    /// Parses an unsigned integer, detecting the base from the prefix when `base` is `None`.
184    pub fn parse_uint(s: &str, base: Option<&str>) -> Result<Self> {
185        let base = Base::unwrap_or_detect(base, s)?;
186        Ok(Self { number: Self::parse_digits(s, base)?, is_nonnegative: true, base })
187    }
188
189    /// Parses the digits of `s` in `base`, stripping only that base's prefix: a leading `0b` is a
190    /// prefix when parsing binary and a pair of hexadecimal digits otherwise.
191    fn parse_digits(s: &str, base: Base) -> Result<U256> {
192        let s = match s.get(..2) {
193            Some(p) if p.eq_ignore_ascii_case(base.prefix()) => &s[2..],
194            _ => s,
195        };
196        U256::from_str_radix(s, base as u64).map_err(Into::into)
197    }
198
199    /// Returns the number as an unsigned integer (two's complement when negative).
200    pub const fn number(&self) -> U256 {
201        self.number
202    }
203
204    /// Returns whether the number is positive or zero.
205    pub const fn is_nonnegative(&self) -> bool {
206        self.is_nonnegative
207    }
208
209    /// Returns a copy of the number formatted in `base`.
210    pub const fn with_base(self, base: Base) -> Self {
211        Self { base, ..self }
212    }
213
214    /// Formats the number's digits in its base, without any prefix, sign or padding.
215    fn format(&self) -> String {
216        match self.base {
217            Base::Binary => format!("{:b}", self.number),
218            Base::Octal => format!("{:o}", self.number),
219            Base::Decimal if self.is_nonnegative => self.number.to_string(),
220            Base::Decimal => I256::from_raw(self.number).to_string().trim_start_matches('-').into(),
221            Base::Hexadecimal => format!("{:x}", self.number),
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use Base::*;
230
231    const NUMS: [i128; 44] = [
232        1,
233        2,
234        3,
235        5,
236        7,
237        8,
238        10,
239        11,
240        13,
241        16,
242        17,
243        19,
244        23,
245        29,
246        31,
247        32,
248        37,
249        41,
250        43,
251        47,
252        53,
253        59,
254        61,
255        64,
256        67,
257        71,
258        73,
259        79,
260        83,
261        89,
262        97,
263        100,
264        128,
265        200,
266        333,
267        500,
268        666,
269        1000,
270        6666,
271        10000,
272        i16::MAX as i128,
273        i32::MAX as i128,
274        i64::MAX as i128,
275        i128::MAX,
276    ];
277
278    fn number(n: i128) -> NumberWithBase {
279        NumberWithBase::from(I256::try_from(n).unwrap())
280    }
281
282    #[test]
283    fn can_parse_base() {
284        for (aliases, base) in [
285            (["2", "b", "bin", "binary"], Binary),
286            (["8", "o", "oct", "octal"], Octal),
287            (["10", "d", "dec", "decimal"], Decimal),
288            (["16", "h", "hex", "hexadecimal"], Hexadecimal),
289        ] {
290            for alias in aliases {
291                assert_eq!(alias.parse::<Base>().unwrap(), base, "{alias}");
292                assert_eq!(alias.to_uppercase().parse::<Base>().unwrap(), base, "{alias}");
293            }
294        }
295        assert!("3".parse::<Base>().is_err());
296    }
297
298    #[test]
299    fn can_detect_base() {
300        assert_eq!(Base::detect("0b100").unwrap(), Binary);
301        assert_eq!(Base::detect("0o100").unwrap(), Octal);
302        assert_eq!(Base::detect("100").unwrap(), Decimal);
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("-0B100").unwrap(), Binary);
310        assert_eq!(Base::detect("-0O100").unwrap(), Octal);
311        assert_eq!(Base::detect("-0X100").unwrap(), Hexadecimal);
312
313        assert_eq!(Base::detect("0123456789abcdef").unwrap(), Hexadecimal);
314
315        let _ = Base::detect("0b234abc").unwrap_err();
316        let _ = Base::detect("0o89cba").unwrap_err();
317        let _ = Base::detect("0123456789abcdefg").unwrap_err();
318        let _ = Base::detect("0x123abclpmk").unwrap_err();
319        let _ = Base::detect("hello world").unwrap_err();
320    }
321
322    #[test]
323    fn strips_only_the_prefix_of_the_requested_base() {
324        // `0b`/`0B` are hexadecimal digits when the base is explicitly hexadecimal.
325        for s in ["0b1010", "0B1010"] {
326            let number = NumberWithBase::parse_uint(s, Some("16")).unwrap();
327            assert_eq!(number.number(), U256::from(0xb1010));
328            assert_eq!(number.base, Hexadecimal);
329        }
330        let number = NumberWithBase::parse_int("-0b11", Some("hex")).unwrap();
331        assert_eq!(number.number(), U256::from(0xb11).wrapping_neg());
332        assert!(!number.is_nonnegative());
333
334        // Prefixes of the requested base are still accepted, in either case.
335        assert_eq!(
336            NumberWithBase::parse_uint("0x1f", Some("16")).unwrap().number(),
337            U256::from(31)
338        );
339        assert_eq!(
340            NumberWithBase::parse_uint("0X1F", Some("16")).unwrap().number(),
341            U256::from(31)
342        );
343        assert_eq!(NumberWithBase::parse_uint("0b11", Some("2")).unwrap().number(), U256::from(3));
344        assert_eq!(NumberWithBase::parse_uint("0o17", Some("8")).unwrap().number(), U256::from(15));
345        assert_eq!(NumberWithBase::parse_uint("42", Some("10")).unwrap().number(), U256::from(42));
346
347        // A prefix of another base is not a valid digit sequence in the requested base.
348        assert!(NumberWithBase::parse_uint("0x10", Some("2")).is_err());
349        assert!(NumberWithBase::parse_uint("0b10", Some("10")).is_err());
350
351        // Detection from the prefix is unaffected.
352        let number = NumberWithBase::parse_uint("0b1010", None).unwrap();
353        assert_eq!(number.number(), U256::from(10));
354        assert_eq!(number.base, Binary);
355    }
356
357    #[test]
358    fn formats_positive_numbers() {
359        for n in NUMS {
360            let num = number(n);
361            assert_eq!(num.with_base(Binary).format(), format!("{n:b}"));
362            assert_eq!(num.with_base(Octal).format(), format!("{n:o}"));
363            assert_eq!(num.with_base(Decimal).format(), n.to_string());
364            assert_eq!(num.with_base(Hexadecimal).format(), format!("{n:x}"));
365
366            assert_eq!(format!("{num}"), n.to_string());
367            assert_eq!(format!("{num:x}"), format!("{n:x}"));
368            assert_eq!(format!("{num:#x}"), format!("{n:#x}"));
369            assert_eq!(format!("{:#?}", num.with_base(Binary)), format!("{n:#b}"));
370            assert_eq!(format!("{:#?}", num.with_base(Octal)), format!("{n:#o}"));
371        }
372    }
373
374    #[test]
375    fn formats_negative_numbers() {
376        for n in NUMS.into_iter().map(|n| -n).chain([i128::MIN]) {
377            let num = number(n);
378            // The underlying number is 256 bits wide, so the two's complement is sign-extended.
379            assert_eq!(num.with_base(Binary).format(), format!("{n:1>256b}"));
380            assert_eq!(num.with_base(Hexadecimal).format(), format!("{n:f>64x}"));
381            // Decimal digits never carry the sign; `Display` adds it back.
382            assert_eq!(num.with_base(Decimal).format(), n.to_string().trim_start_matches('-'));
383            assert_eq!(format!("{num}"), n.to_string());
384        }
385    }
386}