use super::Squeeze;
use crate::interactive::plan_tui::{plan_in, Control, Plan, PlanView, Planner, Setting};
use crate::interactive::ui::Tui;
const BINS: usize = 48;
fn spark(values: &[usize], cutoff: Option<usize>) -> String {
let top = values.iter().copied().max().unwrap_or(0);
let scale = ((top + 1) as f64).ln().max(f64::MIN_POSITIVE);
let bin =
|v: usize| (((v + 1) as f64).ln() / scale * BINS as f64).min((BINS - 1) as f64) as usize;
let mut counts = [0usize; BINS];
for &v in values {
counts[bin(v)] += 1;
}
let most = counts.iter().copied().max().unwrap_or(0).max(1);
const BARS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let edge = cutoff.map(|c| (0..BINS).find(|&b| bin(c) <= b).unwrap_or(BINS));
let mut line = String::new();
for (b, &n) in counts.iter().enumerate() {
if edge == Some(b) {
line.push('┃');
}
let h = if n == 0 {
0
} else {
1 + (n * 7).div_ceil(most).min(7)
};
line.push(BARS[h.min(8)]);
}
format!("0 {line} {top}")
}
fn plan_of(file: &str, rows: &[usize], columns: &[usize], squeeze: Squeeze) -> Plan {
let axis = |name: &str, values: &[usize], cutoff: usize| -> Vec<String> {
let dropped = match squeeze.on {
true => values.iter().filter(|&&v| v < cutoff).count(),
false => 0,
};
vec![
name.into(),
values.len().to_string(),
(values.len() - dropped).to_string(),
dropped.to_string(),
match squeeze.on {
true => format!("< {cutoff} non-zeros"),
false => "off".into(),
},
]
};
let cut = |c: usize| squeeze.on.then_some(c);
let entries: usize = columns.iter().sum();
let mut notes = vec![
format!("{file}: {} entries", entries),
format!("rows {}", spark(rows, cut(squeeze.rows))),
format!("columns {}", spark(columns, cut(squeeze.columns))),
];
if squeeze.on {
notes.push(
"counts of each axis are before the other is cut, so a little more may go".into(),
);
}
Plan {
columns: ["", "total", "kept", "dropped", "dropped when"]
.map(String::from)
.to_vec(),
rows: vec![
axis("rows", rows, squeeze.rows),
axis("columns", columns, squeeze.columns),
],
notes,
blocked: None,
}
}
fn cutoff(s: &Setting, what: &str) -> anyhow::Result<usize> {
match s.number() {
None => Ok(0),
Some(v) if v.fract() == 0.0 && v >= 0.0 => Ok(v as usize),
Some(v) => anyhow::bail!("{what} is a whole number of non-zeros; not {v}"),
}
}
fn squeeze_of(s: &[Setting], base: Squeeze) -> anyhow::Result<Squeeze> {
Ok(Squeeze {
on: s[0].flag(),
rows: cutoff(&s[1], "the row cutoff (r)")?,
columns: cutoff(&s[2], "the column cutoff (c)")?,
..base
})
}
pub(super) fn preview(
tui: &mut Tui<'_>,
command: &str,
file: &str,
rows: &[usize],
columns: &[usize],
squeeze: Squeeze,
) -> anyhow::Result<Option<Squeeze>> {
let controls = vec![
Control::flag('s', "squeeze", squeeze.on),
Control::number('r', "row cutoff", Some(squeeze.rows as f64)),
Control::number('c', "column cutoff", Some(squeeze.columns as f64)),
];
let planner: Planner =
Box::new(move |s: &[Setting]| Ok(plan_of(file, rows, columns, squeeze_of(s, squeeze)?)));
let title = format!("{command} · {file}");
let view = PlanView::new("import", &title, "keep", controls, planner).cancel_label("discard");
match plan_in(tui, view)? {
Some(s) => squeeze_of(&s, squeeze).map(Some),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn squeeze(on: bool, rows: usize, columns: usize) -> Squeeze {
Squeeze {
on,
rows,
columns,
block_size: None,
}
}
#[test]
fn the_plan_counts_what_the_cutoffs_drop() {
let rows = [0, 1, 2, 5, 50];
let columns = [3, 3, 10, 42];
let off = plan_of("x.zarr", &rows, &columns, squeeze(false, 3, 4));
assert_eq!(off.rows[0][1..4], ["5", "5", "0"].map(String::from));
assert_eq!(off.notes[0], "x.zarr: 58 entries");
let on = plan_of("x.zarr", &rows, &columns, squeeze(true, 3, 4));
assert_eq!(
on.rows[0][1..],
["5", "2", "3", "< 3 non-zeros"].map(String::from)
);
assert_eq!(on.rows[1][1..4], ["4", "2", "2"].map(String::from));
assert!(on.notes[1].contains('┃'), "{}", on.notes[1]);
assert!(!off.notes[1].contains('┃'));
}
#[test]
fn a_histogram_line_spans_zero_to_the_most() {
let line = spark(&[1, 1, 1, 100, 1000], None);
assert!(line.starts_with("0 ") && line.ends_with(" 1000"), "{line}");
assert_eq!(line.chars().count(), 2 + BINS + 5);
let marked = spark(&[1, 1, 1, 100, 1000], Some(50));
let at = marked.chars().position(|c| c == '┃').unwrap();
assert!(marked.chars().skip(at + 1).any(|c| c != ' '));
assert!(spark(&[], Some(3)).starts_with("0 "));
}
#[test]
fn settings_read_as_a_squeeze() {
let base = squeeze(false, 1, 1);
let n = |v: Option<f64>| Setting::Number(v);
let s = squeeze_of(&[Setting::Flag(true), n(Some(10.0)), n(None)], base).unwrap();
assert_eq!((s.on, s.rows, s.columns), (true, 10, 0));
assert!(squeeze_of(&[Setting::Flag(true), n(Some(1.5)), n(None)], base).is_err());
}
}