numbers-parser 4.14.4__py3-none-any.whl → 4.16.1__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 +1 -0
- numbers_parser/_unpack_numbers.py +1 -7
- numbers_parser/cell.py +28 -202
- numbers_parser/constants.py +19 -6
- numbers_parser/document.py +54 -11
- numbers_parser/formula.py +423 -56
- numbers_parser/generated/TSCEArchives_pb2.py +209 -193
- numbers_parser/generated/TSSArchives_pb2.py +36 -36
- numbers_parser/generated/TSTArchives_pb2.py +328 -332
- numbers_parser/generated/mapping.py +1 -2
- numbers_parser/model.py +399 -188
- numbers_parser/numbers_cache.py +1 -1
- numbers_parser/numbers_uuid.py +6 -0
- numbers_parser/tokenizer.py +548 -0
- numbers_parser/xrefs.py +850 -0
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/METADATA +26 -28
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/RECORD +24 -22
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info}/WHEEL +2 -1
- numbers_parser-4.16.1.dist-info/entry_points.txt +4 -0
- numbers_parser-4.16.1.dist-info/top_level.txt +1 -0
- numbers_parser/data/empty.numbers +0 -0
- numbers_parser-4.14.4.dist-info/entry_points.txt +0 -5
- {numbers_parser-4.14.4.dist-info → numbers_parser-4.16.1.dist-info/licenses}/LICENSE.rst +0 -0
numbers_parser/__init__.py
CHANGED
|
@@ -13,6 +13,7 @@ with warnings.catch_warnings():
|
|
|
13
13
|
from numbers_parser.constants import * # noqa: F403
|
|
14
14
|
from numbers_parser.document import * # noqa: F403
|
|
15
15
|
from numbers_parser.exceptions import * # noqa: F403
|
|
16
|
+
from numbers_parser.xrefs import * # noqa: F403
|
|
16
17
|
|
|
17
18
|
__version__ = importlib.metadata.version("numbers-parser")
|
|
18
19
|
|
|
@@ -49,13 +49,7 @@ class NumbersUnpacker(IWorkHandler):
|
|
|
49
49
|
if self.compact_json or self.pretty:
|
|
50
50
|
formatter = Formatter()
|
|
51
51
|
formatter.indent_spaces = 2
|
|
52
|
-
formatter.
|
|
53
|
-
formatter.max_compact_list_complexity = 100
|
|
54
|
-
formatter.max_inline_length = 160
|
|
55
|
-
formatter.max_compact_list_complexity = 2
|
|
56
|
-
formatter.simple_bracket_padding = True
|
|
57
|
-
formatter.nested_bracket_padding = False
|
|
58
|
-
formatter.always_expand_depth = 10
|
|
52
|
+
formatter.max_inline_length = 180
|
|
59
53
|
pretty_json = formatter.serialize(data)
|
|
60
54
|
out.write(pretty_json)
|
|
61
55
|
else:
|
numbers_parser/cell.py
CHANGED
|
@@ -22,6 +22,7 @@ from numbers_parser.constants import (
|
|
|
22
22
|
CURRENCY_CELL_TYPE,
|
|
23
23
|
CUSTOM_TEXT_PLACEHOLDER,
|
|
24
24
|
DATETIME_FIELD_MAP,
|
|
25
|
+
DECIMAL128_BIAS,
|
|
25
26
|
DECIMAL_PLACES_AUTO,
|
|
26
27
|
DEFAULT_ALIGNMENT,
|
|
27
28
|
DEFAULT_BORDER_COLOR,
|
|
@@ -55,6 +56,7 @@ from numbers_parser.constants import (
|
|
|
55
56
|
)
|
|
56
57
|
from numbers_parser.currencies import CURRENCIES, CURRENCY_SYMBOLS
|
|
57
58
|
from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
|
|
59
|
+
from numbers_parser.formula import Formula
|
|
58
60
|
from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
|
|
59
61
|
from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
|
|
60
62
|
from numbers_parser.generated.TSWPArchives_pb2 import (
|
|
@@ -62,12 +64,14 @@ from numbers_parser.generated.TSWPArchives_pb2 import (
|
|
|
62
64
|
)
|
|
63
65
|
from numbers_parser.numbers_cache import Cacheable, cache
|
|
64
66
|
from numbers_parser.numbers_uuid import NumbersUUID
|
|
67
|
+
from numbers_parser.xrefs import xl_range
|
|
65
68
|
|
|
66
69
|
logger = logging.getLogger(numbers_parser_name)
|
|
67
70
|
debug = logger.debug
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
__all__ = [
|
|
74
|
+
"RGB",
|
|
71
75
|
"Alignment",
|
|
72
76
|
"BackgroundImage",
|
|
73
77
|
"BoolCell",
|
|
@@ -88,14 +92,9 @@ __all__ = [
|
|
|
88
92
|
"MergedCell",
|
|
89
93
|
"NumberCell",
|
|
90
94
|
"RichTextCell",
|
|
91
|
-
"RGB",
|
|
92
95
|
"Style",
|
|
93
96
|
"TextCell",
|
|
94
97
|
"VerticalJustification",
|
|
95
|
-
"xl_cell_to_rowcol",
|
|
96
|
-
"xl_col_to_name",
|
|
97
|
-
"xl_range",
|
|
98
|
-
"xl_rowcol_to_cell",
|
|
99
98
|
]
|
|
100
99
|
|
|
101
100
|
|
|
@@ -562,8 +561,6 @@ class CellStorageFlags:
|
|
|
562
561
|
_rich_id: int = None
|
|
563
562
|
_cell_style_id: int = None
|
|
564
563
|
_text_style_id: int = None
|
|
565
|
-
# _cond_style_id: int = None
|
|
566
|
-
# _cond_rule_style_id: int = None
|
|
567
564
|
_formula_id: int = None
|
|
568
565
|
_control_id: int = None
|
|
569
566
|
_formula_error_id: int = None
|
|
@@ -574,8 +571,6 @@ class CellStorageFlags:
|
|
|
574
571
|
_duration_format_id: int = None
|
|
575
572
|
_text_format_id: int = None
|
|
576
573
|
_bool_format_id: int = None
|
|
577
|
-
# _comment_id: int = None
|
|
578
|
-
# _import_warning_id: int = None
|
|
579
574
|
|
|
580
575
|
def __str__(self) -> str:
|
|
581
576
|
fields = [
|
|
@@ -599,6 +594,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
599
594
|
self.row = row
|
|
600
595
|
self.col = col
|
|
601
596
|
self._is_bulleted = False
|
|
597
|
+
self._formula_id = None
|
|
602
598
|
self._storage = None
|
|
603
599
|
self._style = None
|
|
604
600
|
self._d128 = None
|
|
@@ -641,11 +637,9 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
641
637
|
@property
|
|
642
638
|
def is_formula(self) -> bool:
|
|
643
639
|
"""bool: ``True`` if the cell contains a formula."""
|
|
644
|
-
|
|
645
|
-
return table_formulas.is_formula(self.row, self.col)
|
|
640
|
+
return self._formula_id is not None
|
|
646
641
|
|
|
647
642
|
@property
|
|
648
|
-
@cache(num_args=0)
|
|
649
643
|
def formula(self) -> str:
|
|
650
644
|
"""
|
|
651
645
|
str: The formula in a cell.
|
|
@@ -666,6 +660,17 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
666
660
|
return table_formulas.formula(self._formula_id, self.row, self.col)
|
|
667
661
|
return None
|
|
668
662
|
|
|
663
|
+
@formula.setter
|
|
664
|
+
def formula(self, value: str) -> None:
|
|
665
|
+
self._formula_id = Formula.from_str(
|
|
666
|
+
self._model,
|
|
667
|
+
self._table_id,
|
|
668
|
+
self.row,
|
|
669
|
+
self.col,
|
|
670
|
+
value,
|
|
671
|
+
)
|
|
672
|
+
self._model.add_formula_dependency(self.row, self.col, self._table_id)
|
|
673
|
+
|
|
669
674
|
@property
|
|
670
675
|
def is_bulleted(self) -> bool:
|
|
671
676
|
"""bool: ``True`` if the cell contains text bullets."""
|
|
@@ -740,7 +745,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
740
745
|
or self._bool_format_id is not None
|
|
741
746
|
):
|
|
742
747
|
return self._custom_format()
|
|
743
|
-
return str(self.value)
|
|
748
|
+
return str(self.value).upper() if isinstance(self.value, bool) else str(self.value)
|
|
744
749
|
|
|
745
750
|
@property
|
|
746
751
|
def style(self) -> Style | None:
|
|
@@ -872,20 +877,16 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
872
877
|
storage_flags._text_style_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
873
878
|
offset += 4
|
|
874
879
|
if flags & 0x80:
|
|
875
|
-
#
|
|
880
|
+
# cond_style_id skipped
|
|
876
881
|
offset += 4
|
|
877
|
-
#
|
|
878
|
-
# storage_flags._cond_rule_style_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
879
|
-
# offset += 4
|
|
882
|
+
# Skip flag 0x100 (cond_rule_style_id)
|
|
880
883
|
if flags & 0x200:
|
|
881
884
|
storage_flags._formula_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
882
885
|
offset += 4
|
|
883
886
|
if flags & 0x400:
|
|
884
887
|
storage_flags._control_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
885
888
|
offset += 4
|
|
886
|
-
#
|
|
887
|
-
# storage_flags._formula_error_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
888
|
-
# offset += 4
|
|
889
|
+
# Skip flag 0x800 (formula_error_id)
|
|
889
890
|
if flags & 0x1000:
|
|
890
891
|
storage_flags._suggest_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
891
892
|
offset += 4
|
|
@@ -909,12 +910,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
909
910
|
if flags & 0x40000:
|
|
910
911
|
storage_flags._bool_format_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
911
912
|
offset += 4
|
|
912
|
-
#
|
|
913
|
-
# cstorage_flags._omment_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
914
|
-
# offset += 4
|
|
915
|
-
# if flags & 0x100000:
|
|
916
|
-
# storage_flags._import_warning_id = unpack("<i", buffer[offset : offset + 4])[0]
|
|
917
|
-
# offset += 4
|
|
913
|
+
# Skip 0x80000 (comment_id) and 0x100000 (import_warning_id)
|
|
918
914
|
|
|
919
915
|
cell_type = buffer[1]
|
|
920
916
|
if cell_type == TSTArchives.genericCellType:
|
|
@@ -1032,7 +1028,6 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1032
1028
|
flags = 4
|
|
1033
1029
|
length += 8
|
|
1034
1030
|
cell_type = TSTArchives.dateCellType
|
|
1035
|
-
# date_delta = self._value.astimezone() - EPOCH
|
|
1036
1031
|
if self._value.tzinfo is None:
|
|
1037
1032
|
date_delta = self._value - EPOCH
|
|
1038
1033
|
else:
|
|
@@ -1104,7 +1099,6 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
1104
1099
|
length += 4
|
|
1105
1100
|
storage += pack("<i", self._num_format_id)
|
|
1106
1101
|
storage[6] |= 1
|
|
1107
|
-
# storage[6:8] = pack("<h", 1)
|
|
1108
1102
|
if self._currency_format_id is not None:
|
|
1109
1103
|
flags |= 0x4000
|
|
1110
1104
|
length += 4
|
|
@@ -1560,8 +1554,8 @@ class MergedCell(Cell):
|
|
|
1560
1554
|
def _pack_decimal128(value: float) -> bytearray:
|
|
1561
1555
|
buffer = bytearray(16)
|
|
1562
1556
|
exp = math.floor(math.log10(math.e) * math.log(abs(value))) if value != 0.0 else 0
|
|
1563
|
-
exp +=
|
|
1564
|
-
mantissa = abs(int(value / math.pow(10, exp -
|
|
1557
|
+
exp += DECIMAL128_BIAS - 16
|
|
1558
|
+
mantissa = abs(int(value / math.pow(10, exp - DECIMAL128_BIAS)))
|
|
1565
1559
|
buffer[15] |= exp >> 7
|
|
1566
1560
|
buffer[14] |= (exp & 0x7F) << 1
|
|
1567
1561
|
i = 0
|
|
@@ -1575,7 +1569,7 @@ def _pack_decimal128(value: float) -> bytearray:
|
|
|
1575
1569
|
|
|
1576
1570
|
|
|
1577
1571
|
def _unpack_decimal128(buffer: bytearray) -> float:
|
|
1578
|
-
exp = (((buffer[15] & 0x7F) << 7) | (buffer[14] >> 1)) -
|
|
1572
|
+
exp = (((buffer[15] & 0x7F) << 7) | (buffer[14] >> 1)) - DECIMAL128_BIAS
|
|
1579
1573
|
mantissa = buffer[14] & 1
|
|
1580
1574
|
for i in range(13, -1, -1):
|
|
1581
1575
|
mantissa = mantissa * 256 + buffer[i]
|
|
@@ -1756,16 +1750,6 @@ def _decode_number_format(number_format, value, name): # noqa: PLR0912
|
|
|
1756
1750
|
int_pad = None
|
|
1757
1751
|
int_width = num_integers
|
|
1758
1752
|
|
|
1759
|
-
# value_1 = str(value).split(".")[0]
|
|
1760
|
-
# value_2 = sigfig(str(value).split(".")[1], sigfig=MAX_SIGNIFICANT_DIGITS, warn=False)
|
|
1761
|
-
# int_pad_space_as_zero = (
|
|
1762
|
-
# num_integers > 0
|
|
1763
|
-
# and num_decimals > 0
|
|
1764
|
-
# and int_pad == CellPadding.SPACE
|
|
1765
|
-
# and dec_pad is None
|
|
1766
|
-
# and num_integers > len(value_1)
|
|
1767
|
-
# and num_decimals > len(value_2)
|
|
1768
|
-
# )
|
|
1769
1753
|
int_pad_space_as_zero = False
|
|
1770
1754
|
|
|
1771
1755
|
# Formatting integer zero:
|
|
@@ -1779,16 +1763,11 @@ def _decode_number_format(number_format, value, name): # noqa: PLR0912
|
|
|
1779
1763
|
formatted_value = "".rjust(int_width)
|
|
1780
1764
|
elif integer == 0 and int_pad is None and dec_pad == CellPadding.SPACE:
|
|
1781
1765
|
formatted_value = ""
|
|
1782
|
-
elif (
|
|
1766
|
+
elif (integer == 0 and int_pad == CellPadding.SPACE and dec_pad is not None) or (
|
|
1783
1767
|
integer == 0
|
|
1784
1768
|
and int_pad == CellPadding.SPACE
|
|
1785
|
-
and dec_pad is
|
|
1786
|
-
|
|
1787
|
-
integer == 0
|
|
1788
|
-
and int_pad == CellPadding.SPACE
|
|
1789
|
-
and dec_pad is None
|
|
1790
|
-
and len(str(decimal)) > num_decimals
|
|
1791
|
-
)
|
|
1769
|
+
and dec_pad is None
|
|
1770
|
+
and len(str(decimal)) > num_decimals
|
|
1792
1771
|
):
|
|
1793
1772
|
formatted_value = "".rjust(int_width)
|
|
1794
1773
|
elif int_pad_space_as_zero or int_pad == CellPadding.ZERO:
|
|
@@ -2016,159 +1995,6 @@ def _auto_units(cell_value, number_format):
|
|
|
2016
1995
|
return unit_smallest, unit_largest
|
|
2017
1996
|
|
|
2018
1997
|
|
|
2019
|
-
# Cell reference conversion from https://github.com/jmcnamara/XlsxWriter
|
|
2020
|
-
# Copyright (c) 2013-2021, John McNamara <jmcnamara@cpan.org>
|
|
2021
|
-
range_parts = re.compile(r"(\$?)([A-Z]{1,3})(\$?)(\d+)")
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
def xl_cell_to_rowcol(cell_str: str) -> tuple:
|
|
2025
|
-
"""
|
|
2026
|
-
Convert a cell reference in A1 notation to a zero indexed row and column.
|
|
2027
|
-
|
|
2028
|
-
Parameters
|
|
2029
|
-
----------
|
|
2030
|
-
cell_str: str
|
|
2031
|
-
A1 notation cell reference
|
|
2032
|
-
|
|
2033
|
-
Returns
|
|
2034
|
-
-------
|
|
2035
|
-
row, col: int, int
|
|
2036
|
-
Cell row and column numbers (zero indexed).
|
|
2037
|
-
|
|
2038
|
-
"""
|
|
2039
|
-
if not cell_str:
|
|
2040
|
-
return 0, 0
|
|
2041
|
-
|
|
2042
|
-
match = range_parts.match(cell_str)
|
|
2043
|
-
if not match:
|
|
2044
|
-
msg = f"invalid cell reference {cell_str}"
|
|
2045
|
-
raise IndexError(msg)
|
|
2046
|
-
|
|
2047
|
-
col_str = match.group(2)
|
|
2048
|
-
row_str = match.group(4)
|
|
2049
|
-
|
|
2050
|
-
# Convert base26 column string to number.
|
|
2051
|
-
col = 0
|
|
2052
|
-
for expn, char in enumerate(reversed(col_str)):
|
|
2053
|
-
col += (ord(char) - ord("A") + 1) * (26**expn)
|
|
2054
|
-
|
|
2055
|
-
# Convert 1-index to zero-index
|
|
2056
|
-
row = int(row_str) - 1
|
|
2057
|
-
col -= 1
|
|
2058
|
-
|
|
2059
|
-
return row, col
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
def xl_range(first_row, first_col, last_row, last_col):
|
|
2063
|
-
"""
|
|
2064
|
-
Convert zero indexed row and col cell references to a A1:B1 range string.
|
|
2065
|
-
|
|
2066
|
-
Parameters
|
|
2067
|
-
----------
|
|
2068
|
-
first_row: int
|
|
2069
|
-
The first cell row.
|
|
2070
|
-
first_col: int
|
|
2071
|
-
The first cell column.
|
|
2072
|
-
last_row: int
|
|
2073
|
-
The last cell row.
|
|
2074
|
-
last_col: int
|
|
2075
|
-
The last cell column.
|
|
2076
|
-
|
|
2077
|
-
Returns
|
|
2078
|
-
-------
|
|
2079
|
-
str:
|
|
2080
|
-
A1:B1 style range string.
|
|
2081
|
-
|
|
2082
|
-
"""
|
|
2083
|
-
range1 = xl_rowcol_to_cell(first_row, first_col)
|
|
2084
|
-
range2 = xl_rowcol_to_cell(last_row, last_col)
|
|
2085
|
-
|
|
2086
|
-
if range1 == range2:
|
|
2087
|
-
return range1
|
|
2088
|
-
return range1 + ":" + range2
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
def xl_rowcol_to_cell(row, col, row_abs=False, col_abs=False):
|
|
2092
|
-
"""
|
|
2093
|
-
Convert a zero indexed row and column cell reference to a A1 style string.
|
|
2094
|
-
|
|
2095
|
-
Parameters
|
|
2096
|
-
----------
|
|
2097
|
-
row: int
|
|
2098
|
-
The cell row.
|
|
2099
|
-
col: int
|
|
2100
|
-
The cell column.
|
|
2101
|
-
row_abs: bool
|
|
2102
|
-
If ``True``, make the row absolute.
|
|
2103
|
-
col_abs: bool
|
|
2104
|
-
If ``True``, make the column absolute.
|
|
2105
|
-
|
|
2106
|
-
Returns
|
|
2107
|
-
-------
|
|
2108
|
-
str:
|
|
2109
|
-
A1 style string.
|
|
2110
|
-
|
|
2111
|
-
"""
|
|
2112
|
-
if row < 0:
|
|
2113
|
-
msg = f"row reference {row} below zero"
|
|
2114
|
-
raise IndexError(msg)
|
|
2115
|
-
|
|
2116
|
-
if col < 0:
|
|
2117
|
-
msg = f"column reference {col} below zero"
|
|
2118
|
-
raise IndexError(msg)
|
|
2119
|
-
|
|
2120
|
-
row += 1 # Change to 1-index.
|
|
2121
|
-
row_abs = "$" if row_abs else ""
|
|
2122
|
-
|
|
2123
|
-
col_str = xl_col_to_name(col, col_abs)
|
|
2124
|
-
|
|
2125
|
-
return col_str + row_abs + str(row)
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
def xl_col_to_name(col, col_abs=False):
|
|
2129
|
-
"""
|
|
2130
|
-
Convert a zero indexed column cell reference to a string.
|
|
2131
|
-
|
|
2132
|
-
Parameters
|
|
2133
|
-
----------
|
|
2134
|
-
col: int
|
|
2135
|
-
The column number (zero indexed).
|
|
2136
|
-
col_abs: bool, default: False
|
|
2137
|
-
If ``True``, make the column absolute.
|
|
2138
|
-
|
|
2139
|
-
Returns
|
|
2140
|
-
-------
|
|
2141
|
-
str:
|
|
2142
|
-
Column in A1 notation.
|
|
2143
|
-
|
|
2144
|
-
"""
|
|
2145
|
-
if col < 0:
|
|
2146
|
-
msg = f"column reference {col} below zero"
|
|
2147
|
-
raise IndexError(msg)
|
|
2148
|
-
|
|
2149
|
-
col += 1 # Change to 1-index.
|
|
2150
|
-
col_str = ""
|
|
2151
|
-
col_abs = "$" if col_abs else ""
|
|
2152
|
-
|
|
2153
|
-
while col:
|
|
2154
|
-
# Set remainder from 1 .. 26
|
|
2155
|
-
remainder = col % 26
|
|
2156
|
-
|
|
2157
|
-
if remainder == 0:
|
|
2158
|
-
remainder = 26
|
|
2159
|
-
|
|
2160
|
-
# Convert the remainder to a character.
|
|
2161
|
-
col_letter = chr(ord("A") + remainder - 1)
|
|
2162
|
-
|
|
2163
|
-
# Accumulate the column letters, right to left.
|
|
2164
|
-
col_str = col_letter + col_str
|
|
2165
|
-
|
|
2166
|
-
# Get the next order of magnitude.
|
|
2167
|
-
col = int((col - 1) / 26)
|
|
2168
|
-
|
|
2169
|
-
return col_abs + col_str
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
1998
|
@dataclass()
|
|
2173
1999
|
class Formatting:
|
|
2174
2000
|
allow_none: bool = False
|
numbers_parser/constants.py
CHANGED
|
@@ -5,23 +5,24 @@ from math import ceil
|
|
|
5
5
|
|
|
6
6
|
import enum_tools.documentation
|
|
7
7
|
|
|
8
|
+
# Path to package date varies by Python version
|
|
8
9
|
try:
|
|
9
10
|
from importlib.resources import files
|
|
10
|
-
# Can't cover exception using modern python
|
|
11
11
|
except ImportError: # pragma: nocover
|
|
12
12
|
from importlib_resources import files
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
-
"CellType",
|
|
16
|
-
"PaddingType",
|
|
17
15
|
"CellPadding",
|
|
16
|
+
"CellType",
|
|
17
|
+
"CellValueType",
|
|
18
|
+
"ControlFormattingType",
|
|
18
19
|
"DurationStyle",
|
|
19
20
|
"DurationUnits",
|
|
20
21
|
"FormatType",
|
|
21
22
|
"FormattingType",
|
|
22
|
-
"NegativeNumberStyle",
|
|
23
23
|
"FractionAccuracy",
|
|
24
|
-
"
|
|
24
|
+
"NegativeNumberStyle",
|
|
25
|
+
"PaddingType",
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
DEFAULT_DOCUMENT = files("numbers_parser") / "data" / "empty.numbers"
|
|
@@ -51,6 +52,8 @@ DEFAULT_DATETIME_FORMAT = "dd MMM YYY HH:MM"
|
|
|
51
52
|
CHECKBOX_FALSE_VALUE = "☐"
|
|
52
53
|
CHECKBOX_TRUE_VALUE = "☑"
|
|
53
54
|
STAR_RATING_VALUE = "★"
|
|
55
|
+
OPERATOR_PRECEDENCE = {"%": 6, "^": 5, "×": 4, "*": 4, "/": 4, "÷": 4, "+": 3, "-": 3, "&": 2}
|
|
56
|
+
|
|
54
57
|
|
|
55
58
|
# Numbers limits
|
|
56
59
|
MAX_TILE_SIZE = 256
|
|
@@ -69,6 +72,7 @@ EPOCH = datetime(2001, 1, 1) # noqa: DTZ001
|
|
|
69
72
|
SECONDS_IN_HOUR = 60 * 60
|
|
70
73
|
SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
|
|
71
74
|
SECONDS_IN_WEEK = SECONDS_IN_DAY * 7
|
|
75
|
+
DECIMAL128_BIAS = 0x1820
|
|
72
76
|
|
|
73
77
|
# File format enumerations
|
|
74
78
|
DECIMAL_PLACES_AUTO = 253
|
|
@@ -91,6 +95,7 @@ SUPPORTED_NUMBERS_VERSIONS = [
|
|
|
91
95
|
"14.1",
|
|
92
96
|
"14.2",
|
|
93
97
|
"14.3",
|
|
98
|
+
"14.4",
|
|
94
99
|
]
|
|
95
100
|
|
|
96
101
|
|
|
@@ -108,7 +113,7 @@ def _day_of_year(value: datetime) -> int:
|
|
|
108
113
|
|
|
109
114
|
def _week_of_month(value: datetime) -> int:
|
|
110
115
|
"""Return the week number in a month for a datetime."""
|
|
111
|
-
return
|
|
116
|
+
return ceil((value.day + value.replace(day=1).weekday()) / 7.0)
|
|
112
117
|
|
|
113
118
|
|
|
114
119
|
DATETIME_FIELD_MAP = OrderedDict(
|
|
@@ -395,6 +400,14 @@ class CellInteractionType(IntEnum):
|
|
|
395
400
|
TOGGLE = 8
|
|
396
401
|
|
|
397
402
|
|
|
403
|
+
class CellValueType(IntEnum):
|
|
404
|
+
NIL_TYPE = 1
|
|
405
|
+
BOOLEAN_TYPE = 2
|
|
406
|
+
DATE_TYPE = 3
|
|
407
|
+
NUMBER_TYPE = 4
|
|
408
|
+
STRING_TYPE = 5
|
|
409
|
+
|
|
410
|
+
|
|
398
411
|
CONTROL_CELL_TYPE_MAP = {
|
|
399
412
|
FormattingType.POPUP: CellInteractionType.POPUP,
|
|
400
413
|
FormattingType.SLIDER: CellInteractionType.SLIDER,
|
numbers_parser/document.py
CHANGED
|
@@ -17,8 +17,6 @@ from numbers_parser.cell import (
|
|
|
17
17
|
Style,
|
|
18
18
|
TextCell,
|
|
19
19
|
UnsupportedWarning,
|
|
20
|
-
xl_cell_to_rowcol,
|
|
21
|
-
xl_range,
|
|
22
20
|
)
|
|
23
21
|
from numbers_parser.constants import (
|
|
24
22
|
CUSTOM_FORMATTING_ALLOWED_CELLS,
|
|
@@ -33,6 +31,7 @@ from numbers_parser.constants import (
|
|
|
33
31
|
from numbers_parser.containers import ItemsList
|
|
34
32
|
from numbers_parser.model import _NumbersModel
|
|
35
33
|
from numbers_parser.numbers_cache import Cacheable
|
|
34
|
+
from numbers_parser.xrefs import xl_cell_to_rowcol, xl_range
|
|
36
35
|
|
|
37
36
|
if TYPE_CHECKING: # pragma: nocover
|
|
38
37
|
from collections.abc import Iterator
|
|
@@ -41,14 +40,6 @@ if TYPE_CHECKING: # pragma: nocover
|
|
|
41
40
|
__all__ = ["Document", "Sheet", "Table"]
|
|
42
41
|
|
|
43
42
|
|
|
44
|
-
# class Sheet:
|
|
45
|
-
# pass
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# class Table:
|
|
49
|
-
# pass
|
|
50
|
-
|
|
51
|
-
|
|
52
43
|
class Document:
|
|
53
44
|
"""
|
|
54
45
|
Create an instance of a new Numbers document.
|
|
@@ -379,6 +370,8 @@ class Sheet:
|
|
|
379
370
|
y: float | None = None,
|
|
380
371
|
num_rows: int | None = DEFAULT_ROW_COUNT,
|
|
381
372
|
num_cols: int | None = DEFAULT_COLUMN_COUNT,
|
|
373
|
+
num_header_rows: int | None = 1,
|
|
374
|
+
num_header_cols: int | None = 1,
|
|
382
375
|
) -> Table:
|
|
383
376
|
"""
|
|
384
377
|
Add a new table to the current sheet.
|
|
@@ -411,6 +404,10 @@ class Sheet:
|
|
|
411
404
|
The number of rows for the new table.
|
|
412
405
|
num_cols: int, optional, default: 10
|
|
413
406
|
The number of columns for the new table.
|
|
407
|
+
num_header_rows: int, optional, default: 1
|
|
408
|
+
The number of header rows for the new table.
|
|
409
|
+
num_header_cols: int, optional, default: 1
|
|
410
|
+
The number of header columns for the new table.
|
|
414
411
|
|
|
415
412
|
Returns
|
|
416
413
|
-------
|
|
@@ -423,7 +420,16 @@ class Sheet:
|
|
|
423
420
|
|
|
424
421
|
"""
|
|
425
422
|
from_table_id = self._tables[-1]._table_id
|
|
426
|
-
return self._add_table(
|
|
423
|
+
return self._add_table(
|
|
424
|
+
table_name,
|
|
425
|
+
from_table_id,
|
|
426
|
+
x,
|
|
427
|
+
y,
|
|
428
|
+
num_rows,
|
|
429
|
+
num_cols,
|
|
430
|
+
num_header_rows,
|
|
431
|
+
num_header_cols,
|
|
432
|
+
)
|
|
427
433
|
|
|
428
434
|
def _add_table(
|
|
429
435
|
self,
|
|
@@ -433,6 +439,8 @@ class Sheet:
|
|
|
433
439
|
y,
|
|
434
440
|
num_rows,
|
|
435
441
|
num_cols,
|
|
442
|
+
num_header_rows,
|
|
443
|
+
num_header_cols,
|
|
436
444
|
) -> object:
|
|
437
445
|
if table_name is not None:
|
|
438
446
|
if table_name in self._tables:
|
|
@@ -452,6 +460,8 @@ class Sheet:
|
|
|
452
460
|
y,
|
|
453
461
|
num_rows,
|
|
454
462
|
num_cols,
|
|
463
|
+
num_header_rows,
|
|
464
|
+
num_header_cols,
|
|
455
465
|
)
|
|
456
466
|
self._tables.append(Table(self._model, new_table_id))
|
|
457
467
|
return self._tables[-1]
|
|
@@ -968,6 +978,11 @@ class Table(Cacheable):
|
|
|
968
978
|
self._data[row][col]._model = self._model
|
|
969
979
|
self._data[row][col]._set_merge(merge_cells.get((row, col)))
|
|
970
980
|
|
|
981
|
+
if row < self._model.num_header_rows(self._table_id) or col < self._model.num_header_cols(
|
|
982
|
+
self._table_id,
|
|
983
|
+
):
|
|
984
|
+
self._model.name_ref_cache.mark_dirty()
|
|
985
|
+
|
|
971
986
|
if style is not None:
|
|
972
987
|
self.set_cell_style(row, col, style)
|
|
973
988
|
|
|
@@ -984,6 +999,34 @@ class Table(Cacheable):
|
|
|
984
999
|
msg = "style must be a Style object or style name"
|
|
985
1000
|
raise TypeError(msg)
|
|
986
1001
|
|
|
1002
|
+
def categorized_data(self) -> dict | None:
|
|
1003
|
+
"""
|
|
1004
|
+
Return the table's data organised into categories, if enabled or ``None``
|
|
1005
|
+
if the table has not had categoried enabled.
|
|
1006
|
+
|
|
1007
|
+
The data is a dictionary with the category names as keys and a list
|
|
1008
|
+
dictionaries for each row in that category of the table. The table heading
|
|
1009
|
+
row is used as the keys for the row dictionary.
|
|
1010
|
+
|
|
1011
|
+
Example
|
|
1012
|
+
-------
|
|
1013
|
+
.. code:: python
|
|
1014
|
+
|
|
1015
|
+
"Transport": [
|
|
1016
|
+
{"Description": "Airplane", "Category": "Transport" },
|
|
1017
|
+
{"Description": "Bicycle", "Category": "Transport" },
|
|
1018
|
+
{"Description": "Bus", "Category": "Transport"},
|
|
1019
|
+
],
|
|
1020
|
+
"Fruit": [
|
|
1021
|
+
{"Description": "Apple", "Category": "Fruit" },
|
|
1022
|
+
{"Description": "Banana", "Category": "Fruit" },
|
|
1023
|
+
],
|
|
1024
|
+
|
|
1025
|
+
For tables with multiple categories, the top-level dictionary is nested.
|
|
1026
|
+
|
|
1027
|
+
"""
|
|
1028
|
+
return self._model.table_category_data(self._table_id)
|
|
1029
|
+
|
|
987
1030
|
def add_row(
|
|
988
1031
|
self,
|
|
989
1032
|
num_rows: int | None = 1,
|