1use elixcee::diagnostics::json_string;
16use elixcee::reader::{BufferSheet, BufferWorkbook, FilterCriteria, SheetCell};
17use elixcee::types::{ArrayShape, CellContent, ExcelError, Variant};
18use std::collections::{HashMap, HashSet};
19use wasm_bindgen::prelude::*;
20
21const MAX_EDITOR_HISTORY: usize = 128;
22const MAX_WORKSHEET_ROW: u32 = 1_048_576;
23const MAX_WORKSHEET_COLUMN: u32 = 16_384;
24
25#[derive(Clone)]
26struct EditorState {
27 sheets: HashMap<String, HashMap<(u32, u32), CellContent>>,
28}
29
30#[derive(Clone)]
31struct EditorTransaction {
32 state: EditorState,
33 undo_len: usize,
34 redo: Vec<EditorState>,
35}
36
37#[wasm_bindgen]
40pub struct WorkbookEditor {
41 workbook: BufferWorkbook,
42 sheets: HashMap<String, HashMap<(u32, u32), CellContent>>,
43 undo: Vec<EditorState>,
44 redo: Vec<EditorState>,
45 transaction: Option<EditorTransaction>,
46 transaction_dirty: bool,
47}
48
49#[wasm_bindgen]
50impl WorkbookEditor {
51 #[wasm_bindgen(constructor)]
52 pub fn new(bytes: &[u8]) -> Result<WorkbookEditor, JsValue> {
53 let workbook =
54 elixcee::reader::read_workbook_from_bytes(bytes).map_err(|e| JsValue::from_str(&e))?;
55 let sheets = calculation_sheets(&workbook);
56 Ok(Self {
57 workbook,
58 sheets,
59 undo: Vec::new(),
60 redo: Vec::new(),
61 transaction: None,
62 transaction_dirty: false,
63 })
64 }
65
66 #[wasm_bindgen(js_name = setNumber)]
67 pub fn set_number(
68 &mut self,
69 sheet: &str,
70 row: u32,
71 col: u32,
72 value: f64,
73 ) -> Result<(), JsValue> {
74 let key = self.validate_cell_target(sheet, row, col)?;
75 if !value.is_finite() {
76 return Err(JsValue::from_str("cell value must be finite"));
77 }
78 self.record_edit();
79 let value = if value.fract() == 0.0 {
80 Variant::Integer(value as i64)
81 } else {
82 Variant::Float(value)
83 };
84 self.sheets.get_mut(&key).expect("checked above").insert(
85 (row, col),
86 CellContent {
87 formula: None,
88 value,
89 },
90 );
91 Ok(())
92 }
93
94 #[wasm_bindgen(js_name = setString)]
95 pub fn set_string(
96 &mut self,
97 sheet: &str,
98 row: u32,
99 col: u32,
100 value: &str,
101 ) -> Result<(), JsValue> {
102 let key = self.validate_cell_target(sheet, row, col)?;
103 self.record_edit();
104 self.sheets.get_mut(&key).expect("checked above").insert(
105 (row, col),
106 CellContent {
107 formula: None,
108 value: Variant::Str(value.to_string()),
109 },
110 );
111 Ok(())
112 }
113
114 #[wasm_bindgen(js_name = setBoolean)]
115 pub fn set_boolean(
116 &mut self,
117 sheet: &str,
118 row: u32,
119 col: u32,
120 value: bool,
121 ) -> Result<(), JsValue> {
122 let key = self.validate_cell_target(sheet, row, col)?;
123 self.record_edit();
124 self.sheets.get_mut(&key).expect("checked above").insert(
125 (row, col),
126 CellContent {
127 formula: None,
128 value: Variant::Boolean(value),
129 },
130 );
131 Ok(())
132 }
133
134 pub fn recalculate(&mut self) -> Result<String, JsValue> {
135 elixcee::formula::calculate_workbook(&mut self.sheets, &HashMap::new())
136 .map_err(|e| JsValue::from_str(&e))?;
137 Ok(self.json_snapshot())
138 }
139
140 pub fn snapshot(&mut self) -> String {
141 self.json_snapshot()
142 }
143
144 pub fn undo(&mut self) -> bool {
145 let Some(previous) = self.undo.pop() else {
146 return false;
147 };
148 self.redo.push(self.capture_state());
149 self.sheets = previous.sheets;
150 true
151 }
152
153 pub fn redo(&mut self) -> bool {
154 let Some(next) = self.redo.pop() else {
155 return false;
156 };
157 self.undo.push(self.capture_state());
158 self.sheets = next.sheets;
159 true
160 }
161
162 #[wasm_bindgen(js_name = beginTransaction)]
163 pub fn begin_transaction(&mut self) -> Result<(), JsValue> {
164 if self.transaction.is_some() {
165 return Err(JsValue::from_str("an edit transaction is already active"));
166 }
167 self.transaction = Some(EditorTransaction {
168 state: self.capture_state(),
169 undo_len: self.undo.len(),
170 redo: self.redo.clone(),
171 });
172 self.transaction_dirty = false;
173 Ok(())
174 }
175
176 #[wasm_bindgen(js_name = commitTransaction)]
177 pub fn commit_transaction(&mut self) -> bool {
178 let Some(transaction) = self.transaction.take() else {
179 return false;
180 };
181 if self.transaction_dirty {
182 self.undo.push(transaction.state);
183 self.undo.truncate(MAX_EDITOR_HISTORY);
184 self.redo.clear();
185 }
186 self.transaction_dirty = false;
187 true
188 }
189
190 #[wasm_bindgen(js_name = abortTransaction)]
191 pub fn abort_transaction(&mut self) -> bool {
192 let Some(transaction) = self.transaction.take() else {
193 return false;
194 };
195 self.sheets = transaction.state.sheets;
196 self.undo.truncate(transaction.undo_len);
197 self.redo = transaction.redo;
198 self.transaction_dirty = false;
199 true
200 }
201
202 #[wasm_bindgen(js_name = canUndo)]
203 pub fn can_undo(&self) -> bool {
204 !self.undo.is_empty()
205 }
206 #[wasm_bindgen(js_name = canRedo)]
207 pub fn can_redo(&self) -> bool {
208 !self.redo.is_empty()
209 }
210
211 fn capture_state(&self) -> EditorState {
212 EditorState {
213 sheets: self.sheets.clone(),
214 }
215 }
216
217 fn validate_cell_target(&self, sheet: &str, row: u32, col: u32) -> Result<String, JsValue> {
218 let key = sheet.to_ascii_lowercase();
219 if !self.sheets.contains_key(&key) {
220 return Err(JsValue::from_str("unknown worksheet"));
221 }
222 if row == 0 || row > MAX_WORKSHEET_ROW || col == 0 || col > MAX_WORKSHEET_COLUMN {
223 return Err(JsValue::from_str(
224 "cell coordinates are outside the worksheet bounds",
225 ));
226 }
227 Ok(key)
228 }
229
230 fn record_edit(&mut self) {
231 if self.transaction.is_some() {
232 self.transaction_dirty = true;
233 return;
234 }
235 self.undo.push(self.capture_state());
236 self.undo.truncate(MAX_EDITOR_HISTORY);
237 self.redo.clear();
238 }
239
240 fn json_snapshot(&mut self) -> String {
241 sync_workbook_values(&mut self.workbook, &self.sheets);
242 workbook_json(&self.workbook)
243 }
244}
245
246#[wasm_bindgen(js_name = readWorkbook)]
271pub fn read_workbook(bytes: &[u8]) -> Result<String, JsValue> {
272 let wb = elixcee::reader::read_workbook_from_bytes(bytes).map_err(|e| JsValue::from_str(&e))?;
273 Ok(workbook_json(&wb))
274}
275
276#[wasm_bindgen(js_name = calculateWorkbook)]
284pub fn calculate_workbook(bytes: &[u8]) -> Result<String, JsValue> {
285 let mut wb =
286 elixcee::reader::read_workbook_from_bytes(bytes).map_err(|e| JsValue::from_str(&e))?;
287 let mut sheets = HashMap::new();
288 for bs in &wb.sheets {
289 let mut cells = HashMap::with_capacity(bs.sheet.cells.len().max(bs.formulas.len()));
290 for (&position, cell) in &bs.sheet.cells {
291 cells.insert(
292 position,
293 CellContent {
294 formula: bs.formulas.get(&position).map(|f| format!("={f}")),
295 value: sheet_cell_to_variant(cell),
296 },
297 );
298 }
299 for (&position, formula) in &bs.formulas {
300 cells.entry(position).or_insert_with(|| CellContent {
301 formula: Some(format!("={formula}")),
302 value: Variant::Empty,
303 });
304 }
305 sheets.insert(bs.sheet.name.to_ascii_lowercase(), cells);
306 }
307 let named_ranges = wb
310 .defined_names
311 .iter()
312 .filter(|defined_name| defined_name.local_sheet_id.is_none())
313 .map(|defined_name| {
314 (
315 defined_name.name.to_ascii_lowercase(),
316 normalize_defined_name_ref(&defined_name.raw_text),
317 )
318 })
319 .collect::<HashMap<_, _>>();
320 let mut scoped_named_ranges = HashMap::<String, HashMap<String, String>>::new();
321 for defined_name in wb.defined_names.iter().filter_map(|defined_name| {
322 defined_name
323 .local_sheet_id
324 .map(|index| (index, defined_name))
325 }) {
326 let Some(sheet) = wb
327 .sheets
328 .get(defined_name.0)
329 .map(|buffer| buffer.sheet.name.clone())
330 else {
331 continue;
332 };
333 scoped_named_ranges
334 .entry(sheet.to_ascii_lowercase())
335 .or_default()
336 .insert(
337 defined_name.1.name.to_ascii_lowercase(),
338 normalize_defined_name_ref(&defined_name.1.raw_text),
339 );
340 }
341 rewrite_table_structured_formulas(&mut sheets, &wb);
346 elixcee::formula::calculate_workbook_with_context(
347 &mut sheets,
348 &named_ranges,
349 &scoped_named_ranges,
350 &HashMap::new(),
351 )
352 .map_err(|e| JsValue::from_str(&e))?;
353 materialize_formula_array_spills(&mut sheets).map_err(|e| JsValue::from_str(&e))?;
354 for bs in &mut wb.sheets {
355 let Some(cells) = sheets.get(&bs.sheet.name.to_ascii_lowercase()) else {
356 continue;
357 };
358 for (&position, cell) in cells {
362 bs.sheet
363 .cells
364 .insert(position, variant_to_sheet_cell(&cell.value));
365 }
366 }
367 Ok(workbook_json(&wb))
368}
369
370fn materialize_formula_array_spills(
374 sheets: &mut HashMap<String, HashMap<(u32, u32), CellContent>>,
375) -> Result<(), String> {
376 let mut pending = Vec::new();
377 let mut spill_errors = Vec::new();
378 let mut planned = HashSet::new();
379 for (sheet, cells) in sheets.iter() {
380 for (&(row, col), cell) in cells {
381 let Variant::Array(values) = &cell.value else {
382 continue;
383 };
384 let shape = formula_array_shape(cell.formula.as_deref(), values.len());
385 if shape.is_empty() {
386 continue;
387 }
388 let mut collision = false;
389 let mut targets = Vec::new();
390 for offset in 1..shape.cell_count() {
391 let value = values
392 .get(offset)
393 .cloned()
394 .unwrap_or(Variant::Error(ExcelError::NA));
395 let row_offset = offset / shape.cols;
396 let col_offset = offset % shape.cols;
397 let target_row = row.checked_add(row_offset as u32).ok_or_else(|| {
398 format!("dynamic array spill exceeds worksheet bounds on {sheet}")
399 })?;
400 let target_col = col.checked_add(col_offset as u32).ok_or_else(|| {
401 format!("dynamic array spill exceeds worksheet bounds on {sheet}")
402 })?;
403 let key = (sheet.clone(), (target_row, target_col));
404 if cells.contains_key(&(target_row, target_col)) || planned.contains(&key) {
405 collision = true;
406 break;
407 }
408 targets.push(((target_row, target_col), value));
409 }
410 if collision {
411 spill_errors.push((sheet.clone(), (row, col)));
412 } else {
413 planned.extend(
414 targets
415 .iter()
416 .map(|(position, _)| (sheet.clone(), *position)),
417 );
418 pending.extend(
419 targets
420 .into_iter()
421 .map(|(position, value)| (sheet.clone(), position, value)),
422 );
423 }
424 }
425 }
426 for (sheet, position, value) in pending {
427 sheets.entry(sheet).or_default().insert(
428 position,
429 CellContent {
430 formula: None,
431 value,
432 },
433 );
434 }
435 for (sheet, position) in spill_errors {
436 if let Some(cell) = sheets
437 .get_mut(&sheet)
438 .and_then(|cells| cells.get_mut(&position))
439 {
440 cell.value = Variant::Str("#SPILL!".to_string());
441 }
442 }
443 Ok(())
444}
445
446fn formula_array_shape(formula: Option<&str>, len: usize) -> ArrayShape {
447 let Some(formula) = formula else {
448 return ArrayShape::new(1, len);
449 };
450 let expression = formula
451 .trim()
452 .trim_start_matches('=')
453 .trim()
454 .to_ascii_uppercase();
455 let Some(open) = expression.find('(') else {
456 return ArrayShape::new(1, len);
457 };
458 if !expression.ends_with(')') {
459 return ArrayShape::new(1, len);
460 }
461 let name = &expression[..open];
462 let arguments = &expression[open + 1..expression.len() - 1];
463 let values = arguments.split(',').map(str::trim).collect::<Vec<_>>();
464 let literal = |index: usize, fallback: usize| {
465 values
466 .get(index)
467 .and_then(|value| value.parse::<usize>().ok())
468 .unwrap_or(fallback)
469 };
470 let shape = match name {
471 "SEQUENCE" | "RANDARRAY" => ArrayShape::new(literal(0, 0), literal(1, 1)),
472 "WRAPROWS" => {
473 let cols = literal(1, 0);
474 ArrayShape::new(
475 if cols == 0 { 0 } else { len.div_ceil(cols) },
476 cols.min(len),
477 )
478 }
479 "WRAPCOLS" => {
480 let rows = literal(1, 0);
481 ArrayShape::new(
482 rows.min(len),
483 if rows == 0 { 0 } else { len.div_ceil(rows) },
484 )
485 }
486 _ => ArrayShape::new(1, len),
487 };
488 if shape.cell_count() >= len && !shape.is_empty() {
489 shape
490 } else {
491 ArrayShape::new(1, len)
492 }
493}
494
495fn normalize_defined_name_ref(raw: &str) -> String {
496 let Some(bang) = raw.rfind('!') else {
497 return raw.replace('$', "");
501 };
502 let qualifier = &raw[..bang];
503 if qualifier.starts_with('\'') && qualifier.ends_with('\'') && qualifier.len() >= 2 {
504 return format!(
505 "{}!{}",
506 qualifier[1..qualifier.len() - 1].replace("''", "'"),
507 &raw[bang + 1..]
508 );
509 }
510 raw.to_string()
511}
512
513fn wasm_column_label(mut column: u32) -> String {
514 let mut label = String::new();
515 loop {
516 label.insert(0, (b'A' + (column % 26) as u8) as char);
517 if column < 26 {
518 break;
519 }
520 column = column / 26 - 1;
521 }
522 label
523}
524
525fn replace_table_ref_case_insensitive(source: &str, needle: &str, replacement: &str) -> String {
526 let source_lower = source.to_ascii_lowercase();
527 let needle_lower = needle.to_ascii_lowercase();
528 let mut out = String::with_capacity(source.len());
529 let mut cursor = 0usize;
530 while let Some(relative) = source_lower[cursor..].find(&needle_lower) {
531 let start = cursor + relative;
532 let end = start + needle.len();
533 let inside_string = {
534 let mut in_string = false;
535 let mut chars = source[..start].chars().peekable();
536 while let Some(ch) = chars.next() {
537 if ch != '"' {
538 continue;
539 }
540 if chars.peek() == Some(&'"') {
541 chars.next();
542 } else {
543 in_string = !in_string;
544 }
545 }
546 in_string
547 };
548 let preceded_by_identifier = source[..start]
549 .chars()
550 .next_back()
551 .is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_');
552 let followed_by_identifier = source[end..]
553 .chars()
554 .next()
555 .is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_');
556 out.push_str(&source[cursor..start]);
557 if inside_string || preceded_by_identifier || followed_by_identifier {
558 out.push_str(&source[start..end]);
559 } else {
560 out.push_str(replacement);
561 }
562 cursor = end;
563 }
564 out.push_str(&source[cursor..]);
565 out
566}
567
568fn rewrite_table_structured_formulas(
569 sheets: &mut HashMap<String, HashMap<(u32, u32), CellContent>>,
570 wb: &BufferWorkbook,
571) {
572 let mut patterns = Vec::<(String, String, String, bool)>::new();
573 for buffer in &wb.sheets {
574 let table_sheet = &buffer.sheet.name;
575 for table in &buffer.sheet.tables {
576 let (table_start, table_left) = table.ref_range.0;
577 let (table_end, table_right) = table.ref_range.1;
578 let data_start = table.ref_range.0.0 + table.header_row_count;
579 let data_end = table.ref_range.1.0.saturating_sub(table.totals_row_count);
580 let headers_end = table_start + table.header_row_count - 1;
581 let table_address = |start: u32, end: u32, left: u32, right: u32| {
582 format!(
583 "{}!{}{}:{}{}",
584 table_sheet,
585 wasm_column_label(left.saturating_sub(1)),
586 start,
587 wasm_column_label(right.saturating_sub(1)),
588 end
589 )
590 };
591 for table_name in [&table.name, &table.display_name] {
592 patterns.push((
593 format!("{}[#Headers]", table_name),
594 table_sheet.clone(),
595 table_address(table_start, headers_end, table_left, table_right),
596 false,
597 ));
598 patterns.push((
599 format!("{}[#All]", table_name),
600 table_sheet.clone(),
601 table_address(table_start, table_end, table_left, table_right),
602 false,
603 ));
604 if data_start <= data_end {
605 patterns.push((
606 format!("{}[#Data]", table_name),
607 table_sheet.clone(),
608 table_address(data_start, data_end, table_left, table_right),
609 false,
610 ));
611 }
612 }
613 if data_start > data_end {
614 continue;
615 }
616 for (index, column) in table.columns.iter().enumerate() {
617 let col = table.ref_range.0.1 + index as u32;
618 let qualified = format!(
619 "{}!{}{}:{}{}",
620 table_sheet,
621 wasm_column_label(col.saturating_sub(1)),
622 data_start,
623 wasm_column_label(col.saturating_sub(1)),
624 data_end
625 );
626 for table_name in [&table.name, &table.display_name] {
627 let column_pattern = format!("{}[{}]", table_name, column.name);
628 patterns.push((
629 column_pattern.clone(),
630 table_sheet.clone(),
631 qualified.clone(),
632 false,
633 ));
634 patterns.push((
635 format!("{}[[#Data],[{}]]", table_name, column.name),
636 table_sheet.clone(),
637 qualified.clone(),
638 false,
639 ));
640 patterns.push((
641 format!("{}[[#Headers],[{}]]", table_name, column.name),
642 table_sheet.clone(),
643 table_address(table_start, headers_end, col, col),
644 false,
645 ));
646 patterns.push((
647 format!("{}[[#All],[{}]]", table_name, column.name),
648 table_sheet.clone(),
649 table_address(table_start, table_end, col, col),
650 false,
651 ));
652 patterns.push((
653 format!("{}[@{}]", table_name, column.name),
654 table_sheet.clone(),
655 qualified.clone(),
656 true,
657 ));
658 patterns.push((
659 format!("{}[[#This Row],[{}]]", table_name, column.name),
660 table_sheet.clone(),
661 qualified.clone(),
662 true,
663 ));
664 }
665 }
666 }
667 }
668 patterns.sort_by_key(|(pattern, _, _, _)| std::cmp::Reverse(pattern.len()));
669 for (host, cells) in sheets.iter_mut() {
670 for (&(row, _), cell) in cells.iter_mut() {
671 let Some(formula) = cell.formula.as_mut() else {
672 continue;
673 };
674 for (pattern, table_sheet, qualified, this_row) in &patterns {
675 if *this_row && !host.eq_ignore_ascii_case(table_sheet) {
676 continue;
677 }
678 let replacement = if *this_row {
679 let Some((_, data_range)) = qualified.split_once('!') else {
680 continue;
681 };
682 let Some((start, end)) = data_range.split_once(':') else {
683 continue;
684 };
685 let col = start.trim_end_matches(|ch: char| ch.is_ascii_digit());
686 let start_row = start
687 .trim_start_matches(|ch: char| ch.is_ascii_alphabetic())
688 .parse::<u32>()
689 .ok();
690 let end_col = end.trim_end_matches(|ch: char| ch.is_ascii_digit());
691 if start_row.is_none() || end_col != col {
692 continue;
693 }
694 format!("{}{}", col, row)
695 } else if host.eq_ignore_ascii_case(table_sheet) {
696 qualified
697 .split_once('!')
698 .map_or(qualified.as_str(), |(_, address)| address)
699 .to_string()
700 } else {
701 qualified.clone()
702 };
703 let rewritten = replace_table_ref_case_insensitive(formula, pattern, &replacement);
704 if rewritten != *formula {
705 *formula = rewritten;
706 }
707 }
708 }
709 }
710}
711
712#[wasm_bindgen(js_name = diagnoseWorkbook)]
716pub fn diagnose_workbook(bytes: &[u8]) -> String {
717 let wb = match elixcee::reader::read_workbook_from_bytes(bytes) {
718 Ok(wb) => wb,
719 Err(error) => {
720 return format!("{{\"ok\":false,\"error\":{}}}", json_string(&error));
721 }
722 };
723 let mut formula_count = 0usize;
724 let mut qualified_formula_count = 0usize;
725 let mut formula_parse_errors = 0usize;
726 let calculation_sheets = calculation_sheets(&wb);
727 for bs in &wb.sheets {
728 for formula in bs.formulas.values() {
729 formula_count += 1;
730 match elixcee::formula::parse(formula) {
731 Ok(expr) => {
732 if contains_qualified_reference(&expr) {
733 qualified_formula_count += 1;
734 }
735 }
736 Err(_) => formula_parse_errors += 1,
737 }
738 }
739 }
740 format!(
741 "{{\"ok\":true,\"sheetCount\":{},\"formulaCount\":{},\"qualifiedFormulaCount\":{},\"formulaParseErrors\":{},\"hasFormulaCycle\":{}}}",
742 wb.sheets.len(),
743 formula_count,
744 qualified_formula_count,
745 formula_parse_errors,
746 if elixcee::formula::workbook_has_formula_cycle(&calculation_sheets) {
747 "true"
748 } else {
749 "false"
750 }
751 )
752}
753
754fn contains_qualified_reference(expr: &elixcee::formula::FormulaExpr) -> bool {
755 use elixcee::formula::FormulaExpr;
756 match expr {
757 FormulaExpr::CellRef { sheet, .. } | FormulaExpr::Range { sheet, .. } => sheet.is_some(),
758 FormulaExpr::BinOp { lhs, rhs, .. } => {
759 contains_qualified_reference(lhs) || contains_qualified_reference(rhs)
760 }
761 FormulaExpr::UnaryMinus(inner) => contains_qualified_reference(inner),
762 FormulaExpr::FuncCall { args, .. } => args.iter().any(contains_qualified_reference),
763 FormulaExpr::Call { callee, args } => {
764 contains_qualified_reference(callee) || args.iter().any(contains_qualified_reference)
765 }
766 FormulaExpr::Number(_)
767 | FormulaExpr::Str(_)
768 | FormulaExpr::Bool(_)
769 | FormulaExpr::Omitted => false,
770 }
771}
772
773fn sheet_cell_to_variant(cell: &SheetCell) -> Variant {
774 match cell {
775 SheetCell::Integer(value) => Variant::Integer(*value),
776 SheetCell::Float(value) => Variant::Float(*value),
777 SheetCell::Str(value) => Variant::Str(value.clone()),
778 SheetCell::Bool(value) => Variant::Boolean(*value),
779 SheetCell::Error(value) => Variant::Error(value.clone()),
780 }
781}
782
783fn calculation_sheets(
784 workbook: &BufferWorkbook,
785) -> HashMap<String, HashMap<(u32, u32), CellContent>> {
786 let mut sheets = HashMap::new();
787 for bs in &workbook.sheets {
788 let mut cells = HashMap::with_capacity(bs.sheet.cells.len().max(bs.formulas.len()));
789 for (&position, cell) in &bs.sheet.cells {
790 cells.insert(
791 position,
792 CellContent {
793 formula: bs.formulas.get(&position).map(|f| format!("={f}")),
794 value: sheet_cell_to_variant(cell),
795 },
796 );
797 }
798 for (&position, formula) in &bs.formulas {
799 cells.entry(position).or_insert_with(|| CellContent {
800 formula: Some(format!("={formula}")),
801 value: Variant::Empty,
802 });
803 }
804 sheets.insert(bs.sheet.name.to_ascii_lowercase(), cells);
805 }
806 sheets
807}
808
809fn sync_workbook_values(
810 workbook: &mut BufferWorkbook,
811 sheets: &HashMap<String, HashMap<(u32, u32), CellContent>>,
812) {
813 for bs in &mut workbook.sheets {
814 let Some(cells) = sheets.get(&bs.sheet.name.to_ascii_lowercase()) else {
815 continue;
816 };
817 for &position in bs.formulas.keys() {
818 if let Some(cell) = cells.get(&position) {
819 bs.sheet
820 .cells
821 .insert(position, variant_to_sheet_cell(&cell.value));
822 }
823 }
824 for (&position, cell) in cells {
825 if cell.formula.is_none() {
826 bs.sheet
827 .cells
828 .insert(position, variant_to_sheet_cell(&cell.value));
829 }
830 }
831 }
832}
833
834fn variant_to_sheet_cell(value: &Variant) -> SheetCell {
835 match value {
836 Variant::Integer(value) => SheetCell::Integer(*value),
837 Variant::Float(value) => SheetCell::Float(*value),
838 Variant::Str(value) => SheetCell::Str(value.clone()),
839 Variant::Boolean(value) => SheetCell::Bool(*value),
840 Variant::Date(value) => SheetCell::Integer(*value),
841 Variant::Error(value) => SheetCell::Error(value.clone()),
842 Variant::Empty | Variant::Null => SheetCell::Str(String::new()),
843 Variant::Array(values) => values
844 .first()
845 .map(variant_to_sheet_cell)
846 .unwrap_or_else(|| SheetCell::Str(String::new())),
847 Variant::VbaArray(_) | Variant::Record(_) => SheetCell::Str(value.to_string()),
848 }
849}
850
851fn workbook_json(wb: &BufferWorkbook) -> String {
852 let mut names = String::from("[");
853 let mut body = String::from("{");
854 for (i, bs) in wb.sheets.iter().enumerate() {
855 if i > 0 {
856 names.push(',');
857 body.push(',');
858 }
859 names.push_str(&json_string(&bs.sheet.name));
860 body.push_str(&json_string(&bs.sheet.name));
861 body.push(':');
862 body.push_str(&worksheet_json(bs));
863 }
864 names.push(']');
865 body.push('}');
866
867 let mut out = format!("{{\"SheetNames\":{},\"Sheets\":{}", names, body);
868 if !wb.number_formats.is_empty() {
869 let mut ids: Vec<_> = wb.number_formats.keys().collect();
871 ids.sort();
872 out.push_str(",\"!numFmts\":{");
873 for (i, id) in ids.iter().enumerate() {
874 if i > 0 {
875 out.push(',');
876 }
877 out.push_str(&json_string(&id.to_string()));
878 out.push(':');
879 out.push_str(&json_string(&wb.number_formats[*id]));
880 }
881 out.push('}');
882 }
883 out.push_str(&format!(",\"!date1904\":{}", wb.date1904));
884 if !wb.defined_names.is_empty() {
885 out.push_str(",\"Workbook\":{\"Names\":[");
886 for (i, defined_name) in wb.defined_names.iter().enumerate() {
887 if i > 0 {
888 out.push(',');
889 }
890 out.push_str("{\"Name\":");
891 out.push_str(&json_string(&defined_name.name));
892 out.push_str(",\"Ref\":");
893 out.push_str(&json_string(&defined_name.raw_text));
894 if let Some(sheet) = defined_name.local_sheet_id {
895 out.push_str(",\"Sheet\":");
896 out.push_str(&sheet.to_string());
897 }
898 out.push('}');
899 }
900 out.push_str("]}");
901 }
902 out.push('}');
903 out
904}
905
906fn table_filter_column_json(column: &elixcee::reader::FilterColumn) -> String {
907 let mut out = format!(
908 "{{\"colId\":{},\"hiddenButton\":{},\"showButton\":{},\"criteria\":",
909 column.col_offset, column.hidden_button, column.show_button
910 );
911 match &column.criteria {
912 FilterCriteria::Values(values) => {
913 out.push_str("{\"kind\":\"values\",\"values\":[");
914 for (index, value) in values.iter().enumerate() {
915 if index > 0 {
916 out.push(',');
917 }
918 out.push_str(&json_string(value));
919 }
920 out.push_str("]}");
921 }
922 FilterCriteria::Blank => out.push_str("{\"kind\":\"blank\"}"),
923 FilterCriteria::Custom {
924 op1,
925 val1,
926 and,
927 op2,
928 val2,
929 } => {
930 out.push_str(&format!(
931 "{{\"kind\":\"custom\",\"op1\":{},\"val1\":{},\"and\":{}",
932 json_string(op1),
933 json_string(val1),
934 and
935 ));
936 if let (Some(op2), Some(val2)) = (op2, val2) {
937 out.push_str(&format!(
938 ",\"op2\":{},\"val2\":{}",
939 json_string(op2),
940 json_string(val2)
941 ));
942 }
943 out.push('}');
944 }
945 FilterCriteria::Top10 { top, percent, val } => {
946 out.push_str(&format!(
947 "{{\"kind\":\"top10\",\"top\":{},\"percent\":{},\"val\":{}}}",
948 top, percent, val
949 ));
950 }
951 FilterCriteria::DateGroup(_) => out.push_str("{\"kind\":\"dateGroup\"}"),
952 }
953 out.push('}');
954 out
955}
956
957fn worksheet_json(bs: &BufferSheet) -> String {
958 let sheet = &bs.sheet;
959 let mut out = String::from("{");
960 let mut first = true;
961 let (mut min_r, mut min_c, mut max_r, mut max_c) = (u32::MAX, u32::MAX, 0u32, 0u32);
962
963 let mut refs: Vec<_> = sheet.cells.iter().collect();
967 refs.sort_by_key(|((r, c), _)| (*r, *c));
968
969 for (&(row, col), cell) in refs {
970 if !first {
971 out.push(',');
972 }
973 first = false;
974 min_r = min_r.min(row);
975 max_r = max_r.max(row);
976 min_c = min_c.min(col);
977 max_c = max_c.max(col);
978 out.push_str(&json_string(&cell_ref(row, col)));
979 out.push(':');
980 out.push_str(&cell_json(
981 cell,
982 bs.formulas.get(&(row, col)),
983 bs.style_ids.get(&(row, col)),
984 bs.cell_styles.get(&(row, col)),
985 ));
986 }
987
988 let ref_range = bs
995 .dimension
996 .or_else(|| (!first).then_some(((min_r, min_c), (max_r, max_c))));
997 if let Some(((r1, c1), (r2, c2))) = ref_range {
998 out.push_str(",\"!ref\":");
999 let start = cell_ref(r1, c1);
1008 if r1 == r2 && c1 == c2 {
1009 out.push_str(&json_string(&start));
1010 } else {
1011 out.push_str(&json_string(&format!("{}:{}", start, cell_ref(r2, c2))));
1012 }
1013 }
1014
1015 if !sheet.merged_ranges.is_empty() {
1016 out.push_str(",\"!merges\":[");
1017 for (i, ((r1, c1), (r2, c2))) in sheet.merged_ranges.iter().enumerate() {
1018 if i > 0 {
1019 out.push(',');
1020 }
1021 out.push_str(&format!(
1027 "{{\"s\":{{\"r\":{},\"c\":{}}},\"e\":{{\"r\":{},\"c\":{}}}}}",
1028 r1.saturating_sub(1),
1029 c1.saturating_sub(1),
1030 r2.saturating_sub(1),
1031 c2.saturating_sub(1)
1032 ));
1033 }
1034 out.push(']');
1035 }
1036
1037 write_hidden_intervals(&mut out, "!hiddenRows", &sheet.hidden_rows);
1038 write_hidden_intervals(&mut out, "!hiddenCols", &sheet.hidden_columns);
1039
1040 if !sheet.data_validations.is_empty() {
1041 out.push_str(",\"!dataValidations\":[");
1042 for (index, validation) in sheet.data_validations.iter().enumerate() {
1043 if index > 0 {
1044 out.push(',');
1045 }
1046 out.push_str("{\"type\":");
1047 out.push_str(&json_string(&validation.validation_type));
1048 out.push_str(",\"sqref\":[");
1049 for (range_index, range) in validation.sqref.iter().enumerate() {
1050 if range_index > 0 {
1051 out.push(',');
1052 }
1053 out.push_str(&json_string(&format_rect(range)));
1054 }
1055 out.push_str("]}");
1056 }
1057 out.push(']');
1058 }
1059
1060 if !sheet.tables.is_empty() {
1061 out.push_str(",\"!tables\":[");
1062 for (index, table) in sheet.tables.iter().enumerate() {
1063 if index > 0 {
1064 out.push(',');
1065 }
1066 out.push_str("{\"name\":");
1067 out.push_str(&json_string(&table.name));
1068 out.push_str(",\"displayName\":");
1069 out.push_str(&json_string(&table.display_name));
1070 out.push_str(",\"ref\":");
1071 out.push_str(&json_string(&format_rect(&table.ref_range)));
1072 if let Some(auto_filter_ref) = &table.auto_filter_ref {
1073 out.push_str(",\"autoFilterRef\":");
1074 out.push_str(&json_string(&format_rect(auto_filter_ref)));
1075 if !table.autofilter_columns.is_empty() {
1076 out.push_str(",\"autoFilterColumns\":[");
1077 for (column_index, column) in table.autofilter_columns.iter().enumerate() {
1078 if column_index > 0 {
1079 out.push(',');
1080 }
1081 out.push_str(&table_filter_column_json(column));
1082 }
1083 out.push(']');
1084 }
1085 }
1086 if !table.columns.is_empty() {
1087 out.push_str(",\"columns\":[");
1088 for (column_index, column) in table.columns.iter().enumerate() {
1089 if column_index > 0 {
1090 out.push(',');
1091 }
1092 out.push_str("{\"name\":");
1093 out.push_str(&json_string(&column.name));
1094 out.push('}');
1095 }
1096 out.push(']');
1097 }
1098 if let Some(style_name) = &table.style_name {
1099 out.push_str(",\"styleName\":");
1100 out.push_str(&json_string(style_name));
1101 }
1102 out.push('}');
1103 }
1104 out.push(']');
1105 }
1106
1107 if !bs.charts.is_empty() {
1108 out.push_str(",\"!charts\":[");
1109 for (index, chart) in bs.charts.iter().enumerate() {
1110 if index > 0 {
1111 out.push(',');
1112 }
1113 out.push_str("{\"ref\":");
1114 out.push_str(&json_string(&format_rect(&chart.ref_range)));
1115 out.push_str(",\"type\":");
1116 out.push_str(&json_string(&chart.chart_type));
1117 out.push_str(",\"title\":");
1118 out.push_str(&json_string(&chart.title));
1119 if let Some(title) = &chart.x_axis_title {
1120 out.push_str(",\"xAxisTitle\":");
1121 out.push_str(&json_string(title));
1122 }
1123 if let Some(title) = &chart.y_axis_title {
1124 out.push_str(",\"yAxisTitle\":");
1125 out.push_str(&json_string(title));
1126 }
1127 out.push_str(",\"legend\":");
1128 out.push_str(if chart.legend { "true" } else { "false" });
1129 if !chart.series_colors.is_empty() {
1130 out.push_str(",\"colors\":[");
1131 for (color_index, color) in chart.series_colors.iter().enumerate() {
1132 if color_index > 0 {
1133 out.push(',');
1134 }
1135 out.push_str(&json_string(color));
1136 }
1137 out.push(']');
1138 }
1139 out.push_str(&format!(
1140 ",\"widthCols\":{},\"heightRows\":{}",
1141 chart.width_cols, chart.height_rows
1142 ));
1143 out.push('}');
1144 }
1145 out.push(']');
1146 }
1147
1148 if !bs.comment_notes.is_empty() {
1149 out.push_str(",\"!comments\":[");
1150 for (index, comment) in bs.comment_notes.iter().enumerate() {
1151 if index > 0 {
1152 out.push(',');
1153 }
1154 out.push_str("{\"ref\":");
1155 out.push_str(&json_string(&cell_ref(comment.cell.0, comment.cell.1)));
1156 out.push_str(",\"author\":");
1157 out.push_str(&json_string(&comment.author));
1158 out.push_str(",\"text\":");
1159 out.push_str(&json_string(&comment.text));
1160 out.push('}');
1161 }
1162 out.push(']');
1163 }
1164
1165 if !bs.conditional_formats.is_empty() {
1166 out.push_str(",\"!conditionalFormats\":[");
1167 for (index, rule) in bs.conditional_formats.iter().enumerate() {
1168 if index > 0 {
1169 out.push(',');
1170 }
1171 out.push_str("{\"type\":");
1172 out.push_str(&json_string(&rule.rule_type));
1173 if let Some(operator) = &rule.operator {
1174 out.push_str(",\"operator\":");
1175 out.push_str(&json_string(operator));
1176 }
1177 out.push_str(",\"sqref\":[");
1178 for (range_index, range) in rule.sqref.iter().enumerate() {
1179 if range_index > 0 {
1180 out.push(',');
1181 }
1182 out.push_str(&json_string(&format_rect(range)));
1183 }
1184 out.push_str("],\"formula\":");
1185 out.push_str(&json_string(&rule.formula1));
1186 if let Some(formula2) = &rule.formula2 {
1187 out.push_str(",\"formula2\":");
1188 out.push_str(&json_string(formula2));
1189 }
1190 if let Some(priority) = rule.priority {
1191 out.push_str(&format!(",\"priority\":{}", priority));
1192 }
1193 if rule.stop_if_true {
1194 out.push_str(",\"stopIfTrue\":true");
1195 }
1196 if let Some(dxf) = &rule.dxf {
1197 out.push_str(",\"dxf\":{");
1198 let mut first = true;
1199 if dxf.bold || dxf.italic || dxf.underline || dxf.font_color.is_some() {
1200 out.push_str("\"font\":{");
1201 let mut font_first = true;
1202 if dxf.bold {
1203 out.push_str("\"bold\":true");
1204 font_first = false;
1205 }
1206 if dxf.italic {
1207 if !font_first {
1208 out.push(',');
1209 }
1210 out.push_str("\"italic\":true");
1211 font_first = false;
1212 }
1213 if dxf.underline {
1214 if !font_first {
1215 out.push(',');
1216 }
1217 out.push_str("\"underline\":true");
1218 font_first = false;
1219 }
1220 if let Some(color) = &dxf.font_color {
1221 if !font_first {
1222 out.push(',');
1223 }
1224 out.push_str("\"color\":{\"rgb\":");
1225 out.push_str(&json_string(color));
1226 out.push('}');
1227 }
1228 out.push('}');
1229 first = false;
1230 }
1231 if let Some(color) = &dxf.fill_color {
1232 if !first {
1233 out.push(',');
1234 }
1235 out.push_str("\"fill\":{\"fgColor\":{\"rgb\":");
1236 out.push_str(&json_string(color));
1237 out.push_str("}}");
1238 }
1239 out.push('}');
1240 }
1241 out.push('}');
1242 }
1243 out.push(']');
1244 }
1245
1246 if let Some(pane) = &bs.freeze_pane {
1247 out.push_str(",\"!freezePane\":{\"rows\":");
1248 out.push_str(&pane.rows.to_string());
1249 out.push_str(",\"cols\":");
1250 out.push_str(&pane.cols.to_string());
1251 out.push('}');
1252 }
1253
1254 out.push('}');
1255 out
1256}
1257
1258fn write_hidden_intervals(out: &mut String, key: &str, intervals: &[(u32, u32)]) {
1262 if intervals.is_empty() {
1263 return;
1264 }
1265 out.push_str(",\"");
1266 out.push_str(key);
1267 out.push_str("\":[");
1268 for (i, (start, end)) in intervals.iter().enumerate() {
1269 if i > 0 {
1270 out.push(',');
1271 }
1272 out.push_str(&format!("[{},{}]", start, end));
1273 }
1274 out.push(']');
1275}
1276
1277fn cell_json(
1278 cell: &SheetCell,
1279 formula: Option<&String>,
1280 fmt_id: Option<&u32>,
1281 style: Option<&elixcee::reader::CellStyleDef>,
1282) -> String {
1283 let mut out = match cell {
1284 SheetCell::Integer(v) => format!("{{\"t\":\"n\",\"v\":{}", v),
1285 SheetCell::Float(v) => format!("{{\"t\":\"n\",\"v\":{}", json_number(*v)),
1286 SheetCell::Str(v) => format!("{{\"t\":\"s\",\"v\":{}", json_string(v)),
1287 SheetCell::Bool(v) => format!("{{\"t\":\"b\",\"v\":{}", v),
1288 SheetCell::Error(e) => format!("{{\"t\":\"e\",\"v\":{}", e.biff_code()),
1292 };
1293 if let Some(f) = formula {
1294 out.push_str(",\"f\":");
1295 out.push_str(&json_string(f));
1296 }
1297 if let Some(id) = fmt_id {
1298 out.push_str(",\"fmtId\":");
1305 out.push_str(&id.to_string());
1306 }
1307 if let Some(style) = style {
1308 let mut parts = Vec::new();
1309 if style.bold || style.italic || style.underline || style.font_color.is_some() {
1310 let mut font = String::new();
1311 if style.bold {
1312 font.push_str("\"bold\":true");
1313 }
1314 if style.italic {
1315 if !font.is_empty() {
1316 font.push(',');
1317 }
1318 font.push_str("\"italic\":true");
1319 }
1320 if style.underline {
1321 if !font.is_empty() {
1322 font.push(',');
1323 }
1324 font.push_str("\"underline\":true");
1325 }
1326 if let Some(color) = &style.font_color {
1327 if !font.is_empty() {
1328 font.push(',');
1329 }
1330 font.push_str("\"color\":{\"rgb\":");
1331 font.push_str(&json_string(color));
1332 font.push('}');
1333 }
1334 parts.push(format!("\"font\":{{{}}}", font));
1335 }
1336 if let Some(color) = &style.fill_color {
1337 parts.push(format!(
1338 "\"fill\":{{\"fgColor\":{{\"rgb\":{}}}}}",
1339 json_string(color)
1340 ));
1341 }
1342 let border_specs = [
1343 (
1344 "bottom",
1345 style.border_bottom.as_ref(),
1346 style.border_bottom_color.as_ref(),
1347 ),
1348 (
1349 "left",
1350 style.border_left.as_ref(),
1351 style.border_left_color.as_ref(),
1352 ),
1353 (
1354 "right",
1355 style.border_right.as_ref(),
1356 style.border_right_color.as_ref(),
1357 ),
1358 (
1359 "top",
1360 style.border_top.as_ref(),
1361 style.border_top_color.as_ref(),
1362 ),
1363 ];
1364 let border_parts: Vec<_> = border_specs
1365 .into_iter()
1366 .filter_map(|(name, border, color)| {
1367 border.map(|value| {
1368 let color = color
1369 .map(|v| format!(",\"color\":{{\"rgb\":{}}}", json_string(v)))
1370 .unwrap_or_default();
1371 format!(
1372 "\"{}\":{{\"style\":{}{}{}}}",
1373 name,
1374 json_string(value),
1375 color,
1376 ""
1377 )
1378 })
1379 })
1380 .collect();
1381 if !border_parts.is_empty() {
1382 parts.push(format!("\"border\":{{{}}}", border_parts.join(",")));
1383 }
1384 if style.horizontal.is_some() || style.vertical.is_some() || style.wrap_text.is_some() {
1385 let mut alignment = String::new();
1386 if let Some(value) = &style.horizontal {
1387 alignment.push_str("\"horizontal\":");
1388 alignment.push_str(&json_string(value));
1389 }
1390 if let Some(value) = &style.vertical {
1391 if !alignment.is_empty() {
1392 alignment.push(',');
1393 }
1394 alignment.push_str("\"vertical\":");
1395 alignment.push_str(&json_string(value));
1396 }
1397 if let Some(value) = style.wrap_text {
1398 if !alignment.is_empty() {
1399 alignment.push(',');
1400 }
1401 alignment.push_str(&format!("\"wrapText\":{}", value));
1402 }
1403 parts.push(format!("\"alignment\":{{{}}}", alignment));
1404 }
1405 if !parts.is_empty() {
1406 out.push_str(",\"s\":{");
1407 out.push_str(&parts.join(","));
1408 out.push('}');
1409 }
1410 }
1411 out.push('}');
1412 out
1413}
1414
1415fn json_number(v: f64) -> String {
1420 if v.is_finite() {
1421 format!("{}", v)
1422 } else {
1423 "null".to_string()
1424 }
1425}
1426
1427fn col_letters(mut col: u32) -> String {
1432 let mut s = String::new();
1433 while col > 0 {
1434 let rem = (col - 1) % 26;
1435 s.insert(0, (b'A' + rem as u8) as char);
1436 col = (col - 1) / 26;
1437 }
1438 s
1439}
1440
1441fn cell_ref(row: u32, col: u32) -> String {
1442 format!("{}{}", col_letters(col), row)
1443}
1444
1445fn format_rect(rect: &((u32, u32), (u32, u32))) -> String {
1446 let ((r1, c1), (r2, c2)) = *rect;
1447 let start = format!("{}{}", col_letters(c1), r1);
1448 if r1 == r2 && c1 == c2 {
1449 start
1450 } else {
1451 format!("{}:{}{}", start, col_letters(c2), r2)
1452 }
1453}
1454
1455#[cfg(test)]
1456mod tests {
1457 use super::*;
1458 use std::collections::HashMap;
1459
1460 fn sheet(name: &str, cells: Vec<((u32, u32), SheetCell)>) -> BufferSheet {
1461 BufferSheet {
1462 sheet: elixcee::reader::WorkbookSheet {
1463 name: name.to_string(),
1464 cells: cells.into_iter().collect::<HashMap<_, _>>(),
1465 sheet_id: None,
1466 workbook_rel_id: None,
1467 source_part_name: None,
1468 merged_ranges: vec![],
1469 hidden_rows: vec![],
1470 hidden_columns: vec![],
1471 raw_style_indices: HashMap::new(),
1472 formulas: HashMap::new(),
1473 cell_number_formats: HashMap::new(),
1474 sheet_state: None,
1475 row_heights: HashMap::new(),
1476 column_widths: Vec::new(),
1477 row_styles: HashMap::new(),
1478 column_styles: Vec::new(),
1479 tables: Vec::new(),
1480 data_validations: Vec::new(),
1481 conditional_format_ranges: Vec::new(),
1482 comment_cells: Vec::new(),
1483 autofilter: None,
1484 },
1485 formulas: HashMap::new(),
1486 dimension: None,
1487 style_ids: HashMap::new(),
1488 charts: Vec::new(),
1489 comment_notes: Vec::new(),
1490 conditional_formats: Vec::new(),
1491 cell_styles: HashMap::new(),
1492 freeze_pane: None,
1493 }
1494 }
1495
1496 fn wb1(s: BufferSheet) -> BufferWorkbook {
1500 BufferWorkbook {
1501 sheets: vec![s],
1502 number_formats: HashMap::new(),
1503 date1904: false,
1504 defined_names: Vec::new(),
1505 }
1506 }
1507
1508 #[test]
1509 fn col_letters_matches_the_usual_a1_z1_aa1_examples() {
1510 assert_eq!(col_letters(1), "A");
1511 assert_eq!(col_letters(26), "Z");
1512 assert_eq!(col_letters(27), "AA");
1513 assert_eq!(col_letters(702), "ZZ");
1514 }
1515
1516 #[test]
1517 fn structured_reference_rewrite_respects_formula_token_boundaries() {
1518 assert_eq!(
1519 replace_table_ref_case_insensitive("=SUM(Sales[Amount])", "Sales[Amount]", "B2:B3"),
1520 "=SUM(B2:B3)"
1521 );
1522 assert_eq!(
1523 replace_table_ref_case_insensitive("=\"Sales[Amount]\"", "Sales[Amount]", "B2:B3"),
1524 "=\"Sales[Amount]\""
1525 );
1526 assert_eq!(
1527 replace_table_ref_case_insensitive("=MySales[Amount]", "Sales[Amount]", "B2:B3"),
1528 "=MySales[Amount]"
1529 );
1530 assert_eq!(
1531 replace_table_ref_case_insensitive(
1532 "=\"Sales[\"\"Amount\"\"]\"",
1533 "Sales[Amount]",
1534 "B2:B3"
1535 ),
1536 "=\"Sales[\"\"Amount\"\"]\""
1537 );
1538 }
1539
1540 #[test]
1541 fn workbook_json_shapes_an_empty_sheet_with_no_ref() {
1542 let json = workbook_json(&wb1(sheet("Sheet1", vec![])));
1543 assert_eq!(
1544 json,
1545 r#"{"SheetNames":["Sheet1"],"Sheets":{"Sheet1":{}},"!date1904":false}"#
1546 );
1547 }
1548
1549 #[test]
1550 fn workbook_json_computes_ref_and_cell_types_from_mixed_cells() {
1551 let json = workbook_json(&wb1(sheet(
1552 "Sheet1",
1553 vec![
1554 ((1, 1), SheetCell::Integer(1)),
1555 ((2, 2), SheetCell::Str("hi".to_string())),
1556 ((3, 1), SheetCell::Bool(true)),
1557 ],
1558 )));
1559 assert!(json.contains(r#""A1":{"t":"n","v":1}"#));
1560 assert!(json.contains(r#""B2":{"t":"s","v":"hi"}"#));
1561 assert!(json.contains(r#""A3":{"t":"b","v":true}"#));
1562 assert!(json.contains(r#""!ref":"A1:B3""#));
1563 }
1564
1565 #[test]
1566 fn workbook_json_includes_merges_as_zero_based_ranges() {
1567 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(1))]);
1568 s.sheet.merged_ranges.push(((1, 1), (1, 3)));
1569 let json = workbook_json(&wb1(s));
1570 assert!(json.contains(r#""!merges":[{"s":{"r":0,"c":0},"e":{"r":0,"c":2}}]"#));
1571 }
1572
1573 #[test]
1574 fn json_number_guards_non_finite_floats() {
1575 assert_eq!(json_number(1.5), "1.5");
1576 assert_eq!(json_number(f64::NAN), "null");
1577 assert_eq!(json_number(f64::INFINITY), "null");
1578 }
1579
1580 #[test]
1583 fn worksheet_json_prefers_dimension_over_the_populated_bounding_box() {
1584 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(1))]);
1585 s.dimension = Some(((1, 1), (10, 5)));
1586 let json = workbook_json(&wb1(s));
1587 assert!(json.contains(r#""!ref":"A1:E10""#));
1588 }
1589
1590 #[test]
1591 fn worksheet_json_uses_dimension_even_when_no_cells_are_populated() {
1592 let mut s = sheet("Sheet1", vec![]);
1593 s.dimension = Some(((1, 1), (3, 3)));
1594 let json = workbook_json(&wb1(s));
1595 assert!(json.contains(r#""!ref":"A1:C3""#));
1596 }
1597
1598 #[test]
1599 fn worksheet_json_falls_back_to_the_bounding_box_when_dimension_is_absent() {
1600 let json = workbook_json(&wb1(sheet(
1601 "Sheet1",
1602 vec![
1603 ((2, 2), SheetCell::Integer(1)),
1604 ((3, 4), SheetCell::Integer(2)),
1605 ],
1606 )));
1607 assert!(json.contains(r#""!ref":"B2:D3""#));
1608 }
1609
1610 #[test]
1614 fn worksheet_json_collapses_a_single_cell_bounding_box_ref_no_colon() {
1615 let json = workbook_json(&wb1(sheet("Sheet1", vec![((2, 2), SheetCell::Integer(1))])));
1616 assert!(json.contains(r#""!ref":"B2""#));
1617 assert!(!json.contains("\"!ref\":\"B2:B2\""));
1618 }
1619
1620 #[test]
1621 fn worksheet_json_collapses_a_single_cell_dimension_ref_no_colon() {
1622 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(1))]);
1623 s.dimension = Some(((1, 1), (1, 1)));
1624 let json = workbook_json(&wb1(s));
1625 assert!(json.contains(r#""!ref":"A1""#));
1626 assert!(!json.contains("\"!ref\":\"A1:A1\""));
1627 }
1628
1629 #[test]
1632 fn cell_json_includes_f_when_a_formula_is_present() {
1633 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(3))]);
1634 s.formulas.insert((1, 1), "SUM(B1:B2)".to_string());
1635 let json = workbook_json(&wb1(s));
1636 assert!(json.contains(r#""A1":{"t":"n","v":3,"f":"SUM(B1:B2)"}"#));
1637 }
1638
1639 #[test]
1640 fn cell_json_omits_f_when_no_formula_is_present() {
1641 let json = workbook_json(&wb1(sheet("Sheet1", vec![((1, 1), SheetCell::Integer(3))])));
1642 assert!(json.contains(r#""A1":{"t":"n","v":3}"#));
1643 assert!(!json.contains("\"f\":"));
1644 }
1645
1646 #[test]
1649 fn worksheet_json_includes_hidden_row_and_col_intervals_when_present() {
1650 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(1))]);
1651 s.sheet.hidden_rows.push((11, 14));
1652 s.sheet.hidden_columns.push((2, 2));
1653 let json = workbook_json(&wb1(s));
1654 assert!(json.contains(r#""!hiddenRows":[[11,14]]"#));
1655 assert!(json.contains(r#""!hiddenCols":[[2,2]]"#));
1656 }
1657
1658 #[test]
1659 fn worksheet_json_omits_hidden_interval_keys_when_none_are_hidden() {
1660 let json = workbook_json(&wb1(sheet("Sheet1", vec![((1, 1), SheetCell::Integer(1))])));
1661 assert!(!json.contains("!hiddenRows"));
1662 assert!(!json.contains("!hiddenCols"));
1663 }
1664
1665 #[test]
1666 fn worksheet_json_projects_data_validation_type_and_ranges() {
1667 let mut s = sheet("Sheet1", vec![]);
1668 s.sheet
1669 .data_validations
1670 .push(elixcee::reader::DataValidationRule {
1671 validation_type: "list".to_string(),
1672 operator: None,
1673 formula1: Some("Yes,No".to_string()),
1674 formula2: None,
1675 allow_blank: true,
1676 show_input_message: false,
1677 prompt_title: None,
1678 prompt: None,
1679 show_error_message: true,
1680 error_style: None,
1681 error_title: None,
1682 error: None,
1683 sqref: vec![((1, 5), (1, 5)), ((2, 5), (4, 5))],
1684 dirty: false,
1685 raw_span: String::new(),
1686 });
1687 let json = workbook_json(&wb1(s));
1688 assert!(json.contains(r#""!dataValidations":[{"type":"list","sqref":["E1","E2:E4"]}]"#));
1689 }
1690
1691 #[test]
1694 fn cell_json_includes_fmt_id_when_a_non_zero_style_id_is_present() {
1695 let mut s = sheet("Sheet1", vec![((1, 1), SheetCell::Integer(3))]);
1696 s.style_ids.insert((1, 1), 14);
1697 let json = workbook_json(&wb1(s));
1698 assert!(json.contains(r#""A1":{"t":"n","v":3,"fmtId":14}"#));
1699 }
1700
1701 #[test]
1702 fn cell_json_omits_fmt_id_when_no_style_id_is_present() {
1703 let json = workbook_json(&wb1(sheet("Sheet1", vec![((1, 1), SheetCell::Integer(3))])));
1704 assert!(!json.contains("\"fmtId\":"));
1705 }
1706
1707 #[test]
1708 fn workbook_json_includes_num_fmts_when_present() {
1709 let mut number_formats = HashMap::new();
1710 number_formats.insert(164u32, "0.00\"kg\"".to_string());
1711 let wb = BufferWorkbook {
1712 sheets: vec![sheet("Sheet1", vec![])],
1713 number_formats,
1714 date1904: false,
1715 defined_names: Vec::new(),
1716 };
1717 let json = workbook_json(&wb);
1718 assert!(json.contains(r#""!numFmts":{"164":"0.00\"kg\""}"#));
1719 }
1720
1721 #[test]
1722 fn workbook_json_omits_num_fmts_when_empty() {
1723 let json = workbook_json(&wb1(sheet("Sheet1", vec![])));
1724 assert!(!json.contains("!numFmts"));
1725 }
1726
1727 #[test]
1728 fn workbook_json_exposes_defined_names_with_scope() {
1729 let mut wb = wb1(sheet("Sheet1", vec![]));
1730 wb.defined_names.push(elixcee::reader::XlsxDefinedName {
1731 name: "SalesRange".to_string(),
1732 local_sheet_id: Some(0),
1733 raw_text: "Sheet1!$A$1:$A$2".to_string(),
1734 });
1735 let json = workbook_json(&wb);
1736 assert!(json.contains(
1737 r#""Workbook":{"Names":[{"Name":"SalesRange","Ref":"Sheet1!$A$1:$A$2","Sheet":0}]}"#
1738 ));
1739 }
1740
1741 #[test]
1742 fn normalize_defined_name_ref_supports_quoted_and_local_absolute_refs() {
1743 assert_eq!(
1744 normalize_defined_name_ref("'Sheet 1'!$A$1:$B$2"),
1745 "Sheet 1!$A$1:$B$2"
1746 );
1747 assert_eq!(normalize_defined_name_ref("$A$1"), "A1");
1748 }
1749
1750 #[test]
1751 fn workbook_json_always_includes_date1904() {
1752 let mut wb = wb1(sheet("Sheet1", vec![]));
1753 wb.date1904 = true;
1754 let json = workbook_json(&wb);
1755 assert!(json.contains(r#""!date1904":true"#));
1756 }
1757
1758 #[test]
1759 fn editor_transaction_coalesces_multiple_typed_writes_into_one_undo() {
1760 let workbook = wb1(sheet("Sheet1", vec![]));
1761 let sheets = calculation_sheets(&workbook);
1762 let mut editor = WorkbookEditor {
1763 workbook,
1764 sheets,
1765 undo: Vec::new(),
1766 redo: Vec::new(),
1767 transaction: None,
1768 transaction_dirty: false,
1769 };
1770
1771 editor.begin_transaction().unwrap();
1772 editor.set_string("Sheet1", 1, 1, "planned").unwrap();
1773 editor.set_boolean("Sheet1", 1, 2, true).unwrap();
1774 assert!(editor.commit_transaction());
1775 assert!(editor.can_undo());
1776 assert!(editor.undo());
1777 let snapshot = editor.snapshot();
1778 assert!(!snapshot.contains("planned"));
1779 assert!(!snapshot.contains("\"B1\""));
1780 assert!(!editor.can_undo());
1781 assert!(editor.redo());
1782 let snapshot = editor.snapshot();
1783 assert!(snapshot.contains("planned"));
1784 assert!(snapshot.contains("\"B1\""));
1785 }
1786
1787 #[test]
1788 fn formula_array_spill_materializes_empty_horizontal_targets_without_overwrite() {
1789 let mut sheets = HashMap::from([(
1790 "sheet1".to_string(),
1791 HashMap::from([
1792 (
1793 (0, 0),
1794 CellContent {
1795 formula: Some("=SEQUENCE(1,3)".to_string()),
1796 value: Variant::Array(vec![
1797 Variant::Integer(1),
1798 Variant::Integer(2),
1799 Variant::Integer(3),
1800 ]),
1801 },
1802 ),
1803 (
1804 (0, 2),
1805 CellContent {
1806 formula: None,
1807 value: Variant::Str("kept".to_string()),
1808 },
1809 ),
1810 ]),
1811 )]);
1812 materialize_formula_array_spills(&mut sheets).unwrap();
1813 assert!(!sheets["sheet1"].contains_key(&(0, 1)));
1814 assert_eq!(
1815 sheets["sheet1"][&(0, 0)].value,
1816 Variant::Str("#SPILL!".to_string())
1817 );
1818 assert_eq!(
1819 sheets["sheet1"][&(0, 2)].value,
1820 Variant::Str("kept".to_string())
1821 );
1822 }
1823
1824 #[test]
1825 fn sequence_spill_materializes_vertical_and_rectangular_shapes() {
1826 let mut sheets = HashMap::from([(
1827 "sheet1".to_string(),
1828 HashMap::from([(
1829 (0, 0),
1830 CellContent {
1831 formula: Some("=SEQUENCE(2,2)".to_string()),
1832 value: Variant::Array(vec![
1833 Variant::Integer(1),
1834 Variant::Integer(2),
1835 Variant::Integer(3),
1836 Variant::Integer(4),
1837 ]),
1838 },
1839 )]),
1840 )]);
1841 materialize_formula_array_spills(&mut sheets).unwrap();
1842 assert_eq!(sheets["sheet1"][&(0, 1)].value, Variant::Integer(2));
1843 assert_eq!(sheets["sheet1"][&(1, 0)].value, Variant::Integer(3));
1844 assert_eq!(sheets["sheet1"][&(1, 1)].value, Variant::Integer(4));
1845 }
1846
1847 #[test]
1848 fn formula_array_shape_recovers_wrapped_vector_layouts() {
1849 assert_eq!(
1850 formula_array_shape(Some("=RANDARRAY(3,2)"), 6),
1851 ArrayShape::new(3, 2)
1852 );
1853 assert_eq!(
1854 formula_array_shape(Some("=WRAPROWS(SEQUENCE(5),2)"), 5),
1855 ArrayShape::new(3, 2)
1856 );
1857 assert_eq!(
1858 formula_array_shape(Some("=WRAPCOLS(SEQUENCE(5),2)"), 5),
1859 ArrayShape::new(2, 3)
1860 );
1861 assert_eq!(
1862 formula_array_shape(Some("=UNIQUE(A1:A5)"), 5),
1863 ArrayShape::new(1, 5)
1864 );
1865 }
1866}