numbers-parser 4.10.1__py3-none-any.whl → 4.10.3__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/cell.py CHANGED
@@ -1,19 +1,30 @@
1
+ import logging
2
+ import math
1
3
  import re
2
4
  from collections import namedtuple
3
- from dataclasses import dataclass, field
5
+ from dataclasses import asdict, dataclass, field, fields
4
6
  from datetime import datetime as builtin_datetime
5
7
  from datetime import timedelta as builtin_timedelta
6
8
  from enum import IntEnum
9
+ from fractions import Fraction
10
+ from hashlib import sha1
7
11
  from os.path import basename
12
+ from struct import pack, unpack
8
13
  from typing import Any, List, Tuple, Union
9
14
  from warnings import warn
10
15
 
11
16
  import sigfig
12
- from pendulum import DateTime, Duration, duration
17
+ from pendulum import DateTime, Duration, datetime, duration
13
18
  from pendulum import instance as pendulum_instance
14
19
 
15
- from numbers_parser.cell_storage import CellStorage, CellType
20
+ from numbers_parser import __name__ as numbers_parser_name
21
+
22
+ # from numbers_parser.cell_storage import CellStorage, CellType
16
23
  from numbers_parser.constants import (
24
+ CHECKBOX_FALSE_VALUE,
25
+ CHECKBOX_TRUE_VALUE,
26
+ CURRENCY_CELL_TYPE,
27
+ CUSTOM_TEXT_PLACEHOLDER,
17
28
  DATETIME_FIELD_MAP,
18
29
  DECIMAL_PLACES_AUTO,
19
30
  DEFAULT_ALIGNMENT,
@@ -26,23 +37,39 @@ from numbers_parser.constants import (
26
37
  DEFAULT_TEXT_INSET,
27
38
  DEFAULT_TEXT_WRAP,
28
39
  EMPTY_STORAGE_BUFFER,
40
+ EPOCH,
29
41
  MAX_BASE,
30
42
  MAX_SIGNIFICANT_DIGITS,
43
+ PACKAGE_ID,
44
+ SECONDS_IN_DAY,
45
+ SECONDS_IN_HOUR,
46
+ SECONDS_IN_WEEK,
47
+ STAR_RATING_VALUE,
48
+ CellPadding,
49
+ CellType,
31
50
  ControlFormattingType,
32
51
  CustomFormattingType,
52
+ DurationStyle,
53
+ DurationUnits,
33
54
  FormattingType,
34
55
  FormatType,
35
56
  FractionAccuracy,
36
57
  NegativeNumberStyle,
37
58
  PaddingType,
38
59
  )
39
- from numbers_parser.currencies import CURRENCIES
60
+ from numbers_parser.currencies import CURRENCIES, CURRENCY_SYMBOLS
40
61
  from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
62
+ from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
41
63
  from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
42
64
  from numbers_parser.generated.TSWPArchives_pb2 import (
43
65
  ParagraphStylePropertiesArchive as ParagraphStyle,
44
66
  )
45
67
  from numbers_parser.numbers_cache import Cacheable, cache
68
+ from numbers_parser.numbers_uuid import NumbersUUID
69
+
70
+ logger = logging.getLogger(numbers_parser_name)
71
+ debug = logger.debug
72
+
46
73
 
47
74
  __all__ = [
48
75
  "Alignment",
@@ -268,30 +295,30 @@ class Style:
268
295
  ]
269
296
 
270
297
  @classmethod
271
- def from_storage(cls, cell_storage: object, model: object):
272
- if cell_storage.image_data is not None:
273
- bg_image = BackgroundImage(*cell_storage.image_data)
298
+ def from_storage(cls, cell: object, model: object):
299
+ if cell._image_data is not None:
300
+ bg_image = BackgroundImage(*cell._image_data)
274
301
  else:
275
302
  bg_image = None
276
303
  return Style(
277
- alignment=model.cell_alignment(cell_storage),
304
+ alignment=model.cell_alignment(cell),
278
305
  bg_image=bg_image,
279
- bg_color=model.cell_bg_color(cell_storage),
280
- font_color=model.cell_font_color(cell_storage),
281
- font_size=model.cell_font_size(cell_storage),
282
- font_name=model.cell_font_name(cell_storage),
283
- bold=model.cell_is_bold(cell_storage),
284
- italic=model.cell_is_italic(cell_storage),
285
- strikethrough=model.cell_is_strikethrough(cell_storage),
286
- underline=model.cell_is_underline(cell_storage),
287
- name=model.cell_style_name(cell_storage),
288
- first_indent=model.cell_first_indent(cell_storage),
289
- left_indent=model.cell_left_indent(cell_storage),
290
- right_indent=model.cell_right_indent(cell_storage),
291
- text_inset=model.cell_text_inset(cell_storage),
292
- text_wrap=model.cell_text_wrap(cell_storage),
293
- _text_style_obj_id=model.text_style_object_id(cell_storage),
294
- _cell_style_obj_id=model.cell_style_object_id(cell_storage),
306
+ bg_color=model.cell_bg_color(cell),
307
+ font_color=model.cell_font_color(cell),
308
+ font_size=model.cell_font_size(cell),
309
+ font_name=model.cell_font_name(cell),
310
+ bold=model.cell_is_bold(cell),
311
+ italic=model.cell_is_italic(cell),
312
+ strikethrough=model.cell_is_strikethrough(cell),
313
+ underline=model.cell_is_underline(cell),
314
+ name=model.cell_style_name(cell),
315
+ first_indent=model.cell_first_indent(cell),
316
+ left_indent=model.cell_left_indent(cell),
317
+ right_indent=model.cell_right_indent(cell),
318
+ text_inset=model.cell_text_inset(cell),
319
+ text_wrap=model.cell_text_wrap(cell),
320
+ _text_style_obj_id=model.text_style_object_id(cell),
321
+ _cell_style_obj_id=model.cell_style_object_id(cell),
295
322
  )
296
323
 
297
324
  def __post_init__(self):
@@ -420,7 +447,7 @@ class Border:
420
447
 
421
448
  self._order = _order
422
449
 
423
- def __repr__(self) -> str:
450
+ def __str__(self) -> str:
424
451
  style_name = BorderType(self.style).name.lower()
425
452
  return f"Border(width={self.width}, color={self.color}, style={style_name})"
426
453
 
@@ -522,134 +549,64 @@ class MergeAnchor:
522
549
  self.size = size
523
550
 
524
551
 
525
- class Cell(Cacheable):
552
+ @dataclass
553
+ class CellStorageFlags:
554
+ _string_id: int = None
555
+ _rich_id: int = None
556
+ _cell_style_id: int = None
557
+ _text_style_id: int = None
558
+ # _cond_style_id: int = None
559
+ # _cond_rule_style_id: int = None
560
+ _formula_id: int = None
561
+ _control_id: int = None
562
+ _formula_error_id: int = None
563
+ _suggest_id: int = None
564
+ _num_format_id: int = None
565
+ _currency_format_id: int = None
566
+ _date_format_id: int = None
567
+ _duration_format_id: int = None
568
+ _text_format_id: int = None
569
+ _bool_format_id: int = None
570
+ # _comment_id: int = None
571
+ # _import_warning_id: int = None
572
+
573
+ def __str__(self) -> str:
574
+ fields = [
575
+ f"{k[1:]}={v}" for k, v in asdict(self).items() if k.endswith("_id") and v is not None
576
+ ]
577
+ fields = ", ".join([x for x in fields if x if not None])
578
+ return fields
579
+
580
+ def flags(self):
581
+ return [x.name for x in fields(self)]
582
+
583
+
584
+ class Cell(CellStorageFlags, Cacheable):
526
585
  """
527
586
  .. NOTE::
528
587
  Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
529
588
  """
530
589
 
531
- @classmethod
532
- def empty_cell(cls, table_id: int, row: int, col: int, model: object):
533
- cell = EmptyCell(row, col)
534
- cell._model = model
535
- cell._table_id = table_id
536
- merge_cells = model.merge_cells(table_id)
537
- cell._set_merge(merge_cells.get((row, col)))
538
-
539
- return cell
540
-
541
- @classmethod
542
- def merged_cell(cls, table_id: int, row: int, col: int, model: object):
543
- cell = MergedCell(row, col)
544
- cell._model = model
545
- cell._table_id = table_id
546
- merge_cells = model.merge_cells(table_id)
547
- cell._set_merge(merge_cells.get((row, col)))
548
- return cell
549
-
550
- @classmethod
551
- def from_storage(cls, cell_storage: CellStorage):
552
- if cell_storage.type == CellType.EMPTY:
553
- cell = EmptyCell(cell_storage.row, cell_storage.col)
554
- elif cell_storage.type == CellType.NUMBER:
555
- cell = NumberCell(cell_storage.row, cell_storage.col, cell_storage.value)
556
- elif cell_storage.type == CellType.TEXT:
557
- cell = TextCell(cell_storage.row, cell_storage.col, cell_storage.value)
558
- elif cell_storage.type == CellType.DATE:
559
- cell = DateCell(cell_storage.row, cell_storage.col, cell_storage.value)
560
- elif cell_storage.type == CellType.BOOL:
561
- cell = BoolCell(cell_storage.row, cell_storage.col, cell_storage.value)
562
- elif cell_storage.type == CellType.DURATION:
563
- value = duration(seconds=cell_storage.value)
564
- cell = DurationCell(cell_storage.row, cell_storage.col, value)
565
- elif cell_storage.type == CellType.ERROR:
566
- cell = ErrorCell(cell_storage.row, cell_storage.col)
567
- elif cell_storage.type == CellType.RICH_TEXT:
568
- cell = RichTextCell(cell_storage.row, cell_storage.col, cell_storage.value)
569
- else:
570
- raise UnsupportedError(
571
- f"Unsupported cell type {cell_storage.type} "
572
- + f"@:({cell_storage.row},{cell_storage.col})"
573
- )
574
-
575
- cell._table_id = cell_storage.table_id
576
- cell._model = cell_storage.model
577
- cell._storage = cell_storage
578
- cell._formula_key = cell_storage.formula_id
579
- merge_cells = cell_storage.model.merge_cells(cell_storage.table_id)
580
- cell._set_merge(merge_cells.get((cell_storage.row, cell_storage.col)))
581
- return cell
582
-
583
- @classmethod
584
- def from_value(cls, row: int, col: int, value):
585
- # TODO: write needs to retain/init the border
586
- if isinstance(value, str):
587
- return TextCell(row, col, value)
588
- elif isinstance(value, bool):
589
- return BoolCell(row, col, value)
590
- elif isinstance(value, int):
591
- return NumberCell(row, col, value)
592
- elif isinstance(value, float):
593
- rounded_value = sigfig.round(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
594
- if rounded_value != value:
595
- warn(
596
- f"'{value}' rounded to {MAX_SIGNIFICANT_DIGITS} significant digits",
597
- RuntimeWarning,
598
- stacklevel=2,
599
- )
600
- return NumberCell(row, col, rounded_value)
601
- elif isinstance(value, (DateTime, builtin_datetime)):
602
- return DateCell(row, col, pendulum_instance(value))
603
- elif isinstance(value, (Duration, builtin_timedelta)):
604
- return DurationCell(row, col, value)
605
- else:
606
- raise ValueError("Can't determine cell type from type " + type(value).__name__)
607
-
608
- def _set_formatting(
609
- self,
610
- format_id: int,
611
- format_type: FormattingType,
612
- control_id: int = None,
613
- is_currency: bool = False,
614
- ) -> None:
615
- self._storage.set_formatting(format_id, format_type, control_id, is_currency)
616
-
617
590
  def __init__(self, row: int, col: int, value):
618
591
  self._value = value
619
592
  self.row = row
620
593
  self.col = col
621
594
  self._is_bulleted = False
622
- self._formula_key = None
623
595
  self._storage = None
624
596
  self._style = None
625
-
626
- def _set_merge(self, merge_ref):
627
- if isinstance(merge_ref, MergeAnchor):
628
- self.is_merged = True
629
- self.size = merge_ref.size
630
- self.merge_range = None
631
- self.rect = None
632
- self._border = CellBorder()
633
- elif isinstance(merge_ref, MergeReference):
634
- self.is_merged = False
635
- self.size = None
636
- self.row_start = merge_ref.rect[0]
637
- self.col_start = merge_ref.rect[1]
638
- self.row_end = merge_ref.rect[2]
639
- self.col_end = merge_ref.rect[3]
640
- self.merge_range = xl_range(*merge_ref.rect)
641
- self.rect = merge_ref.rect
642
- top_merged = self.row > self.row_start
643
- right_merged = self.col < self.col_end
644
- bottom_merged = self.row < self.row_end
645
- left_merged = self.col > self.col_start
646
- self._border = CellBorder(top_merged, right_merged, bottom_merged, left_merged)
647
- else:
648
- self.is_merged = False
649
- self.size = (1, 1)
650
- self.merge_range = None
651
- self.rect = None
652
- self._border = CellBorder()
597
+ self._d128 = None
598
+ self._double = None
599
+ self._seconds = None
600
+ super().__init__()
601
+
602
+ def __str__(self):
603
+ table_name = self._model.table_name(self._table_id)
604
+ sheet_name = self._model.sheet_name(self._model.table_id_to_sheet_id(self._table_id))
605
+ cell_str = f"{sheet_name}@{table_name}[{self.row},{self.col}]:"
606
+ cell_str += f"table_id={self._table_id}, type={self._type.name}, "
607
+ cell_str += f"value={self._value}, flags={self._flags:08x}, extras={self._extras:04x}"
608
+ cell_str = ", ".join([cell_str, super().__str__()])
609
+ return cell_str
653
610
 
654
611
  @property
655
612
  def image_filename(self):
@@ -698,9 +655,9 @@ class Cell(Cacheable):
698
655
  The text of the foruma in a cell, or `None` if there is no formula
699
656
  present in a cell.
700
657
  """
701
- if self._formula_key is not None:
658
+ if self._formula_id is not None:
702
659
  table_formulas = self._model.table_formulas(self._table_id)
703
- return table_formulas.formula(self._formula_key, self.row, self.col)
660
+ return table_formulas.formula(self._formula_id, self.row, self.col)
704
661
  else:
705
662
  return None
706
663
 
@@ -753,7 +710,7 @@ class Cell(Cacheable):
753
710
 
754
711
  .. code-block:: python
755
712
 
756
- >>> table = doc.sheets[0].tables[0]
713
+ >>> table = doc.default_table
757
714
  >>> table.cell(0,0).value
758
715
  False
759
716
  >>> table.cell(0,0).formatted_value
@@ -767,10 +724,19 @@ class Cell(Cacheable):
767
724
  >>> table.cell(1,1).formatted_value
768
725
  '★★★'
769
726
  """
770
- if self._storage is None:
771
- return ""
727
+ if self._duration_format_id is not None and self._double is not None:
728
+ return self._duration_format()
729
+ elif self._date_format_id is not None and self._seconds is not None:
730
+ return self._date_format()
731
+ elif (
732
+ self._text_format_id is not None
733
+ or self._num_format_id is not None
734
+ or self._currency_format_id is not None
735
+ or self._bool_format_id is not None
736
+ ):
737
+ return self._custom_format()
772
738
  else:
773
- return self._storage.formatted
739
+ return str(self.value)
774
740
 
775
741
  @property
776
742
  def style(self) -> Union[Style, None]:
@@ -780,12 +746,8 @@ class Cell(Cacheable):
780
746
  UnsupportedWarning: On assignment; use
781
747
  :py:meth:`numbers_parser.Table.set_cell_style` instead.
782
748
  """
783
- if self._storage is None:
784
- self._storage = CellStorage(
785
- self._model, self._table_id, EMPTY_STORAGE_BUFFER, self.row, self.col
786
- )
787
749
  if self._style is None:
788
- self._style = Style.from_storage(self._storage, self._model)
750
+ self._style = Style.from_storage(self, self._model)
789
751
  return self._style
790
752
 
791
753
  @style.setter
@@ -815,8 +777,565 @@ class Cell(Cacheable):
815
777
  stacklevel=2,
816
778
  )
817
779
 
818
- def update_storage(self, storage: CellStorage) -> None:
819
- self._storage = storage
780
+ @classmethod
781
+ def _empty_cell(cls, table_id: int, row: int, col: int, model: object):
782
+ return Cell._from_storage(table_id, row, col, EMPTY_STORAGE_BUFFER, model)
783
+
784
+ @classmethod
785
+ def _merged_cell(cls, table_id: int, row: int, col: int, model: object):
786
+ cell = MergedCell(row, col)
787
+ cell._model = model
788
+ cell._table_id = table_id
789
+ merge_cells = model.merge_cells(table_id)
790
+ cell._set_merge(merge_cells.get((row, col)))
791
+ return cell
792
+
793
+ @classmethod
794
+ def _from_value(cls, row: int, col: int, value):
795
+ # TODO: write needs to retain/init the border
796
+ if isinstance(value, str):
797
+ cell = TextCell(row, col, value)
798
+ elif isinstance(value, bool):
799
+ cell = BoolCell(row, col, value)
800
+ elif isinstance(value, int):
801
+ cell = NumberCell(row, col, value)
802
+ elif isinstance(value, float):
803
+ rounded_value = sigfig.round(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
804
+ if rounded_value != value:
805
+ warn(
806
+ f"'{value}' rounded to {MAX_SIGNIFICANT_DIGITS} significant digits",
807
+ RuntimeWarning,
808
+ stacklevel=2,
809
+ )
810
+ cell = NumberCell(row, col, rounded_value)
811
+ elif isinstance(value, (DateTime, builtin_datetime)):
812
+ cell = DateCell(row, col, pendulum_instance(value))
813
+ elif isinstance(value, (Duration, builtin_timedelta)):
814
+ cell = DurationCell(row, col, value)
815
+ else:
816
+ raise ValueError("Can't determine cell type from type " + type(value).__name__)
817
+
818
+ return cell
819
+
820
+ @classmethod
821
+ def _from_storage( # noqa: PLR0913, PLR0915, PLR0912
822
+ cls, table_id: int, row: int, col: int, buffer: bytearray, model: object
823
+ ) -> None:
824
+ d128 = None
825
+ double = None
826
+ seconds = None
827
+
828
+ version = buffer[0]
829
+ if version != 5:
830
+ raise UnsupportedError(f"Cell storage version {version} is unsupported")
831
+
832
+ offset = 12
833
+ storage_flags = CellStorageFlags()
834
+ flags = unpack("<i", buffer[8:12])[0]
835
+
836
+ if flags & 0x1:
837
+ d128 = _unpack_decimal128(buffer[offset : offset + 16])
838
+ offset += 16
839
+ if flags & 0x2:
840
+ double = unpack("<d", buffer[offset : offset + 8])[0]
841
+ offset += 8
842
+ if flags & 0x4:
843
+ seconds = unpack("<d", buffer[offset : offset + 8])[0]
844
+ offset += 8
845
+ if flags & 0x8:
846
+ storage_flags._string_id = unpack("<i", buffer[offset : offset + 4])[0]
847
+ offset += 4
848
+ if flags & 0x10:
849
+ storage_flags._rich_id = unpack("<i", buffer[offset : offset + 4])[0]
850
+ offset += 4
851
+ if flags & 0x20:
852
+ storage_flags._cell_style_id = unpack("<i", buffer[offset : offset + 4])[0]
853
+ offset += 4
854
+ if flags & 0x40:
855
+ storage_flags._text_style_id = unpack("<i", buffer[offset : offset + 4])[0]
856
+ offset += 4
857
+ if flags & 0x80:
858
+ # storage_flags._cond_style_id = unpack("<i", buffer[offset : offset + 4])[0]
859
+ offset += 4
860
+ # if flags & 0x100:
861
+ # storage_flags._cond_rule_style_id = unpack("<i", buffer[offset : offset + 4])[0]
862
+ # offset += 4
863
+ if flags & 0x200:
864
+ storage_flags._formula_id = unpack("<i", buffer[offset : offset + 4])[0]
865
+ offset += 4
866
+ if flags & 0x400:
867
+ storage_flags._control_id = unpack("<i", buffer[offset : offset + 4])[0]
868
+ offset += 4
869
+ # if flags & 0x800:
870
+ # storage_flags._formula_error_id = unpack("<i", buffer[offset : offset + 4])[0]
871
+ # offset += 4
872
+ if flags & 0x1000:
873
+ storage_flags._suggest_id = unpack("<i", buffer[offset : offset + 4])[0]
874
+ offset += 4
875
+ # Skip unused flags
876
+ offset += 4 * bin(flags & 0x900).count("1")
877
+ #
878
+ if flags & 0x2000:
879
+ storage_flags._num_format_id = unpack("<i", buffer[offset : offset + 4])[0]
880
+ offset += 4
881
+ if flags & 0x4000:
882
+ storage_flags._currency_format_id = unpack("<i", buffer[offset : offset + 4])[0]
883
+ offset += 4
884
+ if flags & 0x8000:
885
+ storage_flags._date_format_id = unpack("<i", buffer[offset : offset + 4])[0]
886
+ offset += 4
887
+ if flags & 0x10000:
888
+ storage_flags._duration_format_id = unpack("<i", buffer[offset : offset + 4])[0]
889
+ offset += 4
890
+ if flags & 0x20000:
891
+ storage_flags._text_format_id = unpack("<i", buffer[offset : offset + 4])[0]
892
+ offset += 4
893
+ if flags & 0x40000:
894
+ storage_flags._bool_format_id = unpack("<i", buffer[offset : offset + 4])[0]
895
+ offset += 4
896
+ # if flags & 0x80000:
897
+ # cstorage_flags._omment_id = unpack("<i", buffer[offset : offset + 4])[0]
898
+ # offset += 4
899
+ # if flags & 0x100000:
900
+ # storage_flags._import_warning_id = unpack("<i", buffer[offset : offset + 4])[0]
901
+ # offset += 4
902
+
903
+ cell_type = buffer[1]
904
+ if cell_type == TSTArchives.genericCellType:
905
+ cell = EmptyCell(row, col)
906
+ elif cell_type == TSTArchives.numberCellType:
907
+ cell = NumberCell(row, col, d128)
908
+ elif cell_type == TSTArchives.textCellType:
909
+ cell = TextCell(row, col, model.table_string(table_id, storage_flags._string_id))
910
+ elif cell_type == TSTArchives.dateCellType:
911
+ cell = DateCell(row, col, EPOCH + duration(seconds=seconds))
912
+ cell._datetime = cell._value
913
+ elif cell_type == TSTArchives.boolCellType:
914
+ cell = BoolCell(row, col, double > 0.0)
915
+ elif cell_type == TSTArchives.durationCellType:
916
+ cell = DurationCell(row, col, duration(seconds=double))
917
+ elif cell_type == TSTArchives.formulaErrorCellType:
918
+ cell = ErrorCell(row, col)
919
+ elif cell_type == TSTArchives.automaticCellType:
920
+ cell = RichTextCell(row, col, model.table_rich_text(table_id, storage_flags._rich_id))
921
+ elif cell_type == CURRENCY_CELL_TYPE:
922
+ cell = NumberCell(row, col, d128, cell_type=CellType.CURRENCY)
923
+ else:
924
+ raise UnsupportedError(f"Cell type ID {cell_type} is not recognised")
925
+
926
+ cell._copy_flags(storage_flags)
927
+ cell._buffer = buffer
928
+ cell._model = model
929
+ cell._table_id = table_id
930
+ cell._d128 = d128
931
+ cell._double = double
932
+ cell._seconds = seconds
933
+ cell._extras = unpack("<H", buffer[6:8])[0]
934
+ cell._flags = flags
935
+
936
+ merge_cells = model.merge_cells(table_id)
937
+ cell._set_merge(merge_cells.get((row, col)))
938
+
939
+ if logging.getLogger(__package__).level == logging.DEBUG:
940
+ # Guard to reduce expense of computing fields
941
+ debug(str(cell))
942
+
943
+ return cell
944
+
945
+ def _copy_flags(self, storage_flags: CellStorageFlags):
946
+ for flag in storage_flags.flags():
947
+ setattr(self, flag, getattr(storage_flags, flag))
948
+
949
+ def _set_merge(self, merge_ref):
950
+ if isinstance(merge_ref, MergeAnchor):
951
+ self.is_merged = True
952
+ self.size = merge_ref.size
953
+ self.merge_range = None
954
+ self.rect = None
955
+ self._border = CellBorder()
956
+ elif isinstance(merge_ref, MergeReference):
957
+ self.is_merged = False
958
+ self.size = None
959
+ self.row_start = merge_ref.rect[0]
960
+ self.col_start = merge_ref.rect[1]
961
+ self.row_end = merge_ref.rect[2]
962
+ self.col_end = merge_ref.rect[3]
963
+ self.merge_range = xl_range(*merge_ref.rect)
964
+ self.rect = merge_ref.rect
965
+ top_merged = self.row > self.row_start
966
+ right_merged = self.col < self.col_end
967
+ bottom_merged = self.row < self.row_end
968
+ left_merged = self.col > self.col_start
969
+ self._border = CellBorder(top_merged, right_merged, bottom_merged, left_merged)
970
+ else:
971
+ self.is_merged = False
972
+ self.size = (1, 1)
973
+ self.merge_range = None
974
+ self.rect = None
975
+ self._border = CellBorder()
976
+
977
+ def _to_buffer(self) -> bytearray: # noqa: PLR0912, PLR0915
978
+ """Create a storage buffer for a cell using v5 (modern) layout."""
979
+ if self._style is not None:
980
+ if self._style._text_style_obj_id is not None:
981
+ self._text_style_id = self._model._table_styles.lookup_key(
982
+ self._table_id,
983
+ TSPMessages.Reference(identifier=self._style._text_style_obj_id),
984
+ )
985
+ self._model.add_component_reference(
986
+ self._style._text_style_obj_id,
987
+ parent_id=self._model._table_styles.id(self._table_id),
988
+ )
989
+
990
+ if self._style._cell_style_obj_id is not None:
991
+ self._cell_style_id = self._model._table_styles.lookup_key(
992
+ self._table_id,
993
+ TSPMessages.Reference(identifier=self._style._cell_style_obj_id),
994
+ )
995
+ self._model.add_component_reference(
996
+ self._style._cell_style_obj_id,
997
+ parent_id=self._model._table_styles.id(self._table_id),
998
+ )
999
+
1000
+ length = 12
1001
+ if isinstance(self, NumberCell):
1002
+ flags = 1
1003
+ length += 16
1004
+ if self._type == CellType.CURRENCY:
1005
+ cell_type = CURRENCY_CELL_TYPE
1006
+ else:
1007
+ cell_type = TSTArchives.numberCellType
1008
+ value = _pack_decimal128(self.value)
1009
+ elif isinstance(self, TextCell):
1010
+ flags = 8
1011
+ length += 4
1012
+ cell_type = TSTArchives.textCellType
1013
+ value = pack("<i", self._model.table_string_key(self._table_id, self.value))
1014
+ elif isinstance(self, DateCell):
1015
+ flags = 4
1016
+ length += 8
1017
+ cell_type = TSTArchives.dateCellType
1018
+ date_delta = self._value.astimezone() - EPOCH
1019
+ value = pack("<d", float(date_delta.total_seconds()))
1020
+ elif isinstance(self, BoolCell):
1021
+ flags = 2
1022
+ length += 8
1023
+ cell_type = TSTArchives.boolCellType
1024
+ value = pack("<d", float(self.value))
1025
+ elif isinstance(self, DurationCell):
1026
+ flags = 2
1027
+ length += 8
1028
+ cell_type = TSTArchives.durationCellType
1029
+ value = value = pack("<d", float(self.value.total_seconds()))
1030
+ elif isinstance(self, EmptyCell):
1031
+ flags = 0
1032
+ cell_type = TSTArchives.emptyCellValueType
1033
+ value = b""
1034
+ elif isinstance(self, MergedCell):
1035
+ return None
1036
+ elif isinstance(self, RichTextCell):
1037
+ flags = 0
1038
+ length += 4
1039
+ cell_type = TSTArchives.automaticCellType
1040
+ value = pack("<i", self._rich_id)
1041
+ else:
1042
+ data_type = type(self).__name__
1043
+ table_name = self._model.table_name(self._table_id)
1044
+ table_ref = f"@{table_name}:[{self.row},{self.col}]"
1045
+ warn(
1046
+ f"{table_ref}: unsupported data type {data_type} for save",
1047
+ UnsupportedWarning,
1048
+ stacklevel=1,
1049
+ )
1050
+ return None
1051
+
1052
+ storage = bytearray(12)
1053
+ storage[0] = 5
1054
+ storage[1] = cell_type
1055
+ storage += value
1056
+
1057
+ if self._rich_id is not None:
1058
+ flags |= 0x10
1059
+ length += 4
1060
+ storage += pack("<i", self._rich_id)
1061
+ if self._cell_style_id is not None:
1062
+ flags |= 0x20
1063
+ length += 4
1064
+ storage += pack("<i", self._cell_style_id)
1065
+ if self._text_style_id is not None:
1066
+ flags |= 0x40
1067
+ length += 4
1068
+ storage += pack("<i", self._text_style_id)
1069
+ if self._formula_id is not None:
1070
+ flags |= 0x200
1071
+ length += 4
1072
+ storage += pack("<i", self._formula_id)
1073
+ if self._control_id is not None:
1074
+ flags |= 0x400
1075
+ length += 4
1076
+ storage += pack("<i", self._control_id)
1077
+ if self._suggest_id is not None:
1078
+ flags |= 0x1000
1079
+ length += 4
1080
+ storage += pack("<i", self._suggest_id)
1081
+ if self._num_format_id is not None:
1082
+ flags |= 0x2000
1083
+ length += 4
1084
+ storage += pack("<i", self._num_format_id)
1085
+ storage[6] |= 1
1086
+ # storage[6:8] = pack("<h", 1)
1087
+ if self._currency_format_id is not None:
1088
+ flags |= 0x4000
1089
+ length += 4
1090
+ storage += pack("<i", self._currency_format_id)
1091
+ storage[6] |= 2
1092
+ if self._date_format_id is not None:
1093
+ flags |= 0x8000
1094
+ length += 4
1095
+ storage += pack("<i", self._date_format_id)
1096
+ storage[6] |= 8
1097
+ if self._duration_format_id is not None:
1098
+ flags |= 0x10000
1099
+ length += 4
1100
+ storage += pack("<i", self._duration_format_id)
1101
+ storage[6] |= 4
1102
+ if self._text_format_id is not None:
1103
+ flags |= 0x20000
1104
+ length += 4
1105
+ storage += pack("<i", self._text_format_id)
1106
+ if self._bool_format_id is not None:
1107
+ flags |= 0x40000
1108
+ length += 4
1109
+ storage += pack("<i", self._bool_format_id)
1110
+ storage[6] |= 0x20
1111
+ if self._string_id is not None:
1112
+ storage[6] |= 0x80
1113
+
1114
+ storage[8:12] = pack("<i", flags)
1115
+ if len(storage) < 32:
1116
+ storage += bytearray(32 - length)
1117
+
1118
+ return storage[0:length]
1119
+
1120
+ def _update_value(self, value, cell: object) -> None:
1121
+ if cell._type == CellType.NUMBER:
1122
+ self._d128 = value
1123
+ elif cell._type == CellType.DATE:
1124
+ self._datetime = value
1125
+ self._value = value
1126
+
1127
+ @property
1128
+ @cache(num_args=0)
1129
+ def _image_data(self) -> Tuple[bytes, str]:
1130
+ """Return the background image data for a cell or None if no image."""
1131
+ if self._cell_style_id is None:
1132
+ return None
1133
+ style = self._model.table_style(self._table_id, self._cell_style_id)
1134
+ if not style.cell_properties.cell_fill.HasField("image"):
1135
+ return None
1136
+
1137
+ image_id = style.cell_properties.cell_fill.image.imagedata.identifier
1138
+ datas = self._model.objects[PACKAGE_ID].datas
1139
+ stored_filename = [x.file_name for x in datas if x.identifier == image_id][0]
1140
+ preferred_filename = [x.preferred_file_name for x in datas if x.identifier == image_id][0]
1141
+ all_paths = self._model.objects.file_store.keys()
1142
+ image_pathnames = [x for x in all_paths if x == f"Data/{stored_filename}"]
1143
+
1144
+ if len(image_pathnames) == 0:
1145
+ warn(
1146
+ f"Cannot find file '{preferred_filename}' in Numbers archive",
1147
+ RuntimeWarning,
1148
+ stacklevel=3,
1149
+ )
1150
+ else:
1151
+ image_data = self._model.objects.file_store[image_pathnames[0]]
1152
+ digest = sha1(image_data).digest()
1153
+ if digest not in self._model._images:
1154
+ self._model._images[digest] = image_id
1155
+
1156
+ return (image_data, preferred_filename)
1157
+
1158
+ def _custom_format(self) -> str: # noqa: PLR0911
1159
+ if self._text_format_id is not None and self._type == CellType.TEXT:
1160
+ format = self._model.table_format(self._table_id, self._text_format_id)
1161
+ elif self._currency_format_id is not None:
1162
+ format = self._model.table_format(self._table_id, self._currency_format_id)
1163
+ elif self._bool_format_id is not None and self._type == CellType.BOOL:
1164
+ format = self._model.table_format(self._table_id, self._bool_format_id)
1165
+ elif self._num_format_id is not None:
1166
+ format = self._model.table_format(self._table_id, self._num_format_id)
1167
+ else:
1168
+ return str(self.value)
1169
+
1170
+ debug("custom_format: @[%d,%d]: format_type=%s, ", self.row, self.col, format.format_type)
1171
+
1172
+ if format.HasField("custom_uid"):
1173
+ format_uuid = NumbersUUID(format.custom_uid).hex
1174
+ format_map = self._model.custom_format_map()
1175
+ custom_format = format_map[format_uuid].default_format
1176
+ if custom_format.requires_fraction_replacement:
1177
+ formatted_value = _format_fraction(self._d128, custom_format)
1178
+ elif custom_format.format_type == FormatType.CUSTOM_TEXT:
1179
+ formatted_value = _decode_text_format(
1180
+ custom_format,
1181
+ self._model.table_string(self._table_id, self._string_id),
1182
+ )
1183
+ else:
1184
+ formatted_value = _decode_number_format(
1185
+ custom_format, self._d128, format_map[format_uuid].name
1186
+ )
1187
+ elif format.format_type == FormatType.DECIMAL:
1188
+ return _format_decimal(self._d128, format)
1189
+ elif format.format_type == FormatType.CURRENCY:
1190
+ return _format_currency(self._d128, format)
1191
+ elif format.format_type == FormatType.BOOLEAN:
1192
+ return "TRUE" if self.value else "FALSE"
1193
+ elif format.format_type == FormatType.PERCENT:
1194
+ return _format_decimal(self._d128 * 100, format, percent=True)
1195
+ elif format.format_type == FormatType.BASE:
1196
+ return _format_base(self._d128, format)
1197
+ elif format.format_type == FormatType.FRACTION:
1198
+ return _format_fraction(self._d128, format)
1199
+ elif format.format_type == FormatType.SCIENTIFIC:
1200
+ return _format_scientific(self._d128, format)
1201
+ elif format.format_type == FormatType.CHECKBOX:
1202
+ return CHECKBOX_TRUE_VALUE if self.value else CHECKBOX_FALSE_VALUE
1203
+ elif format.format_type == FormatType.RATING:
1204
+ return STAR_RATING_VALUE * int(self._d128)
1205
+ else:
1206
+ formatted_value = str(self.value)
1207
+ return formatted_value
1208
+
1209
+ def _date_format(self) -> str:
1210
+ format = self._model.table_format(self._table_id, self._date_format_id)
1211
+ if format.HasField("custom_uid"):
1212
+ format_uuid = NumbersUUID(format.custom_uid).hex
1213
+ format_map = self._model.custom_format_map()
1214
+ custom_format = format_map[format_uuid].default_format
1215
+ custom_format_string = custom_format.custom_format_string
1216
+ if custom_format.format_type == FormatType.CUSTOM_DATE:
1217
+ formatted_value = _decode_date_format(custom_format_string, self._datetime)
1218
+ else:
1219
+ warn(
1220
+ f"Unexpected custom format type {custom_format.format_type}",
1221
+ UnsupportedWarning,
1222
+ stacklevel=3,
1223
+ )
1224
+ return ""
1225
+ else:
1226
+ formatted_value = _decode_date_format(format.date_time_format, self._datetime)
1227
+ return formatted_value
1228
+
1229
+ def _duration_format(self) -> str:
1230
+ format = self._model.table_format(self._table_id, self._duration_format_id)
1231
+ debug(
1232
+ "duration_format: @[%d,%d]: table_id=%d, duration_format_id=%d, duration_style=%s",
1233
+ self.row,
1234
+ self.col,
1235
+ self._table_id,
1236
+ self._duration_format_id,
1237
+ format.duration_style,
1238
+ )
1239
+
1240
+ duration_style = format.duration_style
1241
+ unit_largest = format.duration_unit_largest
1242
+ unit_smallest = format.duration_unit_smallest
1243
+ if format.use_automatic_duration_units:
1244
+ unit_smallest, unit_largest = _auto_units(self._double, format)
1245
+
1246
+ d = self._double
1247
+ dd = int(self._double)
1248
+ dstr = []
1249
+
1250
+ def unit_in_range(largest, smallest, unit_type):
1251
+ return largest <= unit_type and smallest >= unit_type
1252
+
1253
+ def pad_digits(d, largest, smallest, unit_type):
1254
+ return (largest == unit_type and smallest == unit_type) or d >= 10
1255
+
1256
+ if unit_largest == DurationUnits.WEEK:
1257
+ dd = int(d / SECONDS_IN_WEEK)
1258
+ if unit_smallest != DurationUnits.WEEK:
1259
+ d -= SECONDS_IN_WEEK * dd
1260
+ dstr.append(str(dd) + _unit_format("week", dd, duration_style))
1261
+
1262
+ if unit_in_range(unit_largest, unit_smallest, DurationUnits.DAY):
1263
+ dd = int(d / SECONDS_IN_DAY)
1264
+ if unit_smallest > DurationUnits.DAY:
1265
+ d -= SECONDS_IN_DAY * dd
1266
+ dstr.append(str(dd) + _unit_format("day", dd, duration_style))
1267
+
1268
+ if unit_in_range(unit_largest, unit_smallest, DurationUnits.HOUR):
1269
+ dd = int(d / SECONDS_IN_HOUR)
1270
+ if unit_smallest > DurationUnits.HOUR:
1271
+ d -= SECONDS_IN_HOUR * dd
1272
+ dstr.append(str(dd) + _unit_format("hour", dd, duration_style))
1273
+
1274
+ if unit_in_range(unit_largest, unit_smallest, DurationUnits.MINUTE):
1275
+ dd = int(d / 60)
1276
+ if unit_smallest > DurationUnits.MINUTE:
1277
+ d -= 60 * dd
1278
+ if duration_style == DurationStyle.COMPACT:
1279
+ pad = pad_digits(dd, unit_smallest, unit_largest, DurationUnits.MINUTE)
1280
+ dstr.append(("" if pad else "0") + str(dd))
1281
+ else:
1282
+ dstr.append(str(dd) + _unit_format("minute", dd, duration_style))
1283
+
1284
+ if unit_in_range(unit_largest, unit_smallest, DurationUnits.SECOND):
1285
+ dd = int(d)
1286
+ if unit_smallest > DurationUnits.SECOND:
1287
+ d -= dd
1288
+ if duration_style == DurationStyle.COMPACT:
1289
+ pad = pad_digits(dd, unit_smallest, unit_largest, DurationUnits.SECOND)
1290
+ dstr.append(("" if pad else "0") + str(dd))
1291
+ else:
1292
+ dstr.append(str(dd) + _unit_format("second", dd, duration_style))
1293
+
1294
+ if unit_smallest >= DurationUnits.MILLISECOND:
1295
+ dd = int(round(1000 * d))
1296
+ if duration_style == DurationStyle.COMPACT:
1297
+ padding = "0" if dd >= 10 else "00"
1298
+ padding = "" if dd >= 100 else padding
1299
+ dstr.append(f"{padding}{dd}")
1300
+ else:
1301
+ dstr.append(str(dd) + _unit_format("millisecond", dd, duration_style, "ms"))
1302
+ duration_str = (":" if duration_style == 0 else " ").join(dstr)
1303
+ if duration_style == DurationStyle.COMPACT:
1304
+ duration_str = re.sub(r":(\d\d\d)$", r".\1", duration_str)
1305
+
1306
+ return duration_str
1307
+
1308
+ def _set_formatting(
1309
+ self,
1310
+ format_id: int,
1311
+ format_type: Union[FormattingType, CustomFormattingType],
1312
+ control_id: int = None,
1313
+ is_currency: bool = False,
1314
+ ) -> None:
1315
+ self._is_currency = is_currency
1316
+ if format_type == FormattingType.CURRENCY:
1317
+ self._currency_format_id = format_id
1318
+ elif format_type == FormattingType.TICKBOX:
1319
+ self._bool_format_id = format_id
1320
+ self._control_id = control_id
1321
+ elif format_type == FormattingType.RATING:
1322
+ self._num_format_id = format_id
1323
+ self._control_id = control_id
1324
+ elif format_type in [FormattingType.SLIDER, FormattingType.STEPPER]:
1325
+ if is_currency:
1326
+ self._currency_format_id = format_id
1327
+ else:
1328
+ self._num_format_id = format_id
1329
+ self._control_id = control_id
1330
+ elif format_type == FormattingType.POPUP:
1331
+ self._text_format_id = format_id
1332
+ self._control_id = control_id
1333
+ elif format_type in [FormattingType.DATETIME, CustomFormattingType.DATETIME]:
1334
+ self._date_format_id = format_id
1335
+ elif format_type in [FormattingType.TEXT, CustomFormattingType.TEXT]:
1336
+ self._text_format_id = format_id
1337
+ else:
1338
+ self._num_format_id = format_id
820
1339
 
821
1340
 
822
1341
  class NumberCell(Cell):
@@ -825,8 +1344,8 @@ class NumberCell(Cell):
825
1344
  Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
826
1345
  """
827
1346
 
828
- def __init__(self, row: int, col: int, value: float):
829
- self._type = TSTArchives.numberCellType
1347
+ def __init__(self, row: int, col: int, value: float, cell_type=CellType.NUMBER):
1348
+ self._type = cell_type
830
1349
  super().__init__(row, col, value)
831
1350
 
832
1351
  @property
@@ -836,7 +1355,7 @@ class NumberCell(Cell):
836
1355
 
837
1356
  class TextCell(Cell):
838
1357
  def __init__(self, row: int, col: int, value: str):
839
- self._type = TSTArchives.textCellType
1358
+ self._type = CellType.TEXT
840
1359
  super().__init__(row, col, value)
841
1360
 
842
1361
  @property
@@ -851,8 +1370,8 @@ class RichTextCell(Cell):
851
1370
  """
852
1371
 
853
1372
  def __init__(self, row: int, col: int, value):
854
- self._type = TSTArchives.automaticCellType
855
1373
  super().__init__(row, col, value["text"])
1374
+ self._type = CellType.RICH_TEXT
856
1375
  self._bullets = value["bullets"]
857
1376
  self._hyperlinks = value["hyperlinks"]
858
1377
  if value["bulleted"]:
@@ -915,12 +1434,16 @@ class EmptyCell(Cell):
915
1434
 
916
1435
  def __init__(self, row: int, col: int):
917
1436
  super().__init__(row, col, None)
918
- self._type = None
1437
+ self._type = CellType.EMPTY
919
1438
 
920
1439
  @property
921
1440
  def value(self):
922
1441
  return None
923
1442
 
1443
+ @property
1444
+ def formatted_value(self):
1445
+ return ""
1446
+
924
1447
 
925
1448
  class BoolCell(Cell):
926
1449
  """
@@ -929,9 +1452,9 @@ class BoolCell(Cell):
929
1452
  """
930
1453
 
931
1454
  def __init__(self, row: int, col: int, value: bool):
932
- self._type = TSTArchives.boolCellType
933
- self._value = value
934
1455
  super().__init__(row, col, value)
1456
+ self._type = CellType.BOOL
1457
+ self._value = value
935
1458
 
936
1459
  @property
937
1460
  def value(self) -> bool:
@@ -945,18 +1468,18 @@ class DateCell(Cell):
945
1468
  """
946
1469
 
947
1470
  def __init__(self, row: int, col: int, value: DateTime):
948
- self._type = TSTArchives.dateCellType
949
1471
  super().__init__(row, col, value)
1472
+ self._type = CellType.DATE
950
1473
 
951
1474
  @property
952
- def value(self) -> duration:
1475
+ def value(self) -> datetime:
953
1476
  return self._value
954
1477
 
955
1478
 
956
1479
  class DurationCell(Cell):
957
1480
  def __init__(self, row: int, col: int, value: Duration):
958
- self._type = TSTArchives.durationCellType
959
1481
  super().__init__(row, col, value)
1482
+ self._type = CellType.DURATION
960
1483
 
961
1484
  @property
962
1485
  def value(self) -> duration:
@@ -970,8 +1493,8 @@ class ErrorCell(Cell):
970
1493
  """
971
1494
 
972
1495
  def __init__(self, row: int, col: int):
973
- self._type = TSTArchives.formulaErrorCellType
974
1496
  super().__init__(row, col, None)
1497
+ self._type = CellType.ERROR
975
1498
 
976
1499
  @property
977
1500
  def value(self):
@@ -986,12 +1509,476 @@ class MergedCell(Cell):
986
1509
 
987
1510
  def __init__(self, row: int, col: int):
988
1511
  super().__init__(row, col, None)
1512
+ self._type = CellType.MERGED
989
1513
 
990
1514
  @property
991
1515
  def value(self):
992
1516
  return None
993
1517
 
994
1518
 
1519
+ def _pack_decimal128(value: float) -> bytearray:
1520
+ buffer = bytearray(16)
1521
+ exp = math.floor(math.log10(math.e) * math.log(abs(value))) if value != 0.0 else 0
1522
+ exp += 0x1820 - 16
1523
+ mantissa = abs(int(value / math.pow(10, exp - 0x1820)))
1524
+ buffer[15] |= exp >> 7
1525
+ buffer[14] |= (exp & 0x7F) << 1
1526
+ i = 0
1527
+ while mantissa >= 1:
1528
+ buffer[i] = mantissa & 0xFF
1529
+ i += 1
1530
+ mantissa = int(mantissa / 256)
1531
+ if value < 0:
1532
+ buffer[15] |= 0x80
1533
+ return buffer
1534
+
1535
+
1536
+ def _unpack_decimal128(buffer: bytearray) -> float:
1537
+ exp = (((buffer[15] & 0x7F) << 7) | (buffer[14] >> 1)) - 0x1820
1538
+ mantissa = buffer[14] & 1
1539
+ for i in range(13, -1, -1):
1540
+ mantissa = mantissa * 256 + buffer[i]
1541
+ sign = 1 if buffer[15] & 0x80 else 0
1542
+ if sign == 1:
1543
+ mantissa = -mantissa
1544
+ value = mantissa * 10**exp
1545
+ return float(value)
1546
+
1547
+
1548
+ def _decode_date_format_field(field: str, value: datetime) -> str:
1549
+ if field in DATETIME_FIELD_MAP:
1550
+ s = DATETIME_FIELD_MAP[field]
1551
+ if callable(s):
1552
+ return s(value)
1553
+ else:
1554
+ return value.strftime(s)
1555
+ else:
1556
+ warn(f"Unsupported field code '{field}'", UnsupportedWarning, stacklevel=4)
1557
+ return ""
1558
+
1559
+
1560
+ def _decode_date_format(format, value):
1561
+ """Parse a custom date format string and return a formatted datetime value."""
1562
+ chars = [*format]
1563
+ index = 0
1564
+ in_string = False
1565
+ in_field = False
1566
+ result = ""
1567
+ field = ""
1568
+ while index < len(chars):
1569
+ current_char = chars[index]
1570
+ next_char = chars[index + 1] if index < len(chars) - 1 else None
1571
+ if current_char == "'":
1572
+ if next_char is None:
1573
+ break
1574
+ elif chars[index + 1] == "'":
1575
+ result += "'"
1576
+ index += 2
1577
+ elif in_string:
1578
+ in_string = False
1579
+ index += 1
1580
+ else:
1581
+ in_string = True
1582
+ if in_field:
1583
+ result += _decode_date_format_field(field, value)
1584
+ in_field = False
1585
+ index += 1
1586
+ elif in_string:
1587
+ result += current_char
1588
+ index += 1
1589
+ elif not current_char.isalpha():
1590
+ if in_field:
1591
+ result += _decode_date_format_field(field, value)
1592
+ in_field = False
1593
+ result += current_char
1594
+ index += 1
1595
+ elif in_field:
1596
+ field += current_char
1597
+ index += 1
1598
+ else:
1599
+ in_field = True
1600
+ field = current_char
1601
+ index += 1
1602
+ if in_field:
1603
+ result += _decode_date_format_field(field, value)
1604
+
1605
+ return result
1606
+
1607
+
1608
+ def _decode_text_format(format, value: str):
1609
+ """Parse a custom date format string and return a formatted number value."""
1610
+ custom_format_string = format.custom_format_string
1611
+ return custom_format_string.replace(CUSTOM_TEXT_PLACEHOLDER, value)
1612
+
1613
+
1614
+ def _expand_quotes(value: str) -> str:
1615
+ chars = [*value]
1616
+ index = 0
1617
+ in_string = False
1618
+ formatted_value = ""
1619
+ while index < len(chars):
1620
+ current_char = chars[index]
1621
+ next_char = chars[index + 1] if index < len(chars) - 1 else None
1622
+ if current_char == "'":
1623
+ if next_char is None:
1624
+ break
1625
+ elif chars[index + 1] == "'":
1626
+ formatted_value += "'"
1627
+ index += 2
1628
+ elif in_string:
1629
+ in_string = False
1630
+ index += 1
1631
+ else:
1632
+ in_string = True
1633
+ index += 1
1634
+ else:
1635
+ formatted_value += current_char
1636
+ index += 1
1637
+ return formatted_value
1638
+
1639
+
1640
+ def _decode_number_format(format, value, name): # noqa: PLR0912
1641
+ """Parse a custom date format string and return a formatted number value."""
1642
+ custom_format_string = format.custom_format_string
1643
+ value *= format.scale_factor
1644
+ if "%" in custom_format_string and format.scale_factor == 1.0:
1645
+ # Per cent scale has 100x but % does not
1646
+ value *= 100.0
1647
+
1648
+ if format.currency_code != "":
1649
+ # Replace currency code with symbol and no-break space
1650
+ custom_format_string = custom_format_string.replace(
1651
+ "\u00a4", format.currency_code + "\u00a0"
1652
+ )
1653
+
1654
+ if (match := re.search(r"([#0.,]+(E[+]\d+)?)", custom_format_string)) is None:
1655
+ warn(
1656
+ f"Can't parse format string '{custom_format_string}'; skipping",
1657
+ UnsupportedWarning,
1658
+ stacklevel=1,
1659
+ )
1660
+ return custom_format_string
1661
+ format_spec = match.group(1)
1662
+ scientific_spec = match.group(2)
1663
+
1664
+ if format_spec[0] == ".":
1665
+ (int_part, dec_part) = ("", format_spec[1:])
1666
+ elif "." in custom_format_string:
1667
+ (int_part, dec_part) = format_spec.split(".")
1668
+ else:
1669
+ (int_part, dec_part) = (format_spec, "")
1670
+
1671
+ if scientific_spec is not None:
1672
+ # Scientific notation
1673
+ formatted_value = f"{value:.{len(dec_part) - 4}E}"
1674
+ formatted_value = custom_format_string.replace(format_spec, formatted_value)
1675
+ return _expand_quotes(formatted_value)
1676
+
1677
+ num_decimals = len(dec_part)
1678
+ if num_decimals > 0:
1679
+ if dec_part[0] == "#":
1680
+ dec_pad = None
1681
+ elif format.num_nonspace_decimal_digits > 0:
1682
+ dec_pad = CellPadding.ZERO
1683
+ else:
1684
+ dec_pad = CellPadding.SPACE
1685
+ else:
1686
+ dec_pad = None
1687
+ dec_width = num_decimals
1688
+
1689
+ (integer, decimal) = str(float(value)).split(".")
1690
+ if num_decimals > 0:
1691
+ integer = int(integer)
1692
+ decimal = round(float(f"0.{decimal}"), num_decimals)
1693
+ else:
1694
+ integer = round(value)
1695
+ decimal = float(f"0.{decimal}")
1696
+
1697
+ num_integers = len(int_part.replace(",", ""))
1698
+ if not format.show_thousands_separator:
1699
+ int_part = int_part.replace(",", "")
1700
+ if num_integers > 0:
1701
+ if int_part[0] == "#":
1702
+ int_pad = None
1703
+ int_width = len(int_part)
1704
+ elif format.num_nonspace_integer_digits > 0:
1705
+ int_pad = CellPadding.ZERO
1706
+ if format.show_thousands_separator:
1707
+ num_commas = int(math.floor(math.log10(integer)) / 3) if integer != 0 else 0
1708
+ num_commas = max([num_commas, int((num_integers - 1) / 3)])
1709
+ int_width = num_integers + num_commas
1710
+ else:
1711
+ int_width = num_integers
1712
+ else:
1713
+ int_pad = CellPadding.SPACE
1714
+ int_width = len(int_part)
1715
+ else:
1716
+ int_pad = None
1717
+ int_width = num_integers
1718
+
1719
+ # value_1 = str(value).split(".")[0]
1720
+ # value_2 = sigfig.round(str(value).split(".")[1], sigfig=MAX_SIGNIFICANT_DIGITS, warn=False)
1721
+ # int_pad_space_as_zero = (
1722
+ # num_integers > 0
1723
+ # and num_decimals > 0
1724
+ # and int_pad == CellPadding.SPACE
1725
+ # and dec_pad is None
1726
+ # and num_integers > len(value_1)
1727
+ # and num_decimals > len(value_2)
1728
+ # )
1729
+ int_pad_space_as_zero = False
1730
+
1731
+ # Formatting integer zero:
1732
+ # Blank (padded if needed) if int_pad is SPACE and no decimals
1733
+ # No leading zero if:
1734
+ # int_pad is NONE, dec_pad is SPACE
1735
+ # int_pad is SPACE, dec_pad is SPACE
1736
+ # int_pad is SPACE, dec_pad is ZERO
1737
+ # int_pad is SPACE, dec_pad is NONE if num decimals < decimals length
1738
+ if integer == 0 and int_pad == CellPadding.SPACE and num_decimals == 0:
1739
+ formatted_value = "".rjust(int_width)
1740
+ elif integer == 0 and int_pad is None and dec_pad == CellPadding.SPACE:
1741
+ formatted_value = ""
1742
+ elif integer == 0 and int_pad == CellPadding.SPACE and dec_pad is not None:
1743
+ formatted_value = "".rjust(int_width)
1744
+ elif (
1745
+ integer == 0
1746
+ and int_pad == CellPadding.SPACE
1747
+ and dec_pad is None
1748
+ and len(str(decimal)) > num_decimals
1749
+ ):
1750
+ formatted_value = "".rjust(int_width)
1751
+ elif int_pad_space_as_zero or int_pad == CellPadding.ZERO:
1752
+ if format.show_thousands_separator:
1753
+ formatted_value = f"{integer:0{int_width},}"
1754
+ else:
1755
+ formatted_value = f"{integer:0{int_width}}"
1756
+ elif int_pad == CellPadding.SPACE:
1757
+ if format.show_thousands_separator:
1758
+ formatted_value = f"{integer:,}".rjust(int_width)
1759
+ else:
1760
+ formatted_value = str(integer).rjust(int_width)
1761
+ elif format.show_thousands_separator:
1762
+ formatted_value = f"{integer:,}"
1763
+ else:
1764
+ formatted_value = str(integer)
1765
+
1766
+ if num_decimals:
1767
+ if dec_pad == CellPadding.ZERO or (dec_pad == CellPadding.SPACE and num_integers == 0):
1768
+ formatted_value += "." + f"{decimal:,.{dec_width}f}"[2:]
1769
+ elif dec_pad == CellPadding.SPACE and decimal == 0 and num_integers > 0:
1770
+ formatted_value += ".".ljust(dec_width + 1)
1771
+ elif dec_pad == CellPadding.SPACE:
1772
+ decimal_str = str(decimal)[2:]
1773
+ formatted_value += "." + decimal_str.ljust(dec_width)
1774
+ elif decimal or num_integers == 0:
1775
+ formatted_value += "." + str(decimal)[2:]
1776
+
1777
+ formatted_value = custom_format_string.replace(format_spec, formatted_value)
1778
+ return _expand_quotes(formatted_value)
1779
+
1780
+
1781
+ def _format_decimal(value: float, format, percent: bool = False) -> str:
1782
+ if value is None:
1783
+ return ""
1784
+ if value < 0 and format.negative_style == 1:
1785
+ accounting_style = False
1786
+ value = -value
1787
+ elif value < 0 and format.negative_style >= 2:
1788
+ accounting_style = True
1789
+ value = -value
1790
+ else:
1791
+ accounting_style = False
1792
+ thousands = "," if format.show_thousands_separator else ""
1793
+
1794
+ if value.is_integer() and format.decimal_places >= DECIMAL_PLACES_AUTO:
1795
+ formatted_value = f"{int(value):{thousands}}"
1796
+ else:
1797
+ if format.decimal_places >= DECIMAL_PLACES_AUTO:
1798
+ formatted_value = str(sigfig.round(value, MAX_SIGNIFICANT_DIGITS, warn=False))
1799
+ else:
1800
+ formatted_value = sigfig.round(value, MAX_SIGNIFICANT_DIGITS, type=str, warn=False)
1801
+ formatted_value = sigfig.round(
1802
+ formatted_value, decimals=format.decimal_places, type=str
1803
+ )
1804
+ if format.show_thousands_separator:
1805
+ formatted_value = sigfig.round(formatted_value, spacer=",", spacing=3, type=str)
1806
+ try:
1807
+ (integer, decimal) = formatted_value.split(".")
1808
+ formatted_value = integer + "." + decimal.replace(",", "")
1809
+ except ValueError:
1810
+ pass
1811
+
1812
+ if percent:
1813
+ formatted_value += "%"
1814
+
1815
+ if accounting_style:
1816
+ return f"({formatted_value})"
1817
+ else:
1818
+ return formatted_value
1819
+
1820
+
1821
+ def _format_currency(value: float, format) -> str:
1822
+ formatted_value = _format_decimal(value, format)
1823
+ if format.currency_code in CURRENCY_SYMBOLS:
1824
+ symbol = CURRENCY_SYMBOLS[format.currency_code]
1825
+ else:
1826
+ symbol = format.currency_code + " "
1827
+ if format.use_accounting_style and value < 0:
1828
+ return f"{symbol}\t({formatted_value[1:]})"
1829
+ elif format.use_accounting_style:
1830
+ return f"{symbol}\t{formatted_value}"
1831
+ else:
1832
+ return symbol + formatted_value
1833
+
1834
+
1835
+ INT_TO_BASE_CHAR = [str(x) for x in range(0, 10)] + [chr(x) for x in range(ord("A"), ord("Z") + 1)]
1836
+
1837
+
1838
+ def _invert_bit_str(value: str) -> str:
1839
+ """Invert a binary value"""
1840
+ return "".join(["0" if b == "1" else "1" for b in value])
1841
+
1842
+
1843
+ def _twos_complement(value: int, base: int) -> str:
1844
+ """Calculate the twos complement of a negative integer with minimum 32-bit precision"""
1845
+ num_bits = max([32, math.ceil(math.log2(abs(value))) + 1])
1846
+ bin_value = bin(abs(value))[2:]
1847
+ inverted_bin_value = _invert_bit_str(bin_value).rjust(num_bits, "1")
1848
+ twos_complement_dec = int(inverted_bin_value, 2) + 1
1849
+
1850
+ if base == 2:
1851
+ return bin(twos_complement_dec)[2:].rjust(num_bits, "1")
1852
+ elif base == 8:
1853
+ return oct(twos_complement_dec)[2:]
1854
+ else:
1855
+ return hex(twos_complement_dec)[2:].upper()
1856
+
1857
+
1858
+ def _format_base(value: float, format) -> str:
1859
+ if value == 0:
1860
+ return "0".zfill(format.base_places)
1861
+
1862
+ value = round(value)
1863
+
1864
+ is_negative = False
1865
+ if not format.base_use_minus_sign and format.base in [2, 8, 16]:
1866
+ if value < 0:
1867
+ return _twos_complement(value, format.base)
1868
+ else:
1869
+ value = abs(value)
1870
+ elif value < 0:
1871
+ is_negative = True
1872
+ value = abs(value)
1873
+
1874
+ formatted_value = []
1875
+ while value:
1876
+ formatted_value.append(int(value % format.base))
1877
+ value //= format.base
1878
+ formatted_value = "".join([INT_TO_BASE_CHAR[x] for x in formatted_value[::-1]])
1879
+
1880
+ if is_negative:
1881
+ return "-" + formatted_value.zfill(format.base_places)
1882
+ else:
1883
+ return formatted_value.zfill(format.base_places)
1884
+
1885
+
1886
+ def _format_fraction_parts_to(whole: int, numerator: int, denominator: int):
1887
+ if whole > 0:
1888
+ if numerator == 0:
1889
+ return str(whole)
1890
+ else:
1891
+ return f"{whole} {numerator}/{denominator}"
1892
+ elif numerator == 0:
1893
+ return "0"
1894
+ elif numerator == denominator:
1895
+ return "1"
1896
+ return f"{numerator}/{denominator}"
1897
+
1898
+
1899
+ def _float_to_fraction(value: float, denominator: int) -> str:
1900
+ """Convert a float to the nearest fraction and return as a string."""
1901
+ whole = int(value)
1902
+ numerator = round(denominator * (value - whole))
1903
+ return _format_fraction_parts_to(whole, numerator, denominator)
1904
+
1905
+
1906
+ def _float_to_n_digit_fraction(value: float, max_digits: int) -> str:
1907
+ """Convert a float to a fraction of a maxinum number of digits
1908
+ and return as a string.
1909
+ """
1910
+ max_denominator = 10**max_digits - 1
1911
+ (numerator, denominator) = (
1912
+ Fraction.from_float(value).limit_denominator(max_denominator).as_integer_ratio()
1913
+ )
1914
+ whole = int(value)
1915
+ numerator -= whole * denominator
1916
+ return _format_fraction_parts_to(whole, numerator, denominator)
1917
+
1918
+
1919
+ def _format_fraction(value: float, format) -> str:
1920
+ accuracy = format.fraction_accuracy
1921
+ if accuracy & 0xFF000000:
1922
+ num_digits = 0x100000000 - accuracy
1923
+ return _float_to_n_digit_fraction(value, num_digits)
1924
+ else:
1925
+ return _float_to_fraction(value, accuracy)
1926
+
1927
+
1928
+ def _format_scientific(value: float, format) -> str:
1929
+ formatted_value = sigfig.round(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
1930
+ return f"{formatted_value:.{format.decimal_places}E}"
1931
+
1932
+
1933
+ def _unit_format(unit: str, value: int, style: int, abbrev: str = None):
1934
+ plural = "" if value == 1 else "s"
1935
+ if abbrev is None:
1936
+ abbrev = unit[0]
1937
+ if style == DurationStyle.COMPACT:
1938
+ return ""
1939
+ elif style == DurationStyle.SHORT:
1940
+ return f"{abbrev}"
1941
+ else:
1942
+ return f" {unit}" + plural
1943
+
1944
+
1945
+ def _auto_units(cell_value, format):
1946
+ unit_largest = format.duration_unit_largest
1947
+ unit_smallest = format.duration_unit_smallest
1948
+
1949
+ if cell_value == 0:
1950
+ unit_largest = DurationUnits.DAY
1951
+ unit_smallest = DurationUnits.DAY
1952
+ else:
1953
+ if cell_value >= SECONDS_IN_WEEK:
1954
+ unit_largest = DurationUnits.WEEK
1955
+ elif cell_value >= SECONDS_IN_DAY:
1956
+ unit_largest = DurationUnits.DAY
1957
+ elif cell_value >= SECONDS_IN_HOUR:
1958
+ unit_largest = DurationUnits.HOUR
1959
+ elif cell_value >= 60:
1960
+ unit_largest = DurationUnits.MINUTE
1961
+ elif cell_value >= 1:
1962
+ unit_largest = DurationUnits.SECOND
1963
+ else:
1964
+ unit_largest = DurationUnits.MILLISECOND
1965
+
1966
+ if math.floor(cell_value) != cell_value:
1967
+ unit_smallest = DurationUnits.MILLISECOND
1968
+ elif cell_value % 60:
1969
+ unit_smallest = DurationUnits.SECOND
1970
+ elif cell_value % SECONDS_IN_HOUR:
1971
+ unit_smallest = DurationUnits.MINUTE
1972
+ elif cell_value % SECONDS_IN_DAY:
1973
+ unit_smallest = DurationUnits.HOUR
1974
+ elif cell_value % SECONDS_IN_WEEK:
1975
+ unit_smallest = DurationUnits.DAY
1976
+ if unit_smallest < unit_largest:
1977
+ unit_smallest = unit_largest
1978
+
1979
+ return unit_smallest, unit_largest
1980
+
1981
+
995
1982
  # Cell reference conversion from https://github.com/jmcnamara/XlsxWriter
996
1983
  # Copyright (c) 2013-2021, John McNamara <jmcnamara@cpan.org>
997
1984
  range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")