chore: reformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,16 +1,15 @@
|
||||
use std::io::{Read, Write, BufReader, BufWriter};
|
||||
use std::path::Path;
|
||||
use anyhow::{Context, Result};
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::category::Group;
|
||||
use crate::view::{Axis, GridLayout};
|
||||
use crate::formula::parse_formula;
|
||||
|
||||
use crate::model::category::Group;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
use crate::view::{Axis, GridLayout};
|
||||
|
||||
pub fn save(model: &Model, path: &Path) -> Result<()> {
|
||||
let text = format_md(model);
|
||||
@ -22,15 +21,14 @@ pub fn save(model: &Model, path: &Path) -> Result<()> {
|
||||
encoder.write_all(text.as_bytes())?;
|
||||
encoder.finish()?;
|
||||
} else {
|
||||
std::fs::write(path, &text)
|
||||
.with_context(|| format!("Cannot write {}", path.display()))?;
|
||||
std::fs::write(path, &text).with_context(|| format!("Cannot write {}", path.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Result<Model> {
|
||||
let file = std::fs::File::open(path)
|
||||
.with_context(|| format!("Cannot open {}", path.display()))?;
|
||||
let file =
|
||||
std::fs::File::open(path).with_context(|| format!("Cannot open {}", path.display()))?;
|
||||
|
||||
let text = if path.to_str().map(|s| s.ends_with(".gz")).unwrap_or(false) {
|
||||
let mut decoder = GzDecoder::new(BufReader::new(file));
|
||||
@ -70,7 +68,7 @@ pub fn format_md(model: &Model) -> String {
|
||||
for item in cat.items.values() {
|
||||
match &item.group {
|
||||
Some(g) => writeln!(out, "- {} [{}]", item.name, g).unwrap(),
|
||||
None => writeln!(out, "- {}", item.name).unwrap(),
|
||||
None => writeln!(out, "- {}", item.name).unwrap(),
|
||||
}
|
||||
}
|
||||
// Group hierarchy: lines starting with `>` for groups that have a parent
|
||||
@ -97,7 +95,7 @@ pub fn format_md(model: &Model) -> String {
|
||||
for (key, value) in cells {
|
||||
let val_str = match value {
|
||||
CellValue::Number(_) => value.to_string(),
|
||||
CellValue::Text(s) => format!("\"{}\"", s),
|
||||
CellValue::Text(s) => format!("\"{}\"", s),
|
||||
};
|
||||
writeln!(out, "{} = {}", coord_str(key), val_str).unwrap();
|
||||
}
|
||||
@ -105,25 +103,29 @@ pub fn format_md(model: &Model) -> String {
|
||||
|
||||
// Views
|
||||
for (view_name, view) in &model.views {
|
||||
let active = if view_name == &model.active_view { " (active)" } else { "" };
|
||||
let active = if view_name == &model.active_view {
|
||||
" (active)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
writeln!(out, "\n## View: {}{}", view.name, active).unwrap();
|
||||
for (cat, axis) in &view.category_axes {
|
||||
match axis {
|
||||
Axis::Row => writeln!(out, "{}: row", cat).unwrap(),
|
||||
Axis::Row => writeln!(out, "{}: row", cat).unwrap(),
|
||||
Axis::Column => writeln!(out, "{}: column", cat).unwrap(),
|
||||
Axis::Page => {
|
||||
match view.page_selections.get(cat) {
|
||||
Some(sel) => writeln!(out, "{}: page, {}", cat, sel).unwrap(),
|
||||
None => writeln!(out, "{}: page", cat).unwrap(),
|
||||
}
|
||||
}
|
||||
Axis::Page => match view.page_selections.get(cat) {
|
||||
Some(sel) => writeln!(out, "{}: page, {}", cat, sel).unwrap(),
|
||||
None => writeln!(out, "{}: page", cat).unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if !view.number_format.is_empty() {
|
||||
writeln!(out, "format: {}", view.number_format).unwrap();
|
||||
}
|
||||
// Hidden items (sorted for deterministic diffs)
|
||||
let mut hidden: Vec<(&str, &str)> = view.hidden_items.iter()
|
||||
let mut hidden: Vec<(&str, &str)> = view
|
||||
.hidden_items
|
||||
.iter()
|
||||
.flat_map(|(cat, items)| items.iter().map(move |item| (cat.as_str(), item.as_str())))
|
||||
.collect();
|
||||
hidden.sort();
|
||||
@ -131,7 +133,9 @@ pub fn format_md(model: &Model) -> String {
|
||||
writeln!(out, "hidden: {}/{}", cat, item).unwrap();
|
||||
}
|
||||
// Collapsed groups (sorted for deterministic diffs)
|
||||
let mut collapsed: Vec<(&str, &str)> = view.collapsed_groups.iter()
|
||||
let mut collapsed: Vec<(&str, &str)> = view
|
||||
.collapsed_groups
|
||||
.iter()
|
||||
.flat_map(|(cat, gs)| gs.iter().map(move |g| (cat.as_str(), g.as_str())))
|
||||
.collect();
|
||||
collapsed.sort();
|
||||
@ -153,8 +157,8 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
|
||||
struct PCategory {
|
||||
name: String,
|
||||
items: Vec<(String, Option<String>)>, // (name, group)
|
||||
group_parents: Vec<(String, String)>, // (group, parent)
|
||||
items: Vec<(String, Option<String>)>, // (name, group)
|
||||
group_parents: Vec<(String, String)>, // (group, parent)
|
||||
}
|
||||
|
||||
struct PView {
|
||||
@ -170,50 +174,78 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
// ── Pass 1: collect ───────────────────────────────────────────────────────
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Section { None, Category, Formulas, Data, View }
|
||||
enum Section {
|
||||
None,
|
||||
Category,
|
||||
Formulas,
|
||||
Data,
|
||||
View,
|
||||
}
|
||||
|
||||
let mut model_name: Option<String> = None;
|
||||
let mut categories: Vec<PCategory> = Vec::new();
|
||||
let mut formulas: Vec<(String, String)> = Vec::new(); // (raw, category)
|
||||
let mut data: Vec<(CellKey, CellValue)> = Vec::new();
|
||||
let mut views: Vec<PView> = Vec::new();
|
||||
let mut categories: Vec<PCategory> = Vec::new();
|
||||
let mut formulas: Vec<(String, String)> = Vec::new(); // (raw, category)
|
||||
let mut data: Vec<(CellKey, CellValue)> = Vec::new();
|
||||
let mut views: Vec<PView> = Vec::new();
|
||||
let mut section = Section::None;
|
||||
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() { continue; }
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.starts_with("# ") && !trimmed.starts_with("## ") {
|
||||
model_name = Some(trimmed[2..].trim().to_string());
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = trimmed.strip_prefix("## Category: ") {
|
||||
categories.push(PCategory { name: rest.trim().to_string(),
|
||||
items: Vec::new(), group_parents: Vec::new() });
|
||||
categories.push(PCategory {
|
||||
name: rest.trim().to_string(),
|
||||
items: Vec::new(),
|
||||
group_parents: Vec::new(),
|
||||
});
|
||||
section = Section::Category;
|
||||
continue;
|
||||
}
|
||||
if trimmed == "## Formulas" { section = Section::Formulas; continue; }
|
||||
if trimmed == "## Data" { section = Section::Data; continue; }
|
||||
if trimmed == "## Formulas" {
|
||||
section = Section::Formulas;
|
||||
continue;
|
||||
}
|
||||
if trimmed == "## Data" {
|
||||
section = Section::Data;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = trimmed.strip_prefix("## View: ") {
|
||||
let (name, is_active) = match rest.trim().strip_suffix(" (active)") {
|
||||
Some(n) => (n.trim().to_string(), true),
|
||||
None => (rest.trim().to_string(), false),
|
||||
None => (rest.trim().to_string(), false),
|
||||
};
|
||||
views.push(PView { name, is_active, axes: Vec::new(),
|
||||
page_selections: Vec::new(), format: String::new(),
|
||||
hidden: Vec::new(), collapsed: Vec::new() });
|
||||
views.push(PView {
|
||||
name,
|
||||
is_active,
|
||||
axes: Vec::new(),
|
||||
page_selections: Vec::new(),
|
||||
format: String::new(),
|
||||
hidden: Vec::new(),
|
||||
collapsed: Vec::new(),
|
||||
});
|
||||
section = Section::View;
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with("## ") { continue; }
|
||||
if trimmed.starts_with("## ") {
|
||||
continue;
|
||||
}
|
||||
|
||||
match section {
|
||||
Section::Category => {
|
||||
let Some(cat) = categories.last_mut() else { continue };
|
||||
let Some(cat) = categories.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(rest) = trimmed.strip_prefix("- ") {
|
||||
let (name, group) = parse_bracketed(rest);
|
||||
cat.items.push((name.to_string(), group.map(str::to_string)));
|
||||
cat.items
|
||||
.push((name.to_string(), group.map(str::to_string)));
|
||||
} else if let Some(rest) = trimmed.strip_prefix("> ") {
|
||||
let (group, parent) = parse_bracketed(rest);
|
||||
if let Some(p) = parent {
|
||||
@ -230,14 +262,22 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
}
|
||||
}
|
||||
Section::Data => {
|
||||
let Some(sep) = trimmed.find(" = ") else { continue };
|
||||
let coords: Vec<(String, String)> = trimmed[..sep].split(", ")
|
||||
.filter_map(|p| { let (c, i) = p.split_once('=')?;
|
||||
Some((c.trim().to_string(), i.trim().to_string())) })
|
||||
let Some(sep) = trimmed.find(" = ") else {
|
||||
continue;
|
||||
};
|
||||
let coords: Vec<(String, String)> = trimmed[..sep]
|
||||
.split(", ")
|
||||
.filter_map(|p| {
|
||||
let (c, i) = p.split_once('=')?;
|
||||
Some((c.trim().to_string(), i.trim().to_string()))
|
||||
})
|
||||
.collect();
|
||||
if coords.is_empty() { continue; }
|
||||
if coords.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let vs = trimmed[sep + 3..].trim();
|
||||
let value = if let Some(s) = vs.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
|
||||
let value = if let Some(s) = vs.strip_prefix('"').and_then(|s| s.strip_suffix('"'))
|
||||
{
|
||||
CellValue::Text(s.to_string())
|
||||
} else if let Ok(n) = vs.parse::<f64>() {
|
||||
CellValue::Number(n)
|
||||
@ -247,28 +287,36 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
data.push((CellKey::new(coords), value));
|
||||
}
|
||||
Section::View => {
|
||||
let Some(view) = views.last_mut() else { continue };
|
||||
let Some(view) = views.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(fmt) = trimmed.strip_prefix("format: ") {
|
||||
view.format = fmt.trim().to_string();
|
||||
} else if let Some(rest) = trimmed.strip_prefix("hidden: ") {
|
||||
if let Some((c, i)) = rest.trim().split_once('/') {
|
||||
view.hidden.push((c.trim().to_string(), i.trim().to_string()));
|
||||
view.hidden
|
||||
.push((c.trim().to_string(), i.trim().to_string()));
|
||||
}
|
||||
} else if let Some(rest) = trimmed.strip_prefix("collapsed: ") {
|
||||
if let Some((c, g)) = rest.trim().split_once('/') {
|
||||
view.collapsed.push((c.trim().to_string(), g.trim().to_string()));
|
||||
view.collapsed
|
||||
.push((c.trim().to_string(), g.trim().to_string()));
|
||||
}
|
||||
} else if let Some(colon) = trimmed.find(": ") {
|
||||
let cat = trimmed[..colon].trim();
|
||||
let cat = trimmed[..colon].trim();
|
||||
let rest = trimmed[colon + 2..].trim();
|
||||
if let Some(sel_rest) = rest.strip_prefix("page") {
|
||||
view.axes.push((cat.to_string(), Axis::Page));
|
||||
if let Some(sel) = sel_rest.strip_prefix(", ") {
|
||||
view.page_selections.push((cat.to_string(), sel.trim().to_string()));
|
||||
view.page_selections
|
||||
.push((cat.to_string(), sel.trim().to_string()));
|
||||
}
|
||||
} else {
|
||||
let axis = match rest { "row" => Axis::Row, "column" => Axis::Column,
|
||||
_ => continue };
|
||||
let axis = match rest {
|
||||
"row" => Axis::Row,
|
||||
"column" => Axis::Column,
|
||||
_ => continue,
|
||||
};
|
||||
view.axes.push((cat.to_string(), axis));
|
||||
}
|
||||
}
|
||||
@ -294,13 +342,15 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
cat.add_group(Group::new(g));
|
||||
}
|
||||
}
|
||||
None => { cat.add_item(item_name); }
|
||||
None => {
|
||||
cat.add_item(item_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (group_name, parent) in &pc.group_parents {
|
||||
match cat.groups.iter_mut().find(|g| &g.name == group_name) {
|
||||
Some(g) => g.parent = Some(parent.clone()),
|
||||
None => cat.add_group(Group::new(group_name).with_parent(parent)),
|
||||
None => cat.add_group(Group::new(group_name).with_parent(parent)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -308,14 +358,28 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
// Views — all categories are now registered, so set_axis works correctly
|
||||
let mut active_view = String::new();
|
||||
for pv in &views {
|
||||
if pv.is_active { active_view = pv.name.clone(); }
|
||||
if !m.views.contains_key(&pv.name) { m.create_view(&pv.name); }
|
||||
if pv.is_active {
|
||||
active_view = pv.name.clone();
|
||||
}
|
||||
if !m.views.contains_key(&pv.name) {
|
||||
m.create_view(&pv.name);
|
||||
}
|
||||
let view = m.views.get_mut(&pv.name).unwrap();
|
||||
for (cat, axis) in &pv.axes { view.set_axis(cat, *axis); }
|
||||
for (cat, sel) in &pv.page_selections { view.set_page_selection(cat, sel); }
|
||||
if !pv.format.is_empty() { view.number_format = pv.format.clone(); }
|
||||
for (cat, item) in &pv.hidden { view.hide_item(cat, item); }
|
||||
for (cat, grp) in &pv.collapsed { view.toggle_group_collapse(cat, grp); }
|
||||
for (cat, axis) in &pv.axes {
|
||||
view.set_axis(cat, *axis);
|
||||
}
|
||||
for (cat, sel) in &pv.page_selections {
|
||||
view.set_page_selection(cat, sel);
|
||||
}
|
||||
if !pv.format.is_empty() {
|
||||
view.number_format = pv.format.clone();
|
||||
}
|
||||
for (cat, item) in &pv.hidden {
|
||||
view.hide_item(cat, item);
|
||||
}
|
||||
for (cat, grp) in &pv.collapsed {
|
||||
view.toggle_group_collapse(cat, grp);
|
||||
}
|
||||
}
|
||||
if !active_view.is_empty() && m.views.contains_key(&active_view) {
|
||||
m.active_view = active_view;
|
||||
@ -323,8 +387,7 @@ pub fn parse_md(text: &str) -> Result<Model> {
|
||||
|
||||
// Formulas and data can go in any order relative to each other
|
||||
for (raw, cat_name) in &formulas {
|
||||
m.add_formula(parse_formula(raw, cat_name)
|
||||
.with_context(|| format!("Formula: {raw}"))?);
|
||||
m.add_formula(parse_formula(raw, cat_name).with_context(|| format!("Formula: {raw}"))?);
|
||||
}
|
||||
for (key, value) in data {
|
||||
m.set_cell(key, value);
|
||||
@ -346,11 +409,17 @@ fn parse_bracketed(s: &str) -> (&str, Option<&str>) {
|
||||
}
|
||||
|
||||
fn coord_str(key: &CellKey) -> String {
|
||||
key.0.iter().map(|(c, i)| format!("{}={}", c, i)).collect::<Vec<_>>().join(", ")
|
||||
key.0
|
||||
.iter()
|
||||
.map(|(c, i)| format!("{}={}", c, i))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
pub fn export_csv(model: &Model, view_name: &str, path: &Path) -> Result<()> {
|
||||
let view = model.views.get(view_name)
|
||||
let view = model
|
||||
.views
|
||||
.get(view_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("View '{view_name}' not found"))?;
|
||||
|
||||
let layout = GridLayout::new(model, view);
|
||||
@ -359,16 +428,23 @@ pub fn export_csv(model: &Model, view_name: &str, path: &Path) -> Result<()> {
|
||||
|
||||
// Header row
|
||||
let row_header = layout.row_cats.join("/");
|
||||
let page_label: Vec<String> = layout.page_coords.iter()
|
||||
.map(|(c, v)| format!("{c}={v}")).collect();
|
||||
let header_prefix = if page_label.is_empty() { row_header } else {
|
||||
let page_label: Vec<String> = layout
|
||||
.page_coords
|
||||
.iter()
|
||||
.map(|(c, v)| format!("{c}={v}"))
|
||||
.collect();
|
||||
let header_prefix = if page_label.is_empty() {
|
||||
row_header
|
||||
} else {
|
||||
format!("{} ({})", row_header, page_label.join(", "))
|
||||
};
|
||||
if !header_prefix.is_empty() {
|
||||
out.push_str(&header_prefix);
|
||||
out.push(',');
|
||||
}
|
||||
let col_labels: Vec<String> = (0..layout.col_count()).map(|ci| layout.col_label(ci)).collect();
|
||||
let col_labels: Vec<String> = (0..layout.col_count())
|
||||
.map(|ci| layout.col_label(ci))
|
||||
.collect();
|
||||
out.push_str(&col_labels.join(","));
|
||||
out.push('\n');
|
||||
|
||||
@ -380,10 +456,13 @@ pub fn export_csv(model: &Model, view_name: &str, path: &Path) -> Result<()> {
|
||||
out.push(',');
|
||||
}
|
||||
let row_values: Vec<String> = (0..layout.col_count())
|
||||
.map(|ci| layout.cell_key(ri, ci)
|
||||
.and_then(|key| model.evaluate(&key))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default())
|
||||
.map(|ci| {
|
||||
layout
|
||||
.cell_key(ri, ci)
|
||||
.and_then(|key| model.evaluate(&key))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect();
|
||||
out.push_str(&row_values.join(","));
|
||||
out.push('\n');
|
||||
@ -396,22 +475,31 @@ pub fn export_csv(model: &Model, view_name: &str, path: &Path) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{format_md, parse_md};
|
||||
use crate::model::Model;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::category::Group;
|
||||
use crate::view::Axis;
|
||||
use crate::formula::parse_formula;
|
||||
use crate::model::category::Group;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
use crate::view::Axis;
|
||||
|
||||
fn coord(pairs: &[(&str, &str)]) -> CellKey {
|
||||
CellKey::new(pairs.iter().map(|(c, i)| (c.to_string(), i.to_string())).collect())
|
||||
CellKey::new(
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(c, i)| (c.to_string(), i.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn two_cat_model() -> Model {
|
||||
let mut m = Model::new("Budget");
|
||||
m.add_category("Type").unwrap();
|
||||
m.add_category("Month").unwrap();
|
||||
for item in ["Food", "Gas"] { m.category_mut("Type").unwrap().add_item(item); }
|
||||
for item in ["Jan", "Feb"] { m.category_mut("Month").unwrap().add_item(item); }
|
||||
for item in ["Food", "Gas"] {
|
||||
m.category_mut("Type").unwrap().add_item(item);
|
||||
}
|
||||
for item in ["Jan", "Feb"] {
|
||||
m.category_mut("Month").unwrap().add_item(item);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
@ -438,7 +526,9 @@ mod tests {
|
||||
fn format_md_item_with_group_uses_brackets() {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Month").unwrap();
|
||||
m.category_mut("Month").unwrap().add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_item_in_group("Jan", "Q1");
|
||||
let text = format_md(&m);
|
||||
assert!(text.contains("- Jan [Q1]"), "got:\n{text}");
|
||||
}
|
||||
@ -447,8 +537,12 @@ mod tests {
|
||||
fn format_md_group_hierarchy_uses_angle_prefix() {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Month").unwrap();
|
||||
m.category_mut("Month").unwrap().add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month").unwrap().add_group(Group::new("Q1").with_parent("2025"));
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_group(Group::new("Q1").with_parent("2025"));
|
||||
let text = format_md(&m);
|
||||
assert!(text.contains("> Q1 [2025]"), "got:\n{text}");
|
||||
}
|
||||
@ -456,14 +550,22 @@ mod tests {
|
||||
#[test]
|
||||
fn format_md_data_is_sorted_and_quoted() {
|
||||
let mut m = two_cat_model();
|
||||
m.set_cell(coord(&[("Month", "Feb"), ("Type", "Food")]), CellValue::Number(200.0));
|
||||
m.set_cell(coord(&[("Month", "Jan"), ("Type", "Gas")]), CellValue::Text("N/A".into()));
|
||||
m.set_cell(
|
||||
coord(&[("Month", "Feb"), ("Type", "Food")]),
|
||||
CellValue::Number(200.0),
|
||||
);
|
||||
m.set_cell(
|
||||
coord(&[("Month", "Jan"), ("Type", "Gas")]),
|
||||
CellValue::Text("N/A".into()),
|
||||
);
|
||||
let text = format_md(&m);
|
||||
let data_pos = text.find("## Data").unwrap();
|
||||
let feb_pos = text.find("Month=Feb").unwrap();
|
||||
let jan_pos = text.find("Month=Jan").unwrap();
|
||||
assert!(data_pos < feb_pos && feb_pos < jan_pos,
|
||||
"expected sorted order Feb < Jan:\n{text}");
|
||||
let data_pos = text.find("## Data").unwrap();
|
||||
let feb_pos = text.find("Month=Feb").unwrap();
|
||||
let jan_pos = text.find("Month=Jan").unwrap();
|
||||
assert!(
|
||||
data_pos < feb_pos && feb_pos < jan_pos,
|
||||
"expected sorted order Feb < Jan:\n{text}"
|
||||
);
|
||||
assert!(text.contains("= 200"), "number not quoted:\n{text}");
|
||||
assert!(text.contains("= \"N/A\""), "text should be quoted:\n{text}");
|
||||
}
|
||||
@ -485,7 +587,9 @@ mod tests {
|
||||
m.add_category("Region").unwrap();
|
||||
m.category_mut("Type").unwrap().add_item("Food");
|
||||
m.category_mut("Month").unwrap().add_item("Jan");
|
||||
for r in ["East", "West"] { m.category_mut("Region").unwrap().add_item(r); }
|
||||
for r in ["East", "West"] {
|
||||
m.category_mut("Region").unwrap().add_item(r);
|
||||
}
|
||||
m.active_view_mut().set_page_selection("Region", "West");
|
||||
let text = format_md(&m);
|
||||
assert!(text.contains("Region: page, West"), "got:\n{text}");
|
||||
@ -512,18 +616,28 @@ mod tests {
|
||||
fn parse_md_round_trips_categories_and_items() {
|
||||
let m = two_cat_model();
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
assert!(m2.category("Type").and_then(|c| c.items.get("Food")).is_some());
|
||||
assert!(m2.category("Month").and_then(|c| c.items.get("Feb")).is_some());
|
||||
assert!(m2
|
||||
.category("Type")
|
||||
.and_then(|c| c.items.get("Food"))
|
||||
.is_some());
|
||||
assert!(m2
|
||||
.category("Month")
|
||||
.and_then(|c| c.items.get("Feb"))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_md_round_trips_item_group() {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Month").unwrap();
|
||||
m.category_mut("Month").unwrap().add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_item_in_group("Jan", "Q1");
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
assert_eq!(
|
||||
m2.category("Month").and_then(|c| c.items.get("Jan")).and_then(|i| i.group.as_deref()),
|
||||
m2.category("Month")
|
||||
.and_then(|c| c.items.get("Jan"))
|
||||
.and_then(|i| i.group.as_deref()),
|
||||
Some("Q1")
|
||||
);
|
||||
}
|
||||
@ -532,8 +646,12 @@ mod tests {
|
||||
fn parse_md_round_trips_group_hierarchy() {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Month").unwrap();
|
||||
m.category_mut("Month").unwrap().add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month").unwrap().add_group(Group::new("Q1").with_parent("2025"));
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_item_in_group("Jan", "Q1");
|
||||
m.category_mut("Month")
|
||||
.unwrap()
|
||||
.add_group(Group::new("Q1").with_parent("2025"));
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
let groups = &m2.category("Month").unwrap().groups;
|
||||
let q1 = groups.iter().find(|g| g.name == "Q1").unwrap();
|
||||
@ -543,13 +661,23 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_md_round_trips_data_cells() {
|
||||
let mut m = two_cat_model();
|
||||
m.set_cell(coord(&[("Month", "Jan"), ("Type", "Food")]), CellValue::Number(100.0));
|
||||
m.set_cell(coord(&[("Month", "Feb"), ("Type", "Gas")]), CellValue::Text("N/A".into()));
|
||||
m.set_cell(
|
||||
coord(&[("Month", "Jan"), ("Type", "Food")]),
|
||||
CellValue::Number(100.0),
|
||||
);
|
||||
m.set_cell(
|
||||
coord(&[("Month", "Feb"), ("Type", "Gas")]),
|
||||
CellValue::Text("N/A".into()),
|
||||
);
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
assert_eq!(m2.get_cell(&coord(&[("Month", "Jan"), ("Type", "Food")])),
|
||||
Some(&CellValue::Number(100.0)));
|
||||
assert_eq!(m2.get_cell(&coord(&[("Month", "Feb"), ("Type", "Gas")])),
|
||||
Some(&CellValue::Text("N/A".into())));
|
||||
assert_eq!(
|
||||
m2.get_cell(&coord(&[("Month", "Jan"), ("Type", "Food")])),
|
||||
Some(&CellValue::Number(100.0))
|
||||
);
|
||||
assert_eq!(
|
||||
m2.get_cell(&coord(&[("Month", "Feb"), ("Type", "Gas")])),
|
||||
Some(&CellValue::Text("N/A".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -557,7 +685,7 @@ mod tests {
|
||||
let m = two_cat_model();
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
let v = m2.active_view();
|
||||
assert_eq!(v.axis_of("Type"), Axis::Row);
|
||||
assert_eq!(v.axis_of("Type"), Axis::Row);
|
||||
assert_eq!(v.axis_of("Month"), Axis::Column);
|
||||
}
|
||||
|
||||
@ -569,7 +697,9 @@ mod tests {
|
||||
m.add_category("Region").unwrap();
|
||||
m.category_mut("Type").unwrap().add_item("Food");
|
||||
m.category_mut("Month").unwrap().add_item("Jan");
|
||||
for r in ["East", "West"] { m.category_mut("Region").unwrap().add_item(r); }
|
||||
for r in ["East", "West"] {
|
||||
m.category_mut("Region").unwrap().add_item(r);
|
||||
}
|
||||
m.active_view_mut().set_page_selection("Region", "West");
|
||||
let m2 = parse_md(&format_md(&m)).unwrap();
|
||||
assert_eq!(m2.active_view().page_selection("Region"), Some("West"));
|
||||
@ -621,7 +751,7 @@ mod tests {
|
||||
## Category: Month\n\
|
||||
- Jan\n";
|
||||
let m = parse_md(text).unwrap();
|
||||
assert_eq!(m.active_view().axis_of("Type"), Axis::Row);
|
||||
assert_eq!(m.active_view().axis_of("Type"), Axis::Row);
|
||||
assert_eq!(m.active_view().axis_of("Month"), Axis::Column);
|
||||
}
|
||||
|
||||
@ -641,10 +771,10 @@ mod tests {
|
||||
- Jan\n";
|
||||
let m = parse_md(text).unwrap();
|
||||
let transposed = m.views.get("Transposed").unwrap();
|
||||
assert_eq!(transposed.axis_of("Type"), Axis::Column);
|
||||
assert_eq!(transposed.axis_of("Type"), Axis::Column);
|
||||
assert_eq!(transposed.axis_of("Month"), Axis::Row);
|
||||
let default = m.views.get("Default").unwrap();
|
||||
assert_eq!(default.axis_of("Type"), Axis::Row);
|
||||
assert_eq!(default.axis_of("Type"), Axis::Row);
|
||||
assert_eq!(default.axis_of("Month"), Axis::Column);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user