refactor: make active_view and axis_of infallible

Both functions previously returned Option despite their invariants
guaranteeing a value: active_view always names an existing view
(maintained by new/switch_view/delete_view), and axis_of only returns
None for categories never registered with the view (a programming error).

Callers no longer need to handle the impossible None case, eliminating
~15 match/if-let Option guards across app.rs, dispatch.rs, grid.rs,
tile_bar.rs, and category_panel.rs.

Also adds Model::evaluate_f64 (returns 0.0 for empty cells) and collapses
the double match-on-axis pattern in tile_bar/category_panel into a single
axis_display(Axis) helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ed L
2026-03-24 09:00:25 -07:00
parent a2e519efcc
commit 6038cb2d81
9 changed files with 168 additions and 208 deletions

View File

@ -94,12 +94,14 @@ impl Model {
&self.formulas
}
pub fn active_view(&self) -> Option<&View> {
pub fn active_view(&self) -> &View {
self.views.get(&self.active_view)
.expect("active_view always names an existing view")
}
pub fn active_view_mut(&mut self) -> Option<&mut View> {
pub fn active_view_mut(&mut self) -> &mut View {
self.views.get_mut(&self.active_view)
.expect("active_view always names an existing view")
}
pub fn create_view(&mut self, name: impl Into<String>) -> &mut View {
@ -161,6 +163,11 @@ impl Model {
self.data.get(key).cloned()
}
/// Evaluate a key as a numeric value, returning 0.0 for empty/non-numeric cells.
pub fn evaluate_f64(&self, key: &CellKey) -> f64 {
self.evaluate(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
}
fn eval_formula(&self, formula: &Formula, context: &CellKey) -> Option<CellValue> {
use crate::formula::{Expr, AggFunc};
@ -294,7 +301,8 @@ mod model_tests {
#[test]
fn new_model_has_default_view() {
let m = Model::new("Test");
assert!(m.active_view().is_some());
// active_view() panics if missing; this test just ensures it doesn't panic
let _ = m.active_view();
}
#[test]
@ -324,7 +332,8 @@ mod model_tests {
fn add_category_notifies_existing_views() {
let mut m = Model::new("Test");
m.add_category("Region").unwrap();
assert_ne!(m.active_view().unwrap().axis_of("Region"), None);
// axis_of panics for unknown categories; not panicking here confirms it was registered
let _ = m.active_view().axis_of("Region");
}
#[test]
@ -380,8 +389,9 @@ mod model_tests {
m.add_category("Product").unwrap();
m.create_view("Secondary");
let v = m.views.get("Secondary").unwrap();
assert_ne!(v.axis_of("Region"), None);
assert_ne!(v.axis_of("Product"), None);
// axis_of panics for unknown categories; not panicking confirms categories were registered
let _ = v.axis_of("Region");
let _ = v.axis_of("Product");
}
#[test]
@ -427,10 +437,10 @@ mod model_tests {
m.add_category("Region").unwrap();
m.add_category("Product").unwrap();
m.add_category("Time").unwrap();
let v = m.active_view().unwrap();
assert_eq!(v.axis_of("Region"), Some(Axis::Row));
assert_eq!(v.axis_of("Product"), Some(Axis::Column));
assert_eq!(v.axis_of("Time"), Some(Axis::Page));
let v = m.active_view();
assert_eq!(v.axis_of("Region"), Axis::Row);
assert_eq!(v.axis_of("Product"), Axis::Column);
assert_eq!(v.axis_of("Time"), Axis::Page);
}
#[test]
@ -969,18 +979,19 @@ mod five_category {
#[test]
fn default_view_first_two_on_axes_rest_on_page() {
let m = build_model();
let v = m.active_view().unwrap();
assert_eq!(v.axis_of("Region"), Some(Axis::Row));
assert_eq!(v.axis_of("Product"), Some(Axis::Column));
assert_eq!(v.axis_of("Channel"), Some(Axis::Page));
assert_eq!(v.axis_of("Time"), Some(Axis::Page));
assert_eq!(v.axis_of("Measure"), Some(Axis::Page));
let v = m.active_view();
assert_eq!(v.axis_of("Region"), Axis::Row);
assert_eq!(v.axis_of("Product"), Axis::Column);
assert_eq!(v.axis_of("Channel"), Axis::Page);
assert_eq!(v.axis_of("Time"), Axis::Page);
assert_eq!(v.axis_of("Measure"), Axis::Page);
}
#[test]
fn rearranging_axes_does_not_affect_data() {
let mut m = build_model();
if let Some(v) = m.active_view_mut() {
{
let v = m.active_view_mut();
v.set_axis("Region", Axis::Page);
v.set_axis("Product", Axis::Page);
v.set_axis("Channel", Axis::Row);
@ -994,16 +1005,17 @@ mod five_category {
fn two_views_have_independent_axis_assignments() {
let mut m = build_model();
m.create_view("Pivot");
if let Some(v) = m.views.get_mut("Pivot") {
{
let v = m.views.get_mut("Pivot").unwrap();
v.set_axis("Time", Axis::Row);
v.set_axis("Channel", Axis::Column);
v.set_axis("Region", Axis::Page);
v.set_axis("Product", Axis::Page);
v.set_axis("Measure", Axis::Page);
}
assert_eq!(m.views.get("Default").unwrap().axis_of("Region"), Some(Axis::Row));
assert_eq!(m.views.get("Pivot").unwrap().axis_of("Time"), Some(Axis::Row));
assert_eq!(m.views.get("Pivot").unwrap().axis_of("Channel"), Some(Axis::Column));
assert_eq!(m.views.get("Default").unwrap().axis_of("Region"), Axis::Row);
assert_eq!(m.views.get("Pivot").unwrap().axis_of("Time"), Axis::Row);
assert_eq!(m.views.get("Pivot").unwrap().axis_of("Channel"), Axis::Column);
}
#[test]