]> git.scottworley.com Git - tablify/blobdiff - src/lib.rs
Don't allocate columns to rare events
[tablify] / src / lib.rs
index 357a7eec0afc72411c76398b124c9cf47d16ed76..a7c28198b5faa15056fd274f1b9bd85ea61dd1dc 100644 (file)
@@ -4,7 +4,9 @@ use std::fmt::Write;
 use std::io::BufRead;
 use std::iter::Iterator;
 
 use std::io::BufRead;
 use std::iter::Iterator;
 
-pub struct Config {}
+pub struct Config {
+    pub column_threshold: usize,
+}
 
 const HEADER: &str = r#"<!DOCTYPE html>
 <html>
 
 const HEADER: &str = r#"<!DOCTYPE html>
 <html>
@@ -180,43 +182,46 @@ fn column_counts(rows: &[Row]) -> Vec<(usize, String)> {
     counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol)));
     counts
 }
     counts.sort_unstable_by(|(an, acol), (bn, bcol)| bn.cmp(an).then(acol.cmp(bcol)));
     counts
 }
-fn column_order(rows: &[Row]) -> Vec<String> {
+fn column_order(config: &Config, rows: &[Row]) -> Vec<String> {
     column_counts(rows)
         .into_iter()
     column_counts(rows)
         .into_iter()
-        .map(|(_, col)| col)
+        .filter_map(|(n, col)| (n >= config.column_threshold).then_some(col))
         .collect()
 }
 
         .collect()
 }
 
-fn render_instance(instance: &Option<String>) -> HTML {
+fn render_one_instance(instance: &Option<String>) -> HTML {
     match instance {
         None => HTML::from("✓"),
         Some(instance) => HTML::escape(instance.as_ref()),
     }
 }
 
     match instance {
         None => HTML::from("✓"),
         Some(instance) => HTML::escape(instance.as_ref()),
     }
 }
 
-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 all_empty = instances
-        .iter()
-        .flat_map(|is| is.iter())
-        .all(Option::is_none);
-    let contents = if instances.is_none() || (all_empty && instances.unwrap().len() == 1) {
+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::from("")
     } else if all_empty {
-        HTML(format!("{}", instances.unwrap().len()))
+        HTML(format!("{}", instances.len()))
     } else {
         HTML(
             instances
     } else {
         HTML(
             instances
-                .unwrap()
                 .iter()
                 .iter()
-                .map(render_instance)
+                .map(render_one_instance)
                 .map(|html| html.0) // Waiting for slice_concat_trait to stabilize
                 .collect::<Vec<_>>()
                 .join(" "),
         )
                 .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!(
     };
     row.entries.remove(col);
     HTML(format!(
@@ -224,14 +229,38 @@ fn render_cell(col: &str, row: &mut Row) -> HTML {
     ))
 }
 
     ))
 }
 
+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}"))
+    }
+}
+
+fn render_all_leftovers(row: &Row) -> HTML {
+    let mut order: Vec<_> = row.entries.keys().collect();
+    order.sort_unstable();
+    HTML(
+        order
+            .into_iter()
+            .map(|notcol| render_leftover(notcol, row.entries.get(notcol).expect("Key vanished?!")))
+            .map(|html| html.0) // Waiting for slice_concat_trait to stabilize
+            .collect::<Vec<_>>()
+            .join(", "),
+    )
+}
+
 fn render_row(columns: &[String], row: &mut Row) -> HTML {
     let row_label = HTML::escape(row.label.as_ref());
     let cells = columns
         .iter()
         .map(|col| render_cell(col, row))
         .collect::<HTML>();
 fn render_row(columns: &[String], row: &mut Row) -> HTML {
     let row_label = HTML::escape(row.label.as_ref());
     let cells = columns
         .iter()
         .map(|col| render_cell(col, row))
         .collect::<HTML>();
+    let leftovers = render_all_leftovers(row);
     HTML(format!(
     HTML(format!(
-        "<tr><th id=\"{row_label}\">{row_label}</th>{cells}</tr>\n"
+        "<tr><th id=\"{row_label}\">{row_label}</th>{cells}<td onmouseover=\"highlight('{row_label}')\" onmouseout=\"clear_highlight('{row_label}')\">{leftovers}</td></tr>\n"
     ))
 }
 
     ))
 }
 
@@ -259,7 +288,7 @@ fn render_column_headers(columns: &[String]) -> HTML {
 ///     * an indented line with no preceding non-indented line
 pub fn tablify(config: &Config, input: impl std::io::Read) -> Result<HTML, std::io::Error> {
     let rows = read_rows(input).collect::<Result<Vec<_>, _>>()?;
 ///     * an indented line with no preceding non-indented line
 pub fn tablify(config: &Config, input: impl std::io::Read) -> Result<HTML, std::io::Error> {
     let rows = read_rows(input).collect::<Result<Vec<_>, _>>()?;
-    let columns = column_order(&rows);
+    let columns = column_order(config, &rows);
     Ok(HTML(format!(
         "{HEADER}{}{}{FOOTER}",
         render_column_headers(&columns),
     Ok(HTML(format!(
         "{HEADER}{}{}{FOOTER}",
         render_column_headers(&columns),
@@ -552,6 +581,37 @@ mod tests {
         assert_eq!(r.entries.len(), 0);
     }
 
         assert_eq!(r.entries.len(), 0);
     }
 
+    #[test]
+    fn test_render_leftovers() {
+        assert_eq!(
+            render_all_leftovers(&Row {
+                label: "nope".to_owned(),
+                entries: HashMap::from([("foo".to_owned(), vec![None])]),
+            }),
+            HTML::from("foo")
+        );
+        assert_eq!(
+            render_all_leftovers(&Row {
+                label: "nope".to_owned(),
+                entries: HashMap::from([
+                    ("foo".to_owned(), vec![None]),
+                    ("bar".to_owned(), vec![None])
+                ]),
+            }),
+            HTML::from("bar, foo")
+        );
+        assert_eq!(
+            render_all_leftovers(&Row {
+                label: "nope".to_owned(),
+                entries: HashMap::from([
+                    ("foo".to_owned(), vec![None]),
+                    ("bar".to_owned(), vec![None, None])
+                ]),
+            }),
+            HTML::from("bar: 2, foo")
+        );
+    }
+
     #[test]
     fn test_render_row() {
         assert_eq!(
     #[test]
     fn test_render_row() {
         assert_eq!(
@@ -563,7 +623,7 @@ mod tests {
                 }
             ),
             HTML::from(
                 }
             ),
             HTML::from(
-                r#"<tr><th id="nope">nope</th><td class="" onmouseover="h2('nope','foo')" onmouseout="ch2('nope','foo')"></td></tr>
+                r#"<tr><th id="nope">nope</th><td class="" onmouseover="h2('nope','foo')" onmouseout="ch2('nope','foo')"></td><td onmouseover="highlight('nope')" onmouseout="clear_highlight('nope')">bar</td></tr>
 "#
             )
         );
 "#
             )
         );