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