use crate::error::{FormatError, Result};
#[derive(Debug, PartialEq)]
pub enum Argument<'f> {
Integer(usize),
Identifier(&'f str),
}
#[derive(Debug, PartialEq)]
pub struct FormatSpec<'f> {
pub fill: char,
pub align: Align,
pub sign: Option<Sign>,
pub alternate: bool,
pub zero_pad: bool,
pub width: Option<Count<'f>>,
pub precision: Option<Precision<'f>>,
pub r#type: Type<'f>,
}
#[derive(Debug, PartialEq)]
pub enum Align {
Left,
Center,
Right,
}
#[derive(Debug, PartialEq)]
pub enum Sign {
Plus,
Minus,
}
#[derive(Debug, PartialEq)]
pub enum Precision<'f> {
Count(Count<'f>),
Star,
}
#[derive(Debug, PartialEq)]
pub enum Type<'f> {
Binary,
Custom(&'f str),
Debug,
DebugLowerHex,
DebugUpperHex,
Display,
LowerExp,
LowerHex,
Octal,
Pointer,
UpperExp,
UpperHex,
}
impl Type<'_> {
pub fn to_str(&self) -> &str {
match self {
Type::Binary => "binary",
Type::Custom(name) => name,
Type::Debug => "debug",
Type::DebugLowerHex => "debug_lower_hex",
Type::DebugUpperHex => "debug_upper_hex",
Type::Display => "display",
Type::LowerExp => "lower_exp",
Type::LowerHex => "lower_hex",
Type::Octal => "octal",
Type::Pointer => "pointer",
Type::UpperExp => "upper_exp",
Type::UpperHex => "upper_hex",
}
}
}
#[derive(Debug, PartialEq)]
pub enum Count<'f> {
Argument(Argument<'f>),
Integer(usize),
}
pub fn parse_format_spec(format_spec: &str) -> Result<FormatSpec> {
let mut format_spec_substr = format_spec.trim_end();
let (fill, align) = match (
format_spec_substr.chars().next(),
format_spec_substr.chars().nth(1),
) {
(Some(fill), Some('<')) => (Some(fill), Some(Align::Left)),
(Some(fill), Some('^')) => (Some(fill), Some(Align::Center)),
(Some(fill), Some('>')) => (Some(fill), Some(Align::Right)),
(Some('<'), _) => (None, Some(Align::Left)),
(Some('^'), _) => (None, Some(Align::Center)),
(Some('>'), _) => (None, Some(Align::Right)),
_ => (None, None),
};
if fill.is_some() {
format_spec_substr = &format_spec_substr[fill.unwrap().len_utf8()..];
}
if align.is_some() {
format_spec_substr = &format_spec_substr[1..];
}
let sign = match format_spec_substr.chars().next() {
Some('+') => Some(Sign::Plus),
Some('-') => Some(Sign::Minus),
_ => None,
};
if sign.is_some() {
format_spec_substr = &format_spec_substr[1..];
}
let alternate = matches!(format_spec_substr.chars().next(), Some('#'));
if alternate {
format_spec_substr = &format_spec_substr[1..];
}
let zero_pad = matches!(format_spec_substr.chars().next(), Some('0'));
if zero_pad {
format_spec_substr = &format_spec_substr[1..];
}
let dot = format_spec_substr.chars().position(|c| c == '.');
let dollar = format_spec_substr.chars().position(|c| c == '$');
let non_digit = format_spec_substr.chars().position(|c| !c.is_ascii_digit());
let width = match (dot, dollar, non_digit) {
(Some(dot), _, _) => &format_spec_substr[..dot],
(None, Some(dollar), _) => &format_spec_substr[..=dollar],
(None, None, Some(non_digit)) => &format_spec_substr[..non_digit],
_ => format_spec_substr,
};
format_spec_substr = &format_spec_substr[width.len()..];
let width = parse_count(width)?;
let precision = if format_spec_substr.starts_with(".") {
format_spec_substr = &format_spec_substr[1..];
if format_spec_substr.starts_with('*') {
format_spec_substr = &format_spec_substr[1..];
Some(Precision::Star)
} else {
let dollar = format_spec_substr.chars().position(|c| c == '$');
let non_digit = format_spec_substr.chars().position(|c| !c.is_ascii_digit());
let precision = match (dollar, non_digit) {
(Some(dollar), _) => &format_spec_substr[..=dollar],
(None, Some(non_digit)) => &format_spec_substr[..non_digit],
_ => format_spec_substr,
};
format_spec_substr = &format_spec_substr[precision.len()..];
let precision = parse_count(precision)?
.map(Precision::Count)
.ok_or_else(|| FormatError::ExpectedPrecision(format_spec_substr.to_string()))?;
Some(precision)
}
} else {
None
};
let r#type = parse_type(format_spec_substr)?;
Ok(FormatSpec {
fill: fill.unwrap_or(' '),
align: align.unwrap_or(Align::Right),
sign,
alternate,
zero_pad,
width,
precision,
r#type,
})
}
fn parse_count(count: &str) -> Result<Option<Count<'_>>> {
if count.is_empty() {
return Ok(None);
}
if !count.ends_with('$') {
let parsed = count.parse::<usize>()?;
Ok(Some(Count::Integer(parsed)))
} else {
let argument = &count[..count.len() - 1];
if argument.chars().all(|c| c.is_ascii_digit()) {
let parsed = argument.parse::<usize>()?;
Ok(Some(Count::Argument(Argument::Integer(parsed))))
} else {
Ok(Some(Count::Argument(Argument::Identifier(argument))))
}
}
}
fn parse_type(ty: &str) -> Result<Type> {
Ok(match ty {
"" => Type::Display,
"?" => Type::Debug,
"x?" => Type::DebugLowerHex,
"X?" => Type::DebugUpperHex,
"o" => Type::Octal,
"x" => Type::LowerHex,
"X" => Type::UpperHex,
"p" => Type::Pointer,
"b" => Type::Binary,
"e" => Type::LowerExp,
"E" => Type::UpperExp,
_ => Type::Custom(ty),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_format_spec() {
let spec = parse_format_spec("").unwrap();
assert_eq!(spec.fill, ' ');
assert_eq!(spec.align, Align::Right);
assert_eq!(spec.sign, None);
assert!(!spec.alternate);
assert!(!spec.zero_pad);
assert_eq!(spec.width, None);
assert_eq!(spec.precision, None);
assert_eq!(spec.r#type, Type::Display);
}
#[test]
fn test_fill_and_align() {
let spec = parse_format_spec("_<").unwrap();
assert_eq!(spec.fill, '_');
assert_eq!(spec.align, Align::Left);
let spec = parse_format_spec(" ^").unwrap();
assert_eq!(spec.fill, ' ');
assert_eq!(spec.align, Align::Center);
let spec = parse_format_spec("0>").unwrap();
assert_eq!(spec.fill, '0');
assert_eq!(spec.align, Align::Right);
let spec = parse_format_spec("<").unwrap();
assert_eq!(spec.fill, ' ');
assert_eq!(spec.align, Align::Left);
let spec = parse_format_spec("^").unwrap();
assert_eq!(spec.fill, ' ');
assert_eq!(spec.align, Align::Center);
let spec = parse_format_spec(">").unwrap();
assert_eq!(spec.fill, ' ');
assert_eq!(spec.align, Align::Right);
}
#[test]
fn test_sign() {
let spec = parse_format_spec("+").unwrap();
assert_eq!(spec.sign, Some(Sign::Plus));
let spec = parse_format_spec("-").unwrap();
assert_eq!(spec.sign, Some(Sign::Minus));
}
#[test]
fn test_alternate_form() {
let spec = parse_format_spec("#").unwrap();
assert!(spec.alternate);
let spec = parse_format_spec("").unwrap();
assert!(!spec.alternate);
}
#[test]
fn test_zero_padding() {
let spec = parse_format_spec("0").unwrap();
assert!(spec.zero_pad);
let spec = parse_format_spec("").unwrap();
assert!(!spec.zero_pad);
}
#[test]
fn test_width() {
let spec = parse_format_spec("10").unwrap();
assert_eq!(spec.width, Some(Count::Integer(10)));
let spec = parse_format_spec("1$").unwrap();
assert_eq!(spec.width, Some(Count::Argument(Argument::Integer(1))));
let spec = parse_format_spec("width$").unwrap();
assert_eq!(
spec.width,
Some(Count::Argument(Argument::Identifier("width")))
);
}
#[test]
fn test_precision() {
let spec = parse_format_spec(".5").unwrap();
assert_eq!(spec.precision, Some(Precision::Count(Count::Integer(5))));
let spec = parse_format_spec(".2$").unwrap();
assert_eq!(
spec.precision,
Some(Precision::Count(Count::Argument(Argument::Integer(2))))
);
let spec = parse_format_spec(".prec$").unwrap();
assert_eq!(
spec.precision,
Some(Precision::Count(Count::Argument(Argument::Identifier(
"prec"
))))
);
let spec = parse_format_spec(".*").unwrap();
assert_eq!(spec.precision, Some(Precision::Star));
let spec = parse_format_spec("").unwrap();
assert_eq!(spec.precision, None);
}
#[test]
fn test_type() {
let spec = parse_format_spec("").unwrap();
assert_eq!(spec.r#type, Type::Display);
let spec = parse_format_spec("?").unwrap();
assert_eq!(spec.r#type, Type::Debug);
let spec = parse_format_spec("x?").unwrap();
assert_eq!(spec.r#type, Type::DebugLowerHex);
let spec = parse_format_spec("X?").unwrap();
assert_eq!(spec.r#type, Type::DebugUpperHex);
let spec = parse_format_spec("o").unwrap();
assert_eq!(spec.r#type, Type::Octal);
let spec = parse_format_spec("x").unwrap();
assert_eq!(spec.r#type, Type::LowerHex);
let spec = parse_format_spec("X").unwrap();
assert_eq!(spec.r#type, Type::UpperHex);
let spec = parse_format_spec("p").unwrap();
assert_eq!(spec.r#type, Type::Pointer);
let spec = parse_format_spec("b").unwrap();
assert_eq!(spec.r#type, Type::Binary);
let spec = parse_format_spec("e").unwrap();
assert_eq!(spec.r#type, Type::LowerExp);
let spec = parse_format_spec("E").unwrap();
assert_eq!(spec.r#type, Type::UpperExp);
assert_eq!(parse_format_spec("Z").unwrap().r#type, Type::Custom("Z"));
}
#[test]
fn test_combined_format_specs() {
let spec = parse_format_spec("a^10.5x ").unwrap();
assert_eq!(spec.fill, 'a');
assert_eq!(spec.align, Align::Center);
assert_eq!(spec.sign, None);
assert!(!spec.alternate);
assert!(!spec.zero_pad);
assert_eq!(spec.width, Some(Count::Integer(10)));
assert_eq!(spec.precision, Some(Precision::Count(Count::Integer(5))));
assert_eq!(spec.r#type, Type::LowerHex);
let spec = parse_format_spec("_>+#010.5x ").unwrap();
assert_eq!(spec.fill, '_');
assert_eq!(spec.align, Align::Right);
assert_eq!(spec.sign, Some(Sign::Plus));
assert!(spec.alternate);
assert!(spec.zero_pad);
assert_eq!(spec.width, Some(Count::Integer(10)));
assert_eq!(spec.precision, Some(Precision::Count(Count::Integer(5))));
assert_eq!(spec.r#type, Type::LowerHex);
let spec = parse_format_spec("<width$.prec$?").unwrap();
assert_eq!(spec.align, Align::Left);
assert_eq!(
spec.width,
Some(Count::Argument(Argument::Identifier("width")))
);
assert_eq!(
spec.precision,
Some(Precision::Count(Count::Argument(Argument::Identifier(
"prec"
))))
);
assert_eq!(spec.r#type, Type::Debug);
let spec = parse_format_spec("^1$.*").unwrap();
assert_eq!(spec.width, Some(Count::Argument(Argument::Integer(1))));
assert_eq!(spec.precision, Some(Precision::Star));
}
#[test]
fn test_invalid_precision() {
assert_eq!(
parse_format_spec("0.invalid_precision"),
Err(FormatError::ExpectedPrecision(
"invalid_precision".to_string()
))
);
}
}