VHLGEBT74I5RXLGUWMVCXM64BYG7HOOFT7SURJ5TCTB3AAOQHIKQC
#[error("unexpected data after range specification in header")]
DataAfterRangeSpec,
#[error("section is defined multiple times")]
SectionOverwritten(String),
#[error("invalid item: {0}")]
InvalidItem(String),
#[error("unexpected EOF")]
/// ```
/// use zhed_packlist::{PackList, ParseError};
/// assert_eq!("".parse::<PackList>(), Err(ParseError::UnexpectedEof));
/// ```
#[cfg_attr(feature = "std", error("unexpected EOF"))]
// 1st line: packlist name and range
let mut fst = it.next().map(|x| x.trim()).ok_or(Error::UnexpectedEof)?;
if let Some(x) = fst.strip_suffix(':') {
fst = x;
}
let (name, range) = {
let (rstart, rend) = (fst.find('('), fst.rfind(')'));
if rstart.is_some() && rend.is_some() && rstart.unwrap() < rend.unwrap() {
let (rstart, rend) = (rstart.unwrap(), rend.unwrap());
if rend != (fst.len() - 1) {
return Err(Error::DataAfterRangeSpec);
}
let mut name = fst[..rstart].trim();
if let Some(x) = name.strip_suffix(':') {
name = x.trim();
}
(name, &fst[rstart + 1..rend])
} else {
(fst, "")
}
};
// 1st line: packlist name
let name = it.next().ok_or(Error::UnexpectedEof)?;
let mut sections = BTreeMap::new();
let mut section = (String::new(), Section::default());
macro_rules! commit {
() => {{
if sections.contains_key(§ion.0) {
return Err(Error::SectionOverwritten(section.0));
}
sections.insert(
std::mem::take(&mut section.0),
std::mem::take(&mut section.1),
);
}};
}
for line in it {
if line.trim().is_empty() {
continue;
}
let (itemdata, desc) = parse_item(line)?;
if let Some(x) = desc.strip_prefix('#') {
commit!();
section.0 = x.trim().to_string();
section.1.summary = itemdata;
} else {
section.1.items.push((itemdata, desc));
}
}
if !section.0.is_empty() || !section.1.items.is_empty() {
commit!();
}
let items = it
.map(|line| match line.parse() {
Ok(x) => x,
Err(x) => match x {},
})
.collect();
range: range.to_string(),
sections,
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(),
// parse premarkers
let mut premarkers = Vec::new();
while it.next_if(|i| *i == '[').is_some() {
let premarker = it.next().ok_or_else(mkerr)?;
let _ = it.next().ok_or_else(mkerr)?;
let c2 = it.next().ok_or_else(mkerr)?;
if c2 != ']' {
return Err(mkerr());
// parse markers
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;
premarkers.push(premarker);
while it.next_if(|i| i.is_whitespace()).is_some() {}
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;
// check for multiplier
// btp : backtrack point if no valid multiplier is found
let btp = it.clone();
let mut multiplier = None;
while let Some(x) = it.next_if(|i| i.is_ascii_digit()) {
let m = multiplier.get_or_insert(0);
*m *= 10;
*m += u32::from((x as u8) - b'0');
}
if it.next_if(|&i| i == 'x').is_none() {
multiplier = None;
it = btp;
} else {
while it.next_if(|i| i.is_whitespace()).is_some() {}
if let Some((val, space, rest)) = parse_multiplier(line) {
ret.multiplier = Some(Spaced {
val,
space: space.to_string(),
});
line = rest;
impl fmt::Display for PackList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name)?;
if !self.range.is_empty() {
write!(f, " ({})", self.range)?;
}
writeln!(f)?;
for (section, sdata) in &self.sections {
if !section.is_empty() {
writeln!(f, "\n{}# {}", sdata.summary, section)?;
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));
if self.multiplier != 1 {
write!(f, "{}x ", self.multiplier)?;
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)?;
#[test]
fn ex0() {
let inp1 = r#"Packliste Ölland (80 Tage, 50 Nächtsle):
[ ] 15 Ruß
[ ] 132x Wattestäbe
[# ] 0x Handtuch
[- ] 5000x # Wachsbaum ...
[ ] Ranke
[* ] 5 x Metrik + BNaumbnd
"#;
let inp2 = r#"Packliste Ölland (80 Tage, 50 Nächtsle)
[ ] 15 Ruß
[ ] 132x Wattestäbe
[# ] 0x Handtuch
[- ] 5000x # Wachsbaum ...
[ ] Ranke
[* ] 5 x Metrik + BNaumbnd
"#;
let mut sections = BTreeMap::new();
sections.insert(
String::new(),
Section {
summary: ItemData::default(),
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"15 Ruß".to_string(),
),
(
ItemData {
premarkers: vec![' '],
multiplier: 132,
},
"Wattestäbe".to_string(),
),
(
ItemData {
premarkers: vec!['#'],
multiplier: 0,
},
"Handtuch".to_string(),
),
],
},
);
sections.insert(
"Wachsbaum ...".to_string(),
Section {
summary: ItemData {
premarkers: vec!['-'],
multiplier: 5000,
},
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"Ranke".to_string(),
),
(
ItemData {
premarkers: vec!['*'],
multiplier: 1,
},
"5 x Metrik + BNaumbnd".to_string(),
),
],
},
);
let res = PackList {
name: "Packliste Ölland".to_string(),
range: "80 Tage, 50 Nächtsle".to_string(),
sections,
};
assert_eq!(inp1.parse::<PackList>().unwrap(), res);
assert_eq!(inp2.parse::<PackList>().unwrap(), res);
assert_eq!(res.to_string(), inp2);
}
#[test]
fn fail0() {
assert_eq!(
"Packl (xyz) ...\n".parse::<PackList>(),
Err(Error::DataAfterRangeSpec)
);
assert_eq!(
"P\n[ ] # 1\n[ ] # 1\n".parse::<PackList>(),
Err(Error::SectionOverwritten("1".to_string()))
);
assert_eq!(
"P\n[ ]".parse::<PackList>(),
Err(Error::InvalidItem("[ ]".to_string()))
);
}
let inp2 = r#"Packliste Ölland
[* ] [ ] [. ] 15 132x Ruß
[* ] [ ] [. ] 132x Ruß
"#;
let mut sections = BTreeMap::new();
sections.insert(
String::new(),
Section {
summary: ItemData::default(),
items: vec![
(
ItemData {
premarkers: vec!['*', ' ', '.'],
multiplier: 1,
let res = PackList {
name: "Packliste Ölland:".to_string(),
items: vec![Item {
det: Some(ItemDetails {
markers: vec![
Spaced {
val: ['*', ' '],
space: " ".to_string(),
"Ruß".to_string(),
),
],
},
);
let res = PackList {
name: "Packliste Ölland".to_string(),
range: String::new(),
sections,
Spaced {
val: ['.', ' '],
space: " ".to_string(),
},
],
multiplier: Some(Spaced {
val: 132,
space: " ".to_string(),
}),
}),
text: "Ruß".to_string(),
}],
#[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();
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()))) {
assert_eq!(s, s.parse::<PackList>().unwrap().to_string());
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Section {
pub summary: ItemData,
pub items: Vec<(ItemData, String)>,
/// packlist item details
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ItemDetails {
pub markers: Vec<Spaced<[char; 2]>>,
pub multiplier: Option<Spaced<u32>>,
impl PackList {
pub fn reset_premarkers(&mut self) {
self.sections
.values_mut()
.for_each(|sd| sd.reset_premarkers());
}
pub trait Commands {
fn reset_markers(&mut self);
}
pub mod prelude {
pub use crate::Commands as _;
impl Section {
pub fn reset_premarkers(&mut self) {
core::iter::once(&mut self.summary)
.chain(self.items.iter_mut().map(|item| &mut item.0))
.flat_map(|item| item.premarkers.iter_mut())
.for_each(|i| *i = ' ');
impl Commands for PackList {
fn reset_markers(&mut self) {
self.items.iter_mut().for_each(|i| i.reset_markers());
#[test]
fn ex0rs() {
let mut sections = BTreeMap::new();
sections.insert(
String::new(),
Section {
summary: ItemData::default(),
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"15 Ruß".to_string(),
),
(
ItemData {
premarkers: vec![' '],
multiplier: 132,
},
"Wattestäbe".to_string(),
),
(
ItemData {
premarkers: vec!['#'],
multiplier: 0,
},
"Handtuch".to_string(),
),
],
},
);
sections.insert(
"Wachsbaum ...".to_string(),
Section {
summary: ItemData {
premarkers: vec!['-'],
multiplier: 5000,
},
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"Ranke".to_string(),
),
(
ItemData {
premarkers: vec!['*'],
multiplier: 1,
},
"5 x Metrik + BNaumbnd".to_string(),
),
],
},
);
let mut res = PackList {
name: "Packliste Ölland".to_string(),
range: "80 Tage, 50 Nächtsle".to_string(),
sections,
};
let mut sections2 = BTreeMap::new();
sections2.insert(
String::new(),
Section {
summary: ItemData::default(),
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"15 Ruß".to_string(),
),
(
ItemData {
premarkers: vec![' '],
multiplier: 132,
},
"Wattestäbe".to_string(),
),
(
ItemData {
premarkers: vec![' '],
multiplier: 0,
},
"Handtuch".to_string(),
),
],
},
);
sections2.insert(
"Wachsbaum ...".to_string(),
Section {
summary: ItemData {
premarkers: vec![' '],
multiplier: 5000,
},
items: vec![
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"Ranke".to_string(),
),
(
ItemData {
premarkers: vec![' '],
multiplier: 1,
},
"5 x Metrik + BNaumbnd".to_string(),
),
],
},
);
let res2 = PackList {
name: "Packliste Ölland".to_string(),
range: "80 Tage, 50 Nächtsle".to_string(),
sections: sections2,
};
res.reset_premarkers();
assert_eq!(res, res2);
impl Commands for ItemDetails {
fn reset_markers(&mut self) {
self.markers.iter_mut().for_each(|i| i.val = [' ', ' ']);
[features]
std = [ "thiserror" ]