diff --git a/Cargo.toml b/Cargo.toml index 0dc7f09e..603491fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = ['libs/*', 'snippetter'] ansi_term = "0.12.1" approx = "0.5" assert-impl = "0.1.3" +indoc = "2.0.4" itertools = "0.10" num = "0.4.1" ordered-float = "3.4" diff --git a/libs/lg/Cargo.toml b/libs/lg/Cargo.toml index 56a86b7e..cac7078d 100644 --- a/libs/lg/Cargo.toml +++ b/libs/lg/Cargo.toml @@ -9,4 +9,4 @@ edition = "2018" [dependencies] [dev-dependencies] -test-case = { workspace = true } +indoc = { workspace = true } diff --git a/libs/lg/src/lib.rs b/libs/lg/src/lib.rs index 68562813..1c12f826 100644 --- a/libs/lg/src/lib.rs +++ b/libs/lg/src/lib.rs @@ -1,15 +1,31 @@ //! Provides a macro [`lg`] and formatting utils. +//! +//! # Note +//! +//! `✓` and `✗` is assumed to be half-width characters. use std::borrow::Borrow; use std::fmt; -use std::marker::PhantomData; +use std::iter::once; +/// Print the values with the line number. +/// +/// # Examples +/// +/// ```rust +/// # use lg::*; +/// let x = 42; +/// let y = 43; +/// lg!(x); +/// lg!(x, y); +/// lg!(42, x, 43, y); +/// ``` #[macro_export] macro_rules! lg { (@contents $head:expr $(, $tail:expr)*) => {{ - $crate::__lg_variable!($head); + $crate::__lg_internal!($head); $( eprint!(","); - $crate::__lg_variable!($tail); + $crate::__lg_internal!($tail); )* eprintln!(); }}; @@ -21,7 +37,7 @@ macro_rules! lg { #[doc(hidden)] #[macro_export] -macro_rules! __lg_variable { +macro_rules! __lg_internal { ($value:expr) => {{ match $value { head => { @@ -35,16 +51,93 @@ macro_rules! __lg_variable { }}; } +/// Print many 1D arrays side-by-side with the line number. +/// +/// # Examples +/// ```rust +/// # use lg::*; +/// let a = [1, 2, 3]; +/// let b = [4, 5, 6]; +/// let c = [7, 8, 9]; +/// rows! { +/// "id", // the name of the index +/// @"a" => a, +/// b, +/// @"c" => c, +/// } +/// ``` +#[macro_export] +macro_rules! rows { + { + $index_label:literal, + $(@offset $offset:expr,)? + $(@verticalbar $verticalbar:expr,)* + $($(@$label:literal =>)? $values:expr),* $(,)? + } => {{ + #![allow(unused_assignments)] + let mut rows = $crate::Rows::default(); + rows.line_number(line!()); + $(rows.offset($offset);)? + $(rows.verticalbar($verticalbar);)* + rows.index_label($index_label); + $({ + let mut label = stringify!($values).to_string(); + if label.starts_with("&") { + label = label[1..].to_string(); + } + $({ + let label_: &'static str = $label; + label = label_.to_string(); + })? + rows.row(label, $values); + })* + eprintln!("{}", rows.to_string_table()); + }}; +} + +/// Print the 2D array with the line number. +/// +/// # Examples +/// ```rust +/// # use lg::*; +/// let a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; +/// table! { +/// @"a" => a, +/// } +/// table! { +/// a, +/// } +/// ``` #[macro_export] macro_rules! table { - ($value:expr) => {{ - $crate::Table::new($value).title(stringify!($value)) + { + $(@$name:literal => )? $values:expr $(,)? + } => {{ + #![allow(unused_assignments)] + let mut name = stringify!($values).to_string(); + if name.starts_with("&") { + name = name[1..].to_string(); + } + $({ + let name_: &'static str = $name; + name = name_.to_string(); + })? + let mut rows = $crate::Rows::default(); + rows.line_number(line!()); + rows.table_name(name); + #[allow(array_into_iter)] + for (i, row) in $values.into_iter().enumerate() { + rows.row(i.to_string(), row); + } + eprintln!("{}", rows.to_string_table()); }}; } #[doc(hidden)] pub fn __quiet(s: impl AsRef) -> String { s.as_ref() + .replace("340282366920938463463374607431768211455", "*") // u128 + .replace("170141183460469231731687303715884105727", "*") // i128 .replace("18446744073709551615", "*") // u64 .replace("9223372036854775807", "*") // i64 .replace("-9223372036854775808", "*") // i64 @@ -53,122 +146,215 @@ pub fn __quiet(s: impl AsRef) -> String { .replace("-2147483648", "*") // i32 .replace("None", "*") .replace("Some", "") + .replace("true", "✓") + .replace("false", "✗") + .replace(['"', '\''], "") } -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Table { - __marker: PhantomData<(T, Row)>, - title: String, - storage: Storage, - index_width: usize, - column_width: usize, - heading_newlines: usize, +#[doc(hidden)] +#[derive(Default)] +pub struct Rows { + line_number: String, + index_label: String, + offset: usize, + verticalbars: Vec, + table_name: String, + rows: Vec, } +impl Rows { + pub fn line_number(&mut self, line_number: u32) -> &mut Self { + self.line_number = format!("{}", line_number); + self + } -/// Format a two dimensional container in a table style. -/// -/// -/// # Example -/// -/// ``` -/// # use lg::{table, Table}; -/// let a = vec![vec![0, 1, 2], vec![3, 4, 5]]; -/// -/// eprintln!( -/// "{}", -/// table(&a) // Either a or &a is ok. -/// .heading_newlines(1) // Default: 1 -/// .index_width(1) // Default: 2 -/// .column_width(2), // Default: 3 -/// ); -/// ``` -/// -/// -/// # Automatic quieting -/// -/// ``` -/// # use lg::{table, Table}; -/// eprintln!("{}", table(&[[0, 2147483647, 2], [3, 4, 5],]),); -/// ``` -pub fn table, Storage: AsRef<[Row]>>( - storage: Storage, -) -> Table { - Table::new(storage) -} -impl Table -where - T: Clone + fmt::Debug, - Row: AsRef<[T]>, - Storage: AsRef<[Row]>, -{ - pub fn new(storage: Storage) -> Self { - Self { - __marker: PhantomData, - title: String::new(), - storage, - column_width: 3, - index_width: 2, - heading_newlines: 1, - } + pub fn index_label(&mut self, index_label: impl Into) -> &mut Self { + self.index_label = index_label.into(); + self } - pub fn title(&mut self, title: impl Into) -> &mut Self { - self.title = title.into(); + pub fn offset(&mut self, offset: usize) -> &mut Self { + self.offset = offset; self } - pub fn index_width(&mut self, index_width: usize) -> &mut Self { - self.index_width = index_width; + pub fn verticalbar(&mut self, verticalbar: impl IntoIterator) -> &mut Self { + self.verticalbars.extend(verticalbar); self } - pub fn column_width(&mut self, column_width: usize) -> &mut Self { - self.column_width = column_width; + pub fn table_name(&mut self, table_name: impl Into) -> &mut Self { + self.table_name = table_name.into(); self } - pub fn heading_newlines(&mut self, heading_newlines: usize) -> &mut Self { - self.heading_newlines = heading_newlines; + pub fn row( + &mut self, + label: impl Into, + values: impl IntoIterator, + ) -> &mut Self { + self.rows.push(Row { + label: label.into(), + values: values + .into_iter() + .map(|value| __quiet(format!("{:?}", value))) + .collect(), + }); self } + + pub fn to_string_table(self) -> StringTable { + let Self { + line_number, + index_label, + offset, + verticalbars, + table_name, + rows, + } = self; + let w = rows + .iter() + .map(|row| row.values.len()) + .max() + .unwrap_or_default(); + let mut verticalbar_count = vec![0; w + 1]; + for &v in &verticalbars { + if (offset..=offset + w).contains(&v) { + verticalbar_count[v - offset] += 1; + } + } + + StringTable { + head: StringRow { + label: format!( + "{line_number}❯ {table_name}{index_label}", + index_label = if index_label.is_empty() { + String::new() + } else { + format!("[{}]", index_label) + } + ), + values: (offset..offset + w) + .map(|index| index.to_string()) + .collect(), + }, + body: rows + .iter() + .map(|row| StringRow { + label: row.label.clone(), + values: row.values.clone(), + }) + .collect(), + verticalbar_count, + } + } } -impl fmt::Display for Table -where - T: Clone + fmt::Debug, - Row: AsRef<[T]>, - Storage: AsRef<[Row]>, -{ + +struct Row { + label: String, + values: Vec, +} + +#[doc(hidden)] +pub struct StringTable { + head: StringRow, + body: Vec, + verticalbar_count: Vec, +} +impl fmt::Display for StringTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { - __marker: _, - ref title, - ref storage, - index_width, - column_width, - heading_newlines, - } = *self; - for _ in 0..heading_newlines { + head, + body, + verticalbar_count, + } = self; + let w = body + .iter() + .map(|row| row.values.len()) + .max() + .unwrap_or_default(); + let label_width = once(head.label.chars().count()) + .chain(body.iter().map(|row| row.label.chars().count())) + .max() + .unwrap(); + let value_width = (0..w) + .map(|j| { + once(j.to_string().len()) + .chain( + body.iter() + .map(|row| row.values.get(j).map(|s| s.chars().count()).unwrap_or(0)), + ) + .max() + .unwrap() + }) + .collect::>(); + + // Heading + gray(f)?; + write!( + f, + "{}", + head.to_string(label_width, &value_width, verticalbar_count, true) + )?; + resetln(f)?; + + // Body + for row in body { + write!( + f, + "{}", + row.to_string(label_width, &value_width, verticalbar_count, false) + )?; writeln!(f)?; } - writeln!(f, "{}❯ {}", line!(), title)?; - let ncols = storage.as_ref()[0].as_ref().len(); - write!(f, "\x1b[48;2;127;127;127;37m")?; - write!(f, "{}|", " ".repeat(index_width))?; - for j in 0..ncols { - write!(f, "{j:column_width$}")?; - } - writeln!(f, "\x1b[0m")?; - for (i, row) in storage.as_ref().iter().enumerate() { - write!(f, "{:index_width$}|", i, index_width = index_width)?; - for value in row.as_ref() { - write!(f, "{:>column_width$}", __quiet(format!("{:?}", value)),)?; + Ok(()) + } +} + +struct StringRow { + label: String, + values: Vec, +} +impl StringRow { + fn to_string( + &self, + label_width: usize, + value_width: &[usize], + varticalbars_count: &[usize], + label_align_left: bool, + ) -> String { + let Self { label, values } = self; + let w = value_width.len(); + let mut s = String::new(); + s.push_str(&if label_align_left { + format!("{label: { + s.push_str(&format!(" {value:>value_width$}",)); + } + None => { + s.push_str(" ".repeat(value_width + 1).as_str()); + } } - writeln!(f)?; } - Ok(()) + s } } +const GRAY: &str = "\x1b[48;2;127;127;127;37m"; +const RESET: &str = "\x1b[0m"; + +fn gray(f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{GRAY}") } +fn resetln(f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{RESET}") } + /// Format a iterator of [`bool`]s. pub fn bools(iter: I) -> String where @@ -186,7 +372,9 @@ where #[cfg(test)] mod test { use super::*; + use indoc::indoc; use std::collections::BTreeSet; + use std::collections::HashMap; use std::iter::empty; #[test] @@ -206,24 +394,200 @@ mod test { assert_eq!(__quiet(std::u64::MAX.to_string()).as_str(), "*"); assert_eq!(__quiet(std::i64::MAX.to_string()).as_str(), "*"); assert_eq!(__quiet(std::i64::MIN.to_string()).as_str(), "*"); + assert_eq!(__quiet(std::u128::MAX.to_string()).as_str(), "*"); + assert_eq!(__quiet(std::i128::MAX.to_string()).as_str(), "*"); assert_eq!(__quiet(format!("{:?}", Some(42))).as_str(), "(42)"); assert_eq!(__quiet(format!("{:?}", None::)).as_str(), "*"); } + macro_rules! assert_fmt_eq { + ($left:expr, $right:expr $(,)?) => { + match (&$left, &$right) { + (left_val, right_val) => { + if *left_val != *right_val { + panic!( + "assertion failed: `(left == right)`\n\nleft:\n{}\nright:\n{}\n", + *left_val, *right_val + ) + } + } + } + }; + } + + fn string_table>(table: &[T]) -> StringTable { + let w = table + .iter() + .map(|row| row.borrow().len()) + .max() + .unwrap_or_default(); + StringTable { + head: StringRow { + label: table[0].borrow()[0].to_string(), + values: table[0].borrow()[1..] + .iter() + .map(|s| s.to_string()) + .collect(), + }, + body: table[1..] + .iter() + .map(|row| StringRow { + label: row.borrow()[0].to_string(), + values: row.borrow()[1..].iter().map(|s| s.to_string()).collect(), + }) + .collect(), + verticalbar_count: vec![0; w + 1], + } + } + + fn format_table>(table: &[T]) -> String { + format!("{}", string_table(table)) + } + + #[test] + fn test_string_table_simple() { + #[rustfmt::skip] + let result = format_table(&[ + ["head", "aa", "bb", "cc"], + ["row1", "11", "22", "33"], + ["row2", "44", "55", "66"], + ]); + let expected = indoc! {" + \x1b[48;2;127;127;127;37mhead | aa bb cc\x1b[0m + row1 | 11 22 33 + row2 | 44 55 66 + "}; + assert_fmt_eq!(result, expected); + } + + #[test] + fn test_string_table_with_short_value_in_first() { + #[rustfmt::skip] + let result = format_table(&[ + ["head", "a", "bb", "cc"], + ["row1", "1", "22", "33"], + ["row2", "4", "55", "66"], + ]); + let expected = indoc! {" + \x1b[48;2;127;127;127;37mhead | a bb cc\x1b[0m + row1 | 1 22 33 + row2 | 4 55 66 + "}; + assert_fmt_eq!(result, expected); + } + + #[test] + fn test_string_table_with_short_value_in_middle() { + #[rustfmt::skip] + let result = format_table(&[ + ["head", "aa", "b", "cc"], + ["row1", "11", "2", "33"], + ["row2", "44", "5", "66"], + ]); + let expected = indoc! {" + \x1b[48;2;127;127;127;37mhead | aa b cc\x1b[0m + row1 | 11 2 33 + row2 | 44 5 66 + "}; + assert_fmt_eq!(result, expected); + } + + #[test] + fn test_string_table_with_short_row_in_head() { + #[rustfmt::skip] + let result = format_table(&[ + vec!["head", "aa", "bb"], + vec!["row1", "11", "22", "33"], + vec!["row2", "44", "55", "66"], + ]); + let expected = indoc! {" + \x1b[48;2;127;127;127;37mhead | aa bb \x1b[0m + row1 | 11 22 33 + row2 | 44 55 66 + "}; + assert_fmt_eq!(result, expected); + } + + #[test] + fn test_string_table_with_short_row_in_middle() { + #[rustfmt::skip] + let result = format_table(&[ + vec!["head", "aa", "bb", "cc"], + vec!["row1", "11", "22", "33"], + vec!["row2", "44", "55"], + vec!["row3", "77", "88", "99"], + ]); + let expected = indoc! {" + \x1b[48;2;127;127;127;37mhead | aa bb cc\x1b[0m + row1 | 11 22 33 + row2 | 44 55 + row3 | 77 88 99 + "}; + assert_fmt_eq!(result, expected); + } + + #[test] + fn test_invoke_rows() { + rows! { + "i", + @"a" => [1, 2, 3], + @"b" => [4, 5, 6], + @"c" => [7, 8, 9], + } + } + + #[test] + fn test_invoke_rows_with_index() { + rows! { + "i", + @offset 10, + @"a" => [1, 2, 3], + @"b" => [4, 5, 6], + @"c" => [7, 8, 9], + } + } + + #[test] + fn test_invoke_rows_with_various_types() { + rows! { + "i", + @"a" => [1, 2, 3], + @"b" => BTreeSet::from([4, 5, 6]), + @"c" => vec![7, 8, 9], + @"d" => HashMap::from([("x", 10), ("y", 11), ("z", 12)]), + } + } + + #[test] + fn test_invoke_rows_without_name() { + rows! { + "index", + [1, 2, 3], + @"b" => [4, 5, 6], + [7, 8, 9], + } + } + + #[test] + fn test_invoke_table() { + table! { + @"a" => [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] + } + } + #[test] - fn test_table() { - fn format(a: [[i32; W]; H]) -> String { - format!("{}", table!(a)) + fn test_invoke_table_without_name() { + table! { + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] } - assert_eq!( - format([[0, std::i32::MIN, 2], [3, 4, 5]]), - r" -153❯ a - | 0 1 2 - 0| 0 * 2 - 1| 3 4 5 -" - ); } #[test]