feat: add Axis::None for hidden dimensions with implicit aggregation
Categories on the None axis are excluded from the grid and cell keys. When evaluating cells, values across hidden dimensions are aggregated using a per-measure function (default SUM). Adds evaluate_aggregated to Model, none_cats to GridLayout, and 'n' shortcut in TileSelect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,10 +1,12 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::category::{Category, CategoryId};
|
use super::category::{Category, CategoryId};
|
||||||
use super::cell::{CellKey, CellValue, DataStore};
|
use super::cell::{CellKey, CellValue, DataStore};
|
||||||
use crate::formula::Formula;
|
use crate::formula::{AggFunc, Formula};
|
||||||
use crate::view::View;
|
use crate::view::View;
|
||||||
|
|
||||||
const MAX_CATEGORIES: usize = 12;
|
const MAX_CATEGORIES: usize = 12;
|
||||||
@ -18,6 +20,10 @@ pub struct Model {
|
|||||||
pub views: IndexMap<String, View>,
|
pub views: IndexMap<String, View>,
|
||||||
pub active_view: String,
|
pub active_view: String,
|
||||||
next_category_id: CategoryId,
|
next_category_id: CategoryId,
|
||||||
|
/// Per-measure aggregation function (measure item name → agg func).
|
||||||
|
/// Used when collapsing categories on `Axis::None`. Defaults to SUM.
|
||||||
|
#[serde(default)]
|
||||||
|
pub measure_agg: HashMap<String, AggFunc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
@ -34,6 +40,7 @@ impl Model {
|
|||||||
views,
|
views,
|
||||||
active_view: "Default".to_string(),
|
active_view: "Default".to_string(),
|
||||||
next_category_id: 0,
|
next_category_id: 0,
|
||||||
|
measure_agg: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +179,67 @@ impl Model {
|
|||||||
self.evaluate(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
|
self.evaluate(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate a cell, aggregating over any hidden (None-axis) categories.
|
||||||
|
/// When `none_cats` is empty, delegates to `evaluate`.
|
||||||
|
/// Otherwise, uses `matching_cells` with the partial key and aggregates
|
||||||
|
/// using the measure's agg function (default SUM).
|
||||||
|
pub fn evaluate_aggregated(
|
||||||
|
&self,
|
||||||
|
key: &CellKey,
|
||||||
|
none_cats: &[String],
|
||||||
|
) -> Option<CellValue> {
|
||||||
|
if none_cats.is_empty() {
|
||||||
|
return self.evaluate(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check formulas first — they handle their own aggregation
|
||||||
|
for formula in &self.formulas {
|
||||||
|
if let Some(item_val) = key.get(&formula.target_category) {
|
||||||
|
if item_val == formula.target {
|
||||||
|
return self.eval_formula(formula, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate raw data across all None-axis categories
|
||||||
|
let values: Vec<f64> = self
|
||||||
|
.data
|
||||||
|
.matching_cells(&key.0)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_, v)| v.as_f64())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if values.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine agg func from measure_agg map, defaulting to SUM
|
||||||
|
let agg = key
|
||||||
|
.get("Measure")
|
||||||
|
.and_then(|m| self.measure_agg.get(m))
|
||||||
|
.unwrap_or(&AggFunc::Sum);
|
||||||
|
|
||||||
|
let result = match agg {
|
||||||
|
AggFunc::Sum => values.iter().sum(),
|
||||||
|
AggFunc::Avg => values.iter().sum::<f64>() / values.len() as f64,
|
||||||
|
AggFunc::Min => values.iter().cloned().reduce(f64::min)?,
|
||||||
|
AggFunc::Max => values.iter().cloned().reduce(f64::max)?,
|
||||||
|
AggFunc::Count => values.len() as f64,
|
||||||
|
};
|
||||||
|
Some(CellValue::Number(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate aggregated as f64, returning 0.0 for empty cells.
|
||||||
|
pub fn evaluate_aggregated_f64(
|
||||||
|
&self,
|
||||||
|
key: &CellKey,
|
||||||
|
none_cats: &[String],
|
||||||
|
) -> f64 {
|
||||||
|
self.evaluate_aggregated(key, none_cats)
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
fn eval_formula(&self, formula: &Formula, context: &CellKey) -> Option<CellValue> {
|
fn eval_formula(&self, formula: &Formula, context: &CellKey) -> Option<CellValue> {
|
||||||
use crate::formula::{AggFunc, Expr};
|
use crate::formula::{AggFunc, Expr};
|
||||||
|
|
||||||
@ -490,6 +558,82 @@ mod model_tests {
|
|||||||
m.set_cell(k.clone(), CellValue::Number(77.0));
|
m.set_cell(k.clone(), CellValue::Number(77.0));
|
||||||
assert_eq!(m.get_cell(&k), Some(&CellValue::Number(77.0)));
|
assert_eq!(m.get_cell(&k), Some(&CellValue::Number(77.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluate_aggregated_sums_over_hidden_dimension() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Payee").unwrap();
|
||||||
|
m.add_category("Date").unwrap();
|
||||||
|
m.add_category("Measure").unwrap();
|
||||||
|
m.category_mut("Payee").unwrap().add_item("Acme");
|
||||||
|
m.category_mut("Date").unwrap().add_item("Jan-01");
|
||||||
|
m.category_mut("Date").unwrap().add_item("Jan-02");
|
||||||
|
m.category_mut("Measure").unwrap().add_item("Amount");
|
||||||
|
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Payee", "Acme"), ("Date", "Jan-01"), ("Measure", "Amount")]),
|
||||||
|
CellValue::Number(100.0),
|
||||||
|
);
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Payee", "Acme"), ("Date", "Jan-02"), ("Measure", "Amount")]),
|
||||||
|
CellValue::Number(50.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Without hidden dims, returns None for partial key
|
||||||
|
let partial_key = coord(&[("Payee", "Acme"), ("Measure", "Amount")]);
|
||||||
|
assert_eq!(m.evaluate(&partial_key), None);
|
||||||
|
|
||||||
|
// With Date as hidden dimension, aggregates to SUM
|
||||||
|
let none_cats = vec!["Date".to_string()];
|
||||||
|
let result = m.evaluate_aggregated(&partial_key, &none_cats);
|
||||||
|
assert_eq!(result, Some(CellValue::Number(150.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluate_aggregated_no_hidden_delegates_to_evaluate() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Region").unwrap();
|
||||||
|
m.category_mut("Region").unwrap().add_item("East");
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Region", "East")]),
|
||||||
|
CellValue::Number(42.0),
|
||||||
|
);
|
||||||
|
let key = coord(&[("Region", "East")]);
|
||||||
|
assert_eq!(
|
||||||
|
m.evaluate_aggregated(&key, &[]),
|
||||||
|
Some(CellValue::Number(42.0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluate_aggregated_respects_measure_agg() {
|
||||||
|
use crate::formula::AggFunc;
|
||||||
|
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Payee").unwrap();
|
||||||
|
m.add_category("Date").unwrap();
|
||||||
|
m.add_category("Measure").unwrap();
|
||||||
|
m.category_mut("Payee").unwrap().add_item("Acme");
|
||||||
|
m.category_mut("Date").unwrap().add_item("D1");
|
||||||
|
m.category_mut("Date").unwrap().add_item("D2");
|
||||||
|
m.category_mut("Measure").unwrap().add_item("Price");
|
||||||
|
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Payee", "Acme"), ("Date", "D1"), ("Measure", "Price")]),
|
||||||
|
CellValue::Number(10.0),
|
||||||
|
);
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Payee", "Acme"), ("Date", "D2"), ("Measure", "Price")]),
|
||||||
|
CellValue::Number(30.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
m.measure_agg.insert("Price".to_string(), AggFunc::Avg);
|
||||||
|
|
||||||
|
let key = coord(&[("Payee", "Acme"), ("Measure", "Price")]);
|
||||||
|
let none_cats = vec!["Date".to_string()];
|
||||||
|
let result = m.evaluate_aggregated(&key, &none_cats);
|
||||||
|
assert_eq!(result, Some(CellValue::Number(20.0))); // avg(10, 30) = 20
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -117,6 +117,7 @@ pub fn format_md(model: &Model) -> String {
|
|||||||
Some(sel) => writeln!(out, "{}: page, {}", cat, sel).unwrap(),
|
Some(sel) => writeln!(out, "{}: page, {}", cat, sel).unwrap(),
|
||||||
None => writeln!(out, "{}: page", cat).unwrap(),
|
None => writeln!(out, "{}: page", cat).unwrap(),
|
||||||
},
|
},
|
||||||
|
Axis::None => writeln!(out, "{}: none", cat).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !view.number_format.is_empty() {
|
if !view.number_format.is_empty() {
|
||||||
@ -315,6 +316,7 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
|||||||
let axis = match rest {
|
let axis = match rest {
|
||||||
"row" => Axis::Row,
|
"row" => Axis::Row,
|
||||||
"column" => Axis::Column,
|
"column" => Axis::Column,
|
||||||
|
"none" => Axis::None,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
view.axes.push((cat.to_string(), axis));
|
view.axes.push((cat.to_string(), axis));
|
||||||
|
|||||||
@ -402,7 +402,8 @@ impl App {
|
|||||||
// yy = yank current cell
|
// yy = yank current cell
|
||||||
('y', KeyCode::Char('y')) => {
|
('y', KeyCode::Char('y')) => {
|
||||||
if let Some(key) = self.selected_cell_key() {
|
if let Some(key) = self.selected_cell_key() {
|
||||||
self.yanked = self.model.evaluate(&key);
|
let layout = GridLayout::new(&self.model, self.model.active_view());
|
||||||
|
self.yanked = self.model.evaluate_aggregated(&key, &layout.none_cats);
|
||||||
self.status_msg = "Yanked".to_string();
|
self.status_msg = "Yanked".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1111,6 +1112,19 @@ impl App {
|
|||||||
}
|
}
|
||||||
self.mode = AppMode::Normal;
|
self.mode = AppMode::Normal;
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('n') => {
|
||||||
|
if let Some(name) = cat_names.get(cat_idx) {
|
||||||
|
command::dispatch(
|
||||||
|
&mut self.model,
|
||||||
|
&Command::SetAxis {
|
||||||
|
category: name.clone(),
|
||||||
|
axis: Axis::None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
self.mode = AppMode::Normal;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -1386,7 +1400,7 @@ impl App {
|
|||||||
Some(k) => k,
|
Some(k) => k,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
let s = match self.model.evaluate(&key) {
|
let s = match self.model.evaluate_aggregated(&key, &layout.none_cats) {
|
||||||
Some(CellValue::Number(n)) => format!("{n}"),
|
Some(CellValue::Number(n)) => format!("{n}"),
|
||||||
Some(CellValue::Text(t)) => t,
|
Some(CellValue::Text(t)) => t,
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
@ -1608,7 +1622,7 @@ impl App {
|
|||||||
AppMode::CategoryAdd { .. } => "Enter:add & continue Tab:same Esc:done — type a category name",
|
AppMode::CategoryAdd { .. } => "Enter:add & continue Tab:same Esc:done — type a category name",
|
||||||
AppMode::ItemAdd { .. } => "Enter:add & continue Tab:same Esc:done — type an item name",
|
AppMode::ItemAdd { .. } => "Enter:add & continue Tab:same Esc:done — type an item name",
|
||||||
AppMode::ViewPanel => "jk:nav Enter:switch n:new d:delete Esc:back",
|
AppMode::ViewPanel => "jk:nav Enter:switch n:new d:delete Esc:back",
|
||||||
AppMode::TileSelect { .. } => "hl:select Enter:cycle r/c/p:set-axis Esc:back",
|
AppMode::TileSelect { .. } => "hl:select Enter:cycle r/c/p/n:set-axis Esc:back",
|
||||||
AppMode::CommandMode { .. } => ":q quit :w save :import :add-cat :formula :show-item :help",
|
AppMode::CommandMode { .. } => ":q quit :w save :import :add-cat :formula :show-item :help",
|
||||||
AppMode::ImportWizard => "Space:toggle c:cycle Enter:next Esc:cancel",
|
AppMode::ImportWizard => "Space:toggle c:cycle Enter:next Esc:cancel",
|
||||||
_ => "",
|
_ => "",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
|
|||||||
Axis::Row => ("Row ↕", Color::Green),
|
Axis::Row => ("Row ↕", Color::Green),
|
||||||
Axis::Column => ("Col ↔", Color::Blue),
|
Axis::Column => ("Col ↔", Color::Blue),
|
||||||
Axis::Page => ("Page ☰", Color::Magenta),
|
Axis::Page => ("Page ☰", Color::Magenta),
|
||||||
|
Axis::None => ("None ∅", Color::DarkGray),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -291,7 +291,7 @@ impl<'a> GridWidget<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let value = self.model.evaluate(&key);
|
let value = self.model.evaluate_aggregated(&key, &layout.none_cats);
|
||||||
|
|
||||||
let cell_str = format_value(value.as_ref(), fmt_comma, fmt_decimals);
|
let cell_str = format_value(value.as_ref(), fmt_comma, fmt_decimals);
|
||||||
let is_selected = ri == sel_row && ci == sel_col;
|
let is_selected = ri == sel_row && ci == sel_col;
|
||||||
@ -378,7 +378,7 @@ impl<'a> GridWidget<'a> {
|
|||||||
}
|
}
|
||||||
let total: f64 = (0..layout.row_count())
|
let total: f64 = (0..layout.row_count())
|
||||||
.filter_map(|ri| layout.cell_key(ri, ci))
|
.filter_map(|ri| layout.cell_key(ri, ci))
|
||||||
.map(|key| self.model.evaluate_f64(&key))
|
.map(|key| self.model.evaluate_aggregated_f64(&key, &layout.none_cats))
|
||||||
.sum();
|
.sum();
|
||||||
let total_str = format_f64(total, fmt_comma, fmt_decimals);
|
let total_str = format_f64(total, fmt_comma, fmt_decimals);
|
||||||
buf.set_string(
|
buf.set_string(
|
||||||
|
|||||||
@ -14,6 +14,7 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
|
|||||||
Axis::Row => ("↕", Color::Green),
|
Axis::Row => ("↕", Color::Green),
|
||||||
Axis::Column => ("↔", Color::Blue),
|
Axis::Column => ("↔", Color::Blue),
|
||||||
Axis::Page => ("☰", Color::Magenta),
|
Axis::Page => ("☰", Color::Magenta),
|
||||||
|
Axis::None => ("∅", Color::DarkGray),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub enum Axis {
|
|||||||
Row,
|
Row,
|
||||||
Column,
|
Column,
|
||||||
Page,
|
Page,
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Axis {
|
impl std::fmt::Display for Axis {
|
||||||
@ -14,6 +15,7 @@ impl std::fmt::Display for Axis {
|
|||||||
Axis::Row => write!(f, "Row ↕"),
|
Axis::Row => write!(f, "Row ↕"),
|
||||||
Axis::Column => write!(f, "Col ↔"),
|
Axis::Column => write!(f, "Col ↔"),
|
||||||
Axis::Page => write!(f, "Page ☰"),
|
Axis::Page => write!(f, "Page ☰"),
|
||||||
|
Axis::None => write!(f, "None ∅"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ pub struct GridLayout {
|
|||||||
pub page_coords: Vec<(String, String)>,
|
pub page_coords: Vec<(String, String)>,
|
||||||
pub row_items: Vec<AxisEntry>,
|
pub row_items: Vec<AxisEntry>,
|
||||||
pub col_items: Vec<AxisEntry>,
|
pub col_items: Vec<AxisEntry>,
|
||||||
|
/// Categories on `Axis::None` — hidden, implicitly aggregated.
|
||||||
|
pub none_cats: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GridLayout {
|
impl GridLayout {
|
||||||
@ -46,6 +48,11 @@ impl GridLayout {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
let none_cats: Vec<String> = view
|
||||||
|
.categories_on(Axis::None)
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let page_coords = page_cats
|
let page_coords = page_cats
|
||||||
.iter()
|
.iter()
|
||||||
@ -77,6 +84,7 @@ impl GridLayout {
|
|||||||
page_coords,
|
page_coords,
|
||||||
row_items,
|
row_items,
|
||||||
col_items,
|
col_items,
|
||||||
|
none_cats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -148,12 +148,13 @@ impl View {
|
|||||||
self.col_offset = 0;
|
self.col_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cycle axis for a category: Row → Column → Page → Row
|
/// Cycle axis for a category: Row → Column → Page → None → Row
|
||||||
pub fn cycle_axis(&mut self, cat_name: &str) {
|
pub fn cycle_axis(&mut self, cat_name: &str) {
|
||||||
let next = match self.axis_of(cat_name) {
|
let next = match self.axis_of(cat_name) {
|
||||||
Axis::Row => Axis::Column,
|
Axis::Row => Axis::Column,
|
||||||
Axis::Column => Axis::Page,
|
Axis::Column => Axis::Page,
|
||||||
Axis::Page => Axis::Row,
|
Axis::Page => Axis::None,
|
||||||
|
Axis::None => Axis::Row,
|
||||||
};
|
};
|
||||||
self.set_axis(cat_name, next);
|
self.set_axis(cat_name, next);
|
||||||
self.selected = (0, 0);
|
self.selected = (0, 0);
|
||||||
@ -302,9 +303,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_axis_page_to_row() {
|
fn cycle_axis_page_to_none() {
|
||||||
let mut v = view_with_cats(&["Region", "Product", "Time"]);
|
let mut v = view_with_cats(&["Region", "Product", "Time"]);
|
||||||
v.cycle_axis("Time");
|
v.cycle_axis("Time");
|
||||||
|
assert_eq!(v.axis_of("Time"), Axis::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_axis_none_to_row() {
|
||||||
|
let mut v = view_with_cats(&["Region", "Product", "Time"]);
|
||||||
|
v.set_axis("Time", Axis::None);
|
||||||
|
v.cycle_axis("Time");
|
||||||
assert_eq!(v.axis_of("Time"), Axis::Row);
|
assert_eq!(v.axis_of("Time"), Axis::Row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +360,7 @@ mod prop_tests {
|
|||||||
fn each_category_on_exactly_one_axis(cats in unique_cat_names()) {
|
fn each_category_on_exactly_one_axis(cats in unique_cat_names()) {
|
||||||
let mut v = View::new("T");
|
let mut v = View::new("T");
|
||||||
for c in &cats { v.on_category_added(c); }
|
for c in &cats { v.on_category_added(c); }
|
||||||
let all_axes = [Axis::Row, Axis::Column, Axis::Page];
|
let all_axes = [Axis::Row, Axis::Column, Axis::Page, Axis::None];
|
||||||
for c in &cats {
|
for c in &cats {
|
||||||
let count = all_axes.iter()
|
let count = all_axes.iter()
|
||||||
.filter(|&&ax| v.categories_on(ax).contains(&c.as_str()))
|
.filter(|&&ax| v.categories_on(ax).contains(&c.as_str()))
|
||||||
@ -377,7 +386,7 @@ mod prop_tests {
|
|||||||
fn set_axis_updates_axis_of(
|
fn set_axis_updates_axis_of(
|
||||||
cats in unique_cat_names(),
|
cats in unique_cat_names(),
|
||||||
target_idx in 0usize..8,
|
target_idx in 0usize..8,
|
||||||
axis in prop_oneof![Just(Axis::Row), Just(Axis::Column), Just(Axis::Page)],
|
axis in prop_oneof![Just(Axis::Row), Just(Axis::Column), Just(Axis::Page), Just(Axis::None)],
|
||||||
) {
|
) {
|
||||||
let mut v = View::new("T");
|
let mut v = View::new("T");
|
||||||
for c in &cats { v.on_category_added(c); }
|
for c in &cats { v.on_category_added(c); }
|
||||||
@ -392,7 +401,7 @@ mod prop_tests {
|
|||||||
fn set_axis_exclusive(
|
fn set_axis_exclusive(
|
||||||
cats in unique_cat_names(),
|
cats in unique_cat_names(),
|
||||||
target_idx in 0usize..8,
|
target_idx in 0usize..8,
|
||||||
axis in prop_oneof![Just(Axis::Row), Just(Axis::Column), Just(Axis::Page)],
|
axis in prop_oneof![Just(Axis::Row), Just(Axis::Column), Just(Axis::Page), Just(Axis::None)],
|
||||||
) {
|
) {
|
||||||
let mut v = View::new("T");
|
let mut v = View::new("T");
|
||||||
for c in &cats { v.on_category_added(c); }
|
for c in &cats { v.on_category_added(c); }
|
||||||
|
|||||||
Reference in New Issue
Block a user