+#[derive(Debug, PartialEq, Eq)]
+enum Rowlike {
+ Row(Row),
+ Spacer,
+}
+
+struct Reader<Input: Iterator<Item = Result<String, std::io::Error>>> {
+ input: std::iter::Enumerate<Input>,
+ row: Option<Row>,
+}
+impl<Input: Iterator<Item = Result<String, std::io::Error>>> Reader<Input> {
+ fn new(input: Input) -> Self {
+ Self {
+ input: input.enumerate(),
+ row: None,
+ }
+ }
+}
+impl<Input: Iterator<Item = Result<String, std::io::Error>>> Iterator for Reader<Input> {
+ type Item = Result<Rowlike, std::io::Error>;
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ match self.input.next() {
+ None => return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose(),
+ Some((_, Err(e))) => return Some(Err(e)),
+ Some((n, Ok(line))) => match InputLine::from(line.as_ref()) {
+ InputLine::Blank if self.row.is_some() => {
+ return Ok(std::mem::take(&mut self.row).map(Rowlike::Row)).transpose()
+ }
+ InputLine::Blank => return Some(Ok(Rowlike::Spacer)),
+ InputLine::Entry(col, instance) => 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
+ .entry(col.to_owned())
+ .and_modify(|is| is.push(instance.map(ToOwned::to_owned)))
+ .or_insert_with(|| vec![instance.map(ToOwned::to_owned)]);
+ }
+ },
+ InputLine::RowHeader(row) => {
+ let prev = std::mem::take(&mut self.row);
+ self.row = Some(Row {
+ label: row.to_owned(),
+ entries: HashMap::new(),
+ });
+ if prev.is_some() {
+ return Ok(prev.map(Rowlike::Row)).transpose();
+ }
+ }
+ },
+ }
+ }
+ }
+}
+
+fn read_input(input: impl std::io::Read) -> Result<(Vec<Rowlike>, Config), std::io::Error> {
+ let default_config = Config {
+ column_threshold: 2,
+ };
+ Reader::new(std::io::BufReader::new(input).lines())
+ .collect::<Result<Vec<_>, _>>()
+ .map(|rows| (rows, default_config))
+}
+
+fn column_counts(rows: &[Rowlike]) -> Vec<(usize, String)> {
+ let empty = HashMap::new();
+ let mut counts: Vec<_> = rows
+ .iter()
+ .flat_map(|rl| match rl {
+ Rowlike::Row(r) => r.entries.keys(),
+ Rowlike::Spacer => empty.keys(),
+ })
+ .fold(HashMap::new(), |mut cs, col| {
+ cs.entry(col.to_owned())
+ .and_modify(|n| *n += 1)
+ .or_insert(1);
+ cs
+ })
+ .into_iter()
+ .map(|(col, n)| (n, col))
+ .collect();
+ counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol)));
+ counts
+}
+fn column_order(config: &Config, rows: &[Rowlike]) -> Vec<String> {
+ column_counts(rows)
+ .into_iter()
+ .filter_map(|(n, col)| (n >= config.column_threshold).then_some(col))
+ .collect()
+}
+
+fn render_one_instance(instance: &Option<String>) -> HTML {
+ match instance {
+ None => HTML::from("✓"),
+ Some(instance) => HTML::escape(instance.as_ref()),
+ }
+}
+
+fn render_instances(instances: &[Option<String>]) -> HTML {
+ let all_empty = instances.iter().all(Option::is_none);
+ if all_empty && instances.len() == 1 {
+ HTML::from("")
+ } else if all_empty {
+ HTML(format!("{}", instances.len()))
+ } else {
+ HTML(
+ instances
+ .iter()
+ .map(render_one_instance)
+ .map(|html| html.0) // Waiting for slice_concat_trait to stabilize
+ .collect::<Vec<_>>()
+ .join(" "),
+ )
+ }
+}
+
+fn render_cell(col: &str, row: &mut Row) -> HTML {
+ let row_label = HTML::escape(row.label.as_ref());
+ let col_label = HTML::escape(col);
+ let instances: Option<&Vec<Option<String>>> = row.entries.get(col);
+ let class = HTML::from(if instances.is_none() { "" } else { "yes" });
+ let contents = match instances {
+ None => HTML::from(""),
+ Some(is) => render_instances(is),
+ };
+ row.entries.remove(col);
+ HTML(format!(
+ r#"<td class="{class}" onmouseover="h2('{row_label}','{col_label}')" onmouseout="ch2('{row_label}','{col_label}')">{contents}</td>"#
+ ))
+}
+
+fn render_leftover(notcol: &str, instances: &[Option<String>]) -> HTML {
+ let label = HTML::escape(notcol);
+ let rest = render_instances(instances);
+ if rest == HTML::from("") {
+ HTML(format!("{label}"))
+ } else {
+ HTML(format!("{label}: {rest}"))
+ }