numbers-parser 4.14.3__py3-none-any.whl → 4.15.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.
@@ -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
 
@@ -5,7 +5,15 @@ import sys
5
5
 
6
6
  from sigfig import round as sigfig
7
7
 
8
- from numbers_parser import Document, ErrorCell, FileError, FileFormatError, NumberCell, _get_version
8
+ from numbers_parser import (
9
+ Document,
10
+ ErrorCell,
11
+ FileError,
12
+ FileFormatError,
13
+ NumberCell,
14
+ UnsupportedError,
15
+ _get_version,
16
+ )
9
17
  from numbers_parser import __name__ as numbers_parser_name
10
18
  from numbers_parser.constants import MAX_SIGNIFICANT_DIGITS
11
19
  from numbers_parser.experimental import _enable_experimental_features
@@ -137,11 +145,12 @@ def main() -> None:
137
145
  print_table_names(filename)
138
146
  else:
139
147
  print_table(args, filename)
140
- except FileFormatError as e: # noqa: PERF203
141
- print(f"{filename}:", str(e), file=sys.stderr)
142
- sys.exit(1)
143
- except FileError as e:
144
- print(f"{filename}:", str(e), file=sys.stderr)
148
+ except (FileFormatError, FileError, UnsupportedError) as e: # noqa: PERF203
149
+ err_str = str(e)
150
+ if filename in err_str:
151
+ print(err_str, file=sys.stderr)
152
+ else:
153
+ print(f"{filename}: {err_str}", file=sys.stderr)
145
154
  sys.exit(1)
146
155
 
147
156
 
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
- table_formulas = self._model.table_formulas(self._table_id)
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
- # storage_flags._cond_style_id = unpack("<i", buffer[offset : offset + 4])[0]
880
+ # cond_style_id skipped
876
881
  offset += 4
877
- # if flags & 0x100:
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
- # if flags & 0x800:
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
- # if flags & 0x80000:
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 += 0x1820 - 16
1564
- mantissa = abs(int(value / math.pow(10, exp - 0x1820)))
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)) - 0x1820
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 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
- )
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
@@ -12,16 +12,16 @@ 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
+ "ControlFormattingType",
18
18
  "DurationStyle",
19
19
  "DurationUnits",
20
20
  "FormatType",
21
21
  "FormattingType",
22
- "NegativeNumberStyle",
23
22
  "FractionAccuracy",
24
- "ControlFormattingType",
23
+ "NegativeNumberStyle",
24
+ "PaddingType",
25
25
  ]
26
26
 
27
27
  DEFAULT_DOCUMENT = files("numbers_parser") / "data" / "empty.numbers"
@@ -51,6 +51,8 @@ DEFAULT_DATETIME_FORMAT = "dd MMM YYY HH:MM"
51
51
  CHECKBOX_FALSE_VALUE = "☐"
52
52
  CHECKBOX_TRUE_VALUE = "☑"
53
53
  STAR_RATING_VALUE = "★"
54
+ OPERATOR_PRECEDENCE = {"%": 6, "^": 5, "×": 4, "*": 4, "/": 4, "÷": 4, "+": 3, "-": 3, "&": 2}
55
+
54
56
 
55
57
  # Numbers limits
56
58
  MAX_TILE_SIZE = 256
@@ -69,6 +71,7 @@ EPOCH = datetime(2001, 1, 1) # noqa: DTZ001
69
71
  SECONDS_IN_HOUR = 60 * 60
70
72
  SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
71
73
  SECONDS_IN_WEEK = SECONDS_IN_DAY * 7
74
+ DECIMAL128_BIAS = 0x1820
72
75
 
73
76
  # File format enumerations
74
77
  DECIMAL_PLACES_AUTO = 253
@@ -91,6 +94,7 @@ SUPPORTED_NUMBERS_VERSIONS = [
91
94
  "14.1",
92
95
  "14.2",
93
96
  "14.3",
97
+ "14.4",
94
98
  ]
95
99
 
96
100
 
@@ -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
@@ -379,6 +378,8 @@ class Sheet:
379
378
  y: float | None = None,
380
379
  num_rows: int | None = DEFAULT_ROW_COUNT,
381
380
  num_cols: int | None = DEFAULT_COLUMN_COUNT,
381
+ num_header_rows: int | None = 1,
382
+ num_header_cols: int | None = 1,
382
383
  ) -> Table:
383
384
  """
384
385
  Add a new table to the current sheet.
@@ -411,6 +412,10 @@ class Sheet:
411
412
  The number of rows for the new table.
412
413
  num_cols: int, optional, default: 10
413
414
  The number of columns for the new table.
415
+ num_header_rows: int, optional, default: 1
416
+ The number of header rows for the new table.
417
+ num_header_cols: int, optional, default: 1
418
+ The number of header columns for the new table.
414
419
 
415
420
  Returns
416
421
  -------
@@ -423,7 +428,16 @@ class Sheet:
423
428
 
424
429
  """
425
430
  from_table_id = self._tables[-1]._table_id
426
- return self._add_table(table_name, from_table_id, x, y, num_rows, num_cols)
431
+ return self._add_table(
432
+ table_name,
433
+ from_table_id,
434
+ x,
435
+ y,
436
+ num_rows,
437
+ num_cols,
438
+ num_header_rows,
439
+ num_header_cols,
440
+ )
427
441
 
428
442
  def _add_table(
429
443
  self,
@@ -433,6 +447,8 @@ class Sheet:
433
447
  y,
434
448
  num_rows,
435
449
  num_cols,
450
+ num_header_rows,
451
+ num_header_cols,
436
452
  ) -> object:
437
453
  if table_name is not None:
438
454
  if table_name in self._tables:
@@ -452,6 +468,8 @@ class Sheet:
452
468
  y,
453
469
  num_rows,
454
470
  num_cols,
471
+ num_header_rows,
472
+ num_header_cols,
455
473
  )
456
474
  self._tables.append(Table(self._model, new_table_id))
457
475
  return self._tables[-1]
@@ -968,6 +986,11 @@ class Table(Cacheable):
968
986
  self._data[row][col]._model = self._model
969
987
  self._data[row][col]._set_merge(merge_cells.get((row, col)))
970
988
 
989
+ if row < self._model.num_header_rows(self._table_id) or col < self._model.num_header_cols(
990
+ self._table_id,
991
+ ):
992
+ self._model.name_ref_cache.mark_dirty()
993
+
971
994
  if style is not None:
972
995
  self.set_cell_style(row, col, style)
973
996