use std::collections::{HashMap, HashSet}; use std::io::BufRead; use std::iter::Iterator; const HEADER: &str = " "; const FOOTER: &str = "
"; #[derive(Debug, PartialEq, Eq, Hash)] struct Entry { col: String, instance: Option, } impl From<&str> for Entry { fn from(value: &str) -> Entry { match value.split_once(':') { None => Entry { col: String::from(value), instance: None, }, Some((col, instance)) => Entry { col: String::from(col.trim()), instance: Some(String::from(instance.trim())), }, } } } #[derive(Debug, PartialEq, Eq)] struct RowInput { label: String, entries: Vec, } struct Reader>> { input: std::iter::Enumerate, row: Option, } impl>> Reader { fn new(input: Input) -> Self { Self { input: input.enumerate(), row: None, } } } impl>> Iterator for Reader { type Item = Result; fn next(&mut self) -> Option { loop { match self .input .next() .map(|(n, r)| (n, r.map(|line| String::from(line.trim_end())))) { None => return Ok(std::mem::take(&mut self.row)).transpose(), Some((_, Err(e))) => return Some(Err(e)), Some((_, Ok(line))) if line.is_empty() && self.row.is_some() => { return Ok(std::mem::take(&mut self.row)).transpose() } Some((_, Ok(line))) if line.is_empty() => {} Some((n, Ok(line))) if line.starts_with(' ') => match &mut self.row { None => { return Some(Err(std::io::Error::other(format!( "{}: Entry with no header", n + 1 )))) } Some(ref mut row) => row.entries.push(Entry::from(line.trim())), }, Some((_, Ok(line))) => { let prev = std::mem::take(&mut self.row); self.row = Some(RowInput { label: line, entries: vec![], }); if prev.is_some() { return Ok(prev).transpose(); } } } } } } fn read_rows(input: impl std::io::Read) -> impl Iterator> { Reader::new(std::io::BufReader::new(input).lines()) } fn column_counts(rows: &[RowInput]) -> Vec<(usize, String)> { let mut counts: Vec<_> = rows .iter() .flat_map(|r| { r.entries .iter() .map(|e| &e.col) .collect::>() .into_iter() }) .fold(HashMap::new(), |mut cs, col| { cs.entry(String::from(col)) .and_modify(|n| *n += 1) .or_insert(1); cs }) .into_iter() .map(|(col, n)| (n, col)) .collect(); counts.sort(); counts } fn column_order(rows: &[RowInput]) -> Vec { column_counts(rows) .into_iter() .map(|(_, col)| col) .collect() } /// # Errors /// /// Will return `Err` if /// * there's an i/o error while reading `input` /// * the log has invalid syntax: /// * an indented line with no preceding non-indented line pub fn tablify(input: impl std::io::Read) -> Result { let rows = read_rows(input).collect::, _>>()?; let _columns = column_order(&rows); Ok(String::from(HEADER) + "Hello, world!" + FOOTER) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_entry() { assert_eq!( Entry::from("foo"), Entry { col: String::from("foo"), instance: None } ); assert_eq!( Entry::from("foo:bar"), Entry { col: String::from("foo"), instance: Some(String::from("bar")) } ); assert_eq!( Entry::from("foo: bar"), Entry { col: String::from("foo"), instance: Some(String::from("bar")) } ); } #[test] fn test_read_rows() { assert_eq!( read_rows(&b"foo"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![] }] ); assert_eq!( read_rows(&b"bar"[..]).flatten().collect::>(), vec![RowInput { label: String::from("bar"), entries: vec![] }] ); assert_eq!( read_rows(&b"foo\nbar\n"[..]).flatten().collect::>(), vec![ RowInput { label: String::from("foo"), entries: vec![] }, RowInput { label: String::from("bar"), entries: vec![] } ] ); assert_eq!( read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![Entry::from("bar")] }] ); assert_eq!( read_rows(&b"foo\n bar\n baz\n"[..]) .flatten() .collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![Entry::from("bar"), Entry::from("baz")] }] ); assert_eq!( read_rows(&b"foo\n\nbar\n"[..]) .flatten() .collect::>(), vec![ RowInput { label: String::from("foo"), entries: vec![] }, RowInput { label: String::from("bar"), entries: vec![] } ] ); assert_eq!( read_rows(&b"foo\n \nbar\n"[..]) .flatten() .collect::>(), vec![ RowInput { label: String::from("foo"), entries: vec![] }, RowInput { label: String::from("bar"), entries: vec![] } ] ); assert_eq!( read_rows(&b"foo \n bar \n"[..]) .flatten() .collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![Entry::from("bar")] }] ); let bad = read_rows(&b" foo"[..]).next().unwrap(); assert!(bad.is_err()); assert!(format!("{bad:?}").contains("1: Entry with no header")); let bad2 = read_rows(&b"foo\n\n bar"[..]).nth(1).unwrap(); assert!(bad2.is_err()); assert!(format!("{bad2:?}").contains("3: Entry with no header")); } #[test] fn test_column_counts() { assert_eq!( column_counts( &read_rows(&b"foo\n bar\n baz\n"[..]) .collect::, _>>() .unwrap() ), vec![(1, String::from("bar")), (1, String::from("baz"))] ); assert_eq!( column_counts( &read_rows(&b"foo\n bar\n baz\nquux\n baz"[..]) .collect::, _>>() .unwrap() ), vec![(1, String::from("bar")), (2, String::from("baz"))] ); assert_eq!( column_counts( &read_rows(&b"foo\n bar\n bar\n baz\n bar\nquux\n baz"[..]) .collect::, _>>() .unwrap() ), vec![(1, String::from("bar")), (2, String::from("baz"))] ); assert_eq!( column_counts( &read_rows(&b"foo\n bar: 1\n bar: 2\n baz\n bar\nquux\n baz"[..]) .collect::, _>>() .unwrap() ), vec![(1, String::from("bar")), (2, String::from("baz"))] ); } }