1use std::collections::{BTreeSet, HashMap};
83
84use crate::calc::{self, Extent, Values};
85use crate::machine::load::Datum;
86use crate::messages::{raise, Refusal};
87use crate::projection::{Projection, Window};
88use crate::rows::{line as relation, quoted};
89use crate::rows::{Row, Value};
90use crate::sheet::{
91 cell_ref, column_letters, formula_text, number_text, parse_a1_range, shared_masters, A1Range,
92 Cell, Column, Content, NumFmt, Sheet, Style, Styles, Workbook,
93};
94
95#[derive(Clone, Copy, Debug, Default)]
100pub struct Grid<'v> {
101 pub values: Option<&'v dyn Values>,
102}
103
104impl<'v> Grid<'v> {
105 pub fn valued(values: &'v dyn Values) -> Grid<'v> {
107 Grid {
108 values: Some(values),
109 }
110 }
111}
112
113impl Projection for Grid<'_> {
114 fn name(&self) -> &str {
115 "grid"
116 }
117
118 fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
119 self.rows(model, window, true, out);
120 }
121}
122
123#[derive(Clone, Copy, Debug, Default)]
129pub struct ValueGrid<'v> {
130 pub values: Option<&'v dyn Values>,
131}
132
133impl<'v> ValueGrid<'v> {
134 pub fn valued(values: &'v dyn Values) -> ValueGrid<'v> {
136 ValueGrid {
137 values: Some(values),
138 }
139 }
140}
141
142impl Projection for ValueGrid<'_> {
143 fn name(&self) -> &str {
144 "values"
145 }
146
147 fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
148 Grid {
149 values: self.values,
150 }
151 .rows(model, window, false, out);
152 }
153}
154
155impl Grid<'_> {
156 fn rows(&self, model: &Workbook, window: &Window, formulas: bool, out: &mut dyn FnMut(&str)) {
160 for s in &model.sheets {
161 out(&relation(&Row::Sheet {
162 name: &s.name,
163 visibility: s.visibility,
164 }));
165 }
166 let Some(i) = model.find_sheet(&window.sheet) else {
167 out(&format!(
170 "(window {} {})",
171 quoted(&window.sheet),
172 quoted(&window.range.text())
173 ));
174 return;
175 };
176 let sheet = &model.sheets[i];
177 let name = sheet.name.as_str();
178 let range = window.range;
179 out(&format!(
180 "(window {} {})",
181 quoted(name),
182 quoted(&range.text())
183 ));
184 match extent_in(sheet, i, self.values) {
185 Some(e) => out(&format!("(extent {} {})", quoted(name), quoted(&e.text()))),
186 None => out(&format!("(extent {} none)", quoted(name))),
187 }
188 out(&format!(
189 "(gridlines {} {})",
190 quoted(name),
191 if sheet.gridlines { "on" } else { "off" }
192 ));
193 let mut columns: Vec<&Column> = sheet
194 .columns
195 .iter()
196 .filter(|c| c.index >= range.left && c.index <= range.right)
197 .collect();
198 columns.sort_by_key(|c| c.index);
199 let mut used: BTreeSet<u32> = BTreeSet::new();
200 used.insert(0);
201 for c in &columns {
202 let width = c
203 .width
204 .and_then(number_text)
205 .unwrap_or_else(|| "none".to_string());
206 let style = match c.style {
207 Some(s) => {
208 used.insert(s);
209 s.to_string()
210 }
211 None => "none".to_string(),
212 };
213 out(&format!(
214 "(column {} {} {} {} {})",
215 quoted(name),
216 quoted(&column_letters(c.index)),
217 width,
218 if c.hidden { "hidden" } else { "shown" },
219 style
220 ));
221 }
222 for (n, r) in sheet.rows.range(range.top..=range.bottom) {
225 let height = r
226 .height
227 .and_then(number_text)
228 .unwrap_or_else(|| "none".to_string());
229 out(&format!(
230 "(row {} {} {} {})",
231 quoted(name),
232 n,
233 height,
234 if r.hidden { "hidden" } else { "shown" }
235 ));
236 }
237 let cells: Vec<(&(u32, u32), &Cell)> = sheet
238 .cells
239 .range((range.top, 0)..=(range.bottom, u32::MAX))
240 .filter(|((_, col), _)| *col >= range.left && *col <= range.right)
241 .collect();
242 for (_, cell) in &cells {
243 used.insert(cell.style);
244 }
245 for (_, index) in &sheet.looks {
246 used.insert(*index);
247 }
248 for index in used {
249 out(&format_row(index, model.styles.xfs().get(index as usize)));
250 }
251 for (value, index) in &sheet.looks {
254 out(&format!("(look {} {} {})", quoted(name), value, index));
255 }
256 let mut masters = None;
261 let lags = self.values.filter(|v| v.lags(i));
264 let coded = Coded::of(model, sheet);
265 #[cfg(test)]
266 crate::sheet::work::viewed(cells.len());
267 for (&(row, col), cell) in cells {
268 self.cell_rows(
269 sheet,
270 i,
271 (row, col),
272 cell,
273 lags,
274 formulas,
275 &mut masters,
276 coded.as_ref(),
277 out,
278 );
279 }
280 }
281
282 #[allow(clippy::too_many_arguments)]
291 fn cell_rows<'s>(
292 &self,
293 sheet: &'s Sheet,
294 i: usize,
295 (row, col): (u32, u32),
296 cell: &Cell,
297 lags: Option<&dyn Values>,
298 formulas: bool,
299 masters: &mut Option<HashMap<u32, (&'s str, u32, u32)>>,
300 coded: Option<&Coded<'_>>,
301 out: &mut dyn FnMut(&str),
302 ) -> bool {
303 let name = sheet.name.as_str();
304 let addr = cell_ref(row, col);
305 let newer = lags.and_then(|v| v.content((i, row, col)));
306 let (printed, shown) = match newer.as_ref().unwrap_or(&cell.content) {
308 Content::Text(t) => {
309 let value = Value::Text(t.clone());
310 out(&relation(&Row::Cell {
311 sheet: name,
312 addr: &addr,
313 value: &value,
314 }));
315 (true, Some(calc::Value::Text(t.clone())))
316 }
317 Content::Number(n) => {
318 let value = match number_text(*n) {
321 Some(text) => Value::Number(text),
322 None => Value::Error("#NUM!".to_string()),
323 };
324 out(&relation(&Row::Cell {
325 sheet: name,
326 addr: &addr,
327 value: &value,
328 }));
329 (true, Some(calc::Value::Number(*n)))
330 }
331 Content::Bool(b) => {
332 let value = Value::Bool(*b);
333 out(&relation(&Row::Cell {
334 sheet: name,
335 addr: &addr,
336 value: &value,
337 }));
338 (true, None)
339 }
340 Content::Error(e) => {
341 let value = Value::Error(e.clone());
342 out(&relation(&Row::Cell {
343 sheet: name,
344 addr: &addr,
345 value: &value,
346 }));
347 (true, None)
348 }
349 other => {
350 let computed = self.values.and_then(|calc| calc.computed((i, row, col)));
351 let held = if formulas {
355 let walked = masters.get_or_insert_with(|| shared_masters(sheet));
356 match formula_text(other, row, col, walked) {
357 Some(text) => {
358 let text = format!("={text}");
359 out(&relation(&Row::Formula {
360 sheet: name,
361 addr: &addr,
362 text: &text,
363 }));
364 true
365 }
366 None => false,
367 }
368 } else {
369 match other {
370 Content::SharedChild { si } if computed.is_none() => masters
374 .get_or_insert_with(|| shared_masters(sheet))
375 .contains_key(si),
376 _ => true,
377 }
378 };
379 let mut shown = None;
380 if let Some(computed) = computed.filter(|_| held) {
381 out(&format!(
382 "(value {} {} {})",
383 quoted(name),
384 quoted(&addr),
385 computed.spell()
386 ));
387 if let calc::Computed::Value(v) = computed {
389 shown = Some(v.clone().settled());
390 }
391 }
392 (held, shown)
393 }
394 };
395 if !printed {
396 return false;
397 }
398 if let (Some(coded), Some(v)) = (coded, shown) {
399 if let Some(text) = coded.text(cell.style, &v) {
400 out(&format!(
401 "(text {} {} {})",
402 quoted(name),
403 quoted(&addr),
404 quoted(&text)
405 ));
406 }
407 }
408 if cell.style != 0 {
409 out(&format!(
410 "(style {} {} {})",
411 quoted(name),
412 quoted(&addr),
413 cell.style
414 ));
415 }
416 if let Some(r) = sheet.sentences.get(&(row, col)) {
417 out(&format!(
418 "(sentence {} {} {})",
419 quoted(name),
420 quoted(&addr),
421 r
422 ));
423 }
424 true
425 }
426}
427
428fn format_row(index: u32, style: Option<&Style>) -> String {
433 let default = Style::default();
434 let s = style.unwrap_or(&default);
435 let fill = match s.fill {
436 Some(rgb) => quoted(&format!("{:02X}{:02X}{:02X}", rgb.0, rgb.1, rgb.2)),
437 None => "none".to_string(),
438 };
439 let num = match &s.num_fmt {
440 NumFmt::General => "general".to_string(),
441 NumFmt::Text => "text".to_string(),
442 NumFmt::Code(code) => quoted(code.as_str()),
443 };
444 format!(
445 "(format {index} {fill} {num} {})",
446 if s.wrap { "wrap" } else { "nowrap" }
447 )
448}
449
450struct Coded<'m> {
455 styles: &'m Styles,
456 looks: Vec<(Look, u32)>,
457}
458
459enum Look {
464 Number(f64),
465 Text(String),
466}
467
468impl<'m> Coded<'m> {
469 fn of(model: &'m Workbook, sheet: &Sheet) -> Option<Coded<'m>> {
470 if !model.styles.has_code() {
471 return None;
472 }
473 let looks = sheet
474 .looks
475 .iter()
476 .filter_map(|(spelled, index)| {
477 let forms = crate::reader::read_forms(spelled).ok()?;
478 let look = match crate::machine::load::datum(forms.first()?)? {
479 Datum::Number(n) => Look::Number(n),
480 Datum::Text(t) => Look::Text(t),
481 _ => return None,
482 };
483 Some((look, *index))
484 })
485 .collect();
486 Some(Coded {
487 styles: &model.styles,
488 looks,
489 })
490 }
491
492 fn text(&self, style: u32, value: &calc::Value) -> Option<String> {
498 let met = self.looks.iter().find(|(look, _)| match (look, value) {
499 (Look::Number(a), calc::Value::Number(b)) => a == b,
500 (Look::Text(a), calc::Value::Text(b)) => a == b,
501 _ => false,
502 });
503 let index = met.map_or(style, |(_, index)| *index);
504 match self.styles.get(index).map(|s| &s.num_fmt) {
505 Some(NumFmt::Code(code)) => code.text(value),
506 _ => None,
507 }
508 }
509}
510
511fn extent_of(sheet: &Sheet) -> Option<A1Range> {
513 rectangle(sheet.extent())
514}
515
516fn extent_in(sheet: &Sheet, i: usize, values: Option<&dyn Values>) -> Option<A1Range> {
519 match values.and_then(|v| v.kept_extent(i)) {
520 Some(kept) => rectangle(kept),
521 None => extent_of(sheet),
522 }
523}
524
525fn rectangle(extent: Extent) -> Option<A1Range> {
527 extent.map(|((top, left), (bottom, right))| A1Range {
528 top,
529 left,
530 bottom,
531 right,
532 })
533}
534
535pub fn whole(sheet: &Sheet) -> A1Range {
538 or_a1(extent_of(sheet))
539}
540
541fn or_a1(range: Option<A1Range>) -> A1Range {
543 range.unwrap_or(A1Range {
544 top: 1,
545 left: 1,
546 bottom: 1,
547 right: 1,
548 })
549}
550
551pub fn window_of(model: &Workbook, sheet: &str, range: Option<&str>) -> Result<Window, Refusal> {
559 window_over(model, sheet, range, None)
560}
561
562pub(crate) fn window_over(
565 model: &Workbook,
566 sheet: &str,
567 range: Option<&str>,
568 values: Option<&dyn Values>,
569) -> Result<Window, Refusal> {
570 let Some(i) = model.find_sheet(sheet) else {
571 let sheets = model
572 .sheets
573 .iter()
574 .map(|s| s.name.as_str())
575 .collect::<Vec<_>>()
576 .join(", ");
577 return Err(raise(
578 "view-sheet-unknown",
579 &[("name", sheet), ("sheets", &sheets)],
580 ));
581 };
582 let s = &model.sheets[i];
583 let range = match range {
584 Some(text) => parse_a1_range(text)
585 .ok_or_else(|| raise("view-window-not-a-range", &[("text", text)]))?,
586 None => or_a1(extent_in(s, i, values)),
587 };
588 Ok(Window {
589 sheet: s.name.clone(),
590 range,
591 })
592}
593
594pub fn view_text(model: &Workbook, window: &Window) -> String {
597 let mut out = String::new();
598 Grid::default().project(model, window, &mut |line| {
599 out.push_str(line);
600 out.push('\n');
601 });
602 out
603}
604
605pub fn view_text_valued(model: &Workbook, window: &Window, values: &dyn Values) -> String {
610 let mut out = String::new();
611 Grid::valued(values).project(model, window, &mut |line| {
612 out.push_str(line);
613 out.push('\n');
614 });
615 out
616}
617
618pub fn view_text_values(model: &Workbook, window: &Window, values: &dyn Values) -> String {
622 let mut out = String::new();
623 ValueGrid::valued(values).project(model, window, &mut |line| {
624 out.push_str(line);
625 out.push('\n');
626 });
627 out
628}
629
630pub(crate) fn changes_text(
639 model: &Workbook,
640 window: &Window,
641 values: &dyn Values,
642 (since, at): (u64, u64),
643 cells: Option<&[(u32, u32)]>,
644) -> String {
645 let Some(i) = model.find_sheet(&window.sheet) else {
646 return String::new();
647 };
648 let sheet = &model.sheets[i];
649 let head = |n: &str| {
650 format!(
651 "(changes {} {} {since} {at} {n})\n",
652 quoted(&sheet.name),
653 quoted(&window.range.text())
654 )
655 };
656 let Some(cells) = cells else {
657 return head("whole");
658 };
659 let mut out = head(&cells.len().to_string());
660 #[cfg(test)]
661 crate::sheet::work::viewed(cells.len());
662 let grid = Grid::valued(values);
663 let lags = Some(values).filter(|v| v.lags(i));
664 let coded = Coded::of(model, sheet);
665 let mut masters = None;
666 let mut push = |line: &str| {
667 out.push_str(line);
668 out.push('\n');
669 };
670 for &(row, col) in cells {
671 let printed = sheet.cells.get(&(row, col)).is_some_and(|cell| {
672 grid.cell_rows(
673 sheet,
674 i,
675 (row, col),
676 cell,
677 lags,
678 false,
679 &mut masters,
680 coded.as_ref(),
681 &mut push,
682 )
683 });
684 if !printed {
685 push(&format!(
686 "(empty {} {})",
687 quoted(&sheet.name),
688 quoted(&cell_ref(row, col))
689 ));
690 }
691 }
692 out
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::sheet::{Rgb, Styles};
699
700 fn range(text: &str) -> A1Range {
701 parse_a1_range(text).unwrap()
702 }
703
704 fn lines(text: &str) -> Vec<&str> {
705 text.lines().collect()
706 }
707
708 fn a_model() -> Workbook {
712 let mut wb = Workbook::new();
713 let mut styles = Styles::new();
714 let lavender = styles.id(Style {
715 fill: Some(Rgb(247, 244, 252)),
716 num_fmt: NumFmt::Text,
717 wrap: false,
718 });
719 let gray = styles.id(Style {
720 fill: Some(Rgb(242, 242, 242)),
721 num_fmt: NumFmt::General,
722 wrap: true,
723 });
724 wb.styles = styles;
725 let mut model = Sheet::new("Model");
726 model.gridlines = false;
727 model.columns = vec![
728 Column {
729 index: 3,
730 width: Some(60.0),
731 hidden: false,
732 style: Some(gray),
733 },
734 Column {
735 index: 1,
736 width: None,
737 hidden: true,
738 style: None,
739 },
740 Column {
741 index: 2,
742 width: Some(72.0),
743 hidden: false,
744 style: Some(lavender),
745 },
746 ];
747 model.set(
748 1,
749 2,
750 Cell {
751 content: Content::Text("Revenue".to_string()),
752 style: lavender,
753 },
754 );
755 model.set(
756 2,
757 2,
758 Cell {
759 content: Content::Number(1200.5),
760 style: 0,
761 },
762 );
763 model.set(
764 3,
765 2,
766 Cell {
767 content: Content::Bool(true),
768 style: 0,
769 },
770 );
771 model.set(
772 4,
773 2,
774 Cell {
775 content: Content::Number(f64::INFINITY),
776 style: 0,
777 },
778 );
779 model.set_formula(range("C2:C4"), "B2*2", 0);
780 model.set_formula_dynamic(range("D2"), "_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")", 0);
781 model.wrote(range("B1"), 1);
782 model.wrote(range("B2:B4"), 2);
783 model.wrote(range("C2:C4"), 3);
784 model.wrote(range("D2"), 4);
785 wb.sheets.push(model);
786 wb.sheets.push(Sheet::new("Empty"));
787 wb
788 }
789
790 #[test]
791 fn the_record_s_rows_have_the_fixed_spelling_in_the_fixed_order() {
792 let wb = a_model();
793 let text = view_text(
794 &wb,
795 &Window {
796 sheet: "Model".to_string(),
797 range: range("A1:F20"),
798 },
799 );
800 assert_eq!(
801 lines(&text),
802 vec![
803 "(sheet \"Model\" visible)",
804 "(sheet \"Empty\" visible)",
805 "(window \"Model\" \"A1:F20\")",
806 "(extent \"Model\" \"B1:D4\")",
807 "(gridlines \"Model\" off)",
808 "(column \"Model\" \"A\" none hidden none)",
809 "(column \"Model\" \"B\" 72 shown 1)",
810 "(column \"Model\" \"C\" 60 shown 2)",
811 "(format 0 none general nowrap)",
812 "(format 1 \"F7F4FC\" text nowrap)",
813 "(format 2 \"F2F2F2\" general wrap)",
814 "(cell \"Model\" \"B1\" \"Revenue\")",
815 "(style \"Model\" \"B1\" 1)",
816 "(sentence \"Model\" \"B1\" 1)",
817 "(cell \"Model\" \"B2\" 1200.5)",
818 "(sentence \"Model\" \"B2\" 2)",
819 "(formula \"Model\" \"C2\" \"=B2*2\")",
820 "(sentence \"Model\" \"C2\" 3)",
821 "(formula \"Model\" \"D2\" \"=IFS(B2>3,\\\"big\\\",TRUE,\\\"small\\\")\")",
822 "(sentence \"Model\" \"D2\" 4)",
823 "(cell \"Model\" \"B3\" true)",
824 "(sentence \"Model\" \"B3\" 2)",
825 "(formula \"Model\" \"C3\" \"=B3*2\")",
826 "(sentence \"Model\" \"C3\" 3)",
827 "(cell \"Model\" \"B4\" (error \"#NUM!\"))",
828 "(sentence \"Model\" \"B4\" 2)",
829 "(formula \"Model\" \"C4\" \"=B4*2\")",
830 "(sentence \"Model\" \"C4\" 3)",
831 ]
832 );
833 }
834
835 #[test]
836 fn the_changes_print_each_cell_s_rows_as_the_values_print_them() {
837 let mut wb = a_model();
844 let library = crate::calc::library::language();
845 let calc = crate::calc::Calc::run(&wb, &library);
846 let w = Window {
847 sheet: "Model".to_string(),
848 range: range("A1:F20"),
849 };
850 let values = view_text_values(&wb, &w, &calc);
851 let rows = |addr: &str| -> String {
852 let at = format!(" \"{addr}\" ");
853 values
854 .lines()
855 .filter(|l| l.contains(&at))
856 .map(|l| format!("{l}\n"))
857 .collect()
858 };
859 let text = changes_text(&wb, &w, &calc, (6, 7), Some(&[(1, 2), (2, 3), (9, 9)]));
860 assert_eq!(
861 text,
862 format!(
863 "(changes \"Model\" \"A1:F20\" 6 7 3)\n{}{}(empty \"Model\" \"I9\")\n",
864 rows("B1"),
865 rows("C2")
866 )
867 );
868 assert_eq!(rows("B1").lines().count(), 3, "{values}");
869 assert_eq!(
870 changes_text(&wb, &w, &calc, (6, 7), None),
871 "(changes \"Model\" \"A1:F20\" 6 7 whole)\n"
872 );
873 wb.sheets[0].cells.remove(&(2, 3));
874 let lost = crate::calc::Calc::run(&wb, &library);
875 assert_eq!(
876 changes_text(&wb, &w, &lost, (0, 1), Some(&[(3, 3)])),
877 "(changes \"Model\" \"A1:F20\" 0 1 1)\n(empty \"Model\" \"C3\")\n",
878 "a fill's cell whose master is lost"
879 );
880 }
881
882 #[test]
883 fn a_window_clips_to_its_rectangle_and_an_empty_one_prints_its_header_alone() {
884 let wb = a_model();
885 let text = view_text(
888 &wb,
889 &Window {
890 sheet: "Model".to_string(),
891 range: range("C3:D4"),
892 },
893 );
894 assert_eq!(
895 lines(&text),
896 vec![
897 "(sheet \"Model\" visible)",
898 "(sheet \"Empty\" visible)",
899 "(window \"Model\" \"C3:D4\")",
900 "(extent \"Model\" \"B1:D4\")",
901 "(gridlines \"Model\" off)",
902 "(column \"Model\" \"C\" 60 shown 2)",
903 "(format 0 none general nowrap)",
904 "(format 2 \"F2F2F2\" general wrap)",
905 "(formula \"Model\" \"C3\" \"=B3*2\")",
906 "(sentence \"Model\" \"C3\" 3)",
907 "(formula \"Model\" \"C4\" \"=B4*2\")",
908 "(sentence \"Model\" \"C4\" 3)",
909 ]
910 );
911 let text = view_text(
913 &wb,
914 &Window {
915 sheet: "Model".to_string(),
916 range: range("H10:J12"),
917 },
918 );
919 assert_eq!(
920 lines(&text)[2..],
921 [
922 "(window \"Model\" \"H10:J12\")",
923 "(extent \"Model\" \"B1:D4\")",
924 "(gridlines \"Model\" off)",
925 "(format 0 none general nowrap)",
926 ]
927 );
928 let text = view_text(
930 &wb,
931 &Window {
932 sheet: "Empty".to_string(),
933 range: whole(&wb.sheets[1]),
934 },
935 );
936 assert_eq!(
937 lines(&text)[2..],
938 [
939 "(window \"Empty\" \"A1\")",
940 "(extent \"Empty\" none)",
941 "(gridlines \"Empty\" on)",
942 "(format 0 none general nowrap)",
943 ]
944 );
945 let text = view_text(
948 &wb,
949 &Window {
950 sheet: "Nowhere".to_string(),
951 range: range("A1"),
952 },
953 );
954 assert_eq!(lines(&text).len(), 3);
955 assert_eq!(lines(&text)[2], "(window \"Nowhere\" \"A1\")");
956 }
957
958 #[test]
959 fn row_settings_looks_and_hidden_sheets_print_in_their_places() {
960 let mut wb = a_model();
964 wb.sheets[1].visibility = crate::rows::Visibility::Hidden;
965 let model = &mut wb.sheets[0];
966 model.rows.insert(
967 3,
968 crate::sheet::RowSettings {
969 height: Some(30.0),
970 hidden: false,
971 },
972 );
973 model.rows.insert(
974 9,
975 crate::sheet::RowSettings {
976 height: None,
977 hidden: true,
978 },
979 );
980 model.looks.push(("1".to_string(), 2));
981 let text = view_text(
982 &wb,
983 &Window {
984 sheet: "Model".to_string(),
985 range: range("B2:B4"),
986 },
987 );
988 assert_eq!(
989 lines(&text)[..10],
990 [
991 "(sheet \"Model\" visible)",
992 "(sheet \"Empty\" hidden)",
993 "(window \"Model\" \"B2:B4\")",
994 "(extent \"Model\" \"B1:D4\")",
995 "(gridlines \"Model\" off)",
996 "(column \"Model\" \"B\" 72 shown 1)",
997 "(row \"Model\" 3 30 shown)",
998 "(format 0 none general nowrap)",
999 "(format 1 \"F7F4FC\" text nowrap)",
1000 "(format 2 \"F2F2F2\" general wrap)",
1001 ]
1002 );
1003 assert_eq!(lines(&text)[10], "(look \"Model\" 1 2)");
1004 assert!(
1005 !text.contains("(row \"Model\" 9"),
1006 "row 9 is outside the window"
1007 );
1008 }
1009
1010 #[test]
1011 fn window_of_finds_the_sheet_without_case_and_refuses_the_rest_by_name() {
1012 let wb = a_model();
1013 let w = window_of(&wb, "MODEL", None).unwrap();
1014 assert_eq!(w.sheet, "Model");
1015 assert_eq!(w.range, range("B1:D4"));
1016 let w = window_of(&wb, "empty", None).unwrap();
1017 assert_eq!((w.sheet.as_str(), w.range.text().as_str()), ("Empty", "A1"));
1018 let w = window_of(&wb, "Model", Some("f20:a1")).unwrap();
1019 assert_eq!(w.range.text(), "A1:F20");
1020 let r = window_of(&wb, "Summary", None).unwrap_err();
1021 assert_eq!(
1022 (r.id.as_str(), r.source.as_str()),
1023 ("view-sheet-unknown", "VLA-View")
1024 );
1025 assert!(r.text.contains("Summary"), "{}", r.text);
1026 assert!(r.text.contains("Model, Empty"), "{}", r.text);
1027 for text in ["A:A", "3:3", "nonsense", "", "A1:B"] {
1028 let r = window_of(&wb, "Model", Some(text)).unwrap_err();
1029 assert_eq!(r.id, "view-window-not-a-range", "{text}");
1030 assert!(r.text.contains(text), "{}", r.text);
1031 }
1032 assert_eq!(Grid::default().name(), "grid");
1033 }
1034
1035 fn less_formulas(record: &str) -> String {
1037 record
1038 .lines()
1039 .filter(|l| !l.starts_with("(formula "))
1040 .map(|l| format!("{l}\n"))
1041 .collect()
1042 }
1043
1044 fn values_text(wb: &Workbook, window: &Window, values: Option<&dyn Values>) -> String {
1046 let mut out = String::new();
1047 ValueGrid { values }.project(wb, window, &mut |line| {
1048 out.push_str(line);
1049 out.push('\n');
1050 });
1051 out
1052 }
1053
1054 fn walks(f: impl FnOnce()) -> crate::sheet::work::Work {
1056 crate::sheet::work::begin();
1057 f();
1058 crate::sheet::work::end()
1059 }
1060
1061 #[test]
1062 fn the_values_are_the_grid_less_its_formula_rows() {
1063 let mut wb = a_model();
1067 wb.sheets[0].cells.get_mut(&(3, 3)).unwrap().style = 2;
1068 let library = crate::calc::library::language();
1069 let calc = crate::calc::Calc::run(&wb, &library);
1070 for (sheet, window) in [
1071 ("Model", "A1:F20"),
1072 ("Model", "C3:D4"),
1073 ("Model", "B1:B4"),
1074 ("Model", "H10:J12"),
1075 ("Empty", "A1"),
1076 ] {
1077 let w = Window {
1078 sheet: sheet.to_string(),
1079 range: range(window),
1080 };
1081 let bare = values_text(&wb, &w, None);
1082 assert_eq!(bare, less_formulas(&view_text(&wb, &w)), "{window}");
1083 let valued = view_text_values(&wb, &w, &calc);
1084 assert_eq!(
1085 valued,
1086 less_formulas(&view_text_valued(&wb, &w, &calc)),
1087 "{window}"
1088 );
1089 assert!(!valued.contains("(formula "), "{valued}");
1090 }
1091 let w = Window {
1092 sheet: "Model".to_string(),
1093 range: range("A1:F20"),
1094 };
1095 let bare = values_text(&wb, &w, None);
1096 assert!(bare.contains("(style \"Model\" \"C3\" 2)\n(sentence \"Model\" \"C3\" 3)\n"));
1097 assert!(!bare.contains("(value "), "{bare}");
1098 let valued = view_text_values(&wb, &w, &calc);
1099 assert!(
1100 valued.contains("(value \"Model\" \"C2\" 2401)\n(sentence \"Model\" \"C2\" 3)\n"),
1101 "{valued}"
1102 );
1103 let w = Window {
1105 sheet: "Model".to_string(),
1106 range: range("B1:B4"),
1107 };
1108 assert_eq!(values_text(&wb, &w, None), view_text(&wb, &w));
1109 assert_eq!(ValueGrid::default().name(), "values");
1110 }
1111
1112 #[test]
1113 fn a_fill_s_cell_whose_master_is_lost_shows_in_neither_record() {
1114 let mut wb = Workbook::new();
1120 let mut sheet = Sheet::new("S");
1121 sheet.set(
1122 2,
1123 2,
1124 Cell {
1125 content: Content::Number(5.0),
1126 style: 0,
1127 },
1128 );
1129 sheet.set_formula(range("C2:C4"), "B2+1", 1);
1130 sheet.wrote(range("C2:C4"), 2);
1131 sheet.set(
1132 2,
1133 3,
1134 Cell {
1135 content: Content::Number(9.0),
1136 style: 0,
1137 },
1138 );
1139 sheet.wrote(range("C2"), 3);
1140 wb.sheets.push(sheet);
1141 let w = Window {
1142 sheet: "S".to_string(),
1143 range: range("A1:D5"),
1144 };
1145 let library = crate::calc::library::language();
1146 let calc = crate::calc::Calc::run(&wb, &library);
1147 let grid = view_text(&wb, &w);
1148 let valued = view_text_valued(&wb, &w, &calc);
1149 let mut bare = String::new();
1150 let work = walks(|| bare = values_text(&wb, &w, None));
1151 assert_eq!(bare, less_formulas(&grid));
1152 assert_eq!(work.walked, 4, "the sheet's four cells walked once");
1153 let mut values = String::new();
1154 let work = walks(|| values = view_text_values(&wb, &w, &calc));
1155 assert_eq!(values, less_formulas(&valued));
1156 assert_eq!(work.walked, 4, "no value vouches for C3, so one walk");
1157 assert_eq!(work.extents, 0, "the extent the calculation keeps");
1158 for record in [&grid, &valued, &bare, &values] {
1159 assert!(
1160 !record.contains("\"C3\"") && !record.contains("\"C4\""),
1161 "{record}"
1162 );
1163 assert!(record.contains("(cell \"S\" \"C2\" 9)\n(sentence \"S\" \"C2\" 3)\n"));
1164 }
1165 }
1166
1167 #[test]
1168 fn the_values_of_a_computed_fill_walk_nothing() {
1169 let mut wb = Workbook::new();
1174 let mut sheet = Sheet::new("S");
1175 for row in 1..=4 {
1176 sheet.set(
1177 row,
1178 1,
1179 Cell {
1180 content: Content::Number(f64::from(row)),
1181 style: 0,
1182 },
1183 );
1184 }
1185 sheet.set_formula(range("B1:C4"), "A1*2", 0);
1186 wb.sheets.push(sheet);
1187 let w = Window {
1188 sheet: "S".to_string(),
1189 range: range("B2:C3"),
1190 };
1191 let library = crate::calc::library::language();
1192 let calc = crate::calc::Calc::run(&wb, &library);
1193 let work = walks(|| {
1194 view_text_values(&wb, &w, &calc);
1195 });
1196 assert_eq!((work.walked, work.extents), (0, 0));
1197 let work = walks(|| {
1198 view_text_valued(&wb, &w, &calc);
1199 });
1200 assert_eq!((work.walked, work.extents), (12, 0));
1201 let work = walks(|| {
1202 values_text(&wb, &w, None);
1203 });
1204 assert_eq!((work.walked, work.extents), (12, 12));
1205 let work = walks(|| {
1206 view_text(&wb, &w);
1207 });
1208 assert_eq!((work.walked, work.extents), (12, 12));
1209 }
1210}