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/cell.py CHANGED
@@ -13,17 +13,28 @@ from pendulum import instance as pendulum_instance
13
13
 
14
14
  from numbers_parser.cell_storage import CellStorage, CellType
15
15
  from numbers_parser.constants import (
16
+ DATETIME_FIELD_MAP,
17
+ DECIMAL_PLACES_AUTO,
16
18
  DEFAULT_ALIGNMENT,
17
19
  DEFAULT_BORDER_COLOR,
18
20
  DEFAULT_BORDER_STYLE,
19
21
  DEFAULT_BORDER_WIDTH,
22
+ DEFAULT_DATETIME_FORMAT,
20
23
  DEFAULT_FONT,
21
24
  DEFAULT_FONT_SIZE,
22
25
  DEFAULT_TEXT_INSET,
23
26
  DEFAULT_TEXT_WRAP,
24
27
  EMPTY_STORAGE_BUFFER,
28
+ MAX_BASE,
25
29
  MAX_SIGNIFICANT_DIGITS,
30
+ CustomFormattingType,
31
+ FormattingType,
32
+ FormatType,
33
+ FractionAccuracy,
34
+ NegativeNumberStyle,
35
+ PaddingType,
26
36
  )
37
+ from numbers_parser.currencies import CURRENCIES
27
38
  from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
28
39
  from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
29
40
  from numbers_parser.generated.TSWPArchives_pb2 import (
@@ -40,10 +51,12 @@ __all__ = [
40
51
  "BulletedTextCell",
41
52
  "Cell",
42
53
  "CellBorder",
54
+ "CustomFormatting",
43
55
  "DateCell",
44
56
  "DurationCell",
45
57
  "EmptyCell",
46
58
  "ErrorCell",
59
+ "Formatting",
47
60
  "HorizontalJustification",
48
61
  "MergeAnchor",
49
62
  "MergeReference",
@@ -132,8 +145,49 @@ RGB = namedtuple("RGB", ["r", "g", "b"])
132
145
 
133
146
  @dataclass
134
147
  class Style:
135
- alignment: Alignment = DEFAULT_ALIGNMENT_CLASS
136
- bg_image: object = None
148
+ """
149
+ A named document style that can be applied to cells.
150
+
151
+ Parameters
152
+ ----------
153
+ alignment: Alignment, optional, default: Alignment("auto", "top")
154
+ Horizontal and vertical alignment of the cell
155
+ bg_color: RGB | List[RGB], optional, default: RGB(0, 0, 0)
156
+ Background color or list of colors for gradients
157
+ bold: bool, optional, default: False
158
+ ``True`` if the cell font is bold
159
+ font_color: RGB, optional, default: RGB(0, 0, 0)) – Font color
160
+ font_size: float, optional, default: DEFAULT_FONT_SIZE
161
+ Font size in points
162
+ font_name: str, optional, default: DEFAULT_FONT_SIZE
163
+ Font name
164
+ italic: bool, optional, default: False
165
+ ``True`` if the cell font is italic
166
+ name: str, optional
167
+ Style name
168
+ underline: bool, optional, default: False) – True if the
169
+ cell font is underline
170
+ strikethrough: bool, optional, default: False) – True if
171
+ the cell font is strikethrough
172
+ first_indent: float, optional, default: 0.0) – First line
173
+ indent in points
174
+ left_indent: float, optional, default: 0.0
175
+ Left indent in points
176
+ right_indent: float, optional, default: 0.0
177
+ Right indent in points
178
+ text_inset: float, optional, default: DEFAULT_TEXT_INSET
179
+ Text inset in points
180
+ text_wrap: str, optional, default: True
181
+ ``True`` if text wrapping is enabled
182
+
183
+ Raises
184
+ ------
185
+ TypeError:
186
+ If arguments do not match the specified type or for objects have invalid arguments
187
+ """
188
+
189
+ alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
190
+ bg_image: object = None # : backgroung image
137
191
  bg_color: Union[RGB, List[RGB]] = None
138
192
  font_color: RGB = RGB(0, 0, 0)
139
193
  font_size: float = DEFAULT_FONT_SIZE
@@ -409,48 +463,53 @@ class MergeAnchor:
409
463
 
410
464
 
411
465
  class Cell(Cacheable):
466
+ """
467
+ .. NOTE::
468
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
469
+ """
470
+
412
471
  @classmethod
413
- def empty_cell(cls, table_id: int, row_num: int, col_num: int, model: object):
414
- cell = EmptyCell(row_num, col_num)
472
+ def empty_cell(cls, table_id: int, row: int, col: int, model: object):
473
+ cell = EmptyCell(row, col)
415
474
  cell._model = model
416
475
  cell._table_id = table_id
417
476
  merge_cells = model.merge_cells(table_id)
418
- cell._set_merge(merge_cells.get((row_num, col_num)))
477
+ cell._set_merge(merge_cells.get((row, col)))
419
478
 
420
479
  return cell
421
480
 
422
481
  @classmethod
423
- def merged_cell(cls, table_id: int, row_num: int, col_num: int, model: object):
424
- cell = MergedCell(row_num, col_num)
482
+ def merged_cell(cls, table_id: int, row: int, col: int, model: object):
483
+ cell = MergedCell(row, col)
425
484
  cell._model = model
426
485
  cell._table_id = table_id
427
486
  merge_cells = model.merge_cells(table_id)
428
- cell._set_merge(merge_cells.get((row_num, col_num)))
487
+ cell._set_merge(merge_cells.get((row, col)))
429
488
  return cell
430
489
 
431
490
  @classmethod
432
491
  def from_storage(cls, cell_storage: CellStorage):
433
492
  if cell_storage.type == CellType.EMPTY:
434
- cell = EmptyCell(cell_storage.row_num, cell_storage.col_num)
493
+ cell = EmptyCell(cell_storage.row, cell_storage.col)
435
494
  elif cell_storage.type == CellType.NUMBER:
436
- cell = NumberCell(cell_storage.row_num, cell_storage.col_num, cell_storage.value)
495
+ cell = NumberCell(cell_storage.row, cell_storage.col, cell_storage.value)
437
496
  elif cell_storage.type == CellType.TEXT:
438
- cell = TextCell(cell_storage.row_num, cell_storage.col_num, cell_storage.value)
497
+ cell = TextCell(cell_storage.row, cell_storage.col, cell_storage.value)
439
498
  elif cell_storage.type == CellType.DATE:
440
- cell = DateCell(cell_storage.row_num, cell_storage.col_num, cell_storage.value)
499
+ cell = DateCell(cell_storage.row, cell_storage.col, cell_storage.value)
441
500
  elif cell_storage.type == CellType.BOOL:
442
- cell = BoolCell(cell_storage.row_num, cell_storage.col_num, cell_storage.value)
501
+ cell = BoolCell(cell_storage.row, cell_storage.col, cell_storage.value)
443
502
  elif cell_storage.type == CellType.DURATION:
444
503
  value = duration(seconds=cell_storage.value)
445
- cell = DurationCell(cell_storage.row_num, cell_storage.col_num, value)
504
+ cell = DurationCell(cell_storage.row, cell_storage.col, value)
446
505
  elif cell_storage.type == CellType.ERROR:
447
- cell = ErrorCell(cell_storage.row_num, cell_storage.col_num)
506
+ cell = ErrorCell(cell_storage.row, cell_storage.col)
448
507
  elif cell_storage.type == CellType.RICH_TEXT:
449
- cell = RichTextCell(cell_storage.row_num, cell_storage.col_num, cell_storage.value)
508
+ cell = RichTextCell(cell_storage.row, cell_storage.col, cell_storage.value)
450
509
  else:
451
510
  raise UnsupportedError(
452
511
  f"Unsupported cell type {cell_storage.type} "
453
- + f"@:({cell_storage.row_num},{cell_storage.col_num})"
512
+ + f"@:({cell_storage.row},{cell_storage.col})"
454
513
  )
455
514
 
456
515
  cell._table_id = cell_storage.table_id
@@ -458,18 +517,18 @@ class Cell(Cacheable):
458
517
  cell._storage = cell_storage
459
518
  cell._formula_key = cell_storage.formula_id
460
519
  merge_cells = cell_storage.model.merge_cells(cell_storage.table_id)
461
- cell._set_merge(merge_cells.get((cell_storage.row_num, cell_storage.col_num)))
520
+ cell._set_merge(merge_cells.get((cell_storage.row, cell_storage.col)))
462
521
  return cell
463
522
 
464
523
  @classmethod
465
- def from_value(cls, row_num: int, col_num: int, value):
524
+ def from_value(cls, row: int, col: int, value):
466
525
  # TODO: write needs to retain/init the border
467
526
  if isinstance(value, str):
468
- return TextCell(row_num, col_num, value)
527
+ return TextCell(row, col, value)
469
528
  elif isinstance(value, bool):
470
- return BoolCell(row_num, col_num, value)
529
+ return BoolCell(row, col, value)
471
530
  elif isinstance(value, int):
472
- return NumberCell(row_num, col_num, value)
531
+ return NumberCell(row, col, value)
473
532
  elif isinstance(value, float):
474
533
  rounded_value = sigfig.round(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
475
534
  if rounded_value != value:
@@ -478,22 +537,22 @@ class Cell(Cacheable):
478
537
  RuntimeWarning,
479
538
  stacklevel=2,
480
539
  )
481
- return NumberCell(row_num, col_num, rounded_value)
540
+ return NumberCell(row, col, rounded_value)
482
541
  elif isinstance(value, (DateTime, builtin_datetime)):
483
- return DateCell(row_num, col_num, pendulum_instance(value))
542
+ return DateCell(row, col, pendulum_instance(value))
484
543
  elif isinstance(value, (Duration, builtin_timedelta)):
485
- return DurationCell(row_num, col_num, value)
544
+ return DurationCell(row, col, value)
486
545
  else:
487
546
  raise ValueError("Can't determine cell type from type " + type(value).__name__)
488
547
 
489
- def set_formatting(self, formatting: dict):
490
- raise TypeError(f"Cannot set formatting for cells of type {type(self).__name__}")
548
+ def _set_formatting(self, format_id: int, format_type: FormattingType) -> None:
549
+ self._storage._set_formatting(format_id, format_type)
491
550
 
492
- def __init__(self, row_num: int, col_num: int, value):
551
+ def __init__(self, row: int, col: int, value):
493
552
  self._value = value
494
- self.row = row_num
495
- self.col = col_num
496
- self.is_bulleted = False
553
+ self.row = row
554
+ self.col = col
555
+ self._is_bulleted = False
497
556
  self._formula_key = None
498
557
  self._storage = None
499
558
  self._style = None
@@ -553,13 +612,26 @@ class Cell(Cacheable):
553
612
  return None
554
613
 
555
614
  @property
556
- def is_formula(self):
615
+ def is_formula(self) -> bool:
616
+ """bool: ``True`` if the cell contains a formula."""
557
617
  table_formulas = self._model.table_formulas(self._table_id)
558
618
  return table_formulas.is_formula(self.row, self.col)
559
619
 
560
620
  @property
561
621
  @cache(num_args=0)
562
- def formula(self):
622
+ def formula(self) -> str:
623
+ """
624
+ str: The formula in a cell.
625
+
626
+ Formula evaluation relies on Numbers storing current values which should
627
+ usually be the case. In cells containing a formula, :py:meth:`numbers_parser.Cell.value`
628
+ returns computed value of the formula.
629
+
630
+ Returns:
631
+ str:
632
+ The text of the foruma in a cell, or `None` if there is no formula
633
+ present in a cell.
634
+ """
563
635
  if self._formula_key is not None:
564
636
  table_formulas = self._model.table_formulas(self._table_id)
565
637
  return table_formulas.formula(self._formula_key, self.row, self.col)
@@ -567,18 +639,56 @@ class Cell(Cacheable):
567
639
  return None
568
640
 
569
641
  @property
570
- def bullets(self) -> str:
642
+ def is_bulleted(self) -> bool:
643
+ """bool: ``True`` if the cell contains text bullets."""
644
+ return self._is_bulleted
645
+
646
+ @property
647
+ def bullets(self) -> Union[List[str], None]:
648
+ r"""
649
+ List[str] | None: The bullets in a cell, or ``None``
650
+
651
+ Cells that contain bulleted or numbered lists are identified
652
+ by :py:attr:`numbers_parser.Cell.is_bulleted`. For these cells,
653
+ :py:attr:`numbers_parser.Cell.value` returns the whole cell contents.
654
+ Bullets can also be extracted into a list of paragraphs cell without the
655
+ bullet or numbering character. Newlines are not included in the
656
+ bullet list.
657
+
658
+ Example
659
+ -------
660
+
661
+ .. code-block:: python
662
+
663
+ doc = Document("bullets.numbers")
664
+ sheets = doc.sheets
665
+ tables = sheets[0].tables
666
+ table = tables[0]
667
+ if not table.cell(0, 1).is_bulleted:
668
+ print(table.cell(0, 1).value)
669
+ else:
670
+ bullets = ["* " + s for s in table.cell(0, 1).bullets]
671
+ print("\n".join(bullets))
672
+ return None
673
+ """
571
674
  return None
572
675
 
573
676
  @property
574
- def formatted_value(self):
677
+ def formatted_value(self) -> str:
678
+ """str: The formatted value of the cell as it appears in Numbers."""
575
679
  if self._storage is None:
576
680
  return ""
577
681
  else:
578
682
  return self._storage.formatted
579
683
 
580
684
  @property
581
- def style(self):
685
+ def style(self) -> Union[Style, None]:
686
+ """Style | None: The :class:`Style` associated with the cell or ``None``.
687
+
688
+ Warns:
689
+ UnsupportedWarning: On assignment; use
690
+ :py:meth:`numbers_parser.Table.set_cell_style` instead.
691
+ """
582
692
  if self._storage is None:
583
693
  self._storage = CellStorage(
584
694
  self._model, self._table_id, EMPTY_STORAGE_BUFFER, self.row, self.col
@@ -596,7 +706,13 @@ class Cell(Cacheable):
596
706
  )
597
707
 
598
708
  @property
599
- def border(self):
709
+ def border(self) -> Union[CellBorder, None]:
710
+ """CellBorder| None: The :class:`CellBorder` associated with the cell or ``None``.
711
+
712
+ Warns:
713
+ UnsupportedWarning: On assignment; use
714
+ :py:meth:`numbers_parser.Table.set_cell_border` instead.
715
+ """
600
716
  self._model.extract_strokes(self._table_id)
601
717
  return self._border
602
718
 
@@ -608,24 +724,29 @@ class Cell(Cacheable):
608
724
  stacklevel=2,
609
725
  )
610
726
 
727
+ def update_storage(self, storage: CellStorage) -> None:
728
+ self._storage = storage
729
+
611
730
 
612
731
  class NumberCell(Cell):
613
- def __init__(self, row_num: int, col_num: int, value: float):
732
+ """
733
+ .. NOTE::
734
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
735
+ """
736
+
737
+ def __init__(self, row: int, col: int, value: float):
614
738
  self._type = TSTArchives.numberCellType
615
- super().__init__(row_num, col_num, value)
739
+ super().__init__(row, col, value)
616
740
 
617
741
  @property
618
742
  def value(self) -> int:
619
743
  return self._value
620
744
 
621
- def set_formatting(self, formatting: dict):
622
- self._storage.set_number_formatting(formatting)
623
-
624
745
 
625
746
  class TextCell(Cell):
626
- def __init__(self, row_num: int, col_num: int, value: str):
747
+ def __init__(self, row: int, col: int, value: str):
627
748
  self._type = TSTArchives.textCellType
628
- super().__init__(row_num, col_num, value)
749
+ super().__init__(row, col, value)
629
750
 
630
751
  @property
631
752
  def value(self) -> str:
@@ -633,34 +754,60 @@ class TextCell(Cell):
633
754
 
634
755
 
635
756
  class RichTextCell(Cell):
636
- def __init__(self, row_num: int, col_num: int, value):
757
+ """
758
+ .. NOTE::
759
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
760
+ """
761
+
762
+ def __init__(self, row: int, col: int, value):
637
763
  self._type = TSTArchives.automaticCellType
638
- super().__init__(row_num, col_num, value["text"])
764
+ super().__init__(row, col, value["text"])
639
765
  self._bullets = value["bullets"]
640
766
  self._hyperlinks = value["hyperlinks"]
641
767
  if value["bulleted"]:
642
768
  self._formatted_bullets = [
643
- value["bullet_chars"][i] + " " + value["bullets"][i]
644
- if value["bullet_chars"][i] is not None
645
- else value["bullets"][i]
769
+ (
770
+ value["bullet_chars"][i] + " " + value["bullets"][i]
771
+ if value["bullet_chars"][i] is not None
772
+ else value["bullets"][i]
773
+ )
646
774
  for i in range(len(self._bullets))
647
775
  ]
648
- self.is_bulleted = True
776
+ self._is_bulleted = True
649
777
 
650
778
  @property
651
779
  def value(self) -> str:
652
780
  return self._value
653
781
 
654
782
  @property
655
- def bullets(self) -> str:
783
+ def bullets(self) -> List[str]:
784
+ """List[str]: A list of the text bullets in the cell."""
656
785
  return self._bullets
657
786
 
658
787
  @property
659
788
  def formatted_bullets(self) -> str:
789
+ """str: The bullets as a formatted multi-line string."""
660
790
  return self._formatted_bullets
661
791
 
662
792
  @property
663
- def hyperlinks(self) -> List[Tuple]:
793
+ def hyperlinks(self) -> Union[List[Tuple], None]:
794
+ """
795
+ List[Tuple] | None: the hyperlinks in a cell or ``None``
796
+
797
+ Numbers does not support hyperlinks to cells within a spreadsheet, but does
798
+ allow embedding links in cells. When cells contain hyperlinks,
799
+ `numbers_parser` returns the text version of the cell. The `hyperlinks` property
800
+ of cells where :py:attr:`numbers_parser.Cell.is_bulleted` is ``True`` is a
801
+ list of text and URL tuples.
802
+
803
+ Example
804
+ -------
805
+
806
+ .. code-block:: python
807
+
808
+ cell = table.cell(0, 0)
809
+ (text, url) = cell.hyperlinks[0]
810
+ """
664
811
  return self._hyperlinks
665
812
 
666
813
 
@@ -670,8 +817,13 @@ class BulletedTextCell(RichTextCell):
670
817
 
671
818
 
672
819
  class EmptyCell(Cell):
673
- def __init__(self, row_num: int, col_num: int):
674
- super().__init__(row_num, col_num, None)
820
+ """
821
+ .. NOTE::
822
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
823
+ """
824
+
825
+ def __init__(self, row: int, col: int):
826
+ super().__init__(row, col, None)
675
827
  self._type = None
676
828
 
677
829
  @property
@@ -680,10 +832,15 @@ class EmptyCell(Cell):
680
832
 
681
833
 
682
834
  class BoolCell(Cell):
683
- def __init__(self, row_num: int, col_num: int, value: bool):
835
+ """
836
+ .. NOTE::
837
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
838
+ """
839
+
840
+ def __init__(self, row: int, col: int, value: bool):
684
841
  self._type = TSTArchives.boolCellType
685
842
  self._value = value
686
- super().__init__(row_num, col_num, value)
843
+ super().__init__(row, col, value)
687
844
 
688
845
  @property
689
846
  def value(self) -> bool:
@@ -691,24 +848,24 @@ class BoolCell(Cell):
691
848
 
692
849
 
693
850
  class DateCell(Cell):
694
- def __init__(self, row_num: int, col_num: int, value: DateTime):
851
+ """
852
+ .. NOTE::
853
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
854
+ """
855
+
856
+ def __init__(self, row: int, col: int, value: DateTime):
695
857
  self._type = TSTArchives.dateCellType
696
- super().__init__(row_num, col_num, value)
858
+ super().__init__(row, col, value)
697
859
 
698
860
  @property
699
861
  def value(self) -> duration:
700
862
  return self._value
701
863
 
702
- def set_formatting(self, formatting: dict):
703
- if "date_time_format" not in formatting:
704
- raise TypeError("No date_time_format specified for DateCell formatting")
705
- self._storage.set_date_time_formatting(formatting["date_time_format"])
706
-
707
864
 
708
865
  class DurationCell(Cell):
709
- def __init__(self, row_num: int, col_num: int, value: Duration):
866
+ def __init__(self, row: int, col: int, value: Duration):
710
867
  self._type = TSTArchives.durationCellType
711
- super().__init__(row_num, col_num, value)
868
+ super().__init__(row, col, value)
712
869
 
713
870
  @property
714
871
  def value(self) -> duration:
@@ -716,9 +873,14 @@ class DurationCell(Cell):
716
873
 
717
874
 
718
875
  class ErrorCell(Cell):
719
- def __init__(self, row_num: int, col_num: int):
876
+ """
877
+ .. NOTE::
878
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
879
+ """
880
+
881
+ def __init__(self, row: int, col: int):
720
882
  self._type = TSTArchives.formulaErrorCellType
721
- super().__init__(row_num, col_num, None)
883
+ super().__init__(row, col, None)
722
884
 
723
885
  @property
724
886
  def value(self):
@@ -726,8 +888,13 @@ class ErrorCell(Cell):
726
888
 
727
889
 
728
890
  class MergedCell(Cell):
729
- def __init__(self, row_num: int, col_num: int):
730
- super().__init__(row_num, col_num, None)
891
+ """
892
+ .. NOTE::
893
+ Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
894
+ """
895
+
896
+ def __init__(self, row: int, col: int):
897
+ super().__init__(row, col, None)
731
898
 
732
899
  @property
733
900
  def value(self):
@@ -740,11 +907,18 @@ range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
740
907
 
741
908
 
742
909
  def xl_cell_to_rowcol(cell_str: str) -> tuple:
743
- """Convert a cell reference in A1 notation to a zero indexed row and column.
744
- Args:
745
- cell_str: A1 style string.
746
- Returns:
747
- row, col: Zero indexed cell row and column indices.
910
+ """
911
+ Convert a cell reference in A1 notation to a zero indexed row and column.
912
+
913
+ Parameters
914
+ ----------
915
+ cell_str: str
916
+ A1 notation cell reference
917
+
918
+ Returns
919
+ -------
920
+ row, col: int, int
921
+ Cell row and column numbers (zero indexed).
748
922
  """
749
923
  if not cell_str:
750
924
  return 0, 0
@@ -771,13 +945,23 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
771
945
 
772
946
 
773
947
  def xl_range(first_row, first_col, last_row, last_col):
774
- """Convert zero indexed row and col cell references to a A1:B1 range string.
775
- Args:
776
- first_row: The first cell row. Int.
777
- first_col: The first cell column. Int.
778
- last_row: The last cell row. Int.
779
- last_col: The last cell column. Int.
780
- Returns:
948
+ """
949
+ Convert zero indexed row and col cell references to a A1:B1 range string.
950
+
951
+ Parameters
952
+ ----------
953
+ first_row: int
954
+ The first cell row.
955
+ first_col: int
956
+ The first cell column.
957
+ last_row: int
958
+ The last cell row.
959
+ last_col: int
960
+ The last cell column.
961
+
962
+ Returns
963
+ -------
964
+ str:
781
965
  A1:B1 style range string.
782
966
  """
783
967
  range1 = xl_rowcol_to_cell(first_row, first_col)
@@ -790,13 +974,23 @@ def xl_range(first_row, first_col, last_row, last_col):
790
974
 
791
975
 
792
976
  def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
793
- """Convert a zero indexed row and column cell reference to a A1 style string.
794
- Args:
795
- row: The cell row. Int.
796
- col: The cell column. Int.
797
- row_abs: Optional flag to make the row absolute. Bool.
798
- col_abs: Optional flag to make the column absolute. Bool.
799
- Returns:
977
+ """
978
+ Convert a zero indexed row and column cell reference to a A1 style string.
979
+
980
+ Parameters
981
+ ----------
982
+ row: int
983
+ The cell row.
984
+ col: int
985
+ The cell column.
986
+ row_abs: bool
987
+ If ``True``, make the row absolute.
988
+ col_abs: bool
989
+ If ``True``, make the column absolute.
990
+
991
+ Returns
992
+ -------
993
+ str:
800
994
  A1 style string.
801
995
  """
802
996
  if row < 0:
@@ -814,24 +1008,29 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
814
1008
 
815
1009
 
816
1010
  def xl_col_to_name(col, col_abs=False):
817
- """Convert a zero indexed column cell reference to a string.
818
- Args:
819
- col: The cell column. Int.
820
- col_abs: Optional flag to make the column absolute. Bool.
1011
+ """
1012
+ Convert a zero indexed column cell reference to a string.
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ col: int
1017
+ The column number (zero indexed).
1018
+ col_abs: bool, default: False
1019
+ If ``True``, make the column absolute.
821
1020
  Returns:
822
- Column style string.
1021
+ str:
1022
+ Column in A1 notation.
823
1023
  """
824
- col_num = col
825
- if col_num < 0:
826
- raise IndexError(f"column reference {col_num} below zero")
1024
+ if col < 0:
1025
+ raise IndexError(f"column reference {col} below zero")
827
1026
 
828
- col_num += 1 # Change to 1-index.
1027
+ col += 1 # Change to 1-index.
829
1028
  col_str = ""
830
1029
  col_abs = "$" if col_abs else ""
831
1030
 
832
- while col_num:
1031
+ while col:
833
1032
  # Set remainder from 1 .. 26
834
- remainder = col_num % 26
1033
+ remainder = col % 26
835
1034
 
836
1035
  if remainder == 0:
837
1036
  remainder = 26
@@ -843,6 +1042,92 @@ def xl_col_to_name(col, col_abs=False):
843
1042
  col_str = col_letter + col_str
844
1043
 
845
1044
  # Get the next order of magnitude.
846
- col_num = int((col_num - 1) / 26)
1045
+ col = int((col - 1) / 26)
847
1046
 
848
1047
  return col_abs + col_str
1048
+
1049
+
1050
+ @dataclass()
1051
+ class Formatting:
1052
+ type: FormattingType = FormattingType.NUMBER
1053
+ base_places: int = 0
1054
+ base_use_minus_sign: bool = True
1055
+ base: int = 10
1056
+ currency_code: str = "GBP"
1057
+ date_time_format: str = DEFAULT_DATETIME_FORMAT
1058
+ decimal_places: int = None
1059
+ fraction_accuracy: FractionAccuracy = FractionAccuracy.THREE
1060
+ negative_style: NegativeNumberStyle = NegativeNumberStyle.MINUS
1061
+ show_thousands_separator: bool = False
1062
+ use_accounting_style: bool = False
1063
+ _format_id = None
1064
+
1065
+ def __post_init__(self):
1066
+ if not isinstance(self.type, FormattingType):
1067
+ type_name = type(self.type).__name__
1068
+ raise TypeError(f"Invalid format type '{type_name}'")
1069
+
1070
+ if self.use_accounting_style and self.negative_style != NegativeNumberStyle.MINUS:
1071
+ warn(
1072
+ "use_accounting_style overriding negative_style",
1073
+ RuntimeWarning,
1074
+ stacklevel=4,
1075
+ )
1076
+
1077
+ if self.type == FormattingType.DATETIME:
1078
+ formats = re.sub(r"[^a-zA-Z\s]", " ", self.date_time_format).split()
1079
+ for el in formats:
1080
+ if el not in DATETIME_FIELD_MAP.keys():
1081
+ raise TypeError(f"Invalid format specifier '{el}' in date/time format")
1082
+
1083
+ if self.type == FormattingType.CURRENCY:
1084
+ if self.currency_code not in CURRENCIES:
1085
+ raise TypeError(f"Unsupported currency code '{self.currency_code}'")
1086
+
1087
+ if self.decimal_places is None:
1088
+ if self.type == FormattingType.CURRENCY:
1089
+ self.decimal_places = 2
1090
+ else:
1091
+ self.decimal_places = DECIMAL_PLACES_AUTO
1092
+
1093
+ if (
1094
+ self.type == FormattingType.BASE
1095
+ and not self.base_use_minus_sign
1096
+ and self.base not in (2, 8, 16)
1097
+ ):
1098
+ raise TypeError(f"base_use_minus_sign must be True for base {self.base}")
1099
+
1100
+ if self.type == FormattingType.BASE and (self.base < 2 or self.base > MAX_BASE):
1101
+ raise TypeError("base must be in range 2-36")
1102
+
1103
+
1104
+ @dataclass
1105
+ class CustomFormatting:
1106
+ type: CustomFormattingType = CustomFormattingType.NUMBER
1107
+ name: str = None
1108
+ integer_format: PaddingType = PaddingType.NONE
1109
+ decimal_format: PaddingType = PaddingType.NONE
1110
+ num_integers: int = 0
1111
+ num_decimals: int = 0
1112
+ show_thousands_separator: bool = False
1113
+ format: str = "%s"
1114
+
1115
+ def __post_init__(self):
1116
+ if not isinstance(self.type, CustomFormattingType):
1117
+ type_name = type(self.type).__name__
1118
+ raise TypeError(f"Invalid format type '{type_name}'")
1119
+
1120
+ if self.type == CustomFormattingType.TEXT:
1121
+ if self.format.count("%s") > 1:
1122
+ raise TypeError("Custom formats only allow one text substitution")
1123
+
1124
+ @classmethod
1125
+ def from_archive(cls, archive: object):
1126
+ if archive.format_type == FormatType.CUSTOM_DATE:
1127
+ format_type = CustomFormattingType.DATETIME
1128
+ elif archive.format_type == FormatType.CUSTOM_NUMBER:
1129
+ format_type = CustomFormattingType.NUMBER
1130
+ else:
1131
+ format_type = CustomFormattingType.TEXT
1132
+
1133
+ return CustomFormatting(name=archive.name, type=format_type)