numbers-parser 4.14.3__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
@@ -706,7 +712,10 @@ class _NumbersModel(Cacheable):
706
712
  @cache(num_args=2)
707
713
  def table_string(self, table_id: int, key: int) -> str:
708
714
  """Return the string associated with a string ID for a particular table."""
709
- return self._table_strings.lookup_value(table_id, key).string
715
+ try:
716
+ return self._table_strings.lookup_value(table_id, key).string
717
+ except KeyError:
718
+ return ""
710
719
 
711
720
  def init_table_strings(self, table_id: int) -> None:
712
721
  """Cache table strings reference and delete all existing keys/values."""
@@ -748,51 +757,77 @@ class _NumbersModel(Cacheable):
748
757
  owner_id_map[e.internal_owner_id] = NumbersUUID(e.owner_id).hex
749
758
  return owner_id_map
750
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
+
751
794
  @cache()
752
795
  def table_base_id(self, table_id: int) -> int:
753
796
  """ "Finds the UUID of a table."""
754
- # Look for a TSCE.FormulaOwnerDependenciesArchive objects with the following at the
755
- # root level of the protobuf:
756
- #
757
- # "base_owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b368"",
758
- #   "formula_owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b36d"
759
- #
760
- # The Table UUID is the TSCE.FormulaOwnerDependenciesArchive whose formula_owner_uid
761
- # matches the UUID of the haunted_owner of the Table:
762
- #
763
- # "haunted_owner": {
764
- # "owner_uid": "6a4a5281-7b06-f5a1-904b-7f9ec784b368""
765
- # }
766
- haunted_owner = NumbersUUID(self.objects[table_id].haunted_owner.owner_uid).hex
767
- formula_owner_ids = self.find_refs("FormulaOwnerDependenciesArchive")
768
- for dependency_id in formula_owner_ids: # pragma: no branch
769
- obj = self.objects[dependency_id]
770
- # if obj.owner_kind == OwnerKind.HAUNTED_OWNER:
771
- if obj.HasField("base_owner_uid") and obj.HasField(
772
- "formula_owner_uid",
773
- ): # pragma: no branch
774
- base_owner_uid = NumbersUUID(obj.base_owner_uid).hex
775
- formula_owner_uid = NumbersUUID(obj.formula_owner_uid).hex
776
- if formula_owner_uid == haunted_owner:
777
- return base_owner_uid
778
- 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)
779
799
 
780
- @cache()
781
- def formula_cell_ranges(self, table_id: int) -> list:
782
- """Exract all the formula cell ranges for the Table."""
783
- # 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:
784
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
785
808
 
786
- table_base_id = self.table_base_id(table_id)
787
- cell_records = []
788
- for finfo in calc_engine.dependency_tracker.formula_owner_info:
789
- if finfo.HasField("cell_dependencies"): # pragma: no branch
790
- formula_owner_id = NumbersUUID(finfo.formula_owner_id).hex
791
- if formula_owner_id == table_base_id:
792
- for cell_record in finfo.cell_dependencies.cell_record:
793
- if cell_record.contains_a_formula: # pragma: no branch
794
- cell_records.append((cell_record.row, cell_record.column))
795
- 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])
796
831
 
797
832
  @cache(num_args=0)
798
833
  def calc_engine_id(self):
@@ -812,8 +847,8 @@ class _NumbersModel(Cacheable):
812
847
 
813
848
  @cache()
814
849
  def calculate_merge_cell_ranges(self, table_id) -> None:
815
- """Exract all the merge cell ranges for the Table."""
816
- # 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.
817
852
  owner_id_map = self.owner_id_map()
818
853
  table_base_id = self.table_base_id(table_id)
819
854
 
@@ -876,6 +911,17 @@ class _NumbersModel(Cacheable):
876
911
  return sheet_id
877
912
  return None
878
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
+
879
925
  @cache()
880
926
  def table_uuids_to_id(self, table_uuid) -> int | None:
881
927
  for sheet_id in self.sheet_ids(): # pragma: no branch # noqa: RET503
@@ -883,66 +929,103 @@ class _NumbersModel(Cacheable):
883
929
  if table_uuid == self.table_base_id(table_id):
884
930
  return table_id
885
931
 
886
- 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
+
887
947
  if node.HasField("AST_cross_table_reference_extra_info"):
888
948
  table_uuid = NumbersUUID(node.AST_cross_table_reference_extra_info.table_id).hex
889
- other_table_id = self.table_uuids_to_id(table_uuid)
890
- other_table_name = self.table_name(other_table_id)
949
+ to_table_id = self.table_uuids_to_id(table_uuid)
891
950
  else:
892
- other_table_id = None
893
- other_table_name = None
894
-
895
- if other_table_id is not None:
896
- this_sheet_id = self.table_id_to_sheet_id(this_table_id)
897
- other_sheet_id = self.table_id_to_sheet_id(other_table_id)
898
- if this_sheet_id != other_sheet_id:
899
- other_sheet_name = self.sheet_name(other_sheet_id)
900
- other_table_name = f"{other_sheet_name}::" + other_table_name
951
+ to_table_id = None
901
952
 
902
953
  if node.HasField("AST_colon_tract"):
903
- return self.tract_to_row_col_ref(node, other_table_name, row, col)
904
- if node.HasField("AST_row") and not node.HasField("AST_column"):
905
- return node_to_row_ref(node, other_table_name, row)
906
- if node.HasField("AST_column") and not node.HasField("AST_row"):
907
- return node_to_col_ref(node, other_table_name, col)
908
- 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
+ )
909
961
 
910
- def tract_to_row_col_ref(self, node: object, table_name: str, row: int, col: int) -> str:
911
- if node.AST_sticky_bits.begin_row_is_absolute:
912
- row_begin = node.AST_colon_tract.absolute_row[0].range_begin
913
- else:
914
- 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
+ )
915
969
 
916
- if node.AST_sticky_bits.end_row_is_absolute:
917
- row_end = range_end(node.AST_colon_tract.absolute_row[0])
918
- else:
919
- 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
+ )
920
977
 
921
- if node.AST_sticky_bits.begin_column_is_absolute:
922
- col_begin = node.AST_colon_tract.absolute_column[0].range_begin
923
- else:
924
- 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
+ )
925
985
 
926
- if node.AST_sticky_bits.end_column_is_absolute:
927
- col_end = range_end(node.AST_colon_tract.absolute_column[0])
928
- else:
929
- 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
+ )
930
999
 
931
- begin_ref = xl_rowcol_to_cell(
932
- row_begin,
933
- col_begin,
934
- row_abs=node.AST_sticky_bits.begin_row_is_absolute,
935
- col_abs=node.AST_sticky_bits.begin_column_is_absolute,
936
- )
937
- end_ref = xl_rowcol_to_cell(
938
- row_end,
939
- col_end,
940
- row_abs=node.AST_sticky_bits.end_row_is_absolute,
941
- 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,
942
1028
  )
943
- if table_name is not None:
944
- return f"{table_name}::{begin_ref}:{end_ref}"
945
- return f"{begin_ref}:{end_ref}"
946
1029
 
947
1030
  @cache()
948
1031
  def formula_ast(self, table_id: int):
@@ -1477,12 +1560,6 @@ class _NumbersModel(Cacheable):
1477
1560
  TSPMessages.Reference(identifier=row_headers_id),
1478
1561
  )
1479
1562
 
1480
- self._table_data[table_model_id] = [
1481
- [Cell._empty_cell(table_model_id, row, col, self) for col in range(num_cols)]
1482
- for row in range(num_rows)
1483
- ]
1484
- self.recalculate_table_data(table_model_id, self._table_data[table_model_id])
1485
-
1486
1563
  table_info_id, table_info = self.objects.create_object_from_dict(
1487
1564
  "CalculationEngine",
1488
1565
  {},
@@ -1491,6 +1568,22 @@ class _NumbersModel(Cacheable):
1491
1568
  table_info.tableModel.MergeFrom(TSPMessages.Reference(identifier=table_model_id))
1492
1569
  table_info.super.MergeFrom(self.create_drawable(sheet_id, x, y))
1493
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
+
1494
1587
  self.add_component_reference(
1495
1588
  table_info_id,
1496
1589
  location="Document",
@@ -1499,17 +1592,11 @@ class _NumbersModel(Cacheable):
1499
1592
  self.create_caption_archive(table_model_id)
1500
1593
  self.caption_enabled(table_model_id, False)
1501
1594
 
1502
- self.add_formula_owner(
1503
- table_info_id,
1504
- num_rows,
1505
- num_cols,
1506
- number_of_header_rows,
1507
- number_of_header_columns,
1508
- )
1509
-
1510
1595
  self.objects[sheet_id].drawable_infos.append(
1511
1596
  TSPMessages.Reference(identifier=table_info_id),
1512
1597
  )
1598
+
1599
+ self.name_ref_cache.mark_dirty()
1513
1600
  return table_model_id
1514
1601
 
1515
1602
  def add_formula_owner(
@@ -1519,7 +1606,7 @@ class _NumbersModel(Cacheable):
1519
1606
  num_cols: int,
1520
1607
  number_of_header_rows: int,
1521
1608
  number_of_header_columns: int,
1522
- ) -> None:
1609
+ ) -> NumbersUUID:
1523
1610
  """
1524
1611
  Create a FormulaOwnerDependenciesArchive that references a TableInfoArchive
1525
1612
  so that cross-references to cells in this table will work.
@@ -1528,49 +1615,43 @@ class _NumbersModel(Cacheable):
1528
1615
  calc_engine = self.calc_engine()
1529
1616
  owner_id_map = calc_engine.dependency_tracker.owner_id_map.map_entry
1530
1617
  next_owner_id = max([x.internal_owner_id for x in owner_id_map]) + 1
1531
- 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(
1532
1640
  "CalculationEngine",
1533
1641
  {
1534
1642
  "formula_owner_uid": formula_owner_uuid.dict2,
1535
1643
  "internal_formula_owner_id": next_owner_id,
1536
- "owner_kind": 1,
1644
+ "owner_kind": OwnerKind.TABLE_MODEL,
1537
1645
  "cell_dependencies": {},
1538
1646
  "range_dependencies": {},
1539
- "volatile_dependencies": {
1540
- "volatile_time_cells": {},
1541
- "volatile_random_cells": {},
1542
- "volatile_locale_cells": {},
1543
- "volatile_sheet_table_name_cells": {},
1544
- "volatile_remote_data_cells": {},
1545
- "volatile_geometry_cell_refs": {},
1546
- },
1647
+ "volatile_dependencies": volatile_dependencies,
1547
1648
  "spanning_column_dependencies": {
1548
- "total_range_for_table": {
1549
- "top_left_column": 0,
1550
- "top_left_row": 0,
1551
- "bottom_right_column": num_cols - 1,
1552
- "bottom_right_row": num_cols - 1,
1553
- },
1554
- "body_range_for_table": {
1555
- "top_left_column": number_of_header_columns,
1556
- "top_left_row": number_of_header_rows,
1557
- "bottom_right_column": num_cols - 1,
1558
- "bottom_right_row": num_cols - 1,
1559
- },
1649
+ "total_range_for_table": total_range_for_table,
1650
+ "body_range_for_table": body_range_for_table,
1560
1651
  },
1561
1652
  "spanning_row_dependencies": {
1562
- "total_range_for_table": {
1563
- "top_left_column": 0,
1564
- "top_left_row": 0,
1565
- "bottom_right_column": num_cols - 1,
1566
- "bottom_right_row": num_cols - 1,
1567
- },
1568
- "body_range_for_table": {
1569
- "top_left_column": number_of_header_columns,
1570
- "top_left_row": number_of_header_rows,
1571
- "bottom_right_column": num_cols - 1,
1572
- "bottom_right_row": num_cols - 1,
1573
- },
1653
+ "total_range_for_table": total_range_for_table,
1654
+ "body_range_for_table": body_range_for_table,
1574
1655
  },
1575
1656
  "whole_owner_dependencies": {"dependent_cells": {}},
1576
1657
  "cell_errors": {},
@@ -1591,6 +1672,52 @@ class _NumbersModel(Cacheable):
1591
1672
  ),
1592
1673
  )
1593
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
+
1594
1721
  def add_sheet(self, sheet_name: str) -> int:
1595
1722
  """Add a new sheet with a copy of a table from another sheet."""
1596
1723
  sheet_id, _ = self.objects.create_object_from_dict(
@@ -2445,39 +2572,6 @@ def formatted_number(number_type, index):
2445
2572
  return bullet_char
2446
2573
 
2447
2574
 
2448
- def node_to_col_ref(node: object, table_name: str, col: int) -> str:
2449
- col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2450
-
2451
- col_name = xl_col_to_name(col, node.AST_column.absolute)
2452
- if table_name is not None:
2453
- return f"{table_name}::{col_name}"
2454
- return col_name
2455
-
2456
-
2457
- def node_to_row_ref(node: object, table_name: str, row: int) -> str:
2458
- row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2459
-
2460
- row_name = f"${row + 1}" if node.AST_row.absolute else f"{row + 1}"
2461
- if table_name is not None:
2462
- return f"{table_name}::{row_name}:{row_name}"
2463
- return f"{row_name}:{row_name}"
2464
-
2465
-
2466
- def node_to_row_col_ref(node: object, table_name: str, row: int, col: int) -> str:
2467
- row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2468
- col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2469
-
2470
- ref = xl_rowcol_to_cell(
2471
- row,
2472
- col,
2473
- row_abs=node.AST_row.absolute,
2474
- col_abs=node.AST_column.absolute,
2475
- )
2476
- if table_name is not None:
2477
- return f"{table_name}::{ref}"
2478
- return ref
2479
-
2480
-
2481
2575
  def get_storage_buffers_for_row(
2482
2576
  storage_buffer: bytes,
2483
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