numbers-parser 4.7.1__py3-none-any.whl → 4.8.0__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/model.py CHANGED
@@ -17,14 +17,18 @@ from numbers_parser.cell import (
17
17
  BoolCell,
18
18
  Border,
19
19
  BorderType,
20
+ CustomFormatting,
20
21
  DateCell,
21
22
  DurationCell,
22
23
  EmptyCell,
24
+ Formatting,
25
+ FormattingType,
23
26
  HorizontalJustification,
24
27
  MergeAnchor,
25
28
  MergedCell,
26
29
  MergeReference,
27
30
  NumberCell,
31
+ PaddingType,
28
32
  RichTextCell,
29
33
  Style,
30
34
  TextCell,
@@ -35,7 +39,10 @@ from numbers_parser.cell import (
35
39
  )
36
40
  from numbers_parser.cell_storage import CellStorage
37
41
  from numbers_parser.constants import (
42
+ ALLOWED_FORMATTING_PARAMETERS,
38
43
  CURRENCY_CELL_TYPE,
44
+ CUSTOM_FORMAT_TYPE_MAP,
45
+ CUSTOM_TEXT_PLACEHOLDER,
39
46
  DEFAULT_COLUMN_WIDTH,
40
47
  DEFAULT_DOCUMENT,
41
48
  DEFAULT_PRE_BNC_BYTES,
@@ -46,8 +53,10 @@ from numbers_parser.constants import (
46
53
  DEFAULT_TILE_SIZE,
47
54
  DOCUMENT_ID,
48
55
  EPOCH,
56
+ FORMAT_TYPE_MAP,
49
57
  MAX_TILE_SIZE,
50
58
  PACKAGE_ID,
59
+ FormatType,
51
60
  )
52
61
  from numbers_parser.containers import ObjectStore
53
62
  from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
@@ -55,6 +64,7 @@ from numbers_parser.formula import TableFormulas
55
64
  from numbers_parser.generated import TNArchives_pb2 as TNArchives
56
65
  from numbers_parser.generated import TSCEArchives_pb2 as TSCEArchives
57
66
  from numbers_parser.generated import TSDArchives_pb2 as TSDArchives
67
+ from numbers_parser.generated import TSKArchives_pb2 as TSKArchives
58
68
  from numbers_parser.generated import TSPArchiveMessages_pb2 as TSPArchiveMessages
59
69
  from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
60
70
  from numbers_parser.generated import TSSArchives_pb2 as TSSArchives
@@ -87,11 +97,11 @@ class MergeCells:
87
97
  def __init__(self):
88
98
  self._references = defaultdict(lambda: False)
89
99
 
90
- def add_reference(self, row_num: int, col_num: int, rect: Tuple):
91
- self._references[(row_num, col_num)] = MergeReference(*rect)
100
+ def add_reference(self, row: int, col: int, rect: Tuple):
101
+ self._references[(row, col)] = MergeReference(*rect)
92
102
 
93
- def add_anchor(self, row_num: int, col_num: int, size: Tuple):
94
- self._references[(row_num, col_num)] = MergeAnchor(size)
103
+ def add_anchor(self, row: int, col: int, size: Tuple):
104
+ self._references[(row, col)] = MergeAnchor(size)
95
105
 
96
106
  def is_merge_reference(self, row_col: Tuple) -> bool:
97
107
  return isinstance(self._references[row_col], MergeReference)
@@ -217,6 +227,9 @@ class _NumbersModel(Cacheable):
217
227
  self._table_strings = DataLists(self, "stringTable", "string")
218
228
  self._table_data = {}
219
229
  self._styles = None
230
+ self._custom_formats = None
231
+ self._custom_format_archives = None
232
+ self._custom_format_ids = None
220
233
  self._strokes = {
221
234
  "top": defaultdict(),
222
235
  "right": defaultdict(),
@@ -346,11 +359,126 @@ class _NumbersModel(Cacheable):
346
359
  """Return the format associated with a format ID for a particular table."""
347
360
  return self._table_formats.lookup_value(table_id, key).format
348
361
 
349
- @cache(num_args=2)
350
- def table_format_id(self, table_id: int, format) -> id:
351
- """Return the table format ID for a format string, creating a new one if required."""
362
+ @cache(num_args=3)
363
+ def format_archive(self, table_id: int, format_type: FormattingType, format: Formatting):
364
+ """Create a table format from a Formatting spec and return the table format ID"""
365
+ attrs = {x: getattr(format, x) for x in ALLOWED_FORMATTING_PARAMETERS[format_type]}
366
+ attrs["format_type"] = FORMAT_TYPE_MAP[format_type]
367
+
368
+ format = TSKArchives.FormatStructArchive(**attrs)
352
369
  return self._table_formats.lookup_key(table_id, format)
353
370
 
371
+ def add_custom_decimal_format_archive(self, format: CustomFormatting) -> None:
372
+ """Create a custom format from the format spec"""
373
+ integer_format = format.integer_format
374
+ decimal_format = format.decimal_format
375
+ num_integers = format.num_integers
376
+ num_decimals = format.num_decimals
377
+ show_thousands_separator = format.show_thousands_separator
378
+
379
+ if num_integers == 0:
380
+ format_string = ""
381
+ elif integer_format == PaddingType.NONE:
382
+ format_string = "#" * num_integers
383
+ else:
384
+ format_string = "0" * num_integers
385
+ if num_integers > 6:
386
+ format_string = re.sub(r"(...)(...)$", r",\1,\2", format_string)
387
+ elif num_integers > 3:
388
+ format_string = re.sub(r"(...)$", r",\1", format_string)
389
+ if num_decimals > 0:
390
+ if decimal_format == PaddingType.NONE:
391
+ format_string += "." + "#" * num_decimals
392
+ else:
393
+ format_string += "." + "0" * num_decimals
394
+
395
+ min_integer_width = (
396
+ num_integers if num_integers > 0 and integer_format != PaddingType.NONE else 0
397
+ )
398
+ num_nonspace_decimal_digits = num_decimals if decimal_format == PaddingType.ZEROS else 0
399
+ num_nonspace_integer_digits = num_integers if integer_format == PaddingType.ZEROS else 0
400
+ index_from_right_last_integer = num_decimals + 1 if num_integers > 0 else num_decimals
401
+ # Empirically correct:
402
+ if index_from_right_last_integer == 1:
403
+ index_from_right_last_integer = 0
404
+ elif index_from_right_last_integer == 0:
405
+ index_from_right_last_integer = 1
406
+ decimal_width = num_decimals if decimal_format == PaddingType.SPACES else 0
407
+ is_complex = "0" in format_string and (
408
+ min_integer_width > 0 or num_nonspace_decimal_digits == 0
409
+ )
410
+
411
+ format_archive = TSKArchives.CustomFormatArchive(
412
+ name=format.name,
413
+ format_type_pre_bnc=FormatType.CUSTOM_NUMBER,
414
+ format_type=FormatType.CUSTOM_NUMBER,
415
+ default_format=TSKArchives.FormatStructArchive(
416
+ contains_integer_token=num_integers > 0,
417
+ custom_format_string=format_string,
418
+ decimal_width=decimal_width,
419
+ format_type=FormatType.CUSTOM_NUMBER,
420
+ fraction_accuracy=0xFFFFFFFD,
421
+ index_from_right_last_integer=index_from_right_last_integer,
422
+ is_complex=is_complex,
423
+ min_integer_width=min_integer_width,
424
+ num_hash_decimal_digits=0,
425
+ num_nonspace_decimal_digits=num_nonspace_decimal_digits,
426
+ num_nonspace_integer_digits=num_nonspace_integer_digits,
427
+ requires_fraction_replacement=False,
428
+ scale_factor=1.0,
429
+ show_thousands_separator=show_thousands_separator and num_integers > 0,
430
+ total_num_decimal_digits=decimal_width,
431
+ use_accounting_style=False,
432
+ ),
433
+ )
434
+ self.add_custom_format_archive(format, format_archive)
435
+
436
+ def add_custom_datetime_format_archive(self, format: CustomFormatting) -> None:
437
+ format_archive = TSKArchives.CustomFormatArchive(
438
+ name=format.name,
439
+ format_type_pre_bnc=FormatType.CUSTOM_DATE,
440
+ format_type=FormatType.CUSTOM_DATE,
441
+ default_format=TSKArchives.FormatStructArchive(
442
+ custom_format_string=format.format,
443
+ format_type=FormatType.CUSTOM_DATE,
444
+ ),
445
+ )
446
+ self.add_custom_format_archive(format, format_archive)
447
+
448
+ def add_custom_format_archive(self, format: CustomFormatting, format_archive: object) -> None:
449
+ format_uuid = NumbersUUID().protobuf2
450
+ self._custom_formats[format.name] = format
451
+ self._custom_format_archives[format.name] = format_archive
452
+ self._custom_format_uuids[format.name] = format_uuid
453
+
454
+ custom_format_list_id = self.objects[DOCUMENT_ID].super.custom_format_list.identifier
455
+ custom_format_list = self.objects[custom_format_list_id]
456
+ custom_format_list.custom_formats.append(format_archive)
457
+ custom_format_list.uuids.append(format_uuid)
458
+
459
+ def custom_format_id(self, table_id: int, format: CustomFormatting) -> int:
460
+ """Look up the custom format and return the format ID for the table"""
461
+ format_type = CUSTOM_FORMAT_TYPE_MAP[format.type]
462
+ format_uuid = self._custom_format_uuids[format.name]
463
+ custom_format = TSKArchives.FormatStructArchive(
464
+ format_type=format_type,
465
+ custom_uid=TSPMessages.UUID(lower=format_uuid.lower, upper=format_uuid.upper),
466
+ )
467
+ return self._table_formats.lookup_key(table_id, custom_format)
468
+
469
+ def add_custom_text_format_archive(self, format: CustomFormatting) -> None:
470
+ format_string = format.format.replace("%s", CUSTOM_TEXT_PLACEHOLDER)
471
+ format_archive = TSKArchives.CustomFormatArchive(
472
+ name=format.name,
473
+ format_type_pre_bnc=FormatType.CUSTOM_TEXT,
474
+ format_type=FormatType.CUSTOM_TEXT,
475
+ default_format=TSKArchives.FormatStructArchive(
476
+ custom_format_string=format_string,
477
+ format_type=FormatType.CUSTOM_TEXT,
478
+ ),
479
+ )
480
+ self.add_custom_format_archive(format, format_archive)
481
+
354
482
  @cache(num_args=2)
355
483
  def table_style(self, table_id: int, key: int) -> str:
356
484
  """Return the style associated with a style ID for a particular table."""
@@ -481,10 +609,10 @@ class _NumbersModel(Cacheable):
481
609
  col_start = rect.origin.column
482
610
  col_end = col_start + rect.size.num_columns - 1
483
611
  size = (row_end - row_start + 1, col_end - col_start + 1)
484
- for row_num in range(row_start, row_end + 1):
485
- for col_num in range(col_start, col_end + 1):
612
+ for row in range(row_start, row_end + 1):
613
+ for col in range(col_start, col_end + 1):
486
614
  self._merge_cells[table_id].add_reference(
487
- row_num, col_num, (row_start, col_start, row_end, col_end)
615
+ row, col, (row_start, col_start, row_end, col_end)
488
616
  )
489
617
  self._merge_cells[table_id].add_anchor(row_start, col_start, size)
490
618
 
@@ -504,10 +632,10 @@ class _NumbersModel(Cacheable):
504
632
  )
505
633
  row_end = row_start + num_rows - 1
506
634
  col_end = col_start + num_columns - 1
507
- for row_num in range(row_start, row_end + 1):
508
- for col_num in range(col_start, col_end + 1):
635
+ for row in range(row_start, row_end + 1):
636
+ for col in range(col_start, col_end + 1):
509
637
  self._merge_cells[table_id].add_reference(
510
- row_num, col_num, (row_start, col_start, row_end, col_end)
638
+ row, col, (row_start, col_start, row_end, col_end)
511
639
  )
512
640
  self._merge_cells[table_id].add_anchor(row_start, col_start, (num_rows, num_columns))
513
641
 
@@ -527,7 +655,7 @@ class _NumbersModel(Cacheable):
527
655
  if table_uuid == self.table_base_id(table_id):
528
656
  return table_id
529
657
 
530
- def node_to_ref(self, this_table_id: int, row_num: int, col_num: int, node):
658
+ def node_to_ref(self, this_table_id: int, row: int, col: int, node):
531
659
  if node.HasField("AST_cross_table_reference_extra_info"):
532
660
  table_uuid = NumbersUUID(node.AST_cross_table_reference_extra_info.table_id).hex
533
661
  other_table_id = self.table_uuids_to_id(table_uuid)
@@ -544,36 +672,34 @@ class _NumbersModel(Cacheable):
544
672
  other_table_name = f"{other_sheet_name}::" + other_table_name
545
673
 
546
674
  if node.HasField("AST_colon_tract"):
547
- return self.tract_to_row_col_ref(node, other_table_name, row_num, col_num)
675
+ return self.tract_to_row_col_ref(node, other_table_name, row, col)
548
676
  elif node.HasField("AST_row") and not node.HasField("AST_column"):
549
- return node_to_row_ref(node, other_table_name, row_num)
677
+ return node_to_row_ref(node, other_table_name, row)
550
678
  elif node.HasField("AST_column") and not node.HasField("AST_row"):
551
- return node_to_col_ref(node, other_table_name, col_num)
679
+ return node_to_col_ref(node, other_table_name, col)
552
680
  else:
553
- return node_to_row_col_ref(node, other_table_name, row_num, col_num)
681
+ return node_to_row_col_ref(node, other_table_name, row, col)
554
682
 
555
- def tract_to_row_col_ref(
556
- self, node: object, table_name: str, row_num: int, col_num: int
557
- ) -> str:
683
+ def tract_to_row_col_ref(self, node: object, table_name: str, row: int, col: int) -> str:
558
684
  if node.AST_sticky_bits.begin_row_is_absolute:
559
685
  row_begin = node.AST_colon_tract.absolute_row[0].range_begin
560
686
  else:
561
- row_begin = row_num + node.AST_colon_tract.relative_row[0].range_begin
687
+ row_begin = row + node.AST_colon_tract.relative_row[0].range_begin
562
688
 
563
689
  if node.AST_sticky_bits.end_row_is_absolute:
564
690
  row_end = range_end(node.AST_colon_tract.absolute_row[0])
565
691
  else:
566
- row_end = row_num + range_end(node.AST_colon_tract.relative_row[0])
692
+ row_end = row + range_end(node.AST_colon_tract.relative_row[0])
567
693
 
568
694
  if node.AST_sticky_bits.begin_column_is_absolute:
569
695
  col_begin = node.AST_colon_tract.absolute_column[0].range_begin
570
696
  else:
571
- col_begin = col_num + node.AST_colon_tract.relative_column[0].range_begin
697
+ col_begin = col + node.AST_colon_tract.relative_column[0].range_begin
572
698
 
573
699
  if node.AST_sticky_bits.end_column_is_absolute:
574
700
  col_end = range_end(node.AST_colon_tract.absolute_column[0])
575
701
  else:
576
- col_end = col_num + range_end(node.AST_colon_tract.relative_column[0])
702
+ col_end = col + range_end(node.AST_colon_tract.relative_column[0])
577
703
 
578
704
  begin_ref = xl_rowcol_to_cell(
579
705
  row_begin,
@@ -619,29 +745,29 @@ class _NumbersModel(Cacheable):
619
745
  return buffers
620
746
 
621
747
  @cache(num_args=3)
622
- def storage_buffer(self, table_id: int, row_num: int, col_num: int) -> bytes:
623
- row_offset = self.row_storage_map(table_id)[row_num]
748
+ def storage_buffer(self, table_id: int, row: int, col: int) -> bytes:
749
+ row_offset = self.row_storage_map(table_id)[row]
624
750
  if row_offset is None:
625
751
  return None
626
752
  storage_buffers = self.storage_buffers(table_id)
627
753
  if row_offset >= len(storage_buffers):
628
754
  return None
629
- if col_num >= len(storage_buffers[row_offset]):
755
+ if col >= len(storage_buffers[row_offset]):
630
756
  return None
631
- return storage_buffers[row_offset][col_num]
757
+ return storage_buffers[row_offset][col]
632
758
 
633
759
  def recalculate_row_headers(self, table_id: int, data: List):
634
760
  base_data_store = self.objects[table_id].base_data_store
635
761
  buckets = self.objects[base_data_store.rowHeaders.buckets[0].identifier]
636
762
  clear_field_container(buckets.headers)
637
- for row_num in range(len(data)):
638
- if table_id in self._row_heights and row_num in self._row_heights[table_id]:
639
- height = self._row_heights[table_id][row_num]
763
+ for row in range(len(data)):
764
+ if table_id in self._row_heights and row in self._row_heights[table_id]:
765
+ height = self._row_heights[table_id][row]
640
766
  else:
641
767
  height = 0.0
642
768
  header = TSTArchives.HeaderStorageBucket.Header(
643
- index=row_num,
644
- numberOfCells=len(data[row_num]),
769
+ index=row,
770
+ numberOfCells=len(data[row]),
645
771
  size=height,
646
772
  hidingState=0,
647
773
  )
@@ -649,8 +775,8 @@ class _NumbersModel(Cacheable):
649
775
 
650
776
  def recalculate_column_headers(self, table_id: int, data: List):
651
777
  current_column_widths = {}
652
- for col_num in range(self.number_of_columns(table_id)):
653
- current_column_widths[col_num] = self.col_width(table_id, col_num)
778
+ for col in range(self.number_of_columns(table_id)):
779
+ current_column_widths[col] = self.col_width(table_id, col)
654
780
 
655
781
  base_data_store = self.objects[table_id].base_data_store
656
782
  buckets = self.objects[base_data_store.columnHeaders.identifier]
@@ -658,14 +784,14 @@ class _NumbersModel(Cacheable):
658
784
  # Transpose data to get columns
659
785
  col_data = [list(x) for x in zip(*data)]
660
786
 
661
- for col_num, col in enumerate(col_data):
662
- num_rows = len(col) - sum([isinstance(x, MergedCell) for x in col])
663
- if table_id in self._col_widths and col_num in self._col_widths[table_id]:
664
- width = self._col_widths[table_id][col_num]
787
+ for col, cells in enumerate(col_data):
788
+ num_rows = len(cells) - sum([isinstance(x, MergedCell) for x in cells])
789
+ if table_id in self._col_widths and col in self._col_widths[table_id]:
790
+ width = self._col_widths[table_id][col]
665
791
  else:
666
- width = current_column_widths[col_num]
792
+ width = current_column_widths[col]
667
793
  header = TSTArchives.HeaderStorageBucket.Header(
668
- index=col_num, numberOfCells=num_rows, size=width, hidingState=0
794
+ index=col, numberOfCells=num_rows, size=width, hidingState=0
669
795
  )
670
796
  buckets.headers.append(header)
671
797
 
@@ -690,23 +816,23 @@ class _NumbersModel(Cacheable):
690
816
  base_data_store.merge_region_map.CopyFrom(TSPMessages.Reference(identifier=merge_map_id))
691
817
 
692
818
  def recalculate_row_info(
693
- self, table_id: int, data: List, tile_row_offset: int, row_num: int
819
+ self, table_id: int, data: List, tile_row_offset: int, row: int
694
820
  ) -> TSTArchives.TileRowInfo:
695
821
  row_info = TSTArchives.TileRowInfo()
696
822
  row_info.storage_version = 5
697
- row_info.tile_row_index = row_num - tile_row_offset
823
+ row_info.tile_row_index = row - tile_row_offset
698
824
  row_info.cell_count = 0
699
825
  cell_storage = b""
700
826
 
701
827
  offsets = [-1] * len(data[0])
702
828
  current_offset = 0
703
829
 
704
- for col_num in range(len(data[row_num])):
705
- buffer = self.pack_cell_storage(table_id, data, row_num, col_num)
830
+ for col in range(len(data[row])):
831
+ buffer = self.pack_cell_storage(table_id, data, row, col)
706
832
  if buffer is not None:
707
833
  cell_storage += buffer
708
834
  # Always use wide offsets
709
- offsets[col_num] = current_offset >> 2
835
+ offsets[col] = current_offset >> 2
710
836
  current_offset += len(buffer)
711
837
 
712
838
  row_info.cell_count += 1
@@ -808,8 +934,8 @@ class _NumbersModel(Cacheable):
808
934
  tile_id, tile = self.objects.create_object_from_dict(
809
935
  "Index/Tables/Tile-{}", tile_dict, TSTArchives.Tile
810
936
  )
811
- for row_num in range(row_start, row_end):
812
- row_info = self.recalculate_row_info(table_id, data, row_start, row_num)
937
+ for row in range(row_start, row_end):
938
+ row_info = self.recalculate_row_info(table_id, data, row_start, row)
813
939
  tile.rowInfos.append(row_info)
814
940
 
815
941
  tile_ref = TSTArchives.TileStorage.Tile()
@@ -849,23 +975,23 @@ class _NumbersModel(Cacheable):
849
975
  height += table_model.default_row_height
850
976
  return round(height)
851
977
 
852
- def row_height(self, table_id: int, row_num: int, height: int = None) -> int:
978
+ def row_height(self, table_id: int, row: int, height: int = None) -> int:
853
979
  if height is not None:
854
980
  if table_id not in self._row_heights:
855
981
  self._row_heights[table_id] = {}
856
- self._row_heights[table_id][row_num] = height
982
+ self._row_heights[table_id][row] = height
857
983
  return height
858
984
 
859
- if table_id in self._row_heights and row_num in self._row_heights[table_id]:
860
- return self._row_heights[table_id][row_num]
985
+ if table_id in self._row_heights and row in self._row_heights[table_id]:
986
+ return self._row_heights[table_id][row]
861
987
 
862
988
  table_model = self.objects[table_id]
863
989
  bds = self.objects[table_id].base_data_store
864
990
  bucket_id = bds.rowHeaders.buckets[0].identifier
865
991
  buckets = self.objects[bucket_id].headers
866
992
  bucket_map = {x.index: x for x in buckets}
867
- if row_num in bucket_map and bucket_map[row_num].size != 0.0:
868
- return round(bucket_map[row_num].size)
993
+ if row in bucket_map and bucket_map[row].size != 0.0:
994
+ return round(bucket_map[row].size)
869
995
  else:
870
996
  return round(table_model.default_row_height)
871
997
 
@@ -885,23 +1011,23 @@ class _NumbersModel(Cacheable):
885
1011
  width += table_model.default_column_width
886
1012
  return round(width)
887
1013
 
888
- def col_width(self, table_id: int, col_num: int, width: int = None) -> int:
1014
+ def col_width(self, table_id: int, col: int, width: int = None) -> int:
889
1015
  if width is not None:
890
1016
  if table_id not in self._col_widths:
891
1017
  self._col_widths[table_id] = {}
892
- self._col_widths[table_id][col_num] = width
1018
+ self._col_widths[table_id][col] = width
893
1019
  return width
894
1020
 
895
- if table_id in self._col_widths and col_num in self._col_widths[table_id]:
896
- return self._col_widths[table_id][col_num]
1021
+ if table_id in self._col_widths and col in self._col_widths[table_id]:
1022
+ return self._col_widths[table_id][col]
897
1023
 
898
1024
  table_model = self.objects[table_id]
899
1025
  bds = self.objects[table_id].base_data_store
900
1026
  bucket_id = bds.columnHeaders.identifier
901
1027
  buckets = self.objects[bucket_id].headers
902
1028
  bucket_map = {x.index: x for x in buckets}
903
- if col_num in bucket_map and bucket_map[col_num].size != 0.0:
904
- return round(bucket_map[col_num].size)
1029
+ if col in bucket_map and bucket_map[col].size != 0.0:
1030
+ return round(bucket_map[col].size)
905
1031
  else:
906
1032
  return round(table_model.default_column_width)
907
1033
 
@@ -926,6 +1052,11 @@ class _NumbersModel(Cacheable):
926
1052
  table_info.super.geometry.position.y,
927
1053
  )
928
1054
 
1055
+ def is_a_pivot_table(self, table_id: int) -> bool:
1056
+ """Table is a pivot table."""
1057
+ table_info = self.objects[self.table_info_id(table_id)]
1058
+ return table_info.is_a_pivot_table
1059
+
929
1060
  def last_table_offset(self, sheet_id):
930
1061
  """Y offset of the last table in a sheet."""
931
1062
  table_id = self.table_ids(sheet_id)[-1]
@@ -1046,10 +1177,7 @@ class _NumbersModel(Cacheable):
1046
1177
  )
1047
1178
  )
1048
1179
 
1049
- data = [
1050
- [EmptyCell(row_num, col_num) for col_num in range(num_cols)]
1051
- for row_num in range(num_rows)
1052
- ]
1180
+ data = [[EmptyCell(row, col) for col in range(num_cols)] for row in range(num_rows)]
1053
1181
 
1054
1182
  row_headers_id, _ = self.objects.create_object_from_dict(
1055
1183
  "Index/Tables/HeaderStorageBucket-{}",
@@ -1184,7 +1312,7 @@ class _NumbersModel(Cacheable):
1184
1312
  return self._styles
1185
1313
 
1186
1314
  @cache(num_args=0)
1187
- def available_paragraph_styles(self) -> List[Style]:
1315
+ def available_paragraph_styles(self) -> Dict[str, Style]:
1188
1316
  theme_id = self.objects[DOCUMENT_ID].theme.identifier
1189
1317
  presets = find_extension(self.objects[theme_id].super, "paragraph_style_presets")
1190
1318
  presets_map = {
@@ -1337,8 +1465,8 @@ class _NumbersModel(Cacheable):
1337
1465
  have changes that require a cell style.
1338
1466
  """
1339
1467
  cell_styles = {}
1340
- for _, row in enumerate(data):
1341
- for _, cell in enumerate(row):
1468
+ for _, cells in enumerate(data):
1469
+ for _, cell in enumerate(cells):
1342
1470
  if cell._style is not None and cell._style._update_cell_style:
1343
1471
  fingerprint = (
1344
1472
  str(cell.style.alignment.vertical)
@@ -1422,7 +1550,7 @@ class _NumbersModel(Cacheable):
1422
1550
  entry = self._table_styles.lookup_value(cell_storage.table_id, cell_storage.cell_style_id)
1423
1551
  return entry.reference.identifier
1424
1552
 
1425
- def custom_style_name(self) -> Tuple[str, str]:
1553
+ def custom_style_name(self) -> str:
1426
1554
  """Find custom styles in the current document and return the next
1427
1555
  highest numbered style.
1428
1556
  """
@@ -1442,11 +1570,44 @@ class _NumbersModel(Cacheable):
1442
1570
  else:
1443
1571
  return "Custom Style 1"
1444
1572
 
1573
+ @property
1574
+ def custom_formats(self) -> Dict[str, CustomFormatting]:
1575
+ if self._custom_formats is None:
1576
+ custom_format_list_id = self.objects[DOCUMENT_ID].super.custom_format_list.identifier
1577
+ custom_formats = self.objects[custom_format_list_id].custom_formats
1578
+ custom_format_names = [x.name for x in custom_formats]
1579
+ custom_format_uuids = [x for x in self.objects[custom_format_list_id].uuids]
1580
+ self._custom_formats = {}
1581
+ self._custom_format_archives = {}
1582
+ self._custom_format_uuids = {}
1583
+ for i, format_name in enumerate(custom_format_names):
1584
+ self._custom_formats[format_name] = CustomFormatting.from_archive(custom_formats[i])
1585
+ self._custom_format_archives[format_name] = custom_formats[i]
1586
+ self._custom_format_uuids[format_name] = custom_format_uuids[i]
1587
+
1588
+ return self._custom_formats
1589
+
1590
+ def custom_format_name(self) -> str:
1591
+ """Find custom formats in the current document and return the next
1592
+ highest numbered format.
1593
+ """
1594
+ current_formats = self.custom_formats.keys()
1595
+ if "Custom Format" not in current_formats:
1596
+ return "Custom Format"
1597
+ current_formats = [
1598
+ m.group(1) for x in current_formats if (m := re.fullmatch(r"Custom Format (\d+)", x))
1599
+ ]
1600
+ if len(current_formats) > 0:
1601
+ last_id = int(current_formats[-1])
1602
+ return f"Custom Format {last_id + 1}"
1603
+ else:
1604
+ return "Custom Format 1"
1605
+
1445
1606
  def pack_cell_storage( # noqa: PLR0912, PLR0915
1446
- self, table_id: int, data: List, row_num: int, col_num: int
1607
+ self, table_id: int, data: List, row: int, col: int
1447
1608
  ) -> bytearray:
1448
1609
  """Create a storage buffer for a cell using v5 (modern) layout."""
1449
- cell = data[row_num][col_num]
1610
+ cell = data[row][col]
1450
1611
  if cell._style is not None:
1451
1612
  if cell._style._text_style_obj_id is not None:
1452
1613
  cell._storage.text_style_id = self._table_styles.lookup_key(
@@ -1516,7 +1677,7 @@ class _NumbersModel(Cacheable):
1516
1677
  data_type = type(cell).__name__
1517
1678
  table_name = self.table_name(table_id)
1518
1679
  warn(
1519
- f"@{table_name}:[{row_num},{col_num}]: unsupported data type {data_type} for save",
1680
+ f"@{table_name}:[{row},{col}]: unsupported data type {data_type} for save",
1520
1681
  UnsupportedWarning,
1521
1682
  stacklevel=1,
1522
1683
  )
@@ -1551,20 +1712,23 @@ class _NumbersModel(Cacheable):
1551
1712
  flags |= 0x2000
1552
1713
  length += 4
1553
1714
  storage += pack("<i", cell._storage.num_format_id)
1554
- storage[4:6] = pack("<h", 2)
1555
- storage[6:8] = pack("<h", 1)
1715
+ storage[6] |= 1
1716
+ # storage[6:8] = pack("<h", 1)
1556
1717
  if cell._storage.currency_format_id is not None:
1557
1718
  flags |= 0x4000
1558
1719
  length += 4
1559
1720
  storage += pack("<i", cell._storage.currency_format_id)
1721
+ storage[6] |= 2
1560
1722
  if cell._storage.date_format_id is not None:
1561
1723
  flags |= 0x8000
1562
1724
  length += 4
1563
1725
  storage += pack("<i", cell._storage.date_format_id)
1726
+ storage[6] |= 8
1564
1727
  if cell._storage.duration_format_id is not None:
1565
1728
  flags |= 0x10000
1566
1729
  length += 4
1567
1730
  storage += pack("<i", cell._storage.duration_format_id)
1731
+ storage[6] |= 4
1568
1732
  if cell._storage.text_format_id is not None:
1569
1733
  flags |= 0x20000
1570
1734
  length += 4
@@ -1573,6 +1737,9 @@ class _NumbersModel(Cacheable):
1573
1737
  flags |= 0x40000
1574
1738
  length += 4
1575
1739
  storage += pack("<i", cell._storage.bool_format_id)
1740
+ storage[6] |= 0x20
1741
+ if cell._storage.string_id is not None:
1742
+ storage[6] |= 0x80
1576
1743
 
1577
1744
  storage[8:12] = pack("<i", flags)
1578
1745
  if len(storage) < 32:
@@ -1585,12 +1752,12 @@ class _NumbersModel(Cacheable):
1585
1752
  return TableFormulas(self, table_id)
1586
1753
 
1587
1754
  @cache(num_args=3)
1588
- def table_cell_decode(self, table_id: int, row_num: int, col_num: int) -> Dict:
1589
- buffer = self.storage_buffer(table_id, row_num, col_num)
1755
+ def table_cell_decode(self, table_id: int, row: int, col: int) -> Dict:
1756
+ buffer = self.storage_buffer(table_id, row, col)
1590
1757
  if buffer is None:
1591
1758
  return None
1592
1759
 
1593
- return CellStorage(self, table_id, buffer, row_num, col_num)
1760
+ return CellStorage(self, table_id, buffer, row, col)
1594
1761
 
1595
1762
  @cache(num_args=2)
1596
1763
  def table_rich_text(self, table_id: int, string_key: int) -> Dict:
@@ -1691,14 +1858,14 @@ class _NumbersModel(Cacheable):
1691
1858
  return self.table_style(cell_storage.table_id, cell_storage.text_style_id)
1692
1859
 
1693
1860
  table_model = self.objects[cell_storage.table_id]
1694
- if cell_storage.row_num in range(table_model.number_of_header_rows):
1861
+ if cell_storage.row in range(table_model.number_of_header_rows):
1695
1862
  return self.objects[table_model.header_row_text_style.identifier]
1696
- elif cell_storage.col_num in range(table_model.number_of_header_columns):
1863
+ elif cell_storage.col in range(table_model.number_of_header_columns):
1697
1864
  return self.objects[table_model.header_column_text_style.identifier]
1698
1865
  elif table_model.number_of_footer_rows > 0:
1699
1866
  start_row_num = table_model.number_of_rows - table_model.number_of_footer_rows
1700
1867
  end_row_num = start_row_num + table_model.number_of_footer_rows
1701
- if cell_storage.row_num in range(start_row_num, end_row_num):
1868
+ if cell_storage.row in range(start_row_num, end_row_num):
1702
1869
  return self.objects[table_model.footer_row_text_style.identifier]
1703
1870
  return self.objects[table_model.body_text_style.identifier]
1704
1871
 
@@ -1831,19 +1998,19 @@ class _NumbersModel(Cacheable):
1831
1998
  else:
1832
1999
  return "none"
1833
2000
 
1834
- def cell_for_stroke(self, table_id: int, side: str, row_num: int, col_num: int) -> object:
2001
+ def cell_for_stroke(self, table_id: int, side: str, row: int, col: int) -> object:
1835
2002
  data = self._table_data[table_id]
1836
- if row_num < 0 or col_num < 0:
2003
+ if row < 0 or col < 0:
1837
2004
  return None
1838
- if row_num >= len(data) or col_num >= len(data[row_num]):
2005
+ if row >= len(data) or col >= len(data[row]):
1839
2006
  return None
1840
- cell = self._table_data[table_id][row_num][col_num]
2007
+ cell = self._table_data[table_id][row][col]
1841
2008
  if isinstance(cell, MergedCell):
1842
2009
  if (
1843
- (side == "top" and row_num == cell.row_start)
1844
- or (side == "right" and col_num == cell.col_end)
1845
- or (side == "bottom" and row_num == cell.row_end)
1846
- or (side == "left" and col_num == cell.col_start)
2010
+ (side == "top" and row == cell.row_start)
2011
+ or (side == "right" and col == cell.col_end)
2012
+ or (side == "bottom" and row == cell.row_end)
2013
+ or (side == "left" and col == cell.col_start)
1847
2014
  ):
1848
2015
  return cell
1849
2016
  elif cell.is_merged:
@@ -1854,28 +2021,28 @@ class _NumbersModel(Cacheable):
1854
2021
  return None
1855
2022
 
1856
2023
  def set_cell_border( # noqa: PLR0913
1857
- self, table_id: int, row_num: int, col_num: int, side: str, border_value: Border
2024
+ self, table_id: int, row: int, col: int, side: str, border_value: Border
1858
2025
  ):
1859
2026
  """Set the 2 borders adjacent to a stroke if within the table range."""
1860
2027
  if side == "top":
1861
- if (cell := self.cell_for_stroke(table_id, "top", row_num, col_num)) is not None:
2028
+ if (cell := self.cell_for_stroke(table_id, "top", row, col)) is not None:
1862
2029
  cell._border.top = border_value
1863
- if (cell := self.cell_for_stroke(table_id, "bottom", row_num - 1, col_num)) is not None:
2030
+ if (cell := self.cell_for_stroke(table_id, "bottom", row - 1, col)) is not None:
1864
2031
  cell._border.bottom = border_value
1865
2032
  elif side == "right":
1866
- if (cell := self.cell_for_stroke(table_id, "right", row_num, col_num)) is not None:
2033
+ if (cell := self.cell_for_stroke(table_id, "right", row, col)) is not None:
1867
2034
  cell._border.right = border_value
1868
- if (cell := self.cell_for_stroke(table_id, "left", row_num, col_num + 1)) is not None:
2035
+ if (cell := self.cell_for_stroke(table_id, "left", row, col + 1)) is not None:
1869
2036
  cell._border.left = border_value
1870
2037
  elif side == "bottom":
1871
- if (cell := self.cell_for_stroke(table_id, "bottom", row_num, col_num)) is not None:
2038
+ if (cell := self.cell_for_stroke(table_id, "bottom", row, col)) is not None:
1872
2039
  cell._border.bottom = border_value
1873
- if (cell := self.cell_for_stroke(table_id, "top", row_num + 1, col_num)) is not None:
2040
+ if (cell := self.cell_for_stroke(table_id, "top", row + 1, col)) is not None:
1874
2041
  cell._border.top = border_value
1875
2042
  else: # left border
1876
- if (cell := self.cell_for_stroke(table_id, "left", row_num, col_num)) is not None:
2043
+ if (cell := self.cell_for_stroke(table_id, "left", row, col)) is not None:
1877
2044
  cell._border.left = border_value
1878
- if (cell := self.cell_for_stroke(table_id, "right", row_num, col_num - 1)) is not None:
2045
+ if (cell := self.cell_for_stroke(table_id, "right", row, col - 1)) is not None:
1879
2046
  cell._border.right = border_value
1880
2047
 
1881
2048
  def extract_strokes_in_layers(self, table_id: int, layer_ids: List, side: str):
@@ -1891,13 +2058,13 @@ class _NumbersModel(Cacheable):
1891
2058
  if side in ["top", "bottom"]:
1892
2059
  start_row = stroke_layer.row_column_index
1893
2060
  start_column = stroke_run.origin
1894
- for col_num in range(start_column, start_column + stroke_run.length):
1895
- self.set_cell_border(table_id, start_row, col_num, side, border_value)
2061
+ for col in range(start_column, start_column + stroke_run.length):
2062
+ self.set_cell_border(table_id, start_row, col, side, border_value)
1896
2063
  else:
1897
2064
  start_row = stroke_run.origin
1898
2065
  start_column = stroke_layer.row_column_index
1899
- for row_num in range(start_row, start_row + stroke_run.length):
1900
- self.set_cell_border(table_id, row_num, start_column, side, border_value)
2066
+ for row in range(start_row, start_row + stroke_run.length):
2067
+ self.set_cell_border(table_id, row, start_column, side, border_value)
1901
2068
 
1902
2069
  @cache()
1903
2070
  def extract_strokes(self, table_id: int):
@@ -1968,8 +2135,8 @@ class _NumbersModel(Cacheable):
1968
2135
  def add_stroke( # noqa: PLR0913
1969
2136
  self,
1970
2137
  table_id: int,
1971
- row_num: int,
1972
- col_num: int,
2138
+ row: int,
2139
+ col: int,
1973
2140
  side: str,
1974
2141
  border_value: Border,
1975
2142
  length: int,
@@ -1983,20 +2150,20 @@ class _NumbersModel(Cacheable):
1983
2150
 
1984
2151
  if side == "top":
1985
2152
  layer_ids = sidecar_obj.top_row_stroke_layers
1986
- row_column_index = row_num
1987
- origin = col_num
2153
+ row_column_index = row
2154
+ origin = col
1988
2155
  elif side == "right":
1989
2156
  layer_ids = sidecar_obj.right_column_stroke_layers
1990
- row_column_index = col_num
1991
- origin = row_num
2157
+ row_column_index = col
2158
+ origin = row
1992
2159
  elif side == "bottom":
1993
2160
  layer_ids = sidecar_obj.bottom_row_stroke_layers
1994
- row_column_index = row_num
1995
- origin = col_num
2161
+ row_column_index = row
2162
+ origin = col
1996
2163
  else: # left border
1997
2164
  layer_ids = sidecar_obj.left_column_stroke_layers
1998
- row_column_index = col_num
1999
- origin = row_num
2165
+ row_column_index = col
2166
+ origin = row
2000
2167
 
2001
2168
  stroke_layer = None
2002
2169
  for layer_id in layer_ids:
@@ -2065,8 +2232,8 @@ def formatted_number(number_type, index):
2065
2232
  return bullet_char
2066
2233
 
2067
2234
 
2068
- def node_to_col_ref(node: object, table_name: str, col_num: int) -> str:
2069
- col = node.AST_column.column if node.AST_column.absolute else col_num + node.AST_column.column
2235
+ def node_to_col_ref(node: object, table_name: str, col: int) -> str:
2236
+ col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2070
2237
 
2071
2238
  col_name = xl_col_to_name(col, node.AST_column.absolute)
2072
2239
  if table_name is not None:
@@ -2075,8 +2242,8 @@ def node_to_col_ref(node: object, table_name: str, col_num: int) -> str:
2075
2242
  return col_name
2076
2243
 
2077
2244
 
2078
- def node_to_row_ref(node: object, table_name: str, row_num: int) -> str:
2079
- row = node.AST_row.row if node.AST_row.absolute else row_num + node.AST_row.row
2245
+ def node_to_row_ref(node: object, table_name: str, row: int) -> str:
2246
+ row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2080
2247
 
2081
2248
  row_name = f"${row+1}" if node.AST_row.absolute else f"{row+1}"
2082
2249
  if table_name is not None:
@@ -2085,9 +2252,9 @@ def node_to_row_ref(node: object, table_name: str, row_num: int) -> str:
2085
2252
  return f"{row_name}:{row_name}"
2086
2253
 
2087
2254
 
2088
- def node_to_row_col_ref(node: object, table_name: str, row_num: int, col_num: int) -> str:
2089
- row = node.AST_row.row if node.AST_row.absolute else row_num + node.AST_row.row
2090
- col = node.AST_column.column if node.AST_column.absolute else col_num + node.AST_column.column
2255
+ def node_to_row_col_ref(node: object, table_name: str, row: int, col: int) -> str:
2256
+ row = node.AST_row.row if node.AST_row.absolute else row + node.AST_row.row
2257
+ col = node.AST_column.column if node.AST_column.absolute else col + node.AST_column.column
2091
2258
 
2092
2259
  ref = xl_rowcol_to_cell(
2093
2260
  row,
@@ -2120,23 +2287,23 @@ def get_storage_buffers_for_row(
2120
2287
  offsets = [o * 4 for o in offsets]
2121
2288
 
2122
2289
  data = []
2123
- for col_num in range(num_cols):
2124
- if col_num >= len(offsets):
2290
+ for col in range(num_cols):
2291
+ if col >= len(offsets):
2125
2292
  break
2126
2293
 
2127
- start = offsets[col_num]
2294
+ start = offsets[col]
2128
2295
  if start < 0:
2129
2296
  data.append(None)
2130
2297
  continue
2131
2298
 
2132
- if col_num == (len(offsets) - 1):
2299
+ if col == (len(offsets) - 1):
2133
2300
  end = len(storage_buffer)
2134
2301
  else:
2135
2302
  end = None
2136
2303
  # Find next positive offset
2137
- for i, x in enumerate(offsets[col_num + 1 :]):
2304
+ for i, x in enumerate(offsets[col + 1 :]):
2138
2305
  if x >= 0:
2139
- end = offsets[col_num + i + 1]
2306
+ end = offsets[col + i + 1]
2140
2307
  break
2141
2308
  if end is None:
2142
2309
  end = len(storage_buffer)