numbers-parser 4.13.3__py3-none-any.whl → 4.14.2__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 +5 -4
- numbers_parser/_cat_numbers.py +24 -16
- numbers_parser/_csv2numbers.py +13 -14
- numbers_parser/_unpack_numbers.py +6 -7
- numbers_parser/bullets.py +7 -8
- numbers_parser/cell.py +280 -255
- numbers_parser/constants.py +22 -8
- numbers_parser/containers.py +11 -10
- numbers_parser/document.py +196 -150
- numbers_parser/exceptions.py +1 -8
- numbers_parser/formula.py +29 -32
- numbers_parser/generated/TSKArchives_pb2.py +92 -92
- numbers_parser/generated/TSSArchives_pb2.py +36 -36
- numbers_parser/generated/TSWPCommandArchives_pb2.py +99 -99
- numbers_parser/generated/fontmap.py +16 -10
- numbers_parser/generated/mapping.py +0 -1
- numbers_parser/iwafile.py +16 -16
- numbers_parser/iwork.py +32 -17
- numbers_parser/model.py +222 -210
- numbers_parser/numbers_cache.py +6 -7
- numbers_parser/numbers_uuid.py +4 -1
- numbers_parser/roman.py +32 -0
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/METADATA +18 -22
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/RECORD +27 -26
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/WHEEL +1 -1
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.13.3.dist-info → numbers_parser-4.14.2.dist-info}/entry_points.txt +0 -0
numbers_parser/cell.py
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import math
|
|
3
5
|
import re
|
|
4
|
-
from collections import namedtuple
|
|
5
6
|
from dataclasses import asdict, dataclass, field, fields
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from datetime import timedelta as builtin_timedelta
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
8
|
from enum import IntEnum
|
|
9
9
|
from fractions import Fraction
|
|
10
10
|
from hashlib import sha1
|
|
11
11
|
from os.path import basename
|
|
12
12
|
from struct import pack, unpack
|
|
13
|
-
from typing import Any,
|
|
13
|
+
from typing import Any, NamedTuple
|
|
14
14
|
from warnings import warn
|
|
15
15
|
|
|
16
|
-
import sigfig
|
|
17
|
-
from pendulum import DateTime, Duration, datetime, duration
|
|
18
|
-
from pendulum import instance as pendulum_instance
|
|
16
|
+
from sigfig import round as sigfig
|
|
19
17
|
|
|
20
18
|
from numbers_parser import __name__ as numbers_parser_name
|
|
21
|
-
|
|
22
|
-
# from numbers_parser.cell_storage import CellStorage, CellType
|
|
23
19
|
from numbers_parser.constants import (
|
|
24
20
|
CHECKBOX_FALSE_VALUE,
|
|
25
21
|
CHECKBOX_TRUE_VALUE,
|
|
@@ -104,7 +100,8 @@ __all__ = [
|
|
|
104
100
|
|
|
105
101
|
|
|
106
102
|
class BackgroundImage:
|
|
107
|
-
"""
|
|
103
|
+
"""
|
|
104
|
+
A named document style that can be applied to cells.
|
|
108
105
|
|
|
109
106
|
.. code-block:: python
|
|
110
107
|
|
|
@@ -126,9 +123,10 @@ class BackgroundImage:
|
|
|
126
123
|
Raw image data for a cell background image.
|
|
127
124
|
filename: str
|
|
128
125
|
Path to the image file.
|
|
126
|
+
|
|
129
127
|
"""
|
|
130
128
|
|
|
131
|
-
def __init__(self, data:
|
|
129
|
+
def __init__(self, data: bytes | None = None, filename: str | None = None) -> None:
|
|
132
130
|
self._data = data
|
|
133
131
|
self._filename = basename(filename)
|
|
134
132
|
|
|
@@ -171,7 +169,12 @@ VERTICAL_MAP = {
|
|
|
171
169
|
"bottom": VerticalJustification.BOTTOM,
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
|
|
172
|
+
|
|
173
|
+
class _Alignment(NamedTuple):
|
|
174
|
+
"""Class for internal alignment type."""
|
|
175
|
+
|
|
176
|
+
horizontal: str
|
|
177
|
+
vertical: str
|
|
175
178
|
|
|
176
179
|
|
|
177
180
|
class Alignment(_Alignment):
|
|
@@ -195,12 +198,23 @@ class Alignment(_Alignment):
|
|
|
195
198
|
|
|
196
199
|
DEFAULT_ALIGNMENT_CLASS = Alignment(*DEFAULT_ALIGNMENT)
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
|
|
202
|
+
class RGB(NamedTuple):
|
|
203
|
+
"""A color in RGB."""
|
|
204
|
+
|
|
205
|
+
r: int
|
|
206
|
+
g: int
|
|
207
|
+
b: int
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def default_color() -> RGB:
|
|
211
|
+
return RGB(0, 0, 0)
|
|
199
212
|
|
|
200
213
|
|
|
201
214
|
@dataclass
|
|
202
215
|
class Style:
|
|
203
|
-
"""
|
|
216
|
+
"""
|
|
217
|
+
A named document style that can be applied to cells.
|
|
204
218
|
|
|
205
219
|
Parameters
|
|
206
220
|
----------
|
|
@@ -240,12 +254,13 @@ class Style:
|
|
|
240
254
|
If arguments do not match the specified type or for objects have invalid arguments
|
|
241
255
|
IndexError:
|
|
242
256
|
If an image filename already exists in document
|
|
257
|
+
|
|
243
258
|
"""
|
|
244
259
|
|
|
245
260
|
alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
|
|
246
261
|
bg_image: object = None # : backgroung image
|
|
247
|
-
bg_color:
|
|
248
|
-
font_color: RGB =
|
|
262
|
+
bg_color: RGB | list[RGB] = None
|
|
263
|
+
font_color: RGB = field(default_factory=default_color)
|
|
249
264
|
font_size: float = DEFAULT_FONT_SIZE
|
|
250
265
|
font_name: str = DEFAULT_FONT
|
|
251
266
|
bold: bool = False
|
|
@@ -335,7 +350,8 @@ class Style:
|
|
|
335
350
|
raise TypeError(msg)
|
|
336
351
|
|
|
337
352
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
338
|
-
"""
|
|
353
|
+
"""
|
|
354
|
+
Detect changes to cell styles and flag the style for
|
|
339
355
|
possible updates when saving the document.
|
|
340
356
|
"""
|
|
341
357
|
if name in ["bg_color", "font_color"]:
|
|
@@ -362,7 +378,7 @@ def rgb_color(color) -> RGB:
|
|
|
362
378
|
msg = "RGB color must be an RGB or a tuple of 3 integers"
|
|
363
379
|
raise TypeError(msg)
|
|
364
380
|
return RGB(*color)
|
|
365
|
-
|
|
381
|
+
if isinstance(color, list):
|
|
366
382
|
return [rgb_color(c) for c in color]
|
|
367
383
|
msg = "RGB color must be an RGB or a tuple of 3 integers"
|
|
368
384
|
raise TypeError(msg)
|
|
@@ -394,7 +410,8 @@ class BorderType(IntEnum):
|
|
|
394
410
|
|
|
395
411
|
|
|
396
412
|
class Border:
|
|
397
|
-
"""
|
|
413
|
+
"""
|
|
414
|
+
Create a cell border to use with the :py:class:`~numbers_parser.Table` method
|
|
398
415
|
:py:meth:`~numbers_parser.Table.set_cell_border`.
|
|
399
416
|
|
|
400
417
|
.. code-block:: python
|
|
@@ -421,6 +438,7 @@ class Border:
|
|
|
421
438
|
------
|
|
422
439
|
TypeError:
|
|
423
440
|
If the width is not a float, or the border type is invalid.
|
|
441
|
+
|
|
424
442
|
"""
|
|
425
443
|
|
|
426
444
|
def __init__(
|
|
@@ -481,62 +499,46 @@ class CellBorder:
|
|
|
481
499
|
|
|
482
500
|
@property
|
|
483
501
|
def top(self):
|
|
484
|
-
if self._top_merged:
|
|
485
|
-
return None
|
|
486
|
-
elif self._top is None:
|
|
502
|
+
if self._top_merged or self._top is None:
|
|
487
503
|
return None
|
|
488
504
|
return self._top
|
|
489
505
|
|
|
490
506
|
@top.setter
|
|
491
|
-
def top(self, value):
|
|
492
|
-
if self._top is None:
|
|
493
|
-
self._top = value
|
|
494
|
-
elif value._order > self.top._order:
|
|
507
|
+
def top(self, value) -> None:
|
|
508
|
+
if self._top is None or value._order > self.top._order:
|
|
495
509
|
self._top = value
|
|
496
510
|
|
|
497
511
|
@property
|
|
498
512
|
def right(self):
|
|
499
|
-
if self._right_merged:
|
|
500
|
-
return None
|
|
501
|
-
elif self._right is None:
|
|
513
|
+
if self._right_merged or self._right is None:
|
|
502
514
|
return None
|
|
503
515
|
return self._right
|
|
504
516
|
|
|
505
517
|
@right.setter
|
|
506
|
-
def right(self, value):
|
|
507
|
-
if self._right is None:
|
|
508
|
-
self._right = value
|
|
509
|
-
elif value._order > self._right._order:
|
|
518
|
+
def right(self, value) -> None:
|
|
519
|
+
if self._right is None or value._order > self._right._order:
|
|
510
520
|
self._right = value
|
|
511
521
|
|
|
512
522
|
@property
|
|
513
523
|
def bottom(self):
|
|
514
|
-
if self._bottom_merged:
|
|
515
|
-
return None
|
|
516
|
-
elif self._bottom is None:
|
|
524
|
+
if self._bottom_merged or self._bottom is None:
|
|
517
525
|
return None
|
|
518
526
|
return self._bottom
|
|
519
527
|
|
|
520
528
|
@bottom.setter
|
|
521
|
-
def bottom(self, value):
|
|
522
|
-
if self._bottom is None:
|
|
523
|
-
self._bottom = value
|
|
524
|
-
elif value._order > self._bottom._order:
|
|
529
|
+
def bottom(self, value) -> None:
|
|
530
|
+
if self._bottom is None or value._order > self._bottom._order:
|
|
525
531
|
self._bottom = value
|
|
526
532
|
|
|
527
533
|
@property
|
|
528
534
|
def left(self):
|
|
529
|
-
if self._left_merged:
|
|
530
|
-
return None
|
|
531
|
-
elif self._left is None:
|
|
535
|
+
if self._left_merged or self._left is None:
|
|
532
536
|
return None
|
|
533
537
|
return self._left
|
|
534
538
|
|
|
535
539
|
@left.setter
|
|
536
|
-
def left(self, value):
|
|
537
|
-
if self._left is None:
|
|
538
|
-
self._left = value
|
|
539
|
-
elif value._order > self._left._order:
|
|
540
|
+
def left(self, value) -> None:
|
|
541
|
+
if self._left is None or value._order > self._left._order:
|
|
540
542
|
self._left = value
|
|
541
543
|
|
|
542
544
|
|
|
@@ -550,7 +552,7 @@ class MergeReference:
|
|
|
550
552
|
class MergeAnchor:
|
|
551
553
|
"""Cell reference for the merged cell."""
|
|
552
554
|
|
|
553
|
-
def __init__(self, size:
|
|
555
|
+
def __init__(self, size: tuple) -> None:
|
|
554
556
|
self.size = size
|
|
555
557
|
|
|
556
558
|
|
|
@@ -586,10 +588,11 @@ class CellStorageFlags:
|
|
|
586
588
|
|
|
587
589
|
|
|
588
590
|
class Cell(CellStorageFlags, Cacheable):
|
|
589
|
-
"""
|
|
591
|
+
"""
|
|
592
|
+
.. NOTE::
|
|
590
593
|
|
|
591
|
-
|
|
592
|
-
"""
|
|
594
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
595
|
+
"""
|
|
593
596
|
|
|
594
597
|
def __init__(self, row: int, col: int, value) -> None:
|
|
595
598
|
self._value = value
|
|
@@ -621,8 +624,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
621
624
|
)
|
|
622
625
|
if self.style is not None and self.style.bg_image is not None:
|
|
623
626
|
return self.style.bg_image.filename
|
|
624
|
-
|
|
625
|
-
return None
|
|
627
|
+
return None
|
|
626
628
|
|
|
627
629
|
@property
|
|
628
630
|
def image_data(self):
|
|
@@ -634,8 +636,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
634
636
|
)
|
|
635
637
|
if self.style is not None and self.style.bg_image is not None:
|
|
636
638
|
return self.style.bg_image.data
|
|
637
|
-
|
|
638
|
-
return None
|
|
639
|
+
return None
|
|
639
640
|
|
|
640
641
|
@property
|
|
641
642
|
def is_formula(self) -> bool:
|
|
@@ -646,7 +647,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
646
647
|
@property
|
|
647
648
|
@cache(num_args=0)
|
|
648
649
|
def formula(self) -> str:
|
|
649
|
-
"""
|
|
650
|
+
"""
|
|
651
|
+
str: The formula in a cell.
|
|
650
652
|
|
|
651
653
|
Formula evaluation relies on Numbers storing current values which should
|
|
652
654
|
usually be the case. In cells containing a formula, :py:meth:`numbers_parser.Cell.value`
|
|
@@ -657,12 +659,12 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
657
659
|
str:
|
|
658
660
|
The text of the foruma in a cell, or `None` if there is no formula
|
|
659
661
|
present in a cell.
|
|
662
|
+
|
|
660
663
|
"""
|
|
661
664
|
if self._formula_id is not None:
|
|
662
665
|
table_formulas = self._model.table_formulas(self._table_id)
|
|
663
666
|
return table_formulas.formula(self._formula_id, self.row, self.col)
|
|
664
|
-
|
|
665
|
-
return None
|
|
667
|
+
return None
|
|
666
668
|
|
|
667
669
|
@property
|
|
668
670
|
def is_bulleted(self) -> bool:
|
|
@@ -670,8 +672,9 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
670
672
|
return self._is_bulleted
|
|
671
673
|
|
|
672
674
|
@property
|
|
673
|
-
def bullets(self) ->
|
|
674
|
-
r"""
|
|
675
|
+
def bullets(self) -> list[str] | None:
|
|
676
|
+
r"""
|
|
677
|
+
List[str] | None: The bullets in a cell, or ``None``.
|
|
675
678
|
|
|
676
679
|
Cells that contain bulleted or numbered lists are identified
|
|
677
680
|
by :py:attr:`numbers_parser.Cell.is_bulleted`. For these cells,
|
|
@@ -694,12 +697,14 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
694
697
|
bullets = ["* " + s for s in table.cell(0, 1).bullets]
|
|
695
698
|
print("\n".join(bullets))
|
|
696
699
|
return None
|
|
700
|
+
|
|
697
701
|
"""
|
|
698
702
|
return None
|
|
699
703
|
|
|
700
704
|
@property
|
|
701
705
|
def formatted_value(self) -> str:
|
|
702
|
-
"""
|
|
706
|
+
"""
|
|
707
|
+
str: The formatted value of the cell as it appears in Numbers.
|
|
703
708
|
|
|
704
709
|
Interactive elements are converted into a suitable text format where
|
|
705
710
|
supported, or as their number values where there is no suitable
|
|
@@ -726,33 +731,34 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
726
731
|
"""
|
|
727
732
|
if self._duration_format_id is not None and self._double is not None:
|
|
728
733
|
return self._duration_format()
|
|
729
|
-
|
|
734
|
+
if self._date_format_id is not None and self._seconds is not None:
|
|
730
735
|
return self._date_format()
|
|
731
|
-
|
|
736
|
+
if (
|
|
732
737
|
self._text_format_id is not None
|
|
733
738
|
or self._num_format_id is not None
|
|
734
739
|
or self._currency_format_id is not None
|
|
735
740
|
or self._bool_format_id is not None
|
|
736
741
|
):
|
|
737
742
|
return self._custom_format()
|
|
738
|
-
|
|
739
|
-
return str(self.value)
|
|
743
|
+
return str(self.value)
|
|
740
744
|
|
|
741
745
|
@property
|
|
742
|
-
def style(self) ->
|
|
743
|
-
"""
|
|
746
|
+
def style(self) -> Style | None:
|
|
747
|
+
"""
|
|
748
|
+
Style | None: The :class:`Style` associated with the cell or ``None``.
|
|
744
749
|
|
|
745
750
|
Warns
|
|
746
751
|
-----
|
|
747
752
|
UnsupportedWarning: On assignment; use
|
|
748
753
|
:py:meth:`numbers_parser.Table.set_cell_style` instead.
|
|
754
|
+
|
|
749
755
|
"""
|
|
750
756
|
if self._style is None:
|
|
751
757
|
self._style = Style.from_storage(self, self._model)
|
|
752
758
|
return self._style
|
|
753
759
|
|
|
754
760
|
@style.setter
|
|
755
|
-
def style(self, _):
|
|
761
|
+
def style(self, _) -> None:
|
|
756
762
|
warn(
|
|
757
763
|
"cell style cannot be set; use Table.set_cell_style() instead",
|
|
758
764
|
UnsupportedWarning,
|
|
@@ -760,19 +766,21 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
760
766
|
)
|
|
761
767
|
|
|
762
768
|
@property
|
|
763
|
-
def border(self) ->
|
|
764
|
-
"""
|
|
769
|
+
def border(self) -> CellBorder | None:
|
|
770
|
+
"""
|
|
771
|
+
CellBorder| None: The :class:`CellBorder` associated with the cell or ``None``.
|
|
765
772
|
|
|
766
773
|
Warns
|
|
767
774
|
-----
|
|
768
775
|
UnsupportedWarning: On assignment; use
|
|
769
776
|
:py:meth:`numbers_parser.Table.set_cell_border` instead.
|
|
777
|
+
|
|
770
778
|
"""
|
|
771
779
|
self._model.extract_strokes(self._table_id)
|
|
772
780
|
return self._border
|
|
773
781
|
|
|
774
782
|
@border.setter
|
|
775
|
-
def border(self, _):
|
|
783
|
+
def border(self, _) -> None:
|
|
776
784
|
warn(
|
|
777
785
|
"cell border values cannot be set; use Table.set_cell_border() instead",
|
|
778
786
|
UnsupportedWarning,
|
|
@@ -802,7 +810,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
802
810
|
elif isinstance(value, int):
|
|
803
811
|
cell = NumberCell(row, col, value)
|
|
804
812
|
elif isinstance(value, float):
|
|
805
|
-
rounded_value = sigfig
|
|
813
|
+
rounded_value = sigfig(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
|
|
806
814
|
if rounded_value != value:
|
|
807
815
|
warn(
|
|
808
816
|
f"'{value}' rounded to {MAX_SIGNIFICANT_DIGITS} significant digits",
|
|
@@ -810,17 +818,18 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
810
818
|
stacklevel=2,
|
|
811
819
|
)
|
|
812
820
|
cell = NumberCell(row, col, rounded_value)
|
|
813
|
-
elif isinstance(value,
|
|
814
|
-
cell = DateCell(row, col,
|
|
815
|
-
elif isinstance(value,
|
|
821
|
+
elif isinstance(value, datetime):
|
|
822
|
+
cell = DateCell(row, col, value)
|
|
823
|
+
elif isinstance(value, timedelta):
|
|
816
824
|
cell = DurationCell(row, col, value)
|
|
817
825
|
else:
|
|
818
|
-
|
|
826
|
+
msg = "Can't determine cell type from type " + type(value).__name__
|
|
827
|
+
raise ValueError(msg) # noqa: TRY004
|
|
819
828
|
|
|
820
829
|
return cell
|
|
821
830
|
|
|
822
831
|
@classmethod
|
|
823
|
-
def _from_storage( # noqa:
|
|
832
|
+
def _from_storage( # noqa: PLR0912
|
|
824
833
|
cls,
|
|
825
834
|
table_id: int,
|
|
826
835
|
row: int,
|
|
@@ -882,7 +891,6 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
882
891
|
offset += 4
|
|
883
892
|
# Skip unused flags
|
|
884
893
|
offset += 4 * bin(flags & 0x900).count("1")
|
|
885
|
-
#
|
|
886
894
|
if flags & 0x2000:
|
|
887
895
|
storage_flags._num_format_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
888
896
|
offset += 4
|
|
@@ -916,12 +924,12 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
916
924
|
elif cell_type == TSTArchives.textCellType:
|
|
917
925
|
cell = TextCell(row, col, model.table_string(table_id, storage_flags._string_id))
|
|
918
926
|
elif cell_type == TSTArchives.dateCellType:
|
|
919
|
-
cell = DateCell(row, col, EPOCH +
|
|
927
|
+
cell = DateCell(row, col, EPOCH + timedelta(seconds=seconds))
|
|
920
928
|
cell._datetime = cell._value
|
|
921
929
|
elif cell_type == TSTArchives.boolCellType:
|
|
922
930
|
cell = BoolCell(row, col, double > 0.0)
|
|
923
931
|
elif cell_type == TSTArchives.durationCellType:
|
|
924
|
-
cell = DurationCell(row, col,
|
|
932
|
+
cell = DurationCell(row, col, timedelta(seconds=double))
|
|
925
933
|
elif cell_type == TSTArchives.formulaErrorCellType:
|
|
926
934
|
cell = ErrorCell(row, col)
|
|
927
935
|
elif cell_type == TSTArchives.automaticCellType:
|
|
@@ -951,11 +959,11 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
951
959
|
|
|
952
960
|
return cell
|
|
953
961
|
|
|
954
|
-
def _copy_flags(self, storage_flags: CellStorageFlags):
|
|
962
|
+
def _copy_flags(self, storage_flags: CellStorageFlags) -> None:
|
|
955
963
|
for flag in storage_flags.flags():
|
|
956
964
|
setattr(self, flag, getattr(storage_flags, flag))
|
|
957
965
|
|
|
958
|
-
def _set_merge(self, merge_ref):
|
|
966
|
+
def _set_merge(self, merge_ref) -> None:
|
|
959
967
|
if isinstance(merge_ref, MergeAnchor):
|
|
960
968
|
self.is_merged = True
|
|
961
969
|
self.size = merge_ref.size
|
|
@@ -1024,7 +1032,11 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1024
1032
|
flags = 4
|
|
1025
1033
|
length += 8
|
|
1026
1034
|
cell_type = TSTArchives.dateCellType
|
|
1027
|
-
date_delta = self._value.astimezone() - EPOCH
|
|
1035
|
+
# date_delta = self._value.astimezone() - EPOCH
|
|
1036
|
+
if self._value.tzinfo is None:
|
|
1037
|
+
date_delta = self._value - EPOCH
|
|
1038
|
+
else:
|
|
1039
|
+
date_delta = self._value - EPOCH.astimezone(self._value.tzinfo)
|
|
1028
1040
|
value = pack("<d", float(date_delta.total_seconds()))
|
|
1029
1041
|
elif isinstance(self, BoolCell):
|
|
1030
1042
|
flags = 2
|
|
@@ -1135,7 +1147,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1135
1147
|
|
|
1136
1148
|
@property
|
|
1137
1149
|
@cache(num_args=0)
|
|
1138
|
-
def _image_data(self) ->
|
|
1150
|
+
def _image_data(self) -> tuple[bytes, str]:
|
|
1139
1151
|
"""Return the background image data for a cell or None if no image."""
|
|
1140
1152
|
if self._cell_style_id is None:
|
|
1141
1153
|
return None
|
|
@@ -1161,30 +1173,34 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1161
1173
|
stacklevel=3,
|
|
1162
1174
|
)
|
|
1163
1175
|
return None
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
self._model._images[digest] = image_id
|
|
1176
|
+
image_data = self._model.objects.file_store[image_pathnames[0]]
|
|
1177
|
+
digest = sha1(image_data).digest() # noqa: S324
|
|
1178
|
+
if digest not in self._model._images:
|
|
1179
|
+
self._model._images[digest] = image_id
|
|
1169
1180
|
|
|
1170
|
-
|
|
1181
|
+
return (image_data, preferred_filename)
|
|
1171
1182
|
|
|
1172
1183
|
def _custom_format(self) -> str: # noqa: PLR0911
|
|
1173
1184
|
if self._text_format_id is not None and self._type == CellType.TEXT:
|
|
1174
|
-
|
|
1185
|
+
custom_format = self._model.table_format(self._table_id, self._text_format_id)
|
|
1175
1186
|
elif self._currency_format_id is not None:
|
|
1176
|
-
|
|
1187
|
+
custom_format = self._model.table_format(self._table_id, self._currency_format_id)
|
|
1177
1188
|
elif self._bool_format_id is not None and self._type == CellType.BOOL:
|
|
1178
|
-
|
|
1189
|
+
custom_format = self._model.table_format(self._table_id, self._bool_format_id)
|
|
1179
1190
|
elif self._num_format_id is not None:
|
|
1180
|
-
|
|
1191
|
+
custom_format = self._model.table_format(self._table_id, self._num_format_id)
|
|
1181
1192
|
else:
|
|
1182
1193
|
return str(self.value)
|
|
1183
1194
|
|
|
1184
|
-
debug(
|
|
1195
|
+
debug(
|
|
1196
|
+
"custom_format: @[%d,%d]: format_type=%s, ",
|
|
1197
|
+
self.row,
|
|
1198
|
+
self.col,
|
|
1199
|
+
custom_format.format_type,
|
|
1200
|
+
)
|
|
1185
1201
|
|
|
1186
|
-
if
|
|
1187
|
-
format_uuid = NumbersUUID(
|
|
1202
|
+
if custom_format.HasField("custom_uid"):
|
|
1203
|
+
format_uuid = NumbersUUID(custom_format.custom_uid).hex
|
|
1188
1204
|
format_map = self._model.custom_format_map()
|
|
1189
1205
|
custom_format = format_map[format_uuid].default_format
|
|
1190
1206
|
if custom_format.requires_fraction_replacement:
|
|
@@ -1200,32 +1216,32 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1200
1216
|
self._d128,
|
|
1201
1217
|
format_map[format_uuid].name,
|
|
1202
1218
|
)
|
|
1203
|
-
elif
|
|
1204
|
-
return _format_decimal(self._d128,
|
|
1205
|
-
elif
|
|
1206
|
-
return _format_currency(self._d128,
|
|
1207
|
-
elif
|
|
1219
|
+
elif custom_format.format_type == FormatType.DECIMAL:
|
|
1220
|
+
return _format_decimal(self._d128, custom_format)
|
|
1221
|
+
elif custom_format.format_type == FormatType.CURRENCY:
|
|
1222
|
+
return _format_currency(self._d128, custom_format)
|
|
1223
|
+
elif custom_format.format_type == FormatType.BOOLEAN:
|
|
1208
1224
|
return "TRUE" if self.value else "FALSE"
|
|
1209
|
-
elif
|
|
1210
|
-
return _format_decimal(self._d128 * 100,
|
|
1211
|
-
elif
|
|
1212
|
-
return _format_base(self._d128,
|
|
1213
|
-
elif
|
|
1214
|
-
return _format_fraction(self._d128,
|
|
1215
|
-
elif
|
|
1216
|
-
return _format_scientific(self._d128,
|
|
1217
|
-
elif
|
|
1225
|
+
elif custom_format.format_type == FormatType.PERCENT:
|
|
1226
|
+
return _format_decimal(self._d128 * 100, custom_format, percent=True)
|
|
1227
|
+
elif custom_format.format_type == FormatType.BASE:
|
|
1228
|
+
return _format_base(self._d128, custom_format)
|
|
1229
|
+
elif custom_format.format_type == FormatType.FRACTION:
|
|
1230
|
+
return _format_fraction(self._d128, custom_format)
|
|
1231
|
+
elif custom_format.format_type == FormatType.SCIENTIFIC:
|
|
1232
|
+
return _format_scientific(self._d128, custom_format)
|
|
1233
|
+
elif custom_format.format_type == FormatType.CHECKBOX:
|
|
1218
1234
|
return CHECKBOX_TRUE_VALUE if self.value else CHECKBOX_FALSE_VALUE
|
|
1219
|
-
elif
|
|
1235
|
+
elif custom_format.format_type == FormatType.RATING:
|
|
1220
1236
|
return STAR_RATING_VALUE * int(self._d128)
|
|
1221
1237
|
else:
|
|
1222
1238
|
formatted_value = str(self.value)
|
|
1223
1239
|
return formatted_value
|
|
1224
1240
|
|
|
1225
1241
|
def _date_format(self) -> str:
|
|
1226
|
-
|
|
1227
|
-
if
|
|
1228
|
-
format_uuid = NumbersUUID(
|
|
1242
|
+
date_format = self._model.table_format(self._table_id, self._date_format_id)
|
|
1243
|
+
if date_format.HasField("custom_uid"):
|
|
1244
|
+
format_uuid = NumbersUUID(date_format.custom_uid).hex
|
|
1229
1245
|
format_map = self._model.custom_format_map()
|
|
1230
1246
|
custom_format = format_map[format_uuid].default_format
|
|
1231
1247
|
custom_format_string = custom_format.custom_format_string
|
|
@@ -1239,25 +1255,25 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1239
1255
|
)
|
|
1240
1256
|
return ""
|
|
1241
1257
|
else:
|
|
1242
|
-
formatted_value = _decode_date_format(
|
|
1258
|
+
formatted_value = _decode_date_format(date_format.date_time_format, self._datetime)
|
|
1243
1259
|
return formatted_value
|
|
1244
1260
|
|
|
1245
1261
|
def _duration_format(self) -> str:
|
|
1246
|
-
|
|
1262
|
+
duration_format = self._model.table_format(self._table_id, self._duration_format_id)
|
|
1247
1263
|
debug(
|
|
1248
1264
|
"duration_format: @[%d,%d]: table_id=%d, duration_format_id=%d, duration_style=%s",
|
|
1249
1265
|
self.row,
|
|
1250
1266
|
self.col,
|
|
1251
1267
|
self._table_id,
|
|
1252
1268
|
self._duration_format_id,
|
|
1253
|
-
|
|
1269
|
+
duration_format.duration_style,
|
|
1254
1270
|
)
|
|
1255
1271
|
|
|
1256
|
-
duration_style =
|
|
1257
|
-
unit_largest =
|
|
1258
|
-
unit_smallest =
|
|
1259
|
-
if
|
|
1260
|
-
unit_smallest, unit_largest = _auto_units(self._double,
|
|
1272
|
+
duration_style = duration_format.duration_style
|
|
1273
|
+
unit_largest = duration_format.duration_unit_largest
|
|
1274
|
+
unit_smallest = duration_format.duration_unit_smallest
|
|
1275
|
+
if duration_format.use_automatic_duration_units:
|
|
1276
|
+
unit_smallest, unit_largest = _auto_units(self._double, duration_format)
|
|
1261
1277
|
|
|
1262
1278
|
d = self._double
|
|
1263
1279
|
dd = int(self._double)
|
|
@@ -1324,8 +1340,8 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1324
1340
|
def _set_formatting(
|
|
1325
1341
|
self,
|
|
1326
1342
|
format_id: int,
|
|
1327
|
-
format_type:
|
|
1328
|
-
control_id:
|
|
1343
|
+
format_type: FormattingType | CustomFormattingType,
|
|
1344
|
+
control_id: int | None = None,
|
|
1329
1345
|
is_currency: bool = False,
|
|
1330
1346
|
) -> None:
|
|
1331
1347
|
self._is_currency = is_currency
|
|
@@ -1357,10 +1373,11 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1357
1373
|
|
|
1358
1374
|
|
|
1359
1375
|
class NumberCell(Cell):
|
|
1360
|
-
"""
|
|
1376
|
+
"""
|
|
1377
|
+
.. NOTE::
|
|
1361
1378
|
|
|
1362
|
-
|
|
1363
|
-
"""
|
|
1379
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1380
|
+
"""
|
|
1364
1381
|
|
|
1365
1382
|
def __init__(self, row: int, col: int, value: float, cell_type=CellType.NUMBER) -> None:
|
|
1366
1383
|
self._type = cell_type
|
|
@@ -1382,10 +1399,11 @@ class TextCell(Cell):
|
|
|
1382
1399
|
|
|
1383
1400
|
|
|
1384
1401
|
class RichTextCell(Cell):
|
|
1385
|
-
"""
|
|
1402
|
+
"""
|
|
1403
|
+
.. NOTE::
|
|
1386
1404
|
|
|
1387
|
-
|
|
1388
|
-
"""
|
|
1405
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1406
|
+
"""
|
|
1389
1407
|
|
|
1390
1408
|
def __init__(self, row: int, col: int, value) -> None:
|
|
1391
1409
|
super().__init__(row, col, value["text"])
|
|
@@ -1408,7 +1426,7 @@ class RichTextCell(Cell):
|
|
|
1408
1426
|
return self._value
|
|
1409
1427
|
|
|
1410
1428
|
@property
|
|
1411
|
-
def bullets(self) ->
|
|
1429
|
+
def bullets(self) -> list[str]:
|
|
1412
1430
|
"""List[str]: A list of the text bullets in the cell."""
|
|
1413
1431
|
return self._bullets
|
|
1414
1432
|
|
|
@@ -1418,8 +1436,9 @@ class RichTextCell(Cell):
|
|
|
1418
1436
|
return self._formatted_bullets
|
|
1419
1437
|
|
|
1420
1438
|
@property
|
|
1421
|
-
def hyperlinks(self) ->
|
|
1422
|
-
"""
|
|
1439
|
+
def hyperlinks(self) -> list[tuple] | None:
|
|
1440
|
+
"""
|
|
1441
|
+
List[Tuple] | None: the hyperlinks in a cell or ``None``.
|
|
1423
1442
|
|
|
1424
1443
|
Numbers does not support hyperlinks to cells within a spreadsheet, but does
|
|
1425
1444
|
allow embedding links in cells. When cells contain hyperlinks,
|
|
@@ -1433,6 +1452,7 @@ class RichTextCell(Cell):
|
|
|
1433
1452
|
|
|
1434
1453
|
cell = table.cell(0, 0)
|
|
1435
1454
|
(text, url) = cell.hyperlinks[0]
|
|
1455
|
+
|
|
1436
1456
|
"""
|
|
1437
1457
|
return self._hyperlinks
|
|
1438
1458
|
|
|
@@ -1443,29 +1463,31 @@ class BulletedTextCell(RichTextCell):
|
|
|
1443
1463
|
|
|
1444
1464
|
|
|
1445
1465
|
class EmptyCell(Cell):
|
|
1446
|
-
"""
|
|
1466
|
+
"""
|
|
1467
|
+
.. NOTE::
|
|
1447
1468
|
|
|
1448
|
-
|
|
1449
|
-
"""
|
|
1469
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1470
|
+
"""
|
|
1450
1471
|
|
|
1451
1472
|
def __init__(self, row: int, col: int) -> None:
|
|
1452
1473
|
super().__init__(row, col, None)
|
|
1453
1474
|
self._type = CellType.EMPTY
|
|
1454
1475
|
|
|
1455
1476
|
@property
|
|
1456
|
-
def value(self):
|
|
1477
|
+
def value(self) -> None:
|
|
1457
1478
|
return None
|
|
1458
1479
|
|
|
1459
1480
|
@property
|
|
1460
|
-
def formatted_value(self):
|
|
1481
|
+
def formatted_value(self) -> str:
|
|
1461
1482
|
return ""
|
|
1462
1483
|
|
|
1463
1484
|
|
|
1464
1485
|
class BoolCell(Cell):
|
|
1465
|
-
"""
|
|
1486
|
+
"""
|
|
1487
|
+
.. NOTE::
|
|
1466
1488
|
|
|
1467
|
-
|
|
1468
|
-
"""
|
|
1489
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1490
|
+
"""
|
|
1469
1491
|
|
|
1470
1492
|
def __init__(self, row: int, col: int, value: bool) -> None:
|
|
1471
1493
|
super().__init__(row, col, value)
|
|
@@ -1478,12 +1500,13 @@ class BoolCell(Cell):
|
|
|
1478
1500
|
|
|
1479
1501
|
|
|
1480
1502
|
class DateCell(Cell):
|
|
1481
|
-
"""
|
|
1503
|
+
"""
|
|
1504
|
+
.. NOTE::
|
|
1482
1505
|
|
|
1483
|
-
|
|
1484
|
-
"""
|
|
1506
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1507
|
+
"""
|
|
1485
1508
|
|
|
1486
|
-
def __init__(self, row: int, col: int, value:
|
|
1509
|
+
def __init__(self, row: int, col: int, value: datetime) -> None:
|
|
1487
1510
|
super().__init__(row, col, value)
|
|
1488
1511
|
self._type = CellType.DATE
|
|
1489
1512
|
|
|
@@ -1493,42 +1516,44 @@ class DateCell(Cell):
|
|
|
1493
1516
|
|
|
1494
1517
|
|
|
1495
1518
|
class DurationCell(Cell):
|
|
1496
|
-
def __init__(self, row: int, col: int, value:
|
|
1519
|
+
def __init__(self, row: int, col: int, value: timedelta) -> None:
|
|
1497
1520
|
super().__init__(row, col, value)
|
|
1498
1521
|
self._type = CellType.DURATION
|
|
1499
1522
|
|
|
1500
1523
|
@property
|
|
1501
|
-
def value(self) ->
|
|
1524
|
+
def value(self) -> timedelta:
|
|
1502
1525
|
return self._value
|
|
1503
1526
|
|
|
1504
1527
|
|
|
1505
1528
|
class ErrorCell(Cell):
|
|
1506
|
-
"""
|
|
1529
|
+
"""
|
|
1530
|
+
.. NOTE::
|
|
1507
1531
|
|
|
1508
|
-
|
|
1509
|
-
"""
|
|
1532
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1533
|
+
"""
|
|
1510
1534
|
|
|
1511
1535
|
def __init__(self, row: int, col: int) -> None:
|
|
1512
1536
|
super().__init__(row, col, None)
|
|
1513
1537
|
self._type = CellType.ERROR
|
|
1514
1538
|
|
|
1515
1539
|
@property
|
|
1516
|
-
def value(self):
|
|
1540
|
+
def value(self) -> None:
|
|
1517
1541
|
return None
|
|
1518
1542
|
|
|
1519
1543
|
|
|
1520
1544
|
class MergedCell(Cell):
|
|
1521
|
-
"""
|
|
1545
|
+
"""
|
|
1546
|
+
.. NOTE::
|
|
1522
1547
|
|
|
1523
|
-
|
|
1524
|
-
"""
|
|
1548
|
+
Do not instantiate directly. Cells are created by :py:class:`~numbers_parser.Document`.
|
|
1549
|
+
"""
|
|
1525
1550
|
|
|
1526
1551
|
def __init__(self, row: int, col: int) -> None:
|
|
1527
1552
|
super().__init__(row, col, None)
|
|
1528
1553
|
self._type = CellType.MERGED
|
|
1529
1554
|
|
|
1530
1555
|
@property
|
|
1531
|
-
def value(self):
|
|
1556
|
+
def value(self) -> None:
|
|
1532
1557
|
return None
|
|
1533
1558
|
|
|
1534
1559
|
|
|
@@ -1566,16 +1591,14 @@ def _decode_date_format_field(field: str, value: datetime) -> str:
|
|
|
1566
1591
|
s = DATETIME_FIELD_MAP[field]
|
|
1567
1592
|
if callable(s):
|
|
1568
1593
|
return s(value)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
warn(f"Unsupported field code '{field}'", UnsupportedWarning, stacklevel=4)
|
|
1573
|
-
return ""
|
|
1594
|
+
return value.strftime(s)
|
|
1595
|
+
warn(f"Unsupported field code '{field}'", UnsupportedWarning, stacklevel=4)
|
|
1596
|
+
return ""
|
|
1574
1597
|
|
|
1575
1598
|
|
|
1576
|
-
def _decode_date_format(
|
|
1599
|
+
def _decode_date_format(date_format, value):
|
|
1577
1600
|
"""Parse a custom date format string and return a formatted datetime value."""
|
|
1578
|
-
chars = [*
|
|
1601
|
+
chars = [*date_format]
|
|
1579
1602
|
index = 0
|
|
1580
1603
|
in_string = False
|
|
1581
1604
|
in_field = False
|
|
@@ -1587,7 +1610,7 @@ def _decode_date_format(format, value):
|
|
|
1587
1610
|
if current_char == "'":
|
|
1588
1611
|
if next_char is None:
|
|
1589
1612
|
break
|
|
1590
|
-
|
|
1613
|
+
if chars[index + 1] == "'":
|
|
1591
1614
|
result += "'"
|
|
1592
1615
|
index += 2
|
|
1593
1616
|
elif in_string:
|
|
@@ -1621,9 +1644,9 @@ def _decode_date_format(format, value):
|
|
|
1621
1644
|
return result
|
|
1622
1645
|
|
|
1623
1646
|
|
|
1624
|
-
def _decode_text_format(
|
|
1647
|
+
def _decode_text_format(text_format, value: str):
|
|
1625
1648
|
"""Parse a custom date format string and return a formatted number value."""
|
|
1626
|
-
custom_format_string =
|
|
1649
|
+
custom_format_string = text_format.custom_format_string
|
|
1627
1650
|
return custom_format_string.replace(CUSTOM_TEXT_PLACEHOLDER, value)
|
|
1628
1651
|
|
|
1629
1652
|
|
|
@@ -1638,7 +1661,7 @@ def _expand_quotes(value: str) -> str:
|
|
|
1638
1661
|
if current_char == "'":
|
|
1639
1662
|
if next_char is None:
|
|
1640
1663
|
break
|
|
1641
|
-
|
|
1664
|
+
if chars[index + 1] == "'":
|
|
1642
1665
|
formatted_value += "'"
|
|
1643
1666
|
index += 2
|
|
1644
1667
|
elif in_string:
|
|
@@ -1653,19 +1676,19 @@ def _expand_quotes(value: str) -> str:
|
|
|
1653
1676
|
return formatted_value
|
|
1654
1677
|
|
|
1655
1678
|
|
|
1656
|
-
def _decode_number_format(
|
|
1679
|
+
def _decode_number_format(number_format, value, name): # noqa: PLR0912
|
|
1657
1680
|
"""Parse a custom date format string and return a formatted number value."""
|
|
1658
|
-
custom_format_string =
|
|
1659
|
-
value *=
|
|
1660
|
-
if "%" in custom_format_string and
|
|
1681
|
+
custom_format_string = number_format.custom_format_string
|
|
1682
|
+
value *= number_format.scale_factor
|
|
1683
|
+
if "%" in custom_format_string and number_format.scale_factor == 1.0:
|
|
1661
1684
|
# Per cent scale has 100x but % does not
|
|
1662
1685
|
value *= 100.0
|
|
1663
1686
|
|
|
1664
|
-
if
|
|
1687
|
+
if number_format.currency_code != "":
|
|
1665
1688
|
# Replace currency code with symbol and no-break space
|
|
1666
1689
|
custom_format_string = custom_format_string.replace(
|
|
1667
1690
|
"\u00a4",
|
|
1668
|
-
|
|
1691
|
+
number_format.currency_code + "\u00a0",
|
|
1669
1692
|
)
|
|
1670
1693
|
|
|
1671
1694
|
if (match := re.search(r"([#0.,]+(E[+]\d+)?)", custom_format_string)) is None:
|
|
@@ -1695,7 +1718,7 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1695
1718
|
if num_decimals > 0:
|
|
1696
1719
|
if dec_part[0] == "#":
|
|
1697
1720
|
dec_pad = None
|
|
1698
|
-
elif
|
|
1721
|
+
elif number_format.num_nonspace_decimal_digits > 0:
|
|
1699
1722
|
dec_pad = CellPadding.ZERO
|
|
1700
1723
|
else:
|
|
1701
1724
|
dec_pad = CellPadding.SPACE
|
|
@@ -1712,15 +1735,15 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1712
1735
|
decimal = float(f"0.{decimal}")
|
|
1713
1736
|
|
|
1714
1737
|
num_integers = len(int_part.replace(",", ""))
|
|
1715
|
-
if not
|
|
1738
|
+
if not number_format.show_thousands_separator:
|
|
1716
1739
|
int_part = int_part.replace(",", "")
|
|
1717
1740
|
if num_integers > 0:
|
|
1718
1741
|
if int_part[0] == "#":
|
|
1719
1742
|
int_pad = None
|
|
1720
1743
|
int_width = len(int_part)
|
|
1721
|
-
elif
|
|
1744
|
+
elif number_format.num_nonspace_integer_digits > 0:
|
|
1722
1745
|
int_pad = CellPadding.ZERO
|
|
1723
|
-
if
|
|
1746
|
+
if number_format.show_thousands_separator:
|
|
1724
1747
|
num_commas = int(math.floor(math.log10(integer)) / 3) if integer != 0 else 0
|
|
1725
1748
|
num_commas = max([num_commas, int((num_integers - 1) / 3)])
|
|
1726
1749
|
int_width = num_integers + num_commas
|
|
@@ -1734,7 +1757,7 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1734
1757
|
int_width = num_integers
|
|
1735
1758
|
|
|
1736
1759
|
# value_1 = str(value).split(".")[0]
|
|
1737
|
-
# value_2 = sigfig
|
|
1760
|
+
# value_2 = sigfig(str(value).split(".")[1], sigfig=MAX_SIGNIFICANT_DIGITS, warn=False)
|
|
1738
1761
|
# int_pad_space_as_zero = (
|
|
1739
1762
|
# num_integers > 0
|
|
1740
1763
|
# and num_decimals > 0
|
|
@@ -1756,26 +1779,29 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1756
1779
|
formatted_value = "".rjust(int_width)
|
|
1757
1780
|
elif integer == 0 and int_pad is None and dec_pad == CellPadding.SPACE:
|
|
1758
1781
|
formatted_value = ""
|
|
1759
|
-
elif integer == 0 and int_pad == CellPadding.SPACE and dec_pad is not None:
|
|
1760
|
-
formatted_value = "".rjust(int_width)
|
|
1761
1782
|
elif (
|
|
1762
1783
|
integer == 0
|
|
1763
1784
|
and int_pad == CellPadding.SPACE
|
|
1764
|
-
and dec_pad is None
|
|
1765
|
-
|
|
1785
|
+
and dec_pad is not None
|
|
1786
|
+
or (
|
|
1787
|
+
integer == 0
|
|
1788
|
+
and int_pad == CellPadding.SPACE
|
|
1789
|
+
and dec_pad is None
|
|
1790
|
+
and len(str(decimal)) > num_decimals
|
|
1791
|
+
)
|
|
1766
1792
|
):
|
|
1767
1793
|
formatted_value = "".rjust(int_width)
|
|
1768
1794
|
elif int_pad_space_as_zero or int_pad == CellPadding.ZERO:
|
|
1769
|
-
if
|
|
1795
|
+
if number_format.show_thousands_separator:
|
|
1770
1796
|
formatted_value = f"{integer:0{int_width},}"
|
|
1771
1797
|
else:
|
|
1772
1798
|
formatted_value = f"{integer:0{int_width}}"
|
|
1773
1799
|
elif int_pad == CellPadding.SPACE:
|
|
1774
|
-
if
|
|
1800
|
+
if number_format.show_thousands_separator:
|
|
1775
1801
|
formatted_value = f"{integer:,}".rjust(int_width)
|
|
1776
1802
|
else:
|
|
1777
1803
|
formatted_value = str(integer).rjust(int_width)
|
|
1778
|
-
elif
|
|
1804
|
+
elif number_format.show_thousands_separator:
|
|
1779
1805
|
formatted_value = f"{integer:,}"
|
|
1780
1806
|
else:
|
|
1781
1807
|
formatted_value = str(integer)
|
|
@@ -1795,33 +1821,33 @@ def _decode_number_format(format, value, name): # noqa: PLR0912
|
|
|
1795
1821
|
return _expand_quotes(formatted_value)
|
|
1796
1822
|
|
|
1797
1823
|
|
|
1798
|
-
def _format_decimal(value: float,
|
|
1824
|
+
def _format_decimal(value: float, number_format, percent: bool = False) -> str:
|
|
1799
1825
|
if value is None:
|
|
1800
1826
|
return ""
|
|
1801
|
-
if value < 0 and
|
|
1827
|
+
if value < 0 and number_format.negative_style == 1:
|
|
1802
1828
|
accounting_style = False
|
|
1803
1829
|
value = -value
|
|
1804
|
-
elif value < 0 and
|
|
1830
|
+
elif value < 0 and number_format.negative_style >= 2:
|
|
1805
1831
|
accounting_style = True
|
|
1806
1832
|
value = -value
|
|
1807
1833
|
else:
|
|
1808
1834
|
accounting_style = False
|
|
1809
|
-
thousands = "," if
|
|
1835
|
+
thousands = "," if number_format.show_thousands_separator else ""
|
|
1810
1836
|
|
|
1811
|
-
if value.is_integer() and
|
|
1837
|
+
if value.is_integer() and number_format.decimal_places >= DECIMAL_PLACES_AUTO:
|
|
1812
1838
|
formatted_value = f"{int(value):{thousands}}"
|
|
1813
1839
|
else:
|
|
1814
|
-
if
|
|
1815
|
-
formatted_value = str(sigfig
|
|
1840
|
+
if number_format.decimal_places >= DECIMAL_PLACES_AUTO:
|
|
1841
|
+
formatted_value = str(sigfig(value, MAX_SIGNIFICANT_DIGITS, warn=False))
|
|
1816
1842
|
else:
|
|
1817
|
-
formatted_value = sigfig
|
|
1818
|
-
formatted_value = sigfig
|
|
1843
|
+
formatted_value = sigfig(value, MAX_SIGNIFICANT_DIGITS, type=str, warn=False)
|
|
1844
|
+
formatted_value = sigfig(
|
|
1819
1845
|
formatted_value,
|
|
1820
|
-
decimals=
|
|
1846
|
+
decimals=number_format.decimal_places,
|
|
1821
1847
|
type=str,
|
|
1822
1848
|
)
|
|
1823
|
-
if
|
|
1824
|
-
formatted_value = sigfig
|
|
1849
|
+
if number_format.show_thousands_separator:
|
|
1850
|
+
formatted_value = sigfig(formatted_value, spacer=",", spacing=3, type=str)
|
|
1825
1851
|
try:
|
|
1826
1852
|
(integer, decimal) = formatted_value.split(".")
|
|
1827
1853
|
formatted_value = integer + "." + decimal.replace(",", "")
|
|
@@ -1833,22 +1859,20 @@ def _format_decimal(value: float, format, percent: bool = False) -> str:
|
|
|
1833
1859
|
|
|
1834
1860
|
if accounting_style:
|
|
1835
1861
|
return f"({formatted_value})"
|
|
1836
|
-
|
|
1837
|
-
return formatted_value
|
|
1862
|
+
return formatted_value
|
|
1838
1863
|
|
|
1839
1864
|
|
|
1840
|
-
def _format_currency(value: float,
|
|
1841
|
-
formatted_value = _format_decimal(value,
|
|
1842
|
-
if
|
|
1843
|
-
symbol = CURRENCY_SYMBOLS[
|
|
1865
|
+
def _format_currency(value: float, number_format) -> str:
|
|
1866
|
+
formatted_value = _format_decimal(value, number_format)
|
|
1867
|
+
if number_format.currency_code in CURRENCY_SYMBOLS:
|
|
1868
|
+
symbol = CURRENCY_SYMBOLS[number_format.currency_code]
|
|
1844
1869
|
else:
|
|
1845
|
-
symbol =
|
|
1846
|
-
if
|
|
1870
|
+
symbol = number_format.currency_code + " "
|
|
1871
|
+
if number_format.use_accounting_style and value < 0:
|
|
1847
1872
|
return f"{symbol}\t({formatted_value[1:]})"
|
|
1848
|
-
|
|
1873
|
+
if number_format.use_accounting_style:
|
|
1849
1874
|
return f"{symbol}\t{formatted_value}"
|
|
1850
|
-
|
|
1851
|
-
return symbol + formatted_value
|
|
1875
|
+
return symbol + formatted_value
|
|
1852
1876
|
|
|
1853
1877
|
|
|
1854
1878
|
INT_TO_BASE_CHAR = [str(x) for x in range(10)] + [chr(x) for x in range(ord("A"), ord("Z") + 1)]
|
|
@@ -1868,49 +1892,45 @@ def _twos_complement(value: int, base: int) -> str:
|
|
|
1868
1892
|
|
|
1869
1893
|
if base == 2:
|
|
1870
1894
|
return bin(twos_complement_dec)[2:].rjust(num_bits, "1")
|
|
1871
|
-
|
|
1895
|
+
if base == 8:
|
|
1872
1896
|
return oct(twos_complement_dec)[2:]
|
|
1873
|
-
|
|
1874
|
-
return hex(twos_complement_dec)[2:].upper()
|
|
1897
|
+
return hex(twos_complement_dec)[2:].upper()
|
|
1875
1898
|
|
|
1876
1899
|
|
|
1877
|
-
def _format_base(value: float,
|
|
1900
|
+
def _format_base(value: float, number_format) -> str:
|
|
1878
1901
|
if value == 0:
|
|
1879
|
-
return "0".zfill(
|
|
1902
|
+
return "0".zfill(number_format.base_places)
|
|
1880
1903
|
|
|
1881
1904
|
value = round(value)
|
|
1882
1905
|
|
|
1883
1906
|
is_negative = False
|
|
1884
|
-
if not
|
|
1907
|
+
if not number_format.base_use_minus_sign and number_format.base in [2, 8, 16]:
|
|
1885
1908
|
if value < 0:
|
|
1886
|
-
return _twos_complement(value,
|
|
1887
|
-
|
|
1888
|
-
value = abs(value)
|
|
1909
|
+
return _twos_complement(value, number_format.base)
|
|
1910
|
+
value = abs(value)
|
|
1889
1911
|
elif value < 0:
|
|
1890
1912
|
is_negative = True
|
|
1891
1913
|
value = abs(value)
|
|
1892
1914
|
|
|
1893
1915
|
formatted_value = []
|
|
1894
1916
|
while value:
|
|
1895
|
-
formatted_value.append(int(value %
|
|
1896
|
-
value //=
|
|
1917
|
+
formatted_value.append(int(value % number_format.base))
|
|
1918
|
+
value //= number_format.base
|
|
1897
1919
|
formatted_value = "".join([INT_TO_BASE_CHAR[x] for x in formatted_value[::-1]])
|
|
1898
1920
|
|
|
1899
1921
|
if is_negative:
|
|
1900
|
-
return "-" + formatted_value.zfill(
|
|
1901
|
-
|
|
1902
|
-
return formatted_value.zfill(format.base_places)
|
|
1922
|
+
return "-" + formatted_value.zfill(number_format.base_places)
|
|
1923
|
+
return formatted_value.zfill(number_format.base_places)
|
|
1903
1924
|
|
|
1904
1925
|
|
|
1905
1926
|
def _format_fraction_parts_to(whole: int, numerator: int, denominator: int):
|
|
1906
1927
|
if whole > 0:
|
|
1907
1928
|
if numerator == 0:
|
|
1908
1929
|
return str(whole)
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
elif numerator == 0:
|
|
1930
|
+
return f"{whole} {numerator}/{denominator}"
|
|
1931
|
+
if numerator == 0:
|
|
1912
1932
|
return "0"
|
|
1913
|
-
|
|
1933
|
+
if numerator == denominator:
|
|
1914
1934
|
return "1"
|
|
1915
1935
|
return f"{numerator}/{denominator}"
|
|
1916
1936
|
|
|
@@ -1923,7 +1943,8 @@ def _float_to_fraction(value: float, denominator: int) -> str:
|
|
|
1923
1943
|
|
|
1924
1944
|
|
|
1925
1945
|
def _float_to_n_digit_fraction(value: float, max_digits: int) -> str:
|
|
1926
|
-
"""
|
|
1946
|
+
"""
|
|
1947
|
+
Convert a float to a fraction of a maxinum number of digits
|
|
1927
1948
|
and return as a string.
|
|
1928
1949
|
"""
|
|
1929
1950
|
max_denominator = 10**max_digits - 1
|
|
@@ -1935,35 +1956,33 @@ def _float_to_n_digit_fraction(value: float, max_digits: int) -> str:
|
|
|
1935
1956
|
return _format_fraction_parts_to(whole, numerator, denominator)
|
|
1936
1957
|
|
|
1937
1958
|
|
|
1938
|
-
def _format_fraction(value: float,
|
|
1939
|
-
accuracy =
|
|
1959
|
+
def _format_fraction(value: float, number_format) -> str:
|
|
1960
|
+
accuracy = number_format.fraction_accuracy
|
|
1940
1961
|
if accuracy & 0xFF000000:
|
|
1941
1962
|
num_digits = 0x100000000 - accuracy
|
|
1942
1963
|
return _float_to_n_digit_fraction(value, num_digits)
|
|
1943
|
-
|
|
1944
|
-
return _float_to_fraction(value, accuracy)
|
|
1964
|
+
return _float_to_fraction(value, accuracy)
|
|
1945
1965
|
|
|
1946
1966
|
|
|
1947
|
-
def _format_scientific(value: float,
|
|
1948
|
-
formatted_value = sigfig
|
|
1949
|
-
return f"{formatted_value:.{
|
|
1967
|
+
def _format_scientific(value: float, number_format) -> str:
|
|
1968
|
+
formatted_value = sigfig(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
|
|
1969
|
+
return f"{formatted_value:.{number_format.decimal_places}E}"
|
|
1950
1970
|
|
|
1951
1971
|
|
|
1952
|
-
def _unit_format(unit: str, value: int, style: int, abbrev:
|
|
1972
|
+
def _unit_format(unit: str, value: int, style: int, abbrev: str | None = None):
|
|
1953
1973
|
plural = "" if value == 1 else "s"
|
|
1954
1974
|
if abbrev is None:
|
|
1955
1975
|
abbrev = unit[0]
|
|
1956
1976
|
if style == DurationStyle.COMPACT:
|
|
1957
1977
|
return ""
|
|
1958
|
-
|
|
1978
|
+
if style == DurationStyle.SHORT:
|
|
1959
1979
|
return f"{abbrev}"
|
|
1960
|
-
|
|
1961
|
-
return f" {unit}" + plural
|
|
1980
|
+
return f" {unit}" + plural
|
|
1962
1981
|
|
|
1963
1982
|
|
|
1964
|
-
def _auto_units(cell_value,
|
|
1965
|
-
unit_largest =
|
|
1966
|
-
unit_smallest =
|
|
1983
|
+
def _auto_units(cell_value, number_format):
|
|
1984
|
+
unit_largest = number_format.duration_unit_largest
|
|
1985
|
+
unit_smallest = number_format.duration_unit_smallest
|
|
1967
1986
|
|
|
1968
1987
|
if cell_value == 0:
|
|
1969
1988
|
unit_largest = DurationUnits.DAY
|
|
@@ -1992,8 +2011,7 @@ def _auto_units(cell_value, format):
|
|
|
1992
2011
|
unit_smallest = DurationUnits.HOUR
|
|
1993
2012
|
elif cell_value % SECONDS_IN_WEEK:
|
|
1994
2013
|
unit_smallest = DurationUnits.DAY
|
|
1995
|
-
|
|
1996
|
-
unit_smallest = unit_largest
|
|
2014
|
+
unit_smallest = max(unit_smallest, unit_largest)
|
|
1997
2015
|
|
|
1998
2016
|
return unit_smallest, unit_largest
|
|
1999
2017
|
|
|
@@ -2004,7 +2022,8 @@ range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
|
|
|
2004
2022
|
|
|
2005
2023
|
|
|
2006
2024
|
def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
2007
|
-
"""
|
|
2025
|
+
"""
|
|
2026
|
+
Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
2008
2027
|
|
|
2009
2028
|
Parameters
|
|
2010
2029
|
----------
|
|
@@ -2015,6 +2034,7 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
2015
2034
|
-------
|
|
2016
2035
|
row, col: int, int
|
|
2017
2036
|
Cell row and column numbers (zero indexed).
|
|
2037
|
+
|
|
2018
2038
|
"""
|
|
2019
2039
|
if not cell_str:
|
|
2020
2040
|
return 0, 0
|
|
@@ -2028,11 +2048,9 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
2028
2048
|
row_str = match.group(4)
|
|
2029
2049
|
|
|
2030
2050
|
# Convert base26 column string to number.
|
|
2031
|
-
expn = 0
|
|
2032
2051
|
col = 0
|
|
2033
|
-
for char in reversed(col_str):
|
|
2052
|
+
for expn, char in enumerate(reversed(col_str)):
|
|
2034
2053
|
col += (ord(char) - ord("A") + 1) * (26**expn)
|
|
2035
|
-
expn += 1
|
|
2036
2054
|
|
|
2037
2055
|
# Convert 1-index to zero-index
|
|
2038
2056
|
row = int(row_str) - 1
|
|
@@ -2042,7 +2060,8 @@ def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
|
2042
2060
|
|
|
2043
2061
|
|
|
2044
2062
|
def xl_range(first_row, first_col, last_row, last_col):
|
|
2045
|
-
"""
|
|
2063
|
+
"""
|
|
2064
|
+
Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
2046
2065
|
|
|
2047
2066
|
Parameters
|
|
2048
2067
|
----------
|
|
@@ -2059,18 +2078,19 @@ def xl_range(first_row, first_col, last_row, last_col):
|
|
|
2059
2078
|
-------
|
|
2060
2079
|
str:
|
|
2061
2080
|
A1:B1 style range string.
|
|
2081
|
+
|
|
2062
2082
|
"""
|
|
2063
2083
|
range1 = xl_rowcol_to_cell(first_row, first_col)
|
|
2064
2084
|
range2 = xl_rowcol_to_cell(last_row, last_col)
|
|
2065
2085
|
|
|
2066
2086
|
if range1 == range2:
|
|
2067
2087
|
return range1
|
|
2068
|
-
|
|
2069
|
-
return range1 + ":" + range2
|
|
2088
|
+
return range1 + ":" + range2
|
|
2070
2089
|
|
|
2071
2090
|
|
|
2072
2091
|
def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
2073
|
-
"""
|
|
2092
|
+
"""
|
|
2093
|
+
Convert a zero indexed row and column cell reference to a A1 style string.
|
|
2074
2094
|
|
|
2075
2095
|
Parameters
|
|
2076
2096
|
----------
|
|
@@ -2087,6 +2107,7 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
|
2087
2107
|
-------
|
|
2088
2108
|
str:
|
|
2089
2109
|
A1 style string.
|
|
2110
|
+
|
|
2090
2111
|
"""
|
|
2091
2112
|
if row < 0:
|
|
2092
2113
|
msg = f"row reference {row} below zero"
|
|
@@ -2105,7 +2126,8 @@ def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
|
2105
2126
|
|
|
2106
2127
|
|
|
2107
2128
|
def xl_col_to_name(col, col_abs=False):
|
|
2108
|
-
"""
|
|
2129
|
+
"""
|
|
2130
|
+
Convert a zero indexed column cell reference to a string.
|
|
2109
2131
|
|
|
2110
2132
|
Parameters
|
|
2111
2133
|
----------
|
|
@@ -2118,6 +2140,7 @@ def xl_col_to_name(col, col_abs=False):
|
|
|
2118
2140
|
-------
|
|
2119
2141
|
str:
|
|
2120
2142
|
Column in A1 notation.
|
|
2143
|
+
|
|
2121
2144
|
"""
|
|
2122
2145
|
if col < 0:
|
|
2123
2146
|
msg = f"column reference {col} below zero"
|
|
@@ -2160,7 +2183,7 @@ class Formatting:
|
|
|
2160
2183
|
increment: float = 1.0
|
|
2161
2184
|
maximum: float = 100.0
|
|
2162
2185
|
minimum: float = 1.0
|
|
2163
|
-
popup_values:
|
|
2186
|
+
popup_values: list[str] = field(default_factory=lambda: ["Item 1"])
|
|
2164
2187
|
negative_style: NegativeNumberStyle = NegativeNumberStyle.MINUS
|
|
2165
2188
|
show_thousands_separator: bool = False
|
|
2166
2189
|
type: FormattingType = FormattingType.NUMBER
|
|
@@ -2188,7 +2211,8 @@ class Formatting:
|
|
|
2188
2211
|
raise TypeError(msg)
|
|
2189
2212
|
|
|
2190
2213
|
if self.type == FormattingType.CURRENCY and self.currency_code not in CURRENCIES:
|
|
2191
|
-
|
|
2214
|
+
msg = f"Unsupported currency code '{self.currency_code}'"
|
|
2215
|
+
raise TypeError(msg)
|
|
2192
2216
|
|
|
2193
2217
|
if self.decimal_places is None:
|
|
2194
2218
|
if self.type == FormattingType.CURRENCY:
|
|
@@ -2227,7 +2251,8 @@ class CustomFormatting:
|
|
|
2227
2251
|
raise TypeError(msg)
|
|
2228
2252
|
|
|
2229
2253
|
if self.type == CustomFormattingType.TEXT and self.format.count("%s") > 1:
|
|
2230
|
-
|
|
2254
|
+
msg = "Custom formats only allow one text substitution"
|
|
2255
|
+
raise TypeError(msg)
|
|
2231
2256
|
|
|
2232
2257
|
@classmethod
|
|
2233
2258
|
def from_archive(cls, archive: object):
|