num_runtime_fmt/
parse.rs

1use crate::{Align, Base, NumFmt, Sign};
2use lazy_static::lazy_static;
3use regex::Regex;
4
5lazy_static! {
6    static ref PARSE_RE: Regex = Regex::new(
7        r"(?x)
8        ^
9        (
10            (?P<fill>.)?
11            (?P<align>[<^>v])
12        )?
13        (?P<sign>[-+])?
14        (?P<hash>(?-x:#))?
15        (
16         (?P<zero>0)?
17         (?P<width>[1-9]\d*)
18        )?
19        (
20         \.
21         (?P<precision>\d+)
22        )?
23        (?P<format>[bodxX])?
24        (
25         (?P<separator>(?-x:[_, ]))
26         (?P<spacing>\d+)?
27        )?
28        $"
29    )
30    .unwrap();
31}
32
33#[derive(Debug, thiserror::Error, PartialEq, Eq)]
34pub enum Error {
35    #[error("Input did not match canonical format string regex")]
36    NoMatch,
37    #[error("failed to parse integer value \"{0}\"")]
38    ParseInt(String, #[source] std::num::ParseIntError),
39}
40
41/// Parse a `NumFmt` instance from a format string.
42///
43/// See crate-level docs for the grammar.
44pub(crate) fn parse(s: &str) -> Result<NumFmt, Error> {
45    let captures = PARSE_RE.captures(s).ok_or(Error::NoMatch)?;
46    let str_of = |name: &str| captures.name(name).map(|m| m.as_str());
47    let char_of = |name: &str| str_of(name).and_then(|s| s.chars().next());
48
49    let mut builder = NumFmt::builder();
50
51    if let Some(fill) = char_of("fill") {
52        builder = builder.fill(fill);
53    }
54    if let Some(align) = char_of("align") {
55        builder = builder.align(match align {
56            '<' => Align::Left,
57            '^' => Align::Center,
58            '>' => Align::Right,
59            'v' => Align::Decimal,
60            _ => unreachable!("guaranteed by regex"),
61        });
62    }
63    if let Some(sign) = char_of("sign") {
64        builder = builder.sign(match sign {
65            '-' => Sign::OnlyMinus,
66            '+' => Sign::PlusAndMinus,
67            _ => unreachable!("guaranteed by regex"),
68        });
69    }
70    if char_of("hash").is_some() {
71        builder = builder.hash(true);
72    }
73    if char_of("zero").is_some() {
74        builder = builder.zero(true);
75    }
76    if let Some(width) = str_of("width") {
77        let width = width
78            .parse()
79            .map_err(|err| Error::ParseInt(width.to_string(), err))?;
80        builder = builder.width(width);
81    }
82    if let Some(precision) = str_of("precision") {
83        let precision = precision
84            .parse()
85            .map_err(|err| Error::ParseInt(precision.to_string(), err))?;
86        builder = builder.precision(Some(precision));
87    }
88    if let Some(format) = char_of("format") {
89        builder = builder.base(match format {
90            'b' => Base::Binary,
91            'o' => Base::Octal,
92            'd' => Base::Decimal,
93            'x' => Base::LowerHex,
94            'X' => Base::UpperHex,
95            _ => unreachable!("guaranteed by regex"),
96        });
97    }
98    builder = builder.separator(char_of("separator"));
99    if let Some(spacing) = str_of("spacing") {
100        let spacing = spacing
101            .parse()
102            .map_err(|err| Error::ParseInt(spacing.to_string(), err))?;
103        builder = builder.spacing(spacing);
104    }
105
106    Ok(builder.build())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_parse_re_matches() {
115        for format_str in &[
116            "",
117            "<",
118            "->",
119            "#x",
120            "+#04o",
121            "v-10.2",
122            "#04x_2",
123            "-v-#012.3d 4",
124        ] {
125            println!("{:?}:", format_str);
126            assert!(
127                PARSE_RE.captures(format_str).is_some(),
128                "all valid format strings must be parsed"
129            );
130        }
131    }
132}