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/__init__.py +2 -1
- numbers_parser/cell.py +385 -100
- numbers_parser/cell_storage.py +151 -162
- numbers_parser/constants.py +165 -1
- numbers_parser/document.py +932 -228
- numbers_parser/formula.py +10 -10
- numbers_parser/model.py +291 -124
- numbers_parser-4.8.0.dist-info/METADATA +378 -0
- {numbers_parser-4.7.1.dist-info → numbers_parser-4.8.0.dist-info}/RECORD +12 -12
- {numbers_parser-4.7.1.dist-info → numbers_parser-4.8.0.dist-info}/WHEEL +1 -1
- numbers_parser-4.7.1.dist-info/METADATA +0 -626
- {numbers_parser-4.7.1.dist-info → numbers_parser-4.8.0.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.7.1.dist-info → numbers_parser-4.8.0.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
136
|
-
|
|
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,
|
|
414
|
-
cell = EmptyCell(
|
|
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((
|
|
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,
|
|
424
|
-
cell = MergedCell(
|
|
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((
|
|
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.
|
|
493
|
+
cell = EmptyCell(cell_storage.row, cell_storage.col)
|
|
435
494
|
elif cell_storage.type == CellType.NUMBER:
|
|
436
|
-
cell = NumberCell(cell_storage.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
504
|
+
cell = DurationCell(cell_storage.row, cell_storage.col, value)
|
|
446
505
|
elif cell_storage.type == CellType.ERROR:
|
|
447
|
-
cell = ErrorCell(cell_storage.
|
|
506
|
+
cell = ErrorCell(cell_storage.row, cell_storage.col)
|
|
448
507
|
elif cell_storage.type == CellType.RICH_TEXT:
|
|
449
|
-
cell = RichTextCell(cell_storage.
|
|
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.
|
|
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.
|
|
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,
|
|
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(
|
|
527
|
+
return TextCell(row, col, value)
|
|
469
528
|
elif isinstance(value, bool):
|
|
470
|
-
return BoolCell(
|
|
529
|
+
return BoolCell(row, col, value)
|
|
471
530
|
elif isinstance(value, int):
|
|
472
|
-
return NumberCell(
|
|
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(
|
|
540
|
+
return NumberCell(row, col, rounded_value)
|
|
482
541
|
elif isinstance(value, (DateTime, builtin_datetime)):
|
|
483
|
-
return DateCell(
|
|
542
|
+
return DateCell(row, col, pendulum_instance(value))
|
|
484
543
|
elif isinstance(value, (Duration, builtin_timedelta)):
|
|
485
|
-
return DurationCell(
|
|
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
|
|
490
|
-
|
|
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,
|
|
551
|
+
def __init__(self, row: int, col: int, value):
|
|
493
552
|
self._value = value
|
|
494
|
-
self.row =
|
|
495
|
-
self.col =
|
|
496
|
-
self.
|
|
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
|
|
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
|
-
|
|
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__(
|
|
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,
|
|
747
|
+
def __init__(self, row: int, col: int, value: str):
|
|
627
748
|
self._type = TSTArchives.textCellType
|
|
628
|
-
super().__init__(
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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.
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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__(
|
|
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,
|
|
866
|
+
def __init__(self, row: int, col: int, value: Duration):
|
|
710
867
|
self._type = TSTArchives.durationCellType
|
|
711
|
-
super().__init__(
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
730
|
-
|
|
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
|
-
"""
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
"""
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
"""
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
"""
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
1021
|
+
str:
|
|
1022
|
+
Column in A1 notation.
|
|
823
1023
|
"""
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
1027
|
+
col += 1 # Change to 1-index.
|
|
829
1028
|
col_str = ""
|
|
830
1029
|
col_abs = "$" if col_abs else ""
|
|
831
1030
|
|
|
832
|
-
while
|
|
1031
|
+
while col:
|
|
833
1032
|
# Set remainder from 1 .. 26
|
|
834
|
-
remainder =
|
|
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
|
-
|
|
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)
|