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
41pub(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}