numbers-parser 4.14.4__py3-none-any.whl → 4.16.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.
- numbers_parser/__init__.py +1 -0
- numbers_parser/_unpack_numbers.py +1 -7
- numbers_parser/cell.py +28 -202
- numbers_parser/constants.py +19 -6
- numbers_parser/document.py +54 -11
- numbers_parser/formula.py +423 -56
- numbers_parser/generated/TSCEArchives_pb2.py +209 -193
- numbers_parser/generated/TSSArchives_pb2.py +36 -36
- numbers_parser/generated/TSTArchives_pb2.py +328 -332
- numbers_parser/generated/mapping.py +1 -2
- numbers_parser/model.py +399 -188
- numbers_parser/numbers_cache.py +1 -1
- numbers_parser/numbers_uuid.py +6 -0
- numbers_parser/tokenizer.py +548 -0
- numbers_parser/xrefs.py +850 -0
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/METADATA +26 -28
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/RECORD +24 -22
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/WHEEL +2 -1
- numbers_parser-4.16.1.dist-info/entry_points.txt +4 -0
- numbers_parser-4.16.1.dist-info/top_level.txt +1 -0
- numbers_parser/data/empty.numbers +0 -0
- numbers_parser-4.14.4.dist-info/entry_points.txt +0 -5
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info/licenses}/LICENSE.rst +0 -0
numbers_parser/model.py
CHANGED
|
@@ -4,9 +4,12 @@ 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
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from warnings import warn
|
|
10
13
|
|
|
11
14
|
from numbers_parser.bullets import (
|
|
12
15
|
BULLET_CONVERSION,
|
|
@@ -29,8 +32,6 @@ from numbers_parser.cell import (
|
|
|
29
32
|
PaddingType,
|
|
30
33
|
Style,
|
|
31
34
|
VerticalJustification,
|
|
32
|
-
xl_col_to_name,
|
|
33
|
-
xl_rowcol_to_cell,
|
|
34
35
|
)
|
|
35
36
|
from numbers_parser.constants import (
|
|
36
37
|
ALLOWED_FORMATTING_PARAMETERS,
|
|
@@ -38,6 +39,7 @@ from numbers_parser.constants import (
|
|
|
38
39
|
CUSTOM_TEXT_PLACEHOLDER,
|
|
39
40
|
DEFAULT_COLUMN_WIDTH,
|
|
40
41
|
DEFAULT_DOCUMENT,
|
|
42
|
+
DEFAULT_FONT,
|
|
41
43
|
DEFAULT_PRE_BNC_BYTES,
|
|
42
44
|
DEFAULT_ROW_HEIGHT,
|
|
43
45
|
DEFAULT_TABLE_OFFSET,
|
|
@@ -49,11 +51,12 @@ from numbers_parser.constants import (
|
|
|
49
51
|
MAX_TILE_SIZE,
|
|
50
52
|
PACKAGE_ID,
|
|
51
53
|
CellInteractionType,
|
|
54
|
+
CellValueType,
|
|
52
55
|
FormatType,
|
|
53
56
|
OwnerKind,
|
|
54
57
|
)
|
|
55
58
|
from numbers_parser.containers import ObjectStore
|
|
56
|
-
from numbers_parser.exceptions import UnsupportedError
|
|
59
|
+
from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
|
|
57
60
|
from numbers_parser.formula import TableFormulas
|
|
58
61
|
from numbers_parser.generated import TNArchives_pb2 as TNArchives
|
|
59
62
|
from numbers_parser.generated import TSAArchives_pb2 as TSAArchives
|
|
@@ -74,7 +77,11 @@ from numbers_parser.generated.TSWPArchives_pb2 import (
|
|
|
74
77
|
)
|
|
75
78
|
from numbers_parser.iwafile import find_extension
|
|
76
79
|
from numbers_parser.numbers_cache import Cacheable, cache
|
|
77
|
-
from numbers_parser.numbers_uuid import NumbersUUID
|
|
80
|
+
from numbers_parser.numbers_uuid import NumbersUUID, uuid_to_hex
|
|
81
|
+
from numbers_parser.xrefs import CellRange, ScopedNameRefCache
|
|
82
|
+
|
|
83
|
+
if TYPE_CHECKING:
|
|
84
|
+
from datetime import datetime
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
def create_font_name_map(font_map: dict) -> dict:
|
|
@@ -224,6 +231,7 @@ class _NumbersModel(Cacheable):
|
|
|
224
231
|
self._table_styles = DataLists(self, "styleTable", "reference")
|
|
225
232
|
self._table_strings = DataLists(self, "stringTable", "string")
|
|
226
233
|
self._control_specs = DataLists(self, "control_cell_spec_table", "cell_spec")
|
|
234
|
+
self._formulas = DataLists(self, "formula_table", "formula")
|
|
227
235
|
self._table_data = {}
|
|
228
236
|
self._styles = None
|
|
229
237
|
self._images = {}
|
|
@@ -236,6 +244,9 @@ class _NumbersModel(Cacheable):
|
|
|
236
244
|
"bottom": defaultdict(),
|
|
237
245
|
"left": defaultdict(),
|
|
238
246
|
}
|
|
247
|
+
self.name_ref_cache = ScopedNameRefCache(self)
|
|
248
|
+
self.missing_fonts = {}
|
|
249
|
+
self.calculate_table_uuid_map()
|
|
239
250
|
|
|
240
251
|
def save(self, filepath: Path, package: bool) -> None:
|
|
241
252
|
self.objects.save(filepath, package)
|
|
@@ -258,13 +269,16 @@ class _NumbersModel(Cacheable):
|
|
|
258
269
|
self._table_data[table_id] = data
|
|
259
270
|
|
|
260
271
|
# Don't cache: new tables can be added at runtime
|
|
261
|
-
def table_ids(self, sheet_id: int) -> list:
|
|
262
|
-
"""
|
|
272
|
+
def table_ids(self, sheet_id: int | None = None) -> list:
|
|
273
|
+
"""
|
|
274
|
+
Return a list of table IDs for a given sheet ID or all table
|
|
275
|
+
IDs id the sheet ID is None
|
|
276
|
+
"""
|
|
263
277
|
table_info_ids = self.find_refs("TableInfoArchive")
|
|
264
278
|
return [
|
|
265
279
|
self.objects[t_id].tableModel.identifier
|
|
266
280
|
for t_id in table_info_ids
|
|
267
|
-
if self.objects[t_id].super.parent.identifier == sheet_id
|
|
281
|
+
if (sheet_id is None or self.objects[t_id].super.parent.identifier == sheet_id)
|
|
268
282
|
]
|
|
269
283
|
|
|
270
284
|
# Don't cache: new tables can be added at runtime
|
|
@@ -282,15 +296,8 @@ class _NumbersModel(Cacheable):
|
|
|
282
296
|
# The base data store contains a reference to rowHeaders.buckets
|
|
283
297
|
# which is an ordered list that matches the storage buffers, but
|
|
284
298
|
# identifies which row a storage buffer belongs to (empty rows have
|
|
285
|
-
# no storage buffers).
|
|
286
|
-
|
|
287
|
-
# {
|
|
288
|
-
# "hiding_state": 0,
|
|
289
|
-
# "index": 0,
|
|
290
|
-
# "number_of_cells": 3,
|
|
291
|
-
# "size": 0.0
|
|
292
|
-
# },
|
|
293
|
-
row_bucket_map = {i: None for i in range(self.objects[table_id].number_of_rows)}
|
|
299
|
+
# no storage buffers).
|
|
300
|
+
row_bucket_map = dict.fromkeys(range(self.objects[table_id].number_of_rows))
|
|
294
301
|
bds = self.objects[table_id].base_data_store
|
|
295
302
|
bucket_ids = [x.identifier for x in bds.rowHeaders.buckets]
|
|
296
303
|
idx = 0
|
|
@@ -316,6 +323,13 @@ class _NumbersModel(Cacheable):
|
|
|
316
323
|
self.objects[table_id].table_name = value
|
|
317
324
|
return None
|
|
318
325
|
|
|
326
|
+
def table_names(self):
|
|
327
|
+
return list(
|
|
328
|
+
chain.from_iterable(
|
|
329
|
+
[[self.table_name(tid) for tid in self.table_ids(sid)] for sid in self.sheet_ids()],
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
|
|
319
333
|
def table_name_enabled(self, table_id: int, enabled: bool | None = None):
|
|
320
334
|
if enabled is not None:
|
|
321
335
|
self.objects[table_id].table_name_enabled = enabled
|
|
@@ -751,51 +765,77 @@ class _NumbersModel(Cacheable):
|
|
|
751
765
|
owner_id_map[e.internal_owner_id] = NumbersUUID(e.owner_id).hex
|
|
752
766
|
return owner_id_map
|
|
753
767
|
|
|
768
|
+
def calculate_table_uuid_map(self) -> None:
|
|
769
|
+
# Each Table Model has a UUID which is used in references to the table. See
|
|
770
|
+
# Numbers.md#uuid-mapping for more details.
|
|
771
|
+
|
|
772
|
+
# For haunted owner archive types, map formula_owner_uids to their base_owner_uids
|
|
773
|
+
haunted_owner_ids = [
|
|
774
|
+
obj_id
|
|
775
|
+
for obj_id in self.find_refs("FormulaOwnerDependenciesArchive")
|
|
776
|
+
if self.objects[obj_id].owner_kind == OwnerKind.HAUNTED_OWNER
|
|
777
|
+
]
|
|
778
|
+
if len(haunted_owner_ids) == 0:
|
|
779
|
+
# Some older documents (see issue-18) do not use FormulaOwnerDependenciesArchive
|
|
780
|
+
self._table_id_to_base_id = {}
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
formula_owner_to_base_owner_map = {
|
|
784
|
+
uuid_to_hex(self.objects[obj_id].formula_owner_uid): uuid_to_hex(
|
|
785
|
+
self.objects[obj_id].base_owner_uid,
|
|
786
|
+
)
|
|
787
|
+
for obj_id in haunted_owner_ids
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
# Map table IDs to the base_owner_uids of the formula owners that match
|
|
791
|
+
# the table model's haunted owner
|
|
792
|
+
self._table_id_to_base_id = {
|
|
793
|
+
table_id: formula_owner_to_base_owner_map[
|
|
794
|
+
uuid_to_hex(self.objects[table_id].haunted_owner.owner_uid)
|
|
795
|
+
]
|
|
796
|
+
for table_id in self.table_ids()
|
|
797
|
+
}
|
|
798
|
+
self._table_base_id_to_formula_owner_id = {
|
|
799
|
+
uuid_to_hex(self.objects[obj_id].base_owner_uid): obj_id for obj_id in haunted_owner_ids
|
|
800
|
+
}
|
|
801
|
+
|
|
754
802
|
@cache()
|
|
755
803
|
def table_base_id(self, table_id: int) -> int:
|
|
756
804
|
""" "Finds the UUID of a table."""
|
|
757
|
-
#
|
|
758
|
-
|
|
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
|
|
805
|
+
# Table can be empty if the document does not use FormulaOwnerDependenciesArchive
|
|
806
|
+
return self._table_id_to_base_id.get(table_id)
|
|
782
807
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
808
|
+
def get_formula_owner(self, table_id: int) -> object:
|
|
809
|
+
table_uuid = self.table_base_id(table_id)
|
|
810
|
+
return self.objects[self._table_base_id_to_formula_owner_id[table_uuid]]
|
|
811
|
+
|
|
812
|
+
def add_formula_dependency(self, row: int, col: int, table_id: int) -> None:
|
|
787
813
|
calc_engine = self.calc_engine()
|
|
814
|
+
calc_engine.dependency_tracker.number_of_formulas += 1
|
|
815
|
+
internal_formula_id = calc_engine.dependency_tracker.number_of_formulas
|
|
788
816
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
817
|
+
formula_owner = self.get_formula_owner(table_id)
|
|
818
|
+
formula_owner.cell_dependencies.cell_record.append(
|
|
819
|
+
TSCEArchives.CellRecordExpandedArchive(column=col, row=row),
|
|
820
|
+
)
|
|
821
|
+
if len(formula_owner.tiled_cell_dependencies.cell_record_tiles) == 0:
|
|
822
|
+
cell_record_id, cell_record = self.objects.create_object_from_dict(
|
|
823
|
+
"CalculationEngine",
|
|
824
|
+
{
|
|
825
|
+
"internal_owner_id": internal_formula_id,
|
|
826
|
+
"tile_column_begin": 0,
|
|
827
|
+
"tile_row_begin": 0,
|
|
828
|
+
},
|
|
829
|
+
TSCEArchives.CellRecordTileArchive,
|
|
830
|
+
)
|
|
831
|
+
formula_owner.tiled_cell_dependencies.cell_record_tiles.append(
|
|
832
|
+
TSPMessages.Reference(identifier=cell_record_id),
|
|
833
|
+
)
|
|
834
|
+
else:
|
|
835
|
+
cell_record_id = formula_owner.tiled_cell_dependencies.cell_record_tiles[0].identifier
|
|
836
|
+
cell_record = self.objects[cell_record_id]
|
|
837
|
+
|
|
838
|
+
cell_record.cell_records.append(formula_owner.cell_dependencies.cell_record[-1])
|
|
799
839
|
|
|
800
840
|
@cache(num_args=0)
|
|
801
841
|
def calc_engine_id(self):
|
|
@@ -815,8 +855,8 @@ class _NumbersModel(Cacheable):
|
|
|
815
855
|
|
|
816
856
|
@cache()
|
|
817
857
|
def calculate_merge_cell_ranges(self, table_id) -> None:
|
|
818
|
-
"""
|
|
819
|
-
#
|
|
858
|
+
"""Extract all the merge cell ranges for the Table."""
|
|
859
|
+
# See details in Numbers.md#merge-ranges.
|
|
820
860
|
owner_id_map = self.owner_id_map()
|
|
821
861
|
table_base_id = self.table_base_id(table_id)
|
|
822
862
|
|
|
@@ -879,6 +919,17 @@ class _NumbersModel(Cacheable):
|
|
|
879
919
|
return sheet_id
|
|
880
920
|
return None
|
|
881
921
|
|
|
922
|
+
def table_name_to_uuid(self, sheet_name: str, table_name: str) -> str:
|
|
923
|
+
table_ids = [tid for tid in self.table_ids() if table_name == self.table_name(tid)]
|
|
924
|
+
if len(table_ids) == 1:
|
|
925
|
+
return self.table_base_id(table_ids[0])
|
|
926
|
+
|
|
927
|
+
sheet_name_to_id = {self.sheet_name(x): x for x in self.sheet_ids()}
|
|
928
|
+
sheet_id = sheet_name_to_id[sheet_name]
|
|
929
|
+
table_name_to_id = {self.table_name(x): x for x in self.table_ids(sheet_id)}
|
|
930
|
+
table_id = table_name_to_id[table_name]
|
|
931
|
+
return self.table_base_id(table_id)
|
|
932
|
+
|
|
882
933
|
@cache()
|
|
883
934
|
def table_uuids_to_id(self, table_uuid) -> int | None:
|
|
884
935
|
for sheet_id in self.sheet_ids(): # pragma: no branch # noqa: RET503
|
|
@@ -886,66 +937,103 @@ class _NumbersModel(Cacheable):
|
|
|
886
937
|
if table_uuid == self.table_base_id(table_id):
|
|
887
938
|
return table_id
|
|
888
939
|
|
|
889
|
-
def node_to_ref(self,
|
|
940
|
+
def node_to_ref(self, table_id: int, row: int, col: int, node):
|
|
941
|
+
def resolve_range(is_absolute, absolute_list, relative_list, offset, max_val):
|
|
942
|
+
if is_absolute:
|
|
943
|
+
return absolute_list[0].range_begin
|
|
944
|
+
if not relative_list and absolute_list[0].range_begin == max_val:
|
|
945
|
+
return max_val
|
|
946
|
+
return offset + relative_list[0].range_begin
|
|
947
|
+
|
|
948
|
+
def resolve_range_end(is_absolute, absolute_list, relative_list, offset, max_val):
|
|
949
|
+
if is_absolute:
|
|
950
|
+
return range_end(absolute_list[0])
|
|
951
|
+
if not relative_list and range_end(absolute_list[0]) == max_val:
|
|
952
|
+
return max_val
|
|
953
|
+
return offset + range_end(relative_list[0])
|
|
954
|
+
|
|
890
955
|
if node.HasField("AST_cross_table_reference_extra_info"):
|
|
891
956
|
table_uuid = NumbersUUID(node.AST_cross_table_reference_extra_info.table_id).hex
|
|
892
|
-
|
|
893
|
-
other_table_name = self.table_name(other_table_id)
|
|
957
|
+
to_table_id = self.table_uuids_to_id(table_uuid)
|
|
894
958
|
else:
|
|
895
|
-
|
|
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
|
|
959
|
+
to_table_id = None
|
|
904
960
|
|
|
905
961
|
if node.HasField("AST_colon_tract"):
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
962
|
+
row_begin = resolve_range(
|
|
963
|
+
node.AST_sticky_bits.begin_row_is_absolute,
|
|
964
|
+
node.AST_colon_tract.absolute_row,
|
|
965
|
+
node.AST_colon_tract.relative_row,
|
|
966
|
+
row,
|
|
967
|
+
0x7FFFFFFF,
|
|
968
|
+
)
|
|
912
969
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
970
|
+
row_end = resolve_range_end(
|
|
971
|
+
node.AST_sticky_bits.end_row_is_absolute,
|
|
972
|
+
node.AST_colon_tract.absolute_row,
|
|
973
|
+
node.AST_colon_tract.relative_row,
|
|
974
|
+
row,
|
|
975
|
+
0x7FFFFFFF,
|
|
976
|
+
)
|
|
918
977
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
978
|
+
col_begin = resolve_range(
|
|
979
|
+
node.AST_sticky_bits.begin_column_is_absolute,
|
|
980
|
+
node.AST_colon_tract.absolute_column,
|
|
981
|
+
node.AST_colon_tract.relative_column,
|
|
982
|
+
col,
|
|
983
|
+
0x7FFF,
|
|
984
|
+
)
|
|
923
985
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
986
|
+
col_end = resolve_range_end(
|
|
987
|
+
node.AST_sticky_bits.end_column_is_absolute,
|
|
988
|
+
node.AST_colon_tract.absolute_column,
|
|
989
|
+
node.AST_colon_tract.relative_column,
|
|
990
|
+
col,
|
|
991
|
+
0x7FFF,
|
|
992
|
+
)
|
|
928
993
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
994
|
+
return CellRange(
|
|
995
|
+
model=self,
|
|
996
|
+
row_start=None if row_begin == 0x7FFFFFFF else row_begin,
|
|
997
|
+
row_end=None if row_end == 0x7FFFFFFF else row_end,
|
|
998
|
+
col_start=None if col_begin == 0x7FFF else col_begin,
|
|
999
|
+
col_end=None if col_end == 0x7FFF else col_end,
|
|
1000
|
+
row_start_is_abs=node.AST_sticky_bits.begin_row_is_absolute,
|
|
1001
|
+
row_end_is_abs=node.AST_sticky_bits.end_row_is_absolute,
|
|
1002
|
+
col_start_is_abs=node.AST_sticky_bits.begin_column_is_absolute,
|
|
1003
|
+
col_end_is_abs=node.AST_sticky_bits.end_column_is_absolute,
|
|
1004
|
+
from_table_id=table_id,
|
|
1005
|
+
to_table_id=to_table_id,
|
|
1006
|
+
)
|
|
933
1007
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1008
|
+
row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
|
|
1009
|
+
col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
|
|
1010
|
+
if node.HasField("AST_row") and not node.HasField("AST_column"):
|
|
1011
|
+
return CellRange(
|
|
1012
|
+
model=self,
|
|
1013
|
+
row_start=row,
|
|
1014
|
+
row_start_is_abs=node.AST_row.absolute,
|
|
1015
|
+
from_table_id=table_id,
|
|
1016
|
+
to_table_id=to_table_id,
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
if node.HasField("AST_column") and not node.HasField("AST_row"):
|
|
1020
|
+
return CellRange(
|
|
1021
|
+
model=self,
|
|
1022
|
+
col_start=col,
|
|
1023
|
+
col_start_is_abs=node.AST_column.absolute,
|
|
1024
|
+
from_table_id=table_id,
|
|
1025
|
+
to_table_id=to_table_id,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
return CellRange(
|
|
1029
|
+
model=self,
|
|
1030
|
+
row_start=row,
|
|
1031
|
+
col_start=col,
|
|
1032
|
+
row_start_is_abs=node.AST_row.absolute,
|
|
1033
|
+
col_start_is_abs=node.AST_column.absolute,
|
|
1034
|
+
from_table_id=table_id,
|
|
1035
|
+
to_table_id=to_table_id,
|
|
945
1036
|
)
|
|
946
|
-
if table_name is not None:
|
|
947
|
-
return f"{table_name}::{begin_ref}:{end_ref}"
|
|
948
|
-
return f"{begin_ref}:{end_ref}"
|
|
949
1037
|
|
|
950
1038
|
@cache()
|
|
951
1039
|
def formula_ast(self, table_id: int):
|
|
@@ -1480,12 +1568,6 @@ class _NumbersModel(Cacheable):
|
|
|
1480
1568
|
TSPMessages.Reference(identifier=row_headers_id),
|
|
1481
1569
|
)
|
|
1482
1570
|
|
|
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
1571
|
table_info_id, table_info = self.objects.create_object_from_dict(
|
|
1490
1572
|
"CalculationEngine",
|
|
1491
1573
|
{},
|
|
@@ -1494,6 +1576,22 @@ class _NumbersModel(Cacheable):
|
|
|
1494
1576
|
table_info.tableModel.MergeFrom(TSPMessages.Reference(identifier=table_model_id))
|
|
1495
1577
|
table_info.super.MergeFrom(self.create_drawable(sheet_id, x, y))
|
|
1496
1578
|
|
|
1579
|
+
haunted_owner_uuid = self.add_formula_owner(
|
|
1580
|
+
table_info_id,
|
|
1581
|
+
num_rows,
|
|
1582
|
+
num_cols,
|
|
1583
|
+
number_of_header_rows,
|
|
1584
|
+
number_of_header_columns,
|
|
1585
|
+
)
|
|
1586
|
+
table_model.haunted_owner.owner_uid.MergeFrom(haunted_owner_uuid.protobuf2)
|
|
1587
|
+
self.calculate_table_uuid_map()
|
|
1588
|
+
|
|
1589
|
+
self._table_data[table_model_id] = [
|
|
1590
|
+
[Cell._empty_cell(table_model_id, row, col, self) for col in range(num_cols)]
|
|
1591
|
+
for row in range(num_rows)
|
|
1592
|
+
]
|
|
1593
|
+
self.recalculate_table_data(table_model_id, self._table_data[table_model_id])
|
|
1594
|
+
|
|
1497
1595
|
self.add_component_reference(
|
|
1498
1596
|
table_info_id,
|
|
1499
1597
|
location="Document",
|
|
@@ -1502,17 +1600,11 @@ class _NumbersModel(Cacheable):
|
|
|
1502
1600
|
self.create_caption_archive(table_model_id)
|
|
1503
1601
|
self.caption_enabled(table_model_id, False)
|
|
1504
1602
|
|
|
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
1603
|
self.objects[sheet_id].drawable_infos.append(
|
|
1514
1604
|
TSPMessages.Reference(identifier=table_info_id),
|
|
1515
1605
|
)
|
|
1606
|
+
|
|
1607
|
+
self.name_ref_cache.mark_dirty()
|
|
1516
1608
|
return table_model_id
|
|
1517
1609
|
|
|
1518
1610
|
def add_formula_owner(
|
|
@@ -1522,7 +1614,7 @@ class _NumbersModel(Cacheable):
|
|
|
1522
1614
|
num_cols: int,
|
|
1523
1615
|
number_of_header_rows: int,
|
|
1524
1616
|
number_of_header_columns: int,
|
|
1525
|
-
) ->
|
|
1617
|
+
) -> NumbersUUID:
|
|
1526
1618
|
"""
|
|
1527
1619
|
Create a FormulaOwnerDependenciesArchive that references a TableInfoArchive
|
|
1528
1620
|
so that cross-references to cells in this table will work.
|
|
@@ -1531,49 +1623,43 @@ class _NumbersModel(Cacheable):
|
|
|
1531
1623
|
calc_engine = self.calc_engine()
|
|
1532
1624
|
owner_id_map = calc_engine.dependency_tracker.owner_id_map.map_entry
|
|
1533
1625
|
next_owner_id = max([x.internal_owner_id for x in owner_id_map]) + 1
|
|
1534
|
-
|
|
1626
|
+
volatile_dependencies = {
|
|
1627
|
+
"volatile_time_cells": {},
|
|
1628
|
+
"volatile_random_cells": {},
|
|
1629
|
+
"volatile_locale_cells": {},
|
|
1630
|
+
"volatile_sheet_table_name_cells": {},
|
|
1631
|
+
"volatile_remote_data_cells": {},
|
|
1632
|
+
"volatile_geometry_cell_refs": {},
|
|
1633
|
+
}
|
|
1634
|
+
total_range_for_table = {
|
|
1635
|
+
"top_left_column": 0,
|
|
1636
|
+
"top_left_row": 0,
|
|
1637
|
+
"bottom_right_column": num_cols - 1,
|
|
1638
|
+
"bottom_right_row": num_cols - 1,
|
|
1639
|
+
}
|
|
1640
|
+
body_range_for_table = {
|
|
1641
|
+
"top_left_column": number_of_header_columns,
|
|
1642
|
+
"top_left_row": number_of_header_rows,
|
|
1643
|
+
"bottom_right_column": num_cols - 1,
|
|
1644
|
+
"bottom_right_row": num_cols - 1,
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
formula_deps_id, _ = self.objects.create_object_from_dict(
|
|
1535
1648
|
"CalculationEngine",
|
|
1536
1649
|
{
|
|
1537
1650
|
"formula_owner_uid": formula_owner_uuid.dict2,
|
|
1538
1651
|
"internal_formula_owner_id": next_owner_id,
|
|
1539
|
-
"owner_kind":
|
|
1652
|
+
"owner_kind": OwnerKind.TABLE_MODEL,
|
|
1540
1653
|
"cell_dependencies": {},
|
|
1541
1654
|
"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
|
-
},
|
|
1655
|
+
"volatile_dependencies": volatile_dependencies,
|
|
1550
1656
|
"spanning_column_dependencies": {
|
|
1551
|
-
"total_range_for_table":
|
|
1552
|
-
|
|
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
|
-
},
|
|
1657
|
+
"total_range_for_table": total_range_for_table,
|
|
1658
|
+
"body_range_for_table": body_range_for_table,
|
|
1563
1659
|
},
|
|
1564
1660
|
"spanning_row_dependencies": {
|
|
1565
|
-
"total_range_for_table":
|
|
1566
|
-
|
|
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
|
-
},
|
|
1661
|
+
"total_range_for_table": total_range_for_table,
|
|
1662
|
+
"body_range_for_table": body_range_for_table,
|
|
1577
1663
|
},
|
|
1578
1664
|
"whole_owner_dependencies": {"dependent_cells": {}},
|
|
1579
1665
|
"cell_errors": {},
|
|
@@ -1594,6 +1680,52 @@ class _NumbersModel(Cacheable):
|
|
|
1594
1680
|
),
|
|
1595
1681
|
)
|
|
1596
1682
|
|
|
1683
|
+
# See Numbers.md#uuid-mapping for more details on mapping table model
|
|
1684
|
+
# UUID to the formula owner.
|
|
1685
|
+
formula_owner_uuid = NumbersUUID()
|
|
1686
|
+
base_owner_uuid = NumbersUUID()
|
|
1687
|
+
next_owner_id += 1
|
|
1688
|
+
null_range_ref = {
|
|
1689
|
+
"top_left_column": 0x7FFF,
|
|
1690
|
+
"top_left_row": 0x7FFFFFFF,
|
|
1691
|
+
"bottom_right_column": 0x7FFF,
|
|
1692
|
+
"bottom_right_row": 0x7FFFFFFF,
|
|
1693
|
+
}
|
|
1694
|
+
spanning_depdendencies = {
|
|
1695
|
+
"total_range_for_table": null_range_ref,
|
|
1696
|
+
"body_range_for_table": null_range_ref,
|
|
1697
|
+
}
|
|
1698
|
+
formula_deps_id, formula_deps = self.objects.create_object_from_dict(
|
|
1699
|
+
"CalculationEngine",
|
|
1700
|
+
{
|
|
1701
|
+
"formula_owner_uid": formula_owner_uuid.dict2,
|
|
1702
|
+
"internal_formula_owner_id": next_owner_id,
|
|
1703
|
+
"owner_kind": OwnerKind.HAUNTED_OWNER,
|
|
1704
|
+
"cell_dependencies": {},
|
|
1705
|
+
"range_dependencies": {},
|
|
1706
|
+
"volatile_dependencies": volatile_dependencies,
|
|
1707
|
+
"spanning_column_dependencies": spanning_depdendencies,
|
|
1708
|
+
"spanning_row_dependencies": spanning_depdendencies,
|
|
1709
|
+
"whole_owner_dependencies": {"dependent_cells": {}},
|
|
1710
|
+
"cell_errors": {},
|
|
1711
|
+
"base_owner_uid": base_owner_uuid.dict2,
|
|
1712
|
+
"tiled_cell_dependencies": {},
|
|
1713
|
+
"uuid_references": {},
|
|
1714
|
+
"tiled_range_dependencies": {},
|
|
1715
|
+
},
|
|
1716
|
+
TSCEArchives.FormulaOwnerDependenciesArchive,
|
|
1717
|
+
)
|
|
1718
|
+
calc_engine.dependency_tracker.formula_owner_dependencies.append(
|
|
1719
|
+
TSPMessages.Reference(identifier=formula_deps_id),
|
|
1720
|
+
)
|
|
1721
|
+
owner_id_map.append(
|
|
1722
|
+
TSCEArchives.OwnerIDMapArchive.OwnerIDMapArchiveEntry(
|
|
1723
|
+
internal_owner_id=next_owner_id,
|
|
1724
|
+
owner_id=formula_owner_uuid.protobuf4,
|
|
1725
|
+
),
|
|
1726
|
+
)
|
|
1727
|
+
return formula_owner_uuid
|
|
1728
|
+
|
|
1597
1729
|
def add_sheet(self, sheet_name: str) -> int:
|
|
1598
1730
|
"""Add a new sheet with a copy of a table from another sheet."""
|
|
1599
1731
|
sheet_id, _ = self.objects.create_object_from_dict(
|
|
@@ -2144,6 +2276,16 @@ class _NumbersModel(Cacheable):
|
|
|
2144
2276
|
def cell_font_name(self, obj: Cell | object) -> str:
|
|
2145
2277
|
style = self.cell_text_style(obj) if isinstance(obj, Cell) else obj
|
|
2146
2278
|
font_name = self.char_property(style, "font_name")
|
|
2279
|
+
if font_name not in FONT_NAME_TO_FAMILY:
|
|
2280
|
+
if font_name not in self.missing_fonts:
|
|
2281
|
+
warn(
|
|
2282
|
+
f"Custom font '{font_name}' unsupported; falling back to {DEFAULT_FONT}",
|
|
2283
|
+
UnsupportedWarning,
|
|
2284
|
+
stacklevel=2,
|
|
2285
|
+
)
|
|
2286
|
+
self.missing_fonts[font_name] = True
|
|
2287
|
+
return DEFAULT_FONT
|
|
2288
|
+
|
|
2147
2289
|
return FONT_NAME_TO_FAMILY[font_name]
|
|
2148
2290
|
|
|
2149
2291
|
def cell_first_indent(self, obj: Cell | object) -> float:
|
|
@@ -2426,6 +2568,108 @@ class _NumbersModel(Cacheable):
|
|
|
2426
2568
|
# datas never appears to be an empty list (default themes include images)
|
|
2427
2569
|
return max(image_ids) + 1
|
|
2428
2570
|
|
|
2571
|
+
def table_category_data(self, table_id: int) -> dict | None:
|
|
2572
|
+
category_owner_id = self.objects[table_id].category_owner.identifier
|
|
2573
|
+
category_archive_id = self.objects[category_owner_id].group_by[0].identifier
|
|
2574
|
+
category_archive = self.objects[category_archive_id]
|
|
2575
|
+
if not category_archive.is_enabled:
|
|
2576
|
+
return None
|
|
2577
|
+
|
|
2578
|
+
table_info = self.objects[self.table_info_id(table_id)]
|
|
2579
|
+
category_order = self.objects[table_info.category_order.identifier]
|
|
2580
|
+
row_uid_map = self.objects[category_order.uid_map.identifier]
|
|
2581
|
+
sorted_row_uuids = [
|
|
2582
|
+
NumbersUUID(row_uid_map.sorted_row_uids[i]).hex for i in row_uid_map.row_uid_for_index
|
|
2583
|
+
]
|
|
2584
|
+
|
|
2585
|
+
data = self._table_data[table_id]
|
|
2586
|
+
header = [cell.value for cell in data[0]]
|
|
2587
|
+
|
|
2588
|
+
def index_set_to_offsets(index_set: TSCEArchives.IndexSetArchive) -> list[int]:
|
|
2589
|
+
"""Convert an IndexSetArchive to a list of offsets."""
|
|
2590
|
+
offsets = []
|
|
2591
|
+
for entry in index_set.entries:
|
|
2592
|
+
if entry.HasField("range_end"):
|
|
2593
|
+
offsets += list(range(entry.range_begin, entry.range_end + 1))
|
|
2594
|
+
else:
|
|
2595
|
+
offsets += list(range(entry.range_begin, entry.range_begin + 1))
|
|
2596
|
+
return offsets
|
|
2597
|
+
|
|
2598
|
+
def cell_value_to_key(
|
|
2599
|
+
cell_value: TSCEArchives.CellValueArchive,
|
|
2600
|
+
) -> str | int | bool | datetime:
|
|
2601
|
+
"""Convert a CellValueArchive to a key."""
|
|
2602
|
+
cell_value_type = cell_value.cell_value_type
|
|
2603
|
+
if cell_value_type == CellValueType.STRING_TYPE:
|
|
2604
|
+
return cell_value.string_value.value
|
|
2605
|
+
if cell_value_type == CellValueType.NUMBER_TYPE:
|
|
2606
|
+
return cell_value.number_value.value
|
|
2607
|
+
if cell_value_type == CellValueType.BOOLEAN_TYPE:
|
|
2608
|
+
return cell_value.boolean_value.value
|
|
2609
|
+
# Must be DATE_TYPE
|
|
2610
|
+
return cell_value.date_value.value
|
|
2611
|
+
|
|
2612
|
+
group_node_to_key = {
|
|
2613
|
+
NumbersUUID(self.objects[_id].group_uid).hex: cell_value_to_key(
|
|
2614
|
+
self.objects[_id].group_cell_value,
|
|
2615
|
+
)
|
|
2616
|
+
for _id in self.find_refs("GroupNodeArchive")
|
|
2617
|
+
}
|
|
2618
|
+
group_uuids = [NumbersUUID(x.group_uid).hex for x in category_archive.group_node_root.child]
|
|
2619
|
+
group_uuids = [uuid for uuid in sorted_row_uuids if uuid in group_uuids]
|
|
2620
|
+
|
|
2621
|
+
def group_hierarchy(parent: str, children: list):
|
|
2622
|
+
nodes = {}
|
|
2623
|
+
for child in children:
|
|
2624
|
+
group_uuid = NumbersUUID(child.group_uid).hex
|
|
2625
|
+
if len(child.child) > 0:
|
|
2626
|
+
nodes[group_uuid] = group_hierarchy(group_uuid, child.child)
|
|
2627
|
+
else:
|
|
2628
|
+
nodes[group_uuid] = None
|
|
2629
|
+
return nodes
|
|
2630
|
+
|
|
2631
|
+
def assign_rows_to_categories(parent: str, children: list, categories: dict):
|
|
2632
|
+
for child in children:
|
|
2633
|
+
group_uuid = NumbersUUID(child.group_uid).hex
|
|
2634
|
+
if len(child.child) == 0:
|
|
2635
|
+
key = cell_value_to_key(child.group_cell_value)
|
|
2636
|
+
|
|
2637
|
+
row_offsets = index_set_to_offsets(child.row_lookup_uids)
|
|
2638
|
+
categories[group_uuid] = {
|
|
2639
|
+
"key": key,
|
|
2640
|
+
"parent": parent,
|
|
2641
|
+
"rows": [
|
|
2642
|
+
{header[col]: cell.value for col, cell in enumerate(data[row])}
|
|
2643
|
+
for row in row_offsets
|
|
2644
|
+
],
|
|
2645
|
+
}
|
|
2646
|
+
else:
|
|
2647
|
+
categories[group_uuid] = {
|
|
2648
|
+
"key": group_node_to_key[group_uuid],
|
|
2649
|
+
"parent": parent,
|
|
2650
|
+
"rows": None,
|
|
2651
|
+
}
|
|
2652
|
+
assign_rows_to_categories(group_uuid, child.child, categories)
|
|
2653
|
+
|
|
2654
|
+
category_tree = group_hierarchy(
|
|
2655
|
+
NumbersUUID(category_archive.group_node_root.group_uid).hex,
|
|
2656
|
+
category_archive.group_node_root.child,
|
|
2657
|
+
)
|
|
2658
|
+
|
|
2659
|
+
categories = {}
|
|
2660
|
+
assign_rows_to_categories(None, category_archive.group_node_root.child, categories)
|
|
2661
|
+
|
|
2662
|
+
def merge_trees(a: dict, b: dict):
|
|
2663
|
+
new_tree = {}
|
|
2664
|
+
for k, v in a.items():
|
|
2665
|
+
if v is not None:
|
|
2666
|
+
new_tree[b[k]["key"]] = merge_trees(v, b)
|
|
2667
|
+
else:
|
|
2668
|
+
new_tree[b[k]["key"]] = b[k]["rows"]
|
|
2669
|
+
return new_tree
|
|
2670
|
+
|
|
2671
|
+
return merge_trees(category_tree, categories)
|
|
2672
|
+
|
|
2429
2673
|
|
|
2430
2674
|
def rgb(obj) -> RGB:
|
|
2431
2675
|
"""Convert a TSPArchives.Color into an RGB tuple."""
|
|
@@ -2448,39 +2692,6 @@ def formatted_number(number_type, index):
|
|
|
2448
2692
|
return bullet_char
|
|
2449
2693
|
|
|
2450
2694
|
|
|
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
2695
|
def get_storage_buffers_for_row(
|
|
2485
2696
|
storage_buffer: bytes,
|
|
2486
2697
|
offsets: list,
|