use super::*;
use crate::interactive::plan_tui::{plan_in, Control, Plan, PlanView, Planner, Setting};
fn whole(s: &Setting, what: &str, min: usize) -> anyhow::Result<Option<usize>> {
match s.number() {
None => Ok(None),
Some(v) if v.fract() == 0.0 && v >= min as f64 => Ok(Some(v as usize)),
Some(v) => anyhow::bail!("{what} is a whole number, at least {min}; not {v}"),
}
}
fn choice_of(s: &[Setting], base: Choice) -> anyhow::Result<Choice> {
let test_frac = s[0].number();
let folds = whole(&s[1], "folds (k)", 2)?;
anyhow::ensure!(
!(test_frac.is_some() && folds.is_some()),
"set the test fraction (t) or the folds (k), not both: clear one"
);
let seed = whole(&s[2], "the seed (s)", 0)?.unwrap_or(base.seed as usize) as u64;
let grid = match s.get(3) {
Some(g) => whole(g, "the grid (g)", 2)?.unwrap_or(base.grid),
None => base.grid,
};
Ok(Choice {
test_frac,
folds,
grid,
seed,
})
}
fn plan_of(args: &SplitArgs, names: &[Box<str>], choice: Choice) -> anyhow::Result<Plan> {
let groups = column_groups(args, names, choice.grid)?.len();
let splits = split_columns(args, names, choice)?;
let n = names.len().max(1) as f64;
let rows = splits
.iter()
.enumerate()
.map(|(fold, (train, test))| {
vec![
half_name(args, choice, fold, "{train,test}"),
train.len().to_string(),
test.len().to_string(),
format!("{:.1}%", 100.0 * test.len() as f64 / n),
]
})
.collect();
let unit = match (args.coord.as_deref(), args.groups.as_deref()) {
(Some(path), _) => format!(
"{groups} non-empty tiles of a {g}×{g} grid over {path}",
g = choice.grid
),
(_, Some(path)) => format!("{groups} groups labelled in {path}"),
_ => "one cell each".into(),
};
Ok(Plan {
columns: ["output", "train cells", "test cells", "test share"]
.map(String::from)
.to_vec(),
rows,
notes: vec![
format!(
"{} cells, shuffled and assigned in units of {unit}",
names.len()
),
format!("seed {}", choice.seed),
],
blocked: None,
})
}
pub(super) fn choose(
tui: &mut Tui<'_>,
args: &SplitArgs,
names: &[Box<str>],
choice: Choice,
) -> anyhow::Result<Option<Choice>> {
let mut controls = vec![
Control::number('t', "test fraction", choice.test_frac),
Control::number('k', "folds", choice.folds.map(|k| k as f64)),
Control::number('s', "seed", Some(choice.seed as f64)),
];
if args.coord.is_some() {
controls.push(Control::number('g', "grid", Some(choice.grid as f64)));
}
let planner: Planner = Box::new(move |s: &[Setting]| {
let c = choice_of(s, choice)?;
plan_of(args, names, c)
});
let view = PlanView::new("split", &args.data_file, "split", controls, planner);
match plan_in(tui, view)? {
Some(s) => choice_of(&s, choice).map(Some),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Choice {
Choice {
test_frac: Some(0.2),
folds: None,
grid: 8,
seed: 42,
}
}
#[test]
fn settings_read_as_a_split() {
let n = |v: Option<f64>| Setting::Number(v);
let c = choice_of(&[n(None), n(Some(5.0)), n(Some(7.0))], base()).unwrap();
assert_eq!(
(c.test_frac, c.folds, c.seed, c.grid),
(None, Some(5), 7, 8)
);
let both = choice_of(&[n(Some(0.3)), n(Some(5.0)), n(Some(1.0))], base());
assert!(both.unwrap_err().to_string().contains("not both"));
assert!(choice_of(&[n(None), n(Some(1.0)), n(Some(1.0))], base()).is_err());
assert!(choice_of(&[n(Some(0.2)), n(None), n(Some(1.5))], base()).is_err());
let g = choice_of(&[n(Some(0.2)), n(None), n(Some(1.0)), n(Some(4.0))], base()).unwrap();
assert_eq!(g.grid, 4);
}
}