use crate::{Item, ItemDetails, PackList, Spaced};
use alloc::{string::ToString, vec::Vec};
use core::fmt;
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum Error {
#[cfg_attr(feature = "std", error("unexpected EOF"))]
UnexpectedEof,
}
impl core::str::FromStr for PackList {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let mut it = s.lines().map(|i| i.trim_end());
let name = it.next().ok_or(Error::UnexpectedEof)?;
let items = it
.map(|line| match line.parse() {
Ok(x) => x,
Err(x) => match x {},
})
.collect();
Ok(PackList {
name: name.to_string(),
items,
})
}
}
impl core::str::FromStr for Item {
type Err = core::convert::Infallible;
fn from_str(line: &str) -> Result<Self, core::convert::Infallible> {
let (det, text) = parse_itemdet(line)
.map(|(det, rest)| (Some(det), rest))
.unwrap_or((None, line));
Ok(Self {
det,
text: text.to_string(),
})
}
}
fn parse_itemdet(mut line: &str) -> Option<(ItemDetails, &str)> {
if line.len() < 5 {
return None;
}
let mut ret = ItemDetails {
markers: Vec::new(),
multiplier: None,
};
while let Some(x) = line.strip_prefix('[') {
let mut it = x.chars();
let a = it.next()?;
let b = it.next()?;
if ']' != it.next()? {
return None;
}
line = it.as_str();
let wsl = it
.take_while(|i| i.is_whitespace())
.map(|i| i.len_utf8())
.sum::<usize>();
let (space, rest) = line.split_at(wsl);
ret.markers.push(Spaced {
val: [a, b],
space: space.to_string(),
});
line = rest;
}
if ret.markers.is_empty() {
return None;
}
if let Some((val, space, rest)) = parse_multiplier(line) {
ret.multiplier = Some(Spaced {
val,
space: space.to_string(),
});
line = rest;
}
Some((ret, line))
}
fn parse_multiplier(line: &str) -> Option<(u32, &str, &str)> {
if !line.starts_with(|i: char| i.is_ascii_digit()) {
return None;
}
let mut multiplier = 0;
let mut it = line.chars();
loop {
match it.next() {
Some('x') => {
let tmp = it.as_str();
let wsl = it
.take_while(|i| i.is_whitespace())
.map(|i| i.len_utf8())
.sum::<usize>();
let (a, b) = tmp.split_at(wsl);
return Some((multiplier, a, b));
}
Some(i) if i.is_ascii_digit() => {
multiplier *= 10;
multiplier += u32::from((i as u8) - b'0');
}
_ => return None,
}
}
}
impl fmt::Display for PackList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.name.trim_end())?;
for item in &self.items {
writeln!(f, "{}", item)?;
}
Ok(())
}
}
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(x) = &self.det {
fmt::Display::fmt(x, f)?;
}
f.write_str(self.text.trim_end())
}
}
impl fmt::Display for ItemDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for m in &self.markers {
write!(f, "[{}{}]{}", m.val[0], m.val[1], m.space)?;
}
if let Some(x) = &self.multiplier {
write!(f, "{}x{}", x.val, x.space)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{format, string::String, vec};
use proptest::prelude::*;
#[test]
fn ex1() {
let inp = r#"Packliste Ölland:
[* ] [ _] [. ] 132x Ruß
"#;
let res = PackList {
name: "Packliste Ölland:".to_string(),
items: vec![Item {
det: Some(ItemDetails {
markers: vec![
Spaced {
val: ['*', ' '],
space: " ".to_string(),
},
Spaced {
val: [' ', '_'],
space: " ".to_string(),
},
Spaced {
val: ['.', ' '],
space: " ".to_string(),
},
],
multiplier: Some(Spaced {
val: 132,
space: " ".to_string(),
}),
}),
text: "Ruß".to_string(),
}],
};
assert_eq!(inp.parse::<PackList>().unwrap(), res);
assert_eq!(res.to_string(), inp);
}
proptest! {
#[test]
fn doesnt_crash(s in "\\PC*") {
let _ = s.parse::<PackList>();
}
#[test]
fn correctly_trimmed(s in "(?s:.+)") {
let s2: String = s.lines().flat_map(|i| i.trim_end().chars().chain(core::iter::once('\n'))).collect();
let x1 = s.parse::<PackList>().unwrap();
let x2 = s2.parse::<PackList>().unwrap();
prop_assert_eq!(x1, x2);
}
#[test]
fn roundtrip(s in "(?s:[^\r]+\n)".prop_filter("line ends are trimmed", |v| v.lines().all(|l| l == l.trim_end()))) {
prop_assert_eq!(&s, &s.parse::<PackList>().unwrap().to_string());
}
}
}