#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Align {
Left,
Right,
}
pub struct Table {
headers: Vec<String>,
aligns: Vec<Align>,
rows: Vec<Vec<String>>,
}
impl Table {
pub fn new(headers: &[&str], aligns: &[Align]) -> Self {
debug_assert_eq!(
headers.len(),
aligns.len(),
"headers and aligns must have the same length"
);
Table {
headers: headers.iter().map(|s| s.to_string()).collect(),
aligns: aligns.to_vec(),
rows: Vec::new(),
}
}
pub fn row<I>(&mut self, cells: I)
where
I: IntoIterator<Item = String>,
{
let row: Vec<String> = cells.into_iter().collect();
debug_assert_eq!(
row.len(),
self.headers.len(),
"row cell count must match header count"
);
self.rows.push(row);
}
pub fn render(&self, out: &mut String) {
let mut widths: Vec<usize> = self.headers.iter().map(|h| display_width(h)).collect();
for row in &self.rows {
for (c, cell) in row.iter().enumerate() {
let w = display_width(cell);
if w > widths[c] {
widths[c] = w;
}
}
}
for (c, w) in widths.iter_mut().enumerate() {
let min = if self.aligns[c] == Align::Right { 2 } else { 1 };
if *w < min {
*w = min;
}
}
self.render_line(&self.headers, &widths, out);
self.render_delim(&widths, out);
for row in &self.rows {
self.render_line(row, &widths, out);
}
}
fn render_line(&self, cells: &[String], widths: &[usize], out: &mut String) {
out.push('|');
for (c, cell) in cells.iter().enumerate() {
let pad = widths[c].saturating_sub(display_width(cell));
out.push(' ');
match self.aligns[c] {
Align::Left => {
out.push_str(cell);
for _ in 0..pad {
out.push(' ');
}
}
Align::Right => {
for _ in 0..pad {
out.push(' ');
}
out.push_str(cell);
}
}
out.push(' ');
out.push('|');
}
out.push('\n');
}
fn render_delim(&self, widths: &[usize], out: &mut String) {
out.push('|');
for (c, w) in widths.iter().enumerate() {
out.push(' ');
match self.aligns[c] {
Align::Left => {
for _ in 0..*w {
out.push('-');
}
}
Align::Right => {
for _ in 0..w.saturating_sub(1) {
out.push('-');
}
out.push(':');
}
}
out.push(' ');
out.push('|');
}
out.push('\n');
}
}
fn display_width(s: &str) -> usize {
s.chars().count()
}
const EIGHTHS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
const SPARK: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
pub fn bar(value: u64, max: u64, width: usize) -> String {
if width == 0 {
return String::new();
}
if max == 0 || value == 0 {
return " ".repeat(width);
}
let v = value.min(max);
let mut total_eighths = (v as u128 * (width as u128) * 8) / max as u128;
if total_eighths == 0 {
total_eighths = 1;
}
let full = (total_eighths / 8) as usize;
let rem = (total_eighths % 8) as usize;
let mut s = String::with_capacity(width * 3);
for _ in 0..full.min(width) {
s.push(EIGHTHS[8]);
}
let mut cells = full.min(width);
if cells < width && rem > 0 {
s.push(EIGHTHS[rem]);
cells += 1;
}
for _ in cells..width {
s.push(' ');
}
s
}
#[allow(dead_code)]
pub fn sparkline(values: &[u64]) -> String {
if values.is_empty() {
return String::new();
}
let max = *values.iter().max().unwrap();
if max == 0 {
return SPARK[0].to_string().repeat(values.len());
}
let mut s = String::with_capacity(values.len() * 3);
for &v in values {
let mut idx = ((v as u128 * 7) / max as u128) as usize;
if v > 0 && idx == 0 {
idx = 1;
}
s.push(SPARK[idx.min(7)]);
}
s
}
pub fn tree_prefix(depth: usize, is_last: bool, ancestors_continue: &[bool]) -> String {
if depth == 0 {
return String::new();
}
let mut s = String::with_capacity(depth * 3);
for &cont in ancestors_continue.iter().take(depth.saturating_sub(1)) {
s.push_str(if cont { "│ " } else { " " });
}
s.push_str(if is_last { "└─ " } else { "├─ " });
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pads_columns_to_equal_width() {
let mut t = Table::new(
&["#", "Class", "Retained"],
&[Align::Right, Align::Left, Align::Right],
);
t.row(["1".into(), "java.lang.String".into(), "1.2 MB".into()]);
t.row(["10".into(), "int[]".into(), "999 B".into()]);
let mut out = String::new();
t.render(&mut out);
let lines: Vec<&str> = out.lines().collect();
let hlen = lines[0].len();
for l in &lines {
assert_eq!(l.len(), hlen, "line width mismatch: {l:?}");
}
assert_eq!(lines[0], "| # | Class | Retained |");
assert_eq!(lines[1], "| -: | ---------------- | -------: |");
}
#[test]
fn right_align_delimiter_has_colon() {
let mut t = Table::new(&["N"], &[Align::Right]);
t.row(["1".into()]);
let mut out = String::new();
t.render(&mut out);
let lines: Vec<&str> = out.lines().collect();
assert!(lines[1].ends_with(": |"), "delim: {:?}", lines[1]);
}
#[test]
fn left_align_delimiter_no_colon() {
let mut t = Table::new(&["Name"], &[Align::Left]);
t.row(["ab".into()]);
let mut out = String::new();
t.render(&mut out);
let lines: Vec<&str> = out.lines().collect();
assert!(lines[1].contains("----"), "delim: {:?}", lines[1]);
assert!(
!lines[1].contains(':'),
"delim should have no colon: {:?}",
lines[1]
);
}
#[test]
fn bar_is_fixed_display_width() {
for &(v, m) in &[(0u64, 10u64), (1, 10), (5, 10), (10, 10), (10, 0), (7, 3)] {
assert_eq!(display_width(&bar(v, m, 10)), 10, "bar({v},{m},10)");
}
}
#[test]
fn bar_full_and_empty_endpoints() {
assert_eq!(bar(0, 10, 4), " "); assert_eq!(bar(10, 10, 4), "████"); assert_eq!(bar(5, 0, 4), " "); assert_eq!(bar(20, 10, 4), "████"); }
#[test]
fn bar_partial_uses_eighths() {
assert_eq!(bar(1, 2, 1), "▌");
assert_eq!(bar(3, 10, 10), "███ ");
assert_eq!(bar(1, 4, 1), "▎");
}
#[test]
fn sparkline_scales_and_bounds() {
assert_eq!(sparkline(&[]), "");
assert_eq!(sparkline(&[0, 0, 0]), "▁▁▁"); let s = sparkline(&[1, 2, 4, 8]);
assert_eq!(s.chars().count(), 4);
assert!(s.ends_with('█'), "spark: {s:?}");
}
#[test]
fn bar_tiny_nonzero_floors_to_one_eighth() {
assert_eq!(bar(1, 1000, 4), "▏ ");
assert_eq!(bar(0, 1000, 4), " ");
}
#[test]
fn sparkline_tiny_nonzero_floors_above_baseline() {
let s = sparkline(&[0, 1, 1000]);
let cs: Vec<char> = s.chars().collect();
assert_eq!(cs[0], '▁', "zero stays baseline: {s:?}");
assert_eq!(cs[1], '▂', "tiny nonzero floors to level 1: {s:?}");
assert_eq!(cs[2], '█', "max is tallest: {s:?}");
}
#[test]
fn tree_prefix_shapes() {
assert_eq!(tree_prefix(0, false, &[]), "");
assert_eq!(tree_prefix(1, false, &[true]), "├─ ");
assert_eq!(tree_prefix(1, true, &[true]), "└─ ");
assert_eq!(tree_prefix(2, false, &[true, false]), "│ ├─ ");
assert_eq!(tree_prefix(2, true, &[false, true]), " └─ ");
}
}