aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py')
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py b/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py
new file mode 100644
index 00000000..a7e90ec8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py
@@ -0,0 +1,166 @@
+"""
+This module contains code to translate formulae across cells in a worksheet.
+
+The idea is that if A1 has formula "=B1+C1", then translating it to cell A2
+results in formula "=B2+C2". The algorithm relies on the formula tokenizer
+to identify the parts of the formula that need to change.
+
+"""
+
+import re
+from .tokenizer import Tokenizer, Token
+from openpyxl.utils import (
+ coordinate_to_tuple,
+ column_index_from_string,
+ get_column_letter
+)
+
+class TranslatorError(Exception):
+ """
+ Raised when a formula can't be translated across cells.
+
+ This error arises when a formula's references would be translated outside
+ the worksheet's bounds on the top or left. Excel represents these
+ situations with a #REF! literal error. E.g., if the formula at B2 is
+ '=A1', attempting to translate the formula to B1 raises TranslatorError,
+ since there's no cell above A1. Similarly, translating the same formula
+ from B2 to A2 raises TranslatorError, since there's no cell to the left of
+ A1.
+
+ """
+
+
+class Translator:
+
+ """
+ Modifies a formula so that it can be translated from one cell to another.
+
+ `formula`: The str string to translate. Must include the leading '='
+ character.
+ `origin`: The cell address (in A1 notation) where this formula was
+ defined (excluding the worksheet name).
+
+ """
+
+ def __init__(self, formula, origin):
+ # Excel errors out when a workbook has formulae in R1C1 notation,
+ # regardless of the calcPr:refMode setting, so I'm assuming the
+ # formulae stored in the workbook must be in A1 notation.
+ self.row, self.col = coordinate_to_tuple(origin)
+ self.tokenizer = Tokenizer(formula)
+
+ def get_tokens(self):
+ "Returns a list with the tokens comprising the formula."
+ return self.tokenizer.items
+
+ ROW_RANGE_RE = re.compile(r"(\$?[1-9][0-9]{0,6}):(\$?[1-9][0-9]{0,6})$")
+ COL_RANGE_RE = re.compile(r"(\$?[A-Za-z]{1,3}):(\$?[A-Za-z]{1,3})$")
+ CELL_REF_RE = re.compile(r"(\$?[A-Za-z]{1,3})(\$?[1-9][0-9]{0,6})$")
+
+ @staticmethod
+ def translate_row(row_str, rdelta):
+ """
+ Translate a range row-snippet by the given number of rows.
+ """
+ if row_str.startswith('$'):
+ return row_str
+ else:
+ new_row = int(row_str) + rdelta
+ if new_row <= 0:
+ raise TranslatorError("Formula out of range")
+ return str(new_row)
+
+ @staticmethod
+ def translate_col(col_str, cdelta):
+ """
+ Translate a range col-snippet by the given number of columns
+ """
+ if col_str.startswith('$'):
+ return col_str
+ else:
+ try:
+ return get_column_letter(
+ column_index_from_string(col_str) + cdelta)
+ except ValueError:
+ raise TranslatorError("Formula out of range")
+
+ @staticmethod
+ def strip_ws_name(range_str):
+ "Splits out the worksheet reference, if any, from a range reference."
+ # This code assumes that named ranges cannot contain any exclamation
+ # marks. Excel refuses to create these (even using VBA), and
+ # complains of a corrupt workbook when there are names with
+ # exclamation marks. The ECMA spec only states that named ranges will
+ # be of `ST_Xstring` type, which in theory allows '!' (char code
+ # 0x21) per http://www.w3.org/TR/xml/#charsets
+ if '!' in range_str:
+ sheet, range_str = range_str.rsplit('!', 1)
+ return sheet + "!", range_str
+ return "", range_str
+
+ @classmethod
+ def translate_range(cls, range_str, rdelta, cdelta):
+ """
+ Translate an A1-style range reference to the destination cell.
+
+ `rdelta`: the row offset to add to the range
+ `cdelta`: the column offset to add to the range
+ `range_str`: an A1-style reference to a range. Potentially includes
+ the worksheet reference. Could also be a named range.
+
+ """
+ ws_part, range_str = cls.strip_ws_name(range_str)
+ match = cls.ROW_RANGE_RE.match(range_str) # e.g. `3:4`
+ if match is not None:
+ return (ws_part + cls.translate_row(match.group(1), rdelta) + ":"
+ + cls.translate_row(match.group(2), rdelta))
+ match = cls.COL_RANGE_RE.match(range_str) # e.g. `A:BC`
+ if match is not None:
+ return (ws_part + cls.translate_col(match.group(1), cdelta) + ':'
+ + cls.translate_col(match.group(2), cdelta))
+ if ':' in range_str: # e.g. `A1:B5`
+ # The check is necessarily general because range references can
+ # have one or both endpoints specified by named ranges. I.e.,
+ # `named_range:C2`, `C2:named_range`, and `name1:name2` are all
+ # valid references. Further, Excel allows chaining multiple
+ # colons together (with unclear meaning)
+ return ws_part + ":".join(
+ cls.translate_range(piece, rdelta, cdelta)
+ for piece in range_str.split(':'))
+ match = cls.CELL_REF_RE.match(range_str)
+ if match is None: # Must be a named range
+ return range_str
+ return (ws_part + cls.translate_col(match.group(1), cdelta)
+ + cls.translate_row(match.group(2), rdelta))
+
+ def translate_formula(self, dest=None, row_delta=0, col_delta=0):
+ """
+ Convert the formula into A1 notation, or as row and column coordinates
+
+ The formula is converted into A1 assuming it is assigned to the cell
+ whose address is `dest` (no worksheet name).
+
+ """
+ tokens = self.get_tokens()
+ if not tokens:
+ return ""
+ elif tokens[0].type == Token.LITERAL:
+ return tokens[0].value
+ out = ['=']
+ # per the spec:
+ # A compliant producer or consumer considers a defined name in the
+ # range A1-XFD1048576 to be an error. All other names outside this
+ # range can be defined as names and overrides a cell reference if an
+ # ambiguity exists. (I.18.2.5)
+ if dest:
+ row, col = coordinate_to_tuple(dest)
+ row_delta = row - self.row
+ col_delta = col - self.col
+ for token in tokens:
+ if (token.type == Token.OPERAND
+ and token.subtype == Token.RANGE):
+ out.append(self.translate_range(token.value, row_delta,
+ col_delta))
+ else:
+ out.append(token.value)
+ return "".join(out)