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