Module refinery.lib.scripts.xlm.handlers.lookups
The lookup commands of the macro language: the addresses they compose, the cells they name, and the ranges they count and search.
Expand source code Browse git
"""
The lookup commands of the macro language: the addresses they compose, the cells they name,
and the ranges they count and search.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from refinery.lib.excel import parse_formula, synthesize_formula
from refinery.lib.excel.common import column_letters
from refinery.lib.excel.formula.model import (
XlA1Reference,
XlArrayConstant,
XlR1C1Reference,
)
from refinery.lib.scripts.xlm.evaluate import evaluate_expression
from refinery.lib.scripts.xlm.references import XlmCursor, resolve_reference
from refinery.lib.scripts.xlm.values import (
XlmOutcome,
XlmReference,
XlmValue,
condition,
error_outcome,
is_number,
)
if TYPE_CHECKING:
from refinery.lib.excel.formula.model import XlFunctionCall
from refinery.lib.scripts.xlm.engine import XlmEngine
def _partial(spelled: str) -> XlmOutcome:
return XlmOutcome(value=XlmValue(value=spelled, partial=True))
def _corners(engine: XlmEngine, node, cursor: XlmCursor):
"""
The corners a range argument names: directly when the argument is a range expression, and
through the value a defined name or another expression resolves to otherwise. An array
constant names no corners — its elements are values, not addresses.
"""
corners = engine.range_corners(node, cursor)
if corners is not None:
return corners
if isinstance(node, XlArrayConstant):
return None
value = evaluate_expression(engine, node, cursor)
cells = value.cells
if cells is not None and len(cells) == 2:
first, second = cells
if first.reference is not None and second.reference is not None:
return first.reference, second.reference
return None
def _absref(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
offset = evaluate_expression(engine, call.arguments[0], cursor)
base = call.arguments[1]
if offset.partial or not isinstance(base, (XlA1Reference, XlR1C1Reference)):
return _partial(spelled)
if isinstance(base, XlR1C1Reference) and (base.relative_row or base.relative_col):
return _partial(spelled)
offset_node = parse_formula(offset.unwrap())
if not isinstance(offset_node, XlR1C1Reference):
return _partial(spelled)
address = F'{column_letters(base.col + offset_node.col)}{base.row + offset_node.row}'
return XlmOutcome(value=XlmValue(value=address, text=address))
def _address(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
row = evaluate_expression(engine, call.arguments[0], cursor)
column = evaluate_expression(engine, call.arguments[1], cursor)
if (
row.partial
or column.partial
or not is_number(row.value)
or not is_number(column.value)
):
return _partial(spelled)
abs_num = 1
a1 = True
sheet = cursor.sheet
if len(call.arguments) >= 3:
argument = evaluate_expression(engine, call.arguments[2], cursor)
if argument.partial or not is_number(argument.value):
return _partial(spelled)
abs_num = int(float(argument.value))
if len(call.arguments) >= 4:
argument = evaluate_expression(engine, call.arguments[3], cursor)
if argument.partial:
return _partial(spelled)
truth = condition(argument)
if isinstance(truth, XlmValue):
return error_outcome(truth)
a1 = truth
if len(call.arguments) >= 5:
argument = evaluate_expression(engine, call.arguments[4], cursor)
if argument.partial:
return _partial(spelled)
sheet = argument.unwrap()
if a1:
templates = {1: '${}${}', 2: '{}${}', 3: '${}{}', 4: '{}{}'}
cell = templates.get(abs_num, '${}${}').format(
column_letters(int(float(column.value))),
row.text,
)
else:
templates = {1: 'R{}C{}', 2: 'R{}C[{}]', 3: 'R[{}]C{}', 4: 'R[{}]C[{}]'}
cell = templates.get(abs_num, 'R{}C{}').format(row.text, column.text)
address = F'{sheet}!{cell}'
return XlmOutcome(value=XlmValue(value=address, text=address))
def _hlookup(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
needle = evaluate_expression(engine, call.arguments[0], cursor)
index = evaluate_expression(engine, call.arguments[2], cursor)
exact = evaluate_expression(engine, call.arguments[3], cursor)
corners = _corners(engine, call.arguments[1], cursor)
if (
needle.partial
or index.partial
or exact.partial
or corners is None
or not is_number(index.value)
or exact.value is not False and str(exact.value).upper() != 'FALSE'
):
return _partial(spelled)
pattern = needle.unwrap()
if pattern == '*':
pattern = '.*'
first, last = corners
answer_row = first.row + int(float(index.value)) - 1
if answer_row < first.row or answer_row > last.row:
return _partial(spelled)
try:
for reference in engine.range_cells(corners):
if reference.row != first.row:
break
value = engine.read_reference(reference, cursor).value
if value is not None and value != '' and re.match(pattern, str(value)):
answer = XlmReference(first.sheet, answer_row, reference.col)
return XlmOutcome(value=engine.read_reference(answer, cursor))
except re.error:
return _partial(spelled)
return _partial(spelled)
def _index(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
index = evaluate_expression(engine, call.arguments[1], cursor)
if index.partial or not is_number(index.value):
return _partial(spelled)
position = int(float(index.value)) - 1
node = call.arguments[0]
if isinstance(node, XlArrayConstant):
cells = evaluate_expression(engine, node, cursor).cells
if cells is not None and 0 <= position < len(cells):
return XlmOutcome(value=cells[position])
return _partial(spelled)
corners = _corners(engine, node, cursor)
if corners is not None:
first, _ = corners
reference = XlmReference(first.sheet, first.row + position, first.col)
return XlmOutcome(value=engine.read_reference(reference, cursor))
return _partial(spelled)
def _indirect(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
argument = evaluate_expression(engine, call.arguments[0], cursor)
if argument.partial or not isinstance(argument.value, str):
return _partial(spelled)
parsed = parse_formula(argument.value)
if not isinstance(parsed, (XlA1Reference, XlR1C1Reference)):
return _partial(spelled)
return XlmOutcome(value=engine.read_reference(resolve_reference(parsed, cursor), cursor))
def _rows(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
node = call.arguments[0]
if isinstance(node, XlArrayConstant):
cells = evaluate_expression(engine, node, cursor).cells
if cells is not None:
return XlmOutcome(value=XlmValue(value=len(cells)))
return _partial(spelled)
corners = _corners(engine, node, cursor)
if corners is not None:
first, last = corners
return XlmOutcome(value=XlmValue(value=last.row - first.row + 1))
return _partial(spelled)
def _counta(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
corners = _corners(engine, call.arguments[0], cursor)
if corners is None:
return _partial(spelled)
count = sum(1 for _ in engine.range_cells(corners))
return XlmOutcome(value=XlmValue(value=count))
LOOKUP_HANDLERS = {
'ABSREF': _absref,
'ADDRESS': _address,
'COUNTA': _counta,
'HLOOKUP': _hlookup,
'INDEX': _index,
'INDIRECT': _indirect,
'ROWS': _rows,
}