numbers-parser 4.14.4__py3-none-any.whl → 4.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -524,9 +524,7 @@ TSPRegistryMapping = {
524
524
  "6293": "TST.CommandRewriteFilterFormulasForRewriteSpecArchive",
525
525
  "6294": "TST.CommandRewriteSortOrderForRewriteSpecArchive",
526
526
  "6295": "TST.StrokeSelectionArchive",
527
- "6297": "TST.LetNodeArchive",
528
527
  "6298": "TST.VariableNodeArchive",
529
- "6299": "TST.InNodeArchive",
530
528
  "6300": "TST.CommandInverseMergeArchive",
531
529
  "6301": "TST.CommandMoveCellsArchive",
532
530
  "6302": "TST.DefaultCellStylesContainerArchive",
@@ -579,6 +577,7 @@ TSPRegistryMapping = {
579
577
  "6381": "TST.CommandExtendTableIDHistoryArchive",
580
578
  "6382": "TST.GroupByArchive.AggregatorArchive",
581
579
  "6383": "TST.GroupByArchive.GroupNodeArchive",
580
+ "6384": "TST.SpillOriginRefNodeArchive",
582
581
  "10011": "TSWP.SectionPlaceholderArchive",
583
582
  "10020": "TSWP.ShapeSelectionTransformerArchive",
584
583
  "10021": "TSWP.SelectionTransformerArchive",
numbers_parser/model.py CHANGED
@@ -4,6 +4,7 @@ import re
4
4
  from array import array
5
5
  from collections import defaultdict
6
6
  from hashlib import sha1
7
+ from itertools import chain
7
8
  from math import floor
8
9
  from pathlib import Path
9
10
  from struct import pack
@@ -29,8 +30,6 @@ from numbers_parser.cell import (
29
30
  PaddingType,
30
31
  Style,
31
32
  VerticalJustification,
32
- xl_col_to_name,
33
- xl_rowcol_to_cell,
34
33
  )
35
34
  from numbers_parser.constants import (
36
35
  ALLOWED_FORMATTING_PARAMETERS,
@@ -74,7 +73,8 @@ from numbers_parser.generated.TSWPArchives_pb2 import (
74
73
  )
75
74
  from numbers_parser.iwafile import find_extension
76
75
  from numbers_parser.numbers_cache import Cacheable, cache
77
- from numbers_parser.numbers_uuid import NumbersUUID
76
+ from numbers_parser.numbers_uuid import NumbersUUID, uuid_to_hex
77
+ from numbers_parser.xrefs import CellRange, ScopedNameRefCache
78
78
 
79
79
 
80
80
  def create_font_name_map(font_map: dict) -> dict:
@@ -224,6 +224,7 @@ class _NumbersModel(Cacheable):
224
224
  self._table_styles = DataLists(self, "styleTable", "reference")
225
225
  self._table_strings = DataLists(self, "stringTable", "string")
226
226
  self._control_specs = DataLists(self, "control_cell_spec_table", "cell_spec")
227
+ self._formulas = DataLists(self, "formula_table", "formula")
227
228
  self._table_data = {}
228
229
  self._styles = None
229
230
  self._images = {}
@@ -236,6 +237,8 @@ class _NumbersModel(Cacheable):
236
237
  "bottom": defaultdict(),
237
238
  "left": defaultdict(),
238
239
  }
240
+ self.name_ref_cache = ScopedNameRefCache(self)
241
+ self.calculate_table_uuid_map()
239
242
 
240
243
  def save(self, filepath: Path, package: bool) -> None:
241
244
  self.objects.save(filepath, package)
@@ -258,13 +261,16 @@ class _NumbersModel(Cacheable):
258
261
  self._table_data[table_id] = data
259
262
 
260
263
  # Don't cache: new tables can be added at runtime
261
- def table_ids(self, sheet_id: int) -> list:
262
- """Return a list of table IDs for a given sheet ID."""
264
+ def table_ids(self, sheet_id: int | None = None) -> list:
265
+ """
266
+ Return a list of table IDs for a given sheet ID or all table
267
+ IDs id the sheet ID is None
268
+ """
263
269
  table_info_ids = self.find_refs("TableInfoArchive")
264
270
  return [
265
271
  self.objects[t_id].tableModel.identifier
266
272
  for t_id in table_info_ids
267
- if self.objects[t_id].super.parent.identifier == sheet_id
273
+ if (sheet_id is None or self.objects[t_id].super.parent.identifier == sheet_id)
268
274
  ]
269
275
 
270
276
  # Don't cache: new tables can be added at runtime
@@ -282,14 +288,7 @@ class _NumbersModel(Cacheable):
282
288
  # The base data store contains a reference to rowHeaders.buckets
283
289
  # which is an ordered list that matches the storage buffers, but
284
290
  # identifies which row a storage buffer belongs to (empty rows have
285
- # no storage buffers). Each bucket is:
286
- #
287
- # {
288
- # "hiding_state": 0,
289
- # "index": 0,
290
- # "number_of_cells": 3,
291
- # "size": 0.0
292
- # },
291
+ # no storage buffers).
293
292
  row_bucket_map = {i: None for i in range(self.objects[table_id].number_of_rows)}
294
293
  bds = self.objects[table_id].base_data_store
295
294
  bucket_ids = [x.identifier for x in bds.rowHeaders.buckets]
@@ -316,6 +315,13 @@ class _NumbersModel(Cacheable):
316
315
  self.objects[table_id].table_name = value
317
316
  return None
318
317
 
318
+ def table_names(self):
319
+ return list(
320
+ chain.from_iterable(
321
+ [[self.table_name(tid) for tid in self.table_ids(sid)] for sid in self.sheet_ids()],
322
+ ),
323
+ )
324
+
319
325
  def table_name_enabled(self, table_id: int, enabled: bool | None = None):
320
326
  if enabled is not None:
321
327
  self.objects[table_id].table_name_enabled = enabled
@@ -751,51 +757,77 @@ class _NumbersModel(Cacheable):
751
757
  owner_id_map[e.internal_owner_id] = NumbersUUID(e.owner_id).hex
752
758
  return owner_id_map
753
759
 
760
+ def calculate_table_uuid_map(self) -> None:
761
+ # Each Table Model has a UUID which is used in references to the table. See
762
+ # Numbers.md#uuid-mapping for more details.
763
+
764
+ # For haunted owner archive types, map formula_owner_uids to their base_owner_uids
765
+ haunted_owner_ids = [
766
+ obj_id
767
+ for obj_id in self.find_refs("FormulaOwnerDependenciesArchive")
768
+ if self.objects[obj_id].owner_kind == OwnerKind.HAUNTED_OWNER
769
+ ]
770
+ if len(haunted_owner_ids) == 0:
771
+ # Some older documents (see issue-18) do not use FormulaOwnerDependenciesArchive
772
+ self._table_id_to_base_id = {}
773
+ return
774
+
775
+ formula_owner_to_base_owner_map = {
776
+ uuid_to_hex(self.objects[obj_id].formula_owner_uid): uuid_to_hex(
777
+ self.objects[obj_id].base_owner_uid,
778
+ )
779
+ for obj_id in haunted_owner_ids
780
+ }
781
+
782
+ # Map table IDs to the base_owner_uids of the formula owners that match
783
+ # the table model's haunted owner
784
+ self._table_id_to_base_id = {
785
+ table_id: formula_owner_to_base_owner_map[
786
+ uuid_to_hex(self.objects[table_id].haunted_owner.owner_uid)
787
+ ]
788
+ for table_id in self.table_ids()
789
+ }
790
+ self._table_base_id_to_formula_owner_id = {
791
+ uuid_to_hex(self.objects[obj_id].base_owner_uid): obj_id for obj_id in haunted_owner_ids
792
+ }
793
+
754
794
  @cache()
755
795
  def table_base_id(self, table_id: int) -> int:
756
796
  """ "Finds the UUID of a table."""
757
- # Look for a TSCE.FormulaOwnerDependenciesArchive objects with the following at the
758
- # root level of the protobuf:
759
- #
760
- # "base_owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b368"",
761
- #   "formula_owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b36d"
762
- #
763
- # The Table UUID is the TSCE.FormulaOwnerDependenciesArchive whose formula_owner_uid
764
- # matches the UUID of the haunted_owner of the Table:
765
- #
766
- # "haunted_owner": {
767
- # "owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b368""
768
- # }
769
- haunted_owner = NumbersUUID(self.objects[table_id].haunted_owner.owner_uid).hex
770
- formula_owner_ids = self.find_refs("FormulaOwnerDependenciesArchive")
771
- for dependency_id in formula_owner_ids: # pragma: no branch
772
- obj = self.objects[dependency_id]
773
- # if obj.owner_kind == OwnerKind.HAUNTED_OWNER:
774
- if obj.HasField("base_owner_uid") and obj.HasField(
775
- "formula_owner_uid",
776
- ): # pragma: no branch
777
- base_owner_uid = NumbersUUID(obj.base_owner_uid).hex
778
- formula_owner_uid = NumbersUUID(obj.formula_owner_uid).hex
779
- if formula_owner_uid == haunted_owner:
780
- return base_owner_uid
781
- return None
797
+ # Table can be empty if the document does not use FormulaOwnerDependenciesArchive
798
+ return self._table_id_to_base_id.get(table_id)
782
799
 
783
- @cache()
784
- def formula_cell_ranges(self, table_id: int) -> list:
785
- """Exract all the formula cell ranges for the Table."""
786
- # https://github.com/masaccio/numbers-parser/blob/main/doc/Numbers.md#formula-ranges
800
+ def get_formula_owner(self, table_id: int) -> object:
801
+ table_uuid = self.table_base_id(table_id)
802
+ return self.objects[self._table_base_id_to_formula_owner_id[table_uuid]]
803
+
804
+ def add_formula_dependency(self, row: int, col: int, table_id: int) -> None:
787
805
  calc_engine = self.calc_engine()
806
+ calc_engine.dependency_tracker.number_of_formulas += 1
807
+ internal_formula_id = calc_engine.dependency_tracker.number_of_formulas
788
808
 
789
- table_base_id = self.table_base_id(table_id)
790
- cell_records = []
791
- for finfo in calc_engine.dependency_tracker.formula_owner_info:
792
- if finfo.HasField("cell_dependencies"): # pragma: no branch
793
- formula_owner_id = NumbersUUID(finfo.formula_owner_id).hex
794
- if formula_owner_id == table_base_id:
795
- for cell_record in finfo.cell_dependencies.cell_record:
796
- if cell_record.contains_a_formula: # pragma: no branch
797
- cell_records.append((cell_record.row, cell_record.column))
798
- return cell_records
809
+ formula_owner = self.get_formula_owner(table_id)
810
+ formula_owner.cell_dependencies.cell_record.append(
811
+ TSCEArchives.CellRecordExpandedArchive(column=col, row=row),
812
+ )
813
+ if len(formula_owner.tiled_cell_dependencies.cell_record_tiles) == 0:
814
+ cell_record_id, cell_record = self.objects.create_object_from_dict(
815
+ "CalculationEngine",
816
+ {
817
+ "internal_owner_id": internal_formula_id,
818
+ "tile_column_begin": 0,
819
+ "tile_row_begin": 0,
820
+ },
821
+ TSCEArchives.CellRecordTileArchive,
822
+ )
823
+ formula_owner.tiled_cell_dependencies.cell_record_tiles.append(
824
+ TSPMessages.Reference(identifier=cell_record_id),
825
+ )
826
+ else:
827
+ cell_record_id = formula_owner.tiled_cell_dependencies.cell_record_tiles[0].identifier
828
+ cell_record = self.objects[cell_record_id]
829
+
830
+ cell_record.cell_records.append(formula_owner.cell_dependencies.cell_record[-1])
799
831
 
800
832
  @cache(num_args=0)
801
833
  def calc_engine_id(self):
@@ -815,8 +847,8 @@ class _NumbersModel(Cacheable):
815
847
 
816
848
  @cache()
817
849
  def calculate_merge_cell_ranges(self, table_id) -> None:
818
- """Exract all the merge cell ranges for the Table."""
819
- # https://github.com/masaccio/numbers-parser/blob/main/doc/Numbers.md#merge-ranges
850
+ """Extract all the merge cell ranges for the Table."""
851
+ # See details in Numbers.md#merge-ranges.
820
852
  owner_id_map = self.owner_id_map()
821
853
  table_base_id = self.table_base_id(table_id)
822
854
 
@@ -879,6 +911,17 @@ class _NumbersModel(Cacheable):
879
911
  return sheet_id
880
912
  return None
881
913
 
914
+ def table_name_to_uuid(self, sheet_name: str, table_name: str) -> str:
915
+ table_ids = [tid for tid in self.table_ids() if table_name == self.table_name(tid)]
916
+ if len(table_ids) == 1:
917
+ return self.table_base_id(table_ids[0])
918
+
919
+ sheet_name_to_id = {self.sheet_name(x): x for x in self.sheet_ids()}
920
+ sheet_id = sheet_name_to_id[sheet_name]
921
+ table_name_to_id = {self.table_name(x): x for x in self.table_ids(sheet_id)}
922
+ table_id = table_name_to_id[table_name]
923
+ return self.table_base_id(table_id)
924
+
882
925
  @cache()
883
926
  def table_uuids_to_id(self, table_uuid) -> int | None:
884
927
  for sheet_id in self.sheet_ids(): # pragma: no branch # noqa: RET503
@@ -886,66 +929,103 @@ class _NumbersModel(Cacheable):
886
929
  if table_uuid == self.table_base_id(table_id):
887
930
  return table_id
888
931
 
889
- def node_to_ref(self, this_table_id: int, row: int, col: int, node):
932
+ def node_to_ref(self, table_id: int, row: int, col: int, node):
933
+ def resolve_range(is_absolute, absolute_list, relative_list, offset, max_val):
934
+ if is_absolute:
935
+ return absolute_list[0].range_begin
936
+ if not relative_list and absolute_list[0].range_begin == max_val:
937
+ return max_val
938
+ return offset + relative_list[0].range_begin
939
+
940
+ def resolve_range_end(is_absolute, absolute_list, relative_list, offset, max_val):
941
+ if is_absolute:
942
+ return range_end(absolute_list[0])
943
+ if not relative_list and range_end(absolute_list[0]) == max_val:
944
+ return max_val
945
+ return offset + range_end(relative_list[0])
946
+
890
947
  if node.HasField("AST_cross_table_reference_extra_info"):
891
948
  table_uuid = NumbersUUID(node.AST_cross_table_reference_extra_info.table_id).hex
892
- other_table_id = self.table_uuids_to_id(table_uuid)
893
- other_table_name = self.table_name(other_table_id)
949
+ to_table_id = self.table_uuids_to_id(table_uuid)
894
950
  else:
895
- other_table_id = None
896
- other_table_name = None
897
-
898
- if other_table_id is not None:
899
- this_sheet_id = self.table_id_to_sheet_id(this_table_id)
900
- other_sheet_id = self.table_id_to_sheet_id(other_table_id)
901
- if this_sheet_id != other_sheet_id:
902
- other_sheet_name = self.sheet_name(other_sheet_id)
903
- other_table_name = f"{other_sheet_name}::" + other_table_name
951
+ to_table_id = None
904
952
 
905
953
  if node.HasField("AST_colon_tract"):
906
- return self.tract_to_row_col_ref(node, other_table_name, row, col)
907
- if node.HasField("AST_row") and not node.HasField("AST_column"):
908
- return node_to_row_ref(node, other_table_name, row)
909
- if node.HasField("AST_column") and not node.HasField("AST_row"):
910
- return node_to_col_ref(node, other_table_name, col)
911
- return node_to_row_col_ref(node, other_table_name, row, col)
954
+ row_begin = resolve_range(
955
+ node.AST_sticky_bits.begin_row_is_absolute,
956
+ node.AST_colon_tract.absolute_row,
957
+ node.AST_colon_tract.relative_row,
958
+ row,
959
+ 0x7FFFFFFF,
960
+ )
912
961
 
913
- def tract_to_row_col_ref(self, node: object, table_name: str, row: int, col: int) -> str:
914
- if node.AST_sticky_bits.begin_row_is_absolute:
915
- row_begin = node.AST_colon_tract.absolute_row[0].range_begin
916
- else:
917
- row_begin = row + node.AST_colon_tract.relative_row[0].range_begin
962
+ row_end = resolve_range_end(
963
+ node.AST_sticky_bits.end_row_is_absolute,
964
+ node.AST_colon_tract.absolute_row,
965
+ node.AST_colon_tract.relative_row,
966
+ row,
967
+ 0x7FFFFFFF,
968
+ )
918
969
 
919
- if node.AST_sticky_bits.end_row_is_absolute:
920
- row_end = range_end(node.AST_colon_tract.absolute_row[0])
921
- else:
922
- row_end = row + range_end(node.AST_colon_tract.relative_row[0])
970
+ col_begin = resolve_range(
971
+ node.AST_sticky_bits.begin_column_is_absolute,
972
+ node.AST_colon_tract.absolute_column,
973
+ node.AST_colon_tract.relative_column,
974
+ col,
975
+ 0x7FFF,
976
+ )
923
977
 
924
- if node.AST_sticky_bits.begin_column_is_absolute:
925
- col_begin = node.AST_colon_tract.absolute_column[0].range_begin
926
- else:
927
- col_begin = col + node.AST_colon_tract.relative_column[0].range_begin
978
+ col_end = resolve_range_end(
979
+ node.AST_sticky_bits.end_column_is_absolute,
980
+ node.AST_colon_tract.absolute_column,
981
+ node.AST_colon_tract.relative_column,
982
+ col,
983
+ 0x7FFF,
984
+ )
928
985
 
929
- if node.AST_sticky_bits.end_column_is_absolute:
930
- col_end = range_end(node.AST_colon_tract.absolute_column[0])
931
- else:
932
- col_end = col + range_end(node.AST_colon_tract.relative_column[0])
986
+ return CellRange(
987
+ model=self,
988
+ row_start=None if row_begin == 0x7FFFFFFF else row_begin,
989
+ row_end=None if row_end == 0x7FFFFFFF else row_end,
990
+ col_start=None if col_begin == 0x7FFF else col_begin,
991
+ col_end=None if col_end == 0x7FFF else col_end,
992
+ row_start_is_abs=node.AST_sticky_bits.begin_row_is_absolute,
993
+ row_end_is_abs=node.AST_sticky_bits.end_row_is_absolute,
994
+ col_start_is_abs=node.AST_sticky_bits.begin_column_is_absolute,
995
+ col_end_is_abs=node.AST_sticky_bits.end_column_is_absolute,
996
+ from_table_id=table_id,
997
+ to_table_id=to_table_id,
998
+ )
933
999
 
934
- begin_ref = xl_rowcol_to_cell(
935
- row_begin,
936
- col_begin,
937
- row_abs=node.AST_sticky_bits.begin_row_is_absolute,
938
- col_abs=node.AST_sticky_bits.begin_column_is_absolute,
939
- )
940
- end_ref = xl_rowcol_to_cell(
941
- row_end,
942
- col_end,
943
- row_abs=node.AST_sticky_bits.end_row_is_absolute,
944
- col_abs=node.AST_sticky_bits.end_column_is_absolute,
1000
+ row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
1001
+ col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
1002
+ if node.HasField("AST_row") and not node.HasField("AST_column"):
1003
+ return CellRange(
1004
+ model=self,
1005
+ row_start=row,
1006
+ row_start_is_abs=node.AST_row.absolute,
1007
+ from_table_id=table_id,
1008
+ to_table_id=to_table_id,
1009
+ )
1010
+
1011
+ if node.HasField("AST_column") and not node.HasField("AST_row"):
1012
+ return CellRange(
1013
+ model=self,
1014
+ col_start=col,
1015
+ col_start_is_abs=node.AST_column.absolute,
1016
+ from_table_id=table_id,
1017
+ to_table_id=to_table_id,
1018
+ )
1019
+
1020
+ return CellRange(
1021
+ model=self,
1022
+ row_start=row,
1023
+ col_start=col,
1024
+ row_start_is_abs=node.AST_row.absolute,
1025
+ col_start_is_abs=node.AST_column.absolute,
1026
+ from_table_id=table_id,
1027
+ to_table_id=to_table_id,
945
1028
  )
946
- if table_name is not None:
947
- return f"{table_name}::{begin_ref}:{end_ref}"
948
- return f"{begin_ref}:{end_ref}"
949
1029
 
950
1030
  @cache()
951
1031
  def formula_ast(self, table_id: int):
@@ -1480,12 +1560,6 @@ class _NumbersModel(Cacheable):
1480
1560
  TSPMessages.Reference(identifier=row_headers_id),
1481
1561
  )
1482
1562
 
1483
- self._table_data[table_model_id] = [
1484
- [Cell._empty_cell(table_model_id, row, col, self) for col in range(num_cols)]
1485
- for row in range(num_rows)
1486
- ]
1487
- self.recalculate_table_data(table_model_id, self._table_data[table_model_id])
1488
-
1489
1563
  table_info_id, table_info = self.objects.create_object_from_dict(
1490
1564
  "CalculationEngine",
1491
1565
  {},
@@ -1494,6 +1568,22 @@ class _NumbersModel(Cacheable):
1494
1568
  table_info.tableModel.MergeFrom(TSPMessages.Reference(identifier=table_model_id))
1495
1569
  table_info.super.MergeFrom(self.create_drawable(sheet_id, x, y))
1496
1570
 
1571
+ haunted_owner_uuid = self.add_formula_owner(
1572
+ table_info_id,
1573
+ num_rows,
1574
+ num_cols,
1575
+ number_of_header_rows,
1576
+ number_of_header_columns,
1577
+ )
1578
+ table_model.haunted_owner.owner_uid.MergeFrom(haunted_owner_uuid.protobuf2)
1579
+ self.calculate_table_uuid_map()
1580
+
1581
+ self._table_data[table_model_id] = [
1582
+ [Cell._empty_cell(table_model_id, row, col, self) for col in range(num_cols)]
1583
+ for row in range(num_rows)
1584
+ ]
1585
+ self.recalculate_table_data(table_model_id, self._table_data[table_model_id])
1586
+
1497
1587
  self.add_component_reference(
1498
1588
  table_info_id,
1499
1589
  location="Document",
@@ -1502,17 +1592,11 @@ class _NumbersModel(Cacheable):
1502
1592
  self.create_caption_archive(table_model_id)
1503
1593
  self.caption_enabled(table_model_id, False)
1504
1594
 
1505
- self.add_formula_owner(
1506
- table_info_id,
1507
- num_rows,
1508
- num_cols,
1509
- number_of_header_rows,
1510
- number_of_header_columns,
1511
- )
1512
-
1513
1595
  self.objects[sheet_id].drawable_infos.append(
1514
1596
  TSPMessages.Reference(identifier=table_info_id),
1515
1597
  )
1598
+
1599
+ self.name_ref_cache.mark_dirty()
1516
1600
  return table_model_id
1517
1601
 
1518
1602
  def add_formula_owner(
@@ -1522,7 +1606,7 @@ class _NumbersModel(Cacheable):
1522
1606
  num_cols: int,
1523
1607
  number_of_header_rows: int,
1524
1608
  number_of_header_columns: int,
1525
- ) -> None:
1609
+ ) -> NumbersUUID:
1526
1610
  """
1527
1611
  Create a FormulaOwnerDependenciesArchive that references a TableInfoArchive
1528
1612
  so that cross-references to cells in this table will work.
@@ -1531,49 +1615,43 @@ class _NumbersModel(Cacheable):
1531
1615
  calc_engine = self.calc_engine()
1532
1616
  owner_id_map = calc_engine.dependency_tracker.owner_id_map.map_entry
1533
1617
  next_owner_id = max([x.internal_owner_id for x in owner_id_map]) + 1
1534
- formula_deps_id, formula_deps = self.objects.create_object_from_dict(
1618
+ volatile_dependencies = {
1619
+ "volatile_time_cells": {},
1620
+ "volatile_random_cells": {},
1621
+ "volatile_locale_cells": {},
1622
+ "volatile_sheet_table_name_cells": {},
1623
+ "volatile_remote_data_cells": {},
1624
+ "volatile_geometry_cell_refs": {},
1625
+ }
1626
+ total_range_for_table = {
1627
+ "top_left_column": 0,
1628
+ "top_left_row": 0,
1629
+ "bottom_right_column": num_cols - 1,
1630
+ "bottom_right_row": num_cols - 1,
1631
+ }
1632
+ body_range_for_table = {
1633
+ "top_left_column": number_of_header_columns,
1634
+ "top_left_row": number_of_header_rows,
1635
+ "bottom_right_column": num_cols - 1,
1636
+ "bottom_right_row": num_cols - 1,
1637
+ }
1638
+
1639
+ formula_deps_id, _ = self.objects.create_object_from_dict(
1535
1640
  "CalculationEngine",
1536
1641
  {
1537
1642
  "formula_owner_uid": formula_owner_uuid.dict2,
1538
1643
  "internal_formula_owner_id": next_owner_id,
1539
- "owner_kind": 1,
1644
+ "owner_kind": OwnerKind.TABLE_MODEL,
1540
1645
  "cell_dependencies": {},
1541
1646
  "range_dependencies": {},
1542
- "volatile_dependencies": {
1543
- "volatile_time_cells": {},
1544
- "volatile_random_cells": {},
1545
- "volatile_locale_cells": {},
1546
- "volatile_sheet_table_name_cells": {},
1547
- "volatile_remote_data_cells": {},
1548
- "volatile_geometry_cell_refs": {},
1549
- },
1647
+ "volatile_dependencies": volatile_dependencies,
1550
1648
  "spanning_column_dependencies": {
1551
- "total_range_for_table": {
1552
- "top_left_column": 0,
1553
- "top_left_row": 0,
1554
- "bottom_right_column": num_cols - 1,
1555
- "bottom_right_row": num_cols - 1,
1556
- },
1557
- "body_range_for_table": {
1558
- "top_left_column": number_of_header_columns,
1559
- "top_left_row": number_of_header_rows,
1560
- "bottom_right_column": num_cols - 1,
1561
- "bottom_right_row": num_cols - 1,
1562
- },
1649
+ "total_range_for_table": total_range_for_table,
1650
+ "body_range_for_table": body_range_for_table,
1563
1651
  },
1564
1652
  "spanning_row_dependencies": {
1565
- "total_range_for_table": {
1566
- "top_left_column": 0,
1567
- "top_left_row": 0,
1568
- "bottom_right_column": num_cols - 1,
1569
- "bottom_right_row": num_cols - 1,
1570
- },
1571
- "body_range_for_table": {
1572
- "top_left_column": number_of_header_columns,
1573
- "top_left_row": number_of_header_rows,
1574
- "bottom_right_column": num_cols - 1,
1575
- "bottom_right_row": num_cols - 1,
1576
- },
1653
+ "total_range_for_table": total_range_for_table,
1654
+ "body_range_for_table": body_range_for_table,
1577
1655
  },
1578
1656
  "whole_owner_dependencies": {"dependent_cells": {}},
1579
1657
  "cell_errors": {},
@@ -1594,6 +1672,52 @@ class _NumbersModel(Cacheable):
1594
1672
  ),
1595
1673
  )
1596
1674
 
1675
+ # See Numbers.md#uuid-mapping for more details on mapping table model
1676
+ # UUID to the formula owner.
1677
+ formula_owner_uuid = NumbersUUID()
1678
+ base_owner_uuid = NumbersUUID()
1679
+ next_owner_id += 1
1680
+ null_range_ref = {
1681
+ "top_left_column": 0x7FFF,
1682
+ "top_left_row": 0x7FFFFFFF,
1683
+ "bottom_right_column": 0x7FFF,
1684
+ "bottom_right_row": 0x7FFFFFFF,
1685
+ }
1686
+ spanning_depdendencies = {
1687
+ "total_range_for_table": null_range_ref,
1688
+ "body_range_for_table": null_range_ref,
1689
+ }
1690
+ formula_deps_id, formula_deps = self.objects.create_object_from_dict(
1691
+ "CalculationEngine",
1692
+ {
1693
+ "formula_owner_uid": formula_owner_uuid.dict2,
1694
+ "internal_formula_owner_id": next_owner_id,
1695
+ "owner_kind": OwnerKind.HAUNTED_OWNER,
1696
+ "cell_dependencies": {},
1697
+ "range_dependencies": {},
1698
+ "volatile_dependencies": volatile_dependencies,
1699
+ "spanning_column_dependencies": spanning_depdendencies,
1700
+ "spanning_row_dependencies": spanning_depdendencies,
1701
+ "whole_owner_dependencies": {"dependent_cells": {}},
1702
+ "cell_errors": {},
1703
+ "base_owner_uid": base_owner_uuid.dict2,
1704
+ "tiled_cell_dependencies": {},
1705
+ "uuid_references": {},
1706
+ "tiled_range_dependencies": {},
1707
+ },
1708
+ TSCEArchives.FormulaOwnerDependenciesArchive,
1709
+ )
1710
+ calc_engine.dependency_tracker.formula_owner_dependencies.append(
1711
+ TSPMessages.Reference(identifier=formula_deps_id),
1712
+ )
1713
+ owner_id_map.append(
1714
+ TSCEArchives.OwnerIDMapArchive.OwnerIDMapArchiveEntry(
1715
+ internal_owner_id=next_owner_id,
1716
+ owner_id=formula_owner_uuid.protobuf4,
1717
+ ),
1718
+ )
1719
+ return formula_owner_uuid
1720
+
1597
1721
  def add_sheet(self, sheet_name: str) -> int:
1598
1722
  """Add a new sheet with a copy of a table from another sheet."""
1599
1723
  sheet_id, _ = self.objects.create_object_from_dict(
@@ -2448,39 +2572,6 @@ def formatted_number(number_type, index):
2448
2572
  return bullet_char
2449
2573
 
2450
2574
 
2451
- def node_to_col_ref(node: object, table_name: str, col: int) -> str:
2452
- col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2453
-
2454
- col_name = xl_col_to_name(col, node.AST_column.absolute)
2455
- if table_name is not None:
2456
- return f"{table_name}::{col_name}"
2457
- return col_name
2458
-
2459
-
2460
- def node_to_row_ref(node: object, table_name: str, row: int) -> str:
2461
- row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2462
-
2463
- row_name = f"${row + 1}" if node.AST_row.absolute else f"{row + 1}"
2464
- if table_name is not None:
2465
- return f"{table_name}::{row_name}:{row_name}"
2466
- return f"{row_name}:{row_name}"
2467
-
2468
-
2469
- def node_to_row_col_ref(node: object, table_name: str, row: int, col: int) -> str:
2470
- row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2471
- col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2472
-
2473
- ref = xl_rowcol_to_cell(
2474
- row,
2475
- col,
2476
- row_abs=node.AST_row.absolute,
2477
- col_abs=node.AST_column.absolute,
2478
- )
2479
- if table_name is not None:
2480
- return f"{table_name}::{ref}"
2481
- return ref
2482
-
2483
-
2484
2575
  def get_storage_buffers_for_row(
2485
2576
  storage_buffer: bytes,
2486
2577
  offsets: list,
@@ -76,3 +76,9 @@ class NumbersUUID(UUID):
76
76
  uuid_w1=uuid_w1,
77
77
  uuid_w0=uuid_w0,
78
78
  )
79
+
80
+
81
+ def uuid_to_hex(archive: object) -> str:
82
+ """Convert a protobuf UUID to a hex string"""
83
+ uuid = NumbersUUID(archive)
84
+ return uuid.hex