1use std::collections::BTreeSet;
52
53use crate::calc::Values;
54use crate::messages::{raise, Refusal};
55use crate::projection::{Projection, Window};
56use crate::rows::{line as relation, quoted};
57use crate::rows::{Row, Value};
58use crate::sheet::{
59 cell_ref, column_letters, formula_text, number_text, parse_a1_range, shared_masters, A1Range,
60 Cell, Column, Content, NumFmt, Sheet, Style, Workbook,
61};
62
63#[derive(Clone, Copy, Debug, Default)]
68pub struct Grid<'v> {
69 pub values: Option<&'v dyn Values>,
70}
71
72impl<'v> Grid<'v> {
73 pub fn valued(values: &'v dyn Values) -> Grid<'v> {
75 Grid {
76 values: Some(values),
77 }
78 }
79}
80
81impl Projection for Grid<'_> {
82 fn name(&self) -> &str {
83 "grid"
84 }
85
86 fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
87 for s in &model.sheets {
88 out(&relation(&Row::Sheet {
89 name: &s.name,
90 visibility: s.visibility,
91 }));
92 }
93 let Some(i) = model.find_sheet(&window.sheet) else {
94 out(&format!(
97 "(window {} {})",
98 quoted(&window.sheet),
99 quoted(&window.range.text())
100 ));
101 return;
102 };
103 let sheet = &model.sheets[i];
104 let name = sheet.name.as_str();
105 let range = window.range;
106 out(&format!(
107 "(window {} {})",
108 quoted(name),
109 quoted(&range.text())
110 ));
111 match extent_of(sheet) {
112 Some(e) => out(&format!("(extent {} {})", quoted(name), quoted(&e.text()))),
113 None => out(&format!("(extent {} none)", quoted(name))),
114 }
115 out(&format!(
116 "(gridlines {} {})",
117 quoted(name),
118 if sheet.gridlines { "on" } else { "off" }
119 ));
120 let mut columns: Vec<&Column> = sheet
121 .columns
122 .iter()
123 .filter(|c| c.index >= range.left && c.index <= range.right)
124 .collect();
125 columns.sort_by_key(|c| c.index);
126 let mut used: BTreeSet<u32> = BTreeSet::new();
127 used.insert(0);
128 for c in &columns {
129 let width = c
130 .width
131 .and_then(number_text)
132 .unwrap_or_else(|| "none".to_string());
133 let style = match c.style {
134 Some(s) => {
135 used.insert(s);
136 s.to_string()
137 }
138 None => "none".to_string(),
139 };
140 out(&format!(
141 "(column {} {} {} {} {})",
142 quoted(name),
143 quoted(&column_letters(c.index)),
144 width,
145 if c.hidden { "hidden" } else { "shown" },
146 style
147 ));
148 }
149 for (n, r) in sheet.rows.range(range.top..=range.bottom) {
152 let height = r
153 .height
154 .and_then(number_text)
155 .unwrap_or_else(|| "none".to_string());
156 out(&format!(
157 "(row {} {} {} {})",
158 quoted(name),
159 n,
160 height,
161 if r.hidden { "hidden" } else { "shown" }
162 ));
163 }
164 let cells: Vec<(&(u32, u32), &Cell)> = sheet
165 .cells
166 .range((range.top, 0)..=(range.bottom, u32::MAX))
167 .filter(|((_, col), _)| *col >= range.left && *col <= range.right)
168 .collect();
169 for (_, cell) in &cells {
170 used.insert(cell.style);
171 }
172 for (_, index) in &sheet.looks {
173 used.insert(*index);
174 }
175 for index in used {
176 out(&format_row(index, model.styles.xfs().get(index as usize)));
177 }
178 for (value, index) in &sheet.looks {
181 out(&format!("(look {} {} {})", quoted(name), value, index));
182 }
183 let masters = shared_masters(sheet);
184 let lags = self.values.filter(|v| v.lags(i));
187 for ((row, col), cell) in cells {
188 let addr = cell_ref(*row, *col);
189 let newer = lags.and_then(|v| v.content((i, *row, *col)));
190 let printed = match newer.as_ref().unwrap_or(&cell.content) {
191 Content::Text(t) => {
192 let value = Value::Text(t.clone());
193 out(&relation(&Row::Cell {
194 sheet: name,
195 addr: &addr,
196 value: &value,
197 }));
198 true
199 }
200 Content::Number(n) => {
201 let value = match number_text(*n) {
204 Some(text) => Value::Number(text),
205 None => Value::Error("#NUM!".to_string()),
206 };
207 out(&relation(&Row::Cell {
208 sheet: name,
209 addr: &addr,
210 value: &value,
211 }));
212 true
213 }
214 Content::Bool(b) => {
215 let value = Value::Bool(*b);
216 out(&relation(&Row::Cell {
217 sheet: name,
218 addr: &addr,
219 value: &value,
220 }));
221 true
222 }
223 Content::Error(e) => {
224 let value = Value::Error(e.clone());
225 out(&relation(&Row::Cell {
226 sheet: name,
227 addr: &addr,
228 value: &value,
229 }));
230 true
231 }
232 other => match formula_text(other, *row, *col, &masters) {
233 Some(text) => {
234 let text = format!("={text}");
235 out(&relation(&Row::Formula {
236 sheet: name,
237 addr: &addr,
238 text: &text,
239 }));
240 if let Some(computed) =
241 self.values.and_then(|calc| calc.computed((i, *row, *col)))
242 {
243 out(&format!(
244 "(value {} {} {})",
245 quoted(name),
246 quoted(&addr),
247 computed.spell()
248 ));
249 }
250 true
251 }
252 None => false,
253 },
254 };
255 if !printed {
256 continue;
257 }
258 if cell.style != 0 {
259 out(&format!(
260 "(style {} {} {})",
261 quoted(name),
262 quoted(&addr),
263 cell.style
264 ));
265 }
266 if let Some(r) = sheet.sentences.get(&(*row, *col)) {
267 out(&format!(
268 "(sentence {} {} {})",
269 quoted(name),
270 quoted(&addr),
271 r
272 ));
273 }
274 }
275 }
276}
277
278fn format_row(index: u32, style: Option<&Style>) -> String {
282 let default = Style::default();
283 let s = style.unwrap_or(&default);
284 let fill = match s.fill {
285 Some(rgb) => quoted(&format!("{:02X}{:02X}{:02X}", rgb.0, rgb.1, rgb.2)),
286 None => "none".to_string(),
287 };
288 let num = match s.num_fmt {
289 NumFmt::General => "general",
290 NumFmt::Text => "text",
291 };
292 format!(
293 "(format {index} {fill} {num} {})",
294 if s.wrap { "wrap" } else { "nowrap" }
295 )
296}
297
298fn extent_of(sheet: &Sheet) -> Option<A1Range> {
300 sheet
301 .extent()
302 .map(|((top, left), (bottom, right))| A1Range {
303 top,
304 left,
305 bottom,
306 right,
307 })
308}
309
310pub fn whole(sheet: &Sheet) -> A1Range {
313 extent_of(sheet).unwrap_or(A1Range {
314 top: 1,
315 left: 1,
316 bottom: 1,
317 right: 1,
318 })
319}
320
321pub fn window_of(model: &Workbook, sheet: &str, range: Option<&str>) -> Result<Window, Refusal> {
329 let Some(i) = model.find_sheet(sheet) else {
330 let sheets = model
331 .sheets
332 .iter()
333 .map(|s| s.name.as_str())
334 .collect::<Vec<_>>()
335 .join(", ");
336 return Err(raise(
337 "view-sheet-unknown",
338 &[("name", sheet), ("sheets", &sheets)],
339 ));
340 };
341 let s = &model.sheets[i];
342 let range = match range {
343 Some(text) => parse_a1_range(text)
344 .ok_or_else(|| raise("view-window-not-a-range", &[("text", text)]))?,
345 None => whole(s),
346 };
347 Ok(Window {
348 sheet: s.name.clone(),
349 range,
350 })
351}
352
353pub fn view_text(model: &Workbook, window: &Window) -> String {
356 let mut out = String::new();
357 Grid::default().project(model, window, &mut |line| {
358 out.push_str(line);
359 out.push('\n');
360 });
361 out
362}
363
364pub fn view_text_valued(model: &Workbook, window: &Window, values: &dyn Values) -> String {
369 let mut out = String::new();
370 Grid::valued(values).project(model, window, &mut |line| {
371 out.push_str(line);
372 out.push('\n');
373 });
374 out
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::sheet::{Rgb, Styles};
381
382 fn range(text: &str) -> A1Range {
383 parse_a1_range(text).unwrap()
384 }
385
386 fn lines(text: &str) -> Vec<&str> {
387 text.lines().collect()
388 }
389
390 fn a_model() -> Workbook {
394 let mut wb = Workbook::new();
395 let mut styles = Styles::new();
396 let lavender = styles.id(Style {
397 fill: Some(Rgb(247, 244, 252)),
398 num_fmt: NumFmt::Text,
399 wrap: false,
400 });
401 let gray = styles.id(Style {
402 fill: Some(Rgb(242, 242, 242)),
403 num_fmt: NumFmt::General,
404 wrap: true,
405 });
406 wb.styles = styles;
407 let mut model = Sheet::new("Model");
408 model.gridlines = false;
409 model.columns = vec![
410 Column {
411 index: 3,
412 width: Some(60.0),
413 hidden: false,
414 style: Some(gray),
415 },
416 Column {
417 index: 1,
418 width: None,
419 hidden: true,
420 style: None,
421 },
422 Column {
423 index: 2,
424 width: Some(72.0),
425 hidden: false,
426 style: Some(lavender),
427 },
428 ];
429 model.set(
430 1,
431 2,
432 Cell {
433 content: Content::Text("Revenue".to_string()),
434 style: lavender,
435 },
436 );
437 model.set(
438 2,
439 2,
440 Cell {
441 content: Content::Number(1200.5),
442 style: 0,
443 },
444 );
445 model.set(
446 3,
447 2,
448 Cell {
449 content: Content::Bool(true),
450 style: 0,
451 },
452 );
453 model.set(
454 4,
455 2,
456 Cell {
457 content: Content::Number(f64::INFINITY),
458 style: 0,
459 },
460 );
461 model.set_formula(range("C2:C4"), "B2*2", 0);
462 model.set_formula_dynamic(range("D2"), "_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")", 0);
463 model.wrote(range("B1"), 1);
464 model.wrote(range("B2:B4"), 2);
465 model.wrote(range("C2:C4"), 3);
466 model.wrote(range("D2"), 4);
467 wb.sheets.push(model);
468 wb.sheets.push(Sheet::new("Empty"));
469 wb
470 }
471
472 #[test]
473 fn the_record_s_rows_have_the_fixed_spelling_in_the_fixed_order() {
474 let wb = a_model();
475 let text = view_text(
476 &wb,
477 &Window {
478 sheet: "Model".to_string(),
479 range: range("A1:F20"),
480 },
481 );
482 assert_eq!(
483 lines(&text),
484 vec![
485 "(sheet \"Model\" visible)",
486 "(sheet \"Empty\" visible)",
487 "(window \"Model\" \"A1:F20\")",
488 "(extent \"Model\" \"B1:D4\")",
489 "(gridlines \"Model\" off)",
490 "(column \"Model\" \"A\" none hidden none)",
491 "(column \"Model\" \"B\" 72 shown 1)",
492 "(column \"Model\" \"C\" 60 shown 2)",
493 "(format 0 none general nowrap)",
494 "(format 1 \"F7F4FC\" text nowrap)",
495 "(format 2 \"F2F2F2\" general wrap)",
496 "(cell \"Model\" \"B1\" \"Revenue\")",
497 "(style \"Model\" \"B1\" 1)",
498 "(sentence \"Model\" \"B1\" 1)",
499 "(cell \"Model\" \"B2\" 1200.5)",
500 "(sentence \"Model\" \"B2\" 2)",
501 "(formula \"Model\" \"C2\" \"=B2*2\")",
502 "(sentence \"Model\" \"C2\" 3)",
503 "(formula \"Model\" \"D2\" \"=IFS(B2>3,\\\"big\\\",TRUE,\\\"small\\\")\")",
504 "(sentence \"Model\" \"D2\" 4)",
505 "(cell \"Model\" \"B3\" true)",
506 "(sentence \"Model\" \"B3\" 2)",
507 "(formula \"Model\" \"C3\" \"=B3*2\")",
508 "(sentence \"Model\" \"C3\" 3)",
509 "(cell \"Model\" \"B4\" (error \"#NUM!\"))",
510 "(sentence \"Model\" \"B4\" 2)",
511 "(formula \"Model\" \"C4\" \"=B4*2\")",
512 "(sentence \"Model\" \"C4\" 3)",
513 ]
514 );
515 }
516
517 #[test]
518 fn a_window_clips_to_its_rectangle_and_an_empty_one_prints_its_header_alone() {
519 let wb = a_model();
520 let text = view_text(
523 &wb,
524 &Window {
525 sheet: "Model".to_string(),
526 range: range("C3:D4"),
527 },
528 );
529 assert_eq!(
530 lines(&text),
531 vec![
532 "(sheet \"Model\" visible)",
533 "(sheet \"Empty\" visible)",
534 "(window \"Model\" \"C3:D4\")",
535 "(extent \"Model\" \"B1:D4\")",
536 "(gridlines \"Model\" off)",
537 "(column \"Model\" \"C\" 60 shown 2)",
538 "(format 0 none general nowrap)",
539 "(format 2 \"F2F2F2\" general wrap)",
540 "(formula \"Model\" \"C3\" \"=B3*2\")",
541 "(sentence \"Model\" \"C3\" 3)",
542 "(formula \"Model\" \"C4\" \"=B4*2\")",
543 "(sentence \"Model\" \"C4\" 3)",
544 ]
545 );
546 let text = view_text(
548 &wb,
549 &Window {
550 sheet: "Model".to_string(),
551 range: range("H10:J12"),
552 },
553 );
554 assert_eq!(
555 lines(&text)[2..],
556 [
557 "(window \"Model\" \"H10:J12\")",
558 "(extent \"Model\" \"B1:D4\")",
559 "(gridlines \"Model\" off)",
560 "(format 0 none general nowrap)",
561 ]
562 );
563 let text = view_text(
565 &wb,
566 &Window {
567 sheet: "Empty".to_string(),
568 range: whole(&wb.sheets[1]),
569 },
570 );
571 assert_eq!(
572 lines(&text)[2..],
573 [
574 "(window \"Empty\" \"A1\")",
575 "(extent \"Empty\" none)",
576 "(gridlines \"Empty\" on)",
577 "(format 0 none general nowrap)",
578 ]
579 );
580 let text = view_text(
583 &wb,
584 &Window {
585 sheet: "Nowhere".to_string(),
586 range: range("A1"),
587 },
588 );
589 assert_eq!(lines(&text).len(), 3);
590 assert_eq!(lines(&text)[2], "(window \"Nowhere\" \"A1\")");
591 }
592
593 #[test]
594 fn row_settings_looks_and_hidden_sheets_print_in_their_places() {
595 let mut wb = a_model();
599 wb.sheets[1].visibility = crate::rows::Visibility::Hidden;
600 let model = &mut wb.sheets[0];
601 model.rows.insert(
602 3,
603 crate::sheet::RowSettings {
604 height: Some(30.0),
605 hidden: false,
606 },
607 );
608 model.rows.insert(
609 9,
610 crate::sheet::RowSettings {
611 height: None,
612 hidden: true,
613 },
614 );
615 model.looks.push(("1".to_string(), 2));
616 let text = view_text(
617 &wb,
618 &Window {
619 sheet: "Model".to_string(),
620 range: range("B2:B4"),
621 },
622 );
623 assert_eq!(
624 lines(&text)[..10],
625 [
626 "(sheet \"Model\" visible)",
627 "(sheet \"Empty\" hidden)",
628 "(window \"Model\" \"B2:B4\")",
629 "(extent \"Model\" \"B1:D4\")",
630 "(gridlines \"Model\" off)",
631 "(column \"Model\" \"B\" 72 shown 1)",
632 "(row \"Model\" 3 30 shown)",
633 "(format 0 none general nowrap)",
634 "(format 1 \"F7F4FC\" text nowrap)",
635 "(format 2 \"F2F2F2\" general wrap)",
636 ]
637 );
638 assert_eq!(lines(&text)[10], "(look \"Model\" 1 2)");
639 assert!(
640 !text.contains("(row \"Model\" 9"),
641 "row 9 is outside the window"
642 );
643 }
644
645 #[test]
646 fn window_of_finds_the_sheet_without_case_and_refuses_the_rest_by_name() {
647 let wb = a_model();
648 let w = window_of(&wb, "MODEL", None).unwrap();
649 assert_eq!(w.sheet, "Model");
650 assert_eq!(w.range, range("B1:D4"));
651 let w = window_of(&wb, "empty", None).unwrap();
652 assert_eq!((w.sheet.as_str(), w.range.text().as_str()), ("Empty", "A1"));
653 let w = window_of(&wb, "Model", Some("f20:a1")).unwrap();
654 assert_eq!(w.range.text(), "A1:F20");
655 let r = window_of(&wb, "Summary", None).unwrap_err();
656 assert_eq!(
657 (r.id.as_str(), r.source.as_str()),
658 ("view-sheet-unknown", "VLA-View")
659 );
660 assert!(r.text.contains("Summary"), "{}", r.text);
661 assert!(r.text.contains("Model, Empty"), "{}", r.text);
662 for text in ["A:A", "3:3", "nonsense", "", "A1:B"] {
663 let r = window_of(&wb, "Model", Some(text)).unwrap_err();
664 assert_eq!(r.id, "view-window-not-a-range", "{text}");
665 assert!(r.text.contains(text), "{}", r.text);
666 }
667 assert_eq!(Grid::default().name(), "grid");
668 }
669}