feat(model): introduce virtual '_Measure' category for formulas

Refactor formula handling to use a virtual '_Measure' category.

- Formulas now default to targeting '_Measure' if no category is specified.
- '_Measure' items are dynamically computed from existing data items and
  formula targets, preventing redundant item storage.
- Updated persistence to handle '_Measure' formulas and items correctly in
  Markdown.
- Updated UI and model to use 'effective_item_names' for layout and item
  resolution.
- Updated tests to reflect the new '_Measure' behavior.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-09 14:24:38 -07:00
parent 1690fc317b
commit c8b9d29690
3 changed files with 107 additions and 25 deletions

View File

@ -181,10 +181,13 @@ impl Model {
}
pub fn add_formula(&mut self, formula: Formula) {
// Ensure the formula target exists as an item in the target category
// so the grid layout includes cells for it.
if let Some(cat) = self.categories.get_mut(&formula.target_category) {
cat.add_item(&formula.target);
// For non-_Measure target categories, add the target as a category item
// so it appears in the grid. _Measure targets are dynamically included
// via measure_item_names().
if formula.target_category != "_Measure" {
if let Some(cat) = self.categories.get_mut(&formula.target_category) {
cat.add_item(&formula.target);
}
}
// Replace if same target within the same category
if let Some(pos) = self.formulas.iter().position(|f| {
@ -264,6 +267,36 @@ impl Model {
self.categories.keys().map(|s| s.as_str()).collect()
}
/// Effective item names for the _Measure category: the union of items
/// explicitly in the category plus all formula targets (for formulas
/// targeting _Measure). Preserves insertion order of explicit items,
/// then appends formula targets not already present.
pub fn measure_item_names(&self) -> Vec<String> {
let mut names: Vec<String> = self
.category("_Measure")
.map(|c| c.ordered_item_names().iter().map(|s| s.to_string()).collect())
.unwrap_or_default();
for f in &self.formulas {
if f.target_category == "_Measure" && !names.iter().any(|n| n == &f.target) {
names.push(f.target.clone());
}
}
names
}
/// Effective item names for any category. For _Measure, this includes
/// formula targets dynamically. For all others, delegates to
/// `ordered_item_names()`.
pub fn effective_item_names(&self, cat_name: &str) -> Vec<String> {
if cat_name == "_Measure" {
self.measure_item_names()
} else {
self.category(cat_name)
.map(|c| c.ordered_item_names().iter().map(|s| s.to_string()).collect())
.unwrap_or_default()
}
}
/// Category names excluding virtual categories (_Index, _Dim).
pub fn regular_category_names(&self) -> Vec<&str> {
self.categories
@ -450,6 +483,13 @@ impl Model {
return Some(cat_name.as_str());
}
}
// Fall back to formula targets: if a formula defines this item
// in _Measure, resolve it there.
for f in model.formulas() {
if f.target == item_name && f.target_category == "_Measure" {
return Some("_Measure");
}
}
None
}
@ -616,6 +656,13 @@ impl Model {
return Some(cat_name.as_str());
}
}
// Fall back to formula targets: if a formula defines this item
// in _Measure, resolve it there.
for f in model.formulas() {
if f.target == item_name && f.target_category == "_Measure" {
return Some("_Measure");
}
}
None
}
@ -982,14 +1029,14 @@ mod model_tests {
assert_eq!(result, Some(CellValue::Number(150.0)));
}
/// Model::add_formula should add the formula target as an item to the
/// target category so the grid layout includes cells for it.
/// Formula targets should appear in measure_item_names() without being
/// explicitly added to the _Measure category. _Measure is dynamically
/// computed from data items + formula targets.
#[test]
fn add_formula_adds_target_item() {
fn measure_item_names_includes_formula_targets() {
use crate::formula::parse_formula;
let mut m = Model::new("Test");
m.add_category("_Measure").unwrap();
m.add_category("Region").unwrap();
m.category_mut("Region").unwrap().add_item("East");
m.category_mut("_Measure").unwrap().add_item("Revenue");
@ -997,12 +1044,22 @@ mod model_tests {
m.add_formula(parse_formula("Profit = Revenue - Cost", "_Measure").unwrap());
let names = m.measure_item_names();
assert!(
m.category("_Measure")
names.contains(&"Revenue".to_string()),
"measure_item_names should include data items"
);
assert!(
names.contains(&"Profit".to_string()),
"measure_item_names should include formula targets"
);
// Formula target should NOT be in the category's own items
assert!(
!m.category("_Measure")
.unwrap()
.ordered_item_names()
.contains(&"Profit"),
"add_formula should add target item to category"
"formula targets should not be added to the category directly"
);
}