Module refinery.lib.excel.formula.synth
The synthesizer that prints the formula model back as one line of formula text, without the
leading =. Parentheses are emitted only where the precedence table demands them, so that a
cleaned tree prints as the shortest text that reads back as the same tree. A union is the one
operator that is never spelled bare: outside parentheses a comma is an argument separator, so
every union is wrapped, the formula root included.
Expand source code Browse git
"""
The synthesizer that prints the formula model back as one line of formula text, without the
leading `=`. Parentheses are emitted only where the precedence table demands them, so that a
cleaned tree prints as the shortest text that reads back as the same tree. A union is the one
operator that is never spelled bare: outside parentheses a comma is an argument separator, so
every union is wrapped, the formula root included.
"""
from __future__ import annotations
import re
from refinery.lib.excel.common import column_letters
from refinery.lib.excel.formula.model import (
Expression,
XlA1Reference,
XlArrayConstant,
XlBinaryExpression,
XlBinaryOperator,
XlBoolean,
XlDefinedName,
XlError,
XlFunctionCall,
XlMissingArgument,
XlNumber,
XlParenExpression,
XlR1C1Reference,
XlString,
XlUnaryExpression,
XlUnaryOperator,
XlUnparsedFormula,
)
from refinery.lib.excel.formula.parse import International
from refinery.lib.scripts import TREE_RECURSION_DEPTH, Node, Synthesizer
from refinery.lib.tools import RecursionDepth
_PRECEDENCE = {
XlBinaryOperator.EQ: 1,
XlBinaryOperator.NE: 1,
XlBinaryOperator.LT: 1,
XlBinaryOperator.LE: 1,
XlBinaryOperator.GT: 1,
XlBinaryOperator.GE: 1,
XlBinaryOperator.CONCAT: 2,
XlBinaryOperator.ADD: 3,
XlBinaryOperator.SUB: 3,
XlBinaryOperator.MUL: 4,
XlBinaryOperator.DIV: 4,
XlBinaryOperator.POW: 5,
XlBinaryOperator.UNION: 8,
XlBinaryOperator.ISECT: 9,
XlBinaryOperator.RANGE: 10,
XlUnaryOperator.NEG: 7,
XlUnaryOperator.POS: 7,
XlUnaryOperator.PERCENT: 6,
}
_RE_BARE_SHEET = re.compile(R'[A-Za-z_\\][A-Za-z0-9_.\\?]*')
def _sheet_token(name: str) -> str:
if _RE_BARE_SHEET.fullmatch(name):
return name
escaped = name.replace("'", "''")
return F"'{escaped}'"
def _qualify(sheets: tuple[str, ...]) -> str:
return F"{':'.join(_sheet_token(s) for s in sheets)}!"
class FormulaSynthesizer(Synthesizer):
"""
Print a formula `Expression` as formula text. The list separator of the output follows the
`International` it is given, so a workbook stored under another locale round-trips too.
"""
def __init__(self, international: International | None = None):
international = international or International()
super().__init__()
self._sep = international.list_separator
self._lb = international.left_bracket
self._rb = international.right_bracket
def convert(self, node: Node) -> str:
if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.UNION:
node = XlParenExpression(operand=node)
with RecursionDepth(TREE_RECURSION_DEPTH):
return super().convert(node)
def _emit_operand(self, operand: Expression | None, parent: int, left: bool):
if operand is None:
return
if isinstance(operand, XlBinaryExpression):
if operand.operator is XlBinaryOperator.UNION:
self._write('(')
self.visit(operand)
self._write(')')
return
precedence = _PRECEDENCE[operand.operator]
if precedence < parent or (precedence == parent and not left):
self._write('(')
self.visit(operand)
self._write(')')
return
elif isinstance(operand, XlUnaryExpression):
if _PRECEDENCE[operand.operator] < parent:
self._write('(')
self.visit(operand)
self._write(')')
return
self.visit(operand)
def visit_XlNumber(self, node: XlNumber):
self._write(self._format_number(node))
def visit_XlString(self, node: XlString):
escaped = node.value.replace('"', '""')
self._write(F'"{escaped}"')
def visit_XlBoolean(self, node: XlBoolean):
self._write('TRUE' if node.value else 'FALSE')
def visit_XlError(self, node: XlError):
self._write(node.value)
def visit_XlBinaryExpression(self, node: XlBinaryExpression):
precedence = _PRECEDENCE[node.operator]
text = ' ' if node.operator is XlBinaryOperator.ISECT else node.operator.value
self._emit_operand(node.left, precedence, left=True)
self._write(text)
self._emit_operand(node.right, precedence, left=False)
def visit_XlUnaryExpression(self, node: XlUnaryExpression):
if node.operator is XlUnaryOperator.PERCENT:
self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=False)
self._write(node.operator.value)
return
self._write(node.operator.value)
self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=True)
def visit_XlParenExpression(self, node: XlParenExpression):
self._write('(')
if node.operand is not None:
self.visit(node.operand)
self._write(')')
def visit_XlMissingArgument(self, node: XlMissingArgument):
pass
def visit_XlUnparsedFormula(self, node: XlUnparsedFormula):
self._write(node.text)
def visit_XlA1Reference(self, node: XlA1Reference):
if node.sheets is not None:
self._write(_qualify(node.sheets))
column = column_letters(node.col)
row = F'${node.row}' if not node.relative_row else str(node.row)
col = F'${column}' if not node.relative_col else column
self._write(F'{col}{row}')
def visit_XlR1C1Reference(self, node: XlR1C1Reference):
if node.sheets is not None:
self._write(_qualify(node.sheets))
self._write(
F'{self._axis("R", node.row, node.relative_row)}'
F'{self._axis("C", node.col, node.relative_col)}'
)
def visit_XlDefinedName(self, node: XlDefinedName):
if node.sheet is not None:
self._write(_qualify((node.sheet,)))
self._write(node.name)
def visit_XlFunctionCall(self, node: XlFunctionCall):
if isinstance(node.callee, str):
self._write(node.callee)
else:
self.visit(node.callee)
self._write('(')
for index, argument in enumerate(node.arguments):
if index:
self._write(self._sep)
# a call argument sits in a fully delimited position, so no precedence asks for
# parentheses around it — only a union does, because bare it would read as two
self._emit_operand(argument, 0, left=False)
self._write(')')
def visit_XlArrayConstant(self, node: XlArrayConstant):
self._write('{')
for rindex, row in enumerate(node.rows):
if rindex:
self._write(';')
for cindex, item in enumerate(row):
if cindex:
self._write(self._sep)
self.visit(item)
self._write('}')
def _axis(self, letter: str, value: int, relative: bool) -> str:
if not relative:
return F'{letter}{value}'
if value:
return F'{letter}{self._lb}{value}{self._rb}'
return letter
def _format_number(self, node: XlNumber) -> str:
try:
if float(node.raw) == node.value:
return node.raw
except (TypeError, ValueError):
pass
if isinstance(node.value, int):
return str(node.value)
return repr(node.value)
def synthesize_formula(
expression: Expression,
international: International | None = None,
) -> str:
"""
Print a formula `Expression` as one line of formula text without the leading `=`.
"""
return FormulaSynthesizer(international).convert(expression)
Functions
def synthesize_formula(expression, international=None)-
Print a formula
Expressionas one line of formula text without the leading=.Expand source code Browse git
def synthesize_formula( expression: Expression, international: International | None = None, ) -> str: """ Print a formula `Expression` as one line of formula text without the leading `=`. """ return FormulaSynthesizer(international).convert(expression)
Classes
class FormulaSynthesizer (international=None)-
Print a formula
Expressionas formula text. The list separator of the output follows theInternationalit is given, so a workbook stored under another locale round-trips too.Expand source code Browse git
class FormulaSynthesizer(Synthesizer): """ Print a formula `Expression` as formula text. The list separator of the output follows the `International` it is given, so a workbook stored under another locale round-trips too. """ def __init__(self, international: International | None = None): international = international or International() super().__init__() self._sep = international.list_separator self._lb = international.left_bracket self._rb = international.right_bracket def convert(self, node: Node) -> str: if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.UNION: node = XlParenExpression(operand=node) with RecursionDepth(TREE_RECURSION_DEPTH): return super().convert(node) def _emit_operand(self, operand: Expression | None, parent: int, left: bool): if operand is None: return if isinstance(operand, XlBinaryExpression): if operand.operator is XlBinaryOperator.UNION: self._write('(') self.visit(operand) self._write(')') return precedence = _PRECEDENCE[operand.operator] if precedence < parent or (precedence == parent and not left): self._write('(') self.visit(operand) self._write(')') return elif isinstance(operand, XlUnaryExpression): if _PRECEDENCE[operand.operator] < parent: self._write('(') self.visit(operand) self._write(')') return self.visit(operand) def visit_XlNumber(self, node: XlNumber): self._write(self._format_number(node)) def visit_XlString(self, node: XlString): escaped = node.value.replace('"', '""') self._write(F'"{escaped}"') def visit_XlBoolean(self, node: XlBoolean): self._write('TRUE' if node.value else 'FALSE') def visit_XlError(self, node: XlError): self._write(node.value) def visit_XlBinaryExpression(self, node: XlBinaryExpression): precedence = _PRECEDENCE[node.operator] text = ' ' if node.operator is XlBinaryOperator.ISECT else node.operator.value self._emit_operand(node.left, precedence, left=True) self._write(text) self._emit_operand(node.right, precedence, left=False) def visit_XlUnaryExpression(self, node: XlUnaryExpression): if node.operator is XlUnaryOperator.PERCENT: self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=False) self._write(node.operator.value) return self._write(node.operator.value) self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=True) def visit_XlParenExpression(self, node: XlParenExpression): self._write('(') if node.operand is not None: self.visit(node.operand) self._write(')') def visit_XlMissingArgument(self, node: XlMissingArgument): pass def visit_XlUnparsedFormula(self, node: XlUnparsedFormula): self._write(node.text) def visit_XlA1Reference(self, node: XlA1Reference): if node.sheets is not None: self._write(_qualify(node.sheets)) column = column_letters(node.col) row = F'${node.row}' if not node.relative_row else str(node.row) col = F'${column}' if not node.relative_col else column self._write(F'{col}{row}') def visit_XlR1C1Reference(self, node: XlR1C1Reference): if node.sheets is not None: self._write(_qualify(node.sheets)) self._write( F'{self._axis("R", node.row, node.relative_row)}' F'{self._axis("C", node.col, node.relative_col)}' ) def visit_XlDefinedName(self, node: XlDefinedName): if node.sheet is not None: self._write(_qualify((node.sheet,))) self._write(node.name) def visit_XlFunctionCall(self, node: XlFunctionCall): if isinstance(node.callee, str): self._write(node.callee) else: self.visit(node.callee) self._write('(') for index, argument in enumerate(node.arguments): if index: self._write(self._sep) # a call argument sits in a fully delimited position, so no precedence asks for # parentheses around it — only a union does, because bare it would read as two self._emit_operand(argument, 0, left=False) self._write(')') def visit_XlArrayConstant(self, node: XlArrayConstant): self._write('{') for rindex, row in enumerate(node.rows): if rindex: self._write(';') for cindex, item in enumerate(row): if cindex: self._write(self._sep) self.visit(item) self._write('}') def _axis(self, letter: str, value: int, relative: bool) -> str: if not relative: return F'{letter}{value}' if value: return F'{letter}{self._lb}{value}{self._rb}' return letter def _format_number(self, node: XlNumber) -> str: try: if float(node.raw) == node.value: return node.raw except (TypeError, ValueError): pass if isinstance(node.value, int): return str(node.value) return repr(node.value)Ancestors
Methods
def convert(self, node)-
Expand source code Browse git
def convert(self, node: Node) -> str: if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.UNION: node = XlParenExpression(operand=node) with RecursionDepth(TREE_RECURSION_DEPTH): return super().convert(node) def visit_XlNumber(self, node)-
Expand source code Browse git
def visit_XlNumber(self, node: XlNumber): self._write(self._format_number(node)) def visit_XlString(self, node)-
Expand source code Browse git
def visit_XlString(self, node: XlString): escaped = node.value.replace('"', '""') self._write(F'"{escaped}"') def visit_XlBoolean(self, node)-
Expand source code Browse git
def visit_XlBoolean(self, node: XlBoolean): self._write('TRUE' if node.value else 'FALSE') def visit_XlError(self, node)-
Expand source code Browse git
def visit_XlError(self, node: XlError): self._write(node.value) def visit_XlBinaryExpression(self, node)-
Expand source code Browse git
def visit_XlBinaryExpression(self, node: XlBinaryExpression): precedence = _PRECEDENCE[node.operator] text = ' ' if node.operator is XlBinaryOperator.ISECT else node.operator.value self._emit_operand(node.left, precedence, left=True) self._write(text) self._emit_operand(node.right, precedence, left=False) def visit_XlUnaryExpression(self, node)-
Expand source code Browse git
def visit_XlUnaryExpression(self, node: XlUnaryExpression): if node.operator is XlUnaryOperator.PERCENT: self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=False) self._write(node.operator.value) return self._write(node.operator.value) self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=True) def visit_XlParenExpression(self, node)-
Expand source code Browse git
def visit_XlParenExpression(self, node: XlParenExpression): self._write('(') if node.operand is not None: self.visit(node.operand) self._write(')') def visit_XlMissingArgument(self, node)-
Expand source code Browse git
def visit_XlMissingArgument(self, node: XlMissingArgument): pass def visit_XlUnparsedFormula(self, node)-
Expand source code Browse git
def visit_XlUnparsedFormula(self, node: XlUnparsedFormula): self._write(node.text) def visit_XlA1Reference(self, node)-
Expand source code Browse git
def visit_XlA1Reference(self, node: XlA1Reference): if node.sheets is not None: self._write(_qualify(node.sheets)) column = column_letters(node.col) row = F'${node.row}' if not node.relative_row else str(node.row) col = F'${column}' if not node.relative_col else column self._write(F'{col}{row}') def visit_XlR1C1Reference(self, node)-
Expand source code Browse git
def visit_XlR1C1Reference(self, node: XlR1C1Reference): if node.sheets is not None: self._write(_qualify(node.sheets)) self._write( F'{self._axis("R", node.row, node.relative_row)}' F'{self._axis("C", node.col, node.relative_col)}' ) def visit_XlDefinedName(self, node)-
Expand source code Browse git
def visit_XlDefinedName(self, node: XlDefinedName): if node.sheet is not None: self._write(_qualify((node.sheet,))) self._write(node.name) def visit_XlFunctionCall(self, node)-
Expand source code Browse git
def visit_XlFunctionCall(self, node: XlFunctionCall): if isinstance(node.callee, str): self._write(node.callee) else: self.visit(node.callee) self._write('(') for index, argument in enumerate(node.arguments): if index: self._write(self._sep) # a call argument sits in a fully delimited position, so no precedence asks for # parentheses around it — only a union does, because bare it would read as two self._emit_operand(argument, 0, left=False) self._write(')') def visit_XlArrayConstant(self, node)-
Expand source code Browse git
def visit_XlArrayConstant(self, node: XlArrayConstant): self._write('{') for rindex, row in enumerate(node.rows): if rindex: self._write(';') for cindex, item in enumerate(row): if cindex: self._write(self._sep) self.visit(item) self._write('}')
Inherited members