Skip to main content

ravenrs_sheet/engine/
sample.rs

1//! Sample workbooks built in code (original content), for the start screen, demos, README
2//! screenshots and tests.
3
4use std::sync::Arc;
5
6use crate::model::{Sheet, Workbook};
7use serde_json::json;
8
9use crate::engine::{DocState, Session};
10
11pub const SAMPLES: &[(&str, &str)] = &[("budget", "Household Budget"), ("sales", "Quarterly Sales Dashboard"), ("grades", "Gradebook")];
12
13pub fn title(name: &str) -> String {
14    SAMPLES.iter().find(|(n, _)| *n == name).map(|(_, t)| t.to_string()).unwrap_or_else(|| "Sample".into())
15}
16
17/// Builds a sample by replaying commands (so samples also exercise the command layer).
18pub fn build(name: &str) -> Option<Workbook> {
19    let mut s = Session::new();
20    let mut wb = Workbook::new();
21    if let Some(sh) = wb.sheet_mut(0) {
22        sh.name = match name {
23            "budget" => "Budget".into(),
24            "sales" => "Sales".into(),
25            "grades" => "Grades".into(),
26            _ => return None,
27        };
28    }
29    s.add_document(DocState::new(wb, None, title(name), crate::engine::Trust::Local));
30    let ok = match name {
31        "budget" => budget(&mut s),
32        "sales" => sales(&mut s),
33        "grades" => grades(&mut s),
34        _ => false,
35    };
36    if !ok {
37        return None;
38    }
39    let mut wb = (*s.doc().ok()?.wb).clone();
40    wb.set_active_sheet(0);
41    if let Some(sh) = wb.sheet_mut(0) {
42        sh.view_active = crate::base::CellRef::default();
43    }
44    Some(wb)
45}
46
47fn run(s: &mut Session, id: &str, p: serde_json::Value) -> bool {
48    match s.execute(id, p) {
49        Ok(_) => true,
50        Err(e) => {
51            log::warn!("sample: {id} failed: {e}");
52            false
53        }
54    }
55}
56
57fn sales(s: &mut Session) -> bool {
58    let rows = json!([
59        ["Region", "Q1", "Q2", "Q3", "Q4", "Total", "Growth", "Trend"],
60        ["North", 48200, 51340, 56010, 61870],
61        ["South", 39150, 37980, 42400, 45120],
62        ["East", 55300, 58910, 57230, 63480],
63        ["West", 61020, 64400, 70150, 76390],
64        ["Central", 28740, 31260, 33980, 36510],
65        ["Online", 72410, 81630, 95220, 112840],
66    ]);
67    let mut ok = run(s, "range.setValues", json!({"range": "B4", "values": rows}));
68    ok &= run(s, "range.setValues", json!({"range": "B2", "values": [["Quarterly Sales Dashboard"]]}));
69    ok &= run(s, "home.cellStyle", json!({"range": "B2", "name": "Title"}));
70    ok &= run(s, "range.setValues", json!({"range": "B3", "values": [["Fiscal year 2026 · all figures in USD"]]}));
71    ok &= run(s, "home.cellStyle", json!({"range": "B3", "name": "Explanatory Text"}));
72    for r in 5..=10 {
73        ok &= run(s, "cell.set", json!({"cell": format!("G{r}"), "input": format!("=SUM(C{r}:F{r})")}));
74        ok &= run(s, "cell.set", json!({"cell": format!("H{r}"), "input": format!("=F{r}/C{r}-1")}));
75        ok &= run(s, "insert.sparkline", json!({"range": format!("C{r}:F{r}"), "location": format!("I{r}"), "type": "line", "markers": true}));
76    }
77    ok &= run(s, "insert.table", json!({"range": "B4:I10", "style": "TableStyleMedium2", "name": "Sales"}));
78    ok &= run(s, "table.totalRow", json!({"table": "Sales", "on": true}));
79    for col in ["Q1", "Q2", "Q3", "Q4", "Total"] {
80        ok &= run(s, "table.totalFunction", json!({"table": "Sales", "column": col, "function": "sum"}));
81    }
82    ok &= run(s, "table.totalFunction", json!({"table": "Sales", "column": "Growth", "function": "average"}));
83    ok &= run(s, "table.totalFunction", json!({"table": "Sales", "column": "Trend", "function": "none"}));
84    ok &= run(s, "home.numberFormat", json!({"range": "C5:G11", "code": "\"$\"#,##0"}));
85    ok &= run(s, "home.numberFormat", json!({"range": "H5:H11", "code": "0.0%"}));
86    ok &= run(s, "home.conditionalFormat", json!({"range": "G5:G10", "rule": {"type": "dataBar", "color": "#5B9BD5"}}));
87    ok &= run(s, "home.conditionalFormat", json!({"range": "H5:H10", "rule": {"type": "iconSet", "set": "3Arrows"}}));
88    ok &= run(s, "home.columnWidth", json!({"cols": "A:A", "width": 20}));
89    ok &= run(s, "home.columnWidth", json!({"cols": "B:B", "width": 132}));
90    ok &= run(s, "home.columnWidth", json!({"cols": "C:G", "width": 84}));
91    ok &= run(s, "home.columnWidth", json!({"cols": "H:I", "width": 76}));
92    ok &= run(s, "selection.set", json!({"range": "B4:F10"}));
93    ok &= run(s, "insert.chart", json!({"type": "column", "title": "Revenue by Region", "at": "K3", "width": 460, "height": 270}));
94    ok &= run(s, "selection.set", json!({"range": "B4:B10"}));
95    // Pie of totals.
96    ok &=
97        run(s, "insert.chart", json!({"range": "B4:B10", "type": "pie", "title": "Share of Annual Total", "at": "K19", "width": 460, "height": 260}));
98    if let Ok(d) = s.doc_mut() {
99        let wb = Arc::make_mut(&mut d.wb);
100        if let Some(sh) = wb.sheet_mut(0)
101            && let Some(c) = sh.charts.last_mut()
102        {
103            c.series = vec![crate::model::Series {
104                name: Some("Sales!$G$4".into()),
105                categories: Some("Sales!$B$5:$B$10".into()),
106                values: "Sales!$G$5:$G$10".into(),
107                bubble_sizes: None,
108                color: None,
109                secondary: false,
110                kind: None,
111                quartiles: Default::default(),
112                smooth: false,
113                trendlines: Vec::new(),
114            }];
115        }
116    }
117    ok &= run(s, "range.setValues", json!({"range": "B14", "values": [["Best region"], ["Average quarter"], ["Online share"], ["Year-over-year"]]}));
118    ok &= run(s, "cell.set", json!({"cell": "C14", "input": "=INDEX(Sales[Region],MATCH(MAX(Sales[Total]),Sales[Total],0))"}));
119    ok &= run(s, "cell.set", json!({"cell": "C15", "input": "=AVERAGE(C5:F10)"}));
120    ok &= run(s, "cell.set", json!({"cell": "C16", "input": "=G10/SUM(G5:G10)"}));
121    ok &= run(s, "cell.set", json!({"cell": "C17", "input": "=SUM(F5:F10)/SUM(C5:C10)-1"}));
122    ok &= run(s, "home.numberFormat", json!({"range": "C15", "code": "\"$\"#,##0"}));
123    ok &= run(s, "home.numberFormat", json!({"range": "C16:C17", "code": "0.0%"}));
124    ok &= run(s, "home.bold", json!({"range": "B14:B17", "on": true}));
125    ok &= run(s, "home.borders", json!({"range": "B14:C17", "preset": "outside"}));
126    ok &= run(s, "home.fillColor", json!({"range": "B14:C17", "color": "#F2F7FC"}));
127    ok &= run(s, "review.newNote", json!({"cell": "H4", "text": "Q4 vs Q1 growth for each region."}));
128    ok &= run(s, "selection.set", json!({"cell": "B2"}));
129    ok
130}
131
132fn budget(s: &mut Session) -> bool {
133    let rows = json!([
134        ["Category", "Budget", "Actual", "Difference", "% Used"],
135        ["Rent", 1850, 1850],
136        ["Groceries", 620, 684.35],
137        ["Utilities", 240, 212.8],
138        ["Transport", 180, 203.5],
139        ["Internet & phone", 95, 95],
140        ["Insurance", 210, 210],
141        ["Dining out", 200, 265.9],
142        ["Entertainment", 120, 88.25],
143        ["Savings", 600, 600],
144        ["Art supplies", 150, 172.4],
145    ]);
146    let mut ok = run(s, "range.setValues", json!({"range": "B2", "values": [["Monthly Budget — October 2026"]]}));
147    ok &= run(s, "home.cellStyle", json!({"range": "B2", "name": "Title"}));
148    ok &= run(s, "range.setValues", json!({"range": "B4", "values": rows}));
149    for r in 5..=14 {
150        ok &= run(s, "cell.set", json!({"cell": format!("E{r}"), "input": format!("=C{r}-D{r}")}));
151        ok &= run(s, "cell.set", json!({"cell": format!("F{r}"), "input": format!("=D{r}/C{r}")}));
152    }
153    ok &= run(s, "range.setValues", json!({"range": "B15", "values": [["Total"]]}));
154    for c in ["C", "D", "E"] {
155        ok &= run(s, "cell.set", json!({"cell": format!("{c}15"), "input": format!("=SUM({c}5:{c}14)")}));
156    }
157    ok &= run(s, "cell.set", json!({"cell": "F15", "input": "=D15/C15"}));
158    ok &= run(s, "home.cellStyle", json!({"range": "B4:F4", "name": "Heading 3"}));
159    ok &= run(s, "home.cellStyle", json!({"range": "B15:F15", "name": "Total"}));
160    ok &= run(s, "home.numberFormat", json!({"range": "C5:E15", "format": "Accounting"}));
161    ok &= run(s, "home.numberFormat", json!({"range": "F5:F15", "code": "0%"}));
162    ok &= run(
163        s,
164        "home.conditionalFormat",
165        json!({"range": "E5:E14", "rule": {"type": "cellIs", "operator": "less", "value": 0, "preset": "lightRedFillDarkRedText"}}),
166    );
167    ok &= run(s, "home.conditionalFormat", json!({"range": "F5:F14", "rule": {"type": "colorScale", "colors": ["#63BE7B", "#FFEB84", "#F8696B"]}}));
168    ok &= run(s, "home.columnWidth", json!({"cols": "B:B", "width": 130}));
169    ok &= run(s, "home.columnWidth", json!({"cols": "C:E", "width": 90}));
170    ok &= run(s, "selection.set", json!({"range": "B4:D14"}));
171    ok &= run(s, "insert.chart", json!({"type": "bar", "title": "Budget vs Actual", "at": "H3", "width": 440, "height": 300}));
172    ok &= run(s, "view.freezePanes", json!({"cell": "A5"}));
173    ok &= run(s, "selection.set", json!({"cell": "B2"}));
174    ok
175}
176
177fn grades(s: &mut Session) -> bool {
178    let rows = json!([
179        ["Student", "Essay", "Midterm", "Project", "Final", "Average", "Grade"],
180        ["Amara Okafor", 92, 88, 95, 91],
181        ["Bruno Silva", 78, 82, 74, 80],
182        ["Chen Wei", 85, 91, 89, 94],
183        ["Dana Kowalski", 67, 72, 80, 70],
184        ["Elif Yılmaz", 98, 95, 97, 99],
185        ["Farid Haddad", 74, 69, 71, 77],
186        ["Grace Mensah", 88, 84, 90, 86],
187        ["Hiro Tanaka", 59, 64, 70, 62],
188    ]);
189    let mut ok = run(s, "range.setValues", json!({"range": "A1", "values": rows}));
190    for r in 2..=9 {
191        ok &= run(s, "cell.set", json!({"cell": format!("F{r}"), "input": format!("=AVERAGE(B{r}:E{r})")}));
192        ok &= run(
193            s,
194            "cell.set",
195            json!({"cell": format!("G{r}"), "input": format!("=IFS(F{r}>=90,\"A\",F{r}>=80,\"B\",F{r}>=70,\"C\",F{r}>=60,\"D\",TRUE,\"F\")")}),
196        );
197    }
198    ok &= run(s, "home.numberFormat", json!({"range": "F2:F9", "code": "0.0"}));
199    ok &= run(s, "insert.table", json!({"range": "A1:G9", "style": "TableStyleLight9", "name": "Grades"}));
200    ok &= run(s, "home.columnWidth", json!({"cols": "A:A", "width": 120}));
201    ok &= run(
202        s,
203        "home.conditionalFormat",
204        json!({"range": "G2:G9", "rule": {"type": "cellIs", "operator": "equal", "value": "A", "preset": "greenFillDarkGreenText"}}),
205    );
206    ok &= run(s, "home.conditionalFormat", json!({"range": "F2:F9", "rule": {"type": "dataBar", "color": "#63BE7B"}}));
207    ok
208}
209
210/// A blank sheet (for callers that need one without a session).
211pub fn blank_sheet(name: &str) -> Sheet {
212    Sheet::new(name)
213}