1use 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
17pub 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 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
210pub fn blank_sheet(name: &str) -> Sheet {
212 Sheet::new(name)
213}