Module refinery.lib.scripts.xlm.values

The value algebra of the XLM macro language: the values the emulator computes, the quoting the trace spells them with, and the operations the evaluator applies to them.

Expand source code Browse git
"""
The value algebra of the XLM macro language: the values the emulator computes, the quoting
the trace spells them with, and the operations the evaluator applies to them.
"""
from __future__ import annotations

import datetime
import math
import operator

from dataclasses import dataclass, replace
from typing import TYPE_CHECKING, Any, NamedTuple

from refinery.lib.excel.common import column_letters
from refinery.lib.excel.formula.model import XlBinaryOperator
from refinery.lib.scripts.xlm.trace import XlmStatus

if TYPE_CHECKING:
    from refinery.lib.scripts.xlm.references import XlmArrival, XlmCursor

_DATETIME_FORMATS = (
    '%Y-%m-%d %H:%M:%S.%f',
    '%H:%M:%S',
)


class XlmReference(NamedTuple):
    """
    The address of one cell: the sheet it sits on — `None` names the sheet the reading
    formula is on — and its one-based row and column.
    """

    sheet: str | None
    row: int
    col: int

    def a1(self) -> str:
        """
        The A1 spelling of the address, qualified by its sheet when it names one.
        """
        cell = F'{column_letters(self.col)}{self.row}'
        if self.sheet is None:
            return cell
        return F'{self.sheet}!{cell}'


def is_number(value: Any) -> bool:
    """
    Whether a value reads as a number, as the macro language reads one: a finite number is one,
    and so is the text of one. The digit separators, the infinities, and the missing number
    that Python reads in a text spell no number of the macro language.
    """
    if isinstance(value, str) and '_' in value:
        return False
    try:
        number = float(value)
    except (ValueError, TypeError, OverflowError):
        return False
    return math.isfinite(number)


def condition(value: XlmValue) -> bool | XlmValue:
    """
    The truth of a value, as every command that reads a condition reads it: the spellings of the
    two truth values are theirs, a number holds when it is not zero — by its type, so the text
    of a number holds nothing — a value that holds nothing answers `FALSE`, and a value that is
    an error value is that error. Any other text is the `#VALUE!` error Excel answers for a
    condition that spells no truth value.
    """
    if value.error:
        return value
    data = value.value
    if data is None or data == '':
        return False
    if isinstance(data, bool):
        return data
    if isinstance(data, (int, float)):
        return data != 0
    if isinstance(data, str):
        lowered = data.lower()
        if lowered == 'true':
            return True
        if lowered == 'false':
            return False
    return error_value('#VALUE!')


def error_outcome(
    truth: XlmValue,
    spelled: str | None = None,
    halts: bool = False,
) -> XlmOutcome:
    """
    The outcome a command that reads a condition answers for one that spells no truth value:
    the error value the condition is, spelled as the command when it names one, and an error
    the macro halts on when its grammar makes the condition fatal.
    """
    if spelled is not None:
        truth = replace(truth, text=spelled)
    return XlmOutcome(value=truth, status=XlmStatus.Error if halts else None)


def unwrap_literal(text: str) -> str:
    """
    The text of a quoted string literal without its quotes; a text that carries no quotes
    keeps its spelling, and a doubled quote inside the literal spells one quote.
    """
    if len(text) > 1 and text.startswith('"') and text.endswith('"'):
        return text[1:-1].replace('""', '"')
    return text


def ansi_bytes(text: str) -> bytes:
    """
    The bytes a text occupies in the ANSI code page the macro language writes: a character
    `CHAR` answers is the byte it was made from, any other character its byte in code page
    1252, and a character that code page cannot spell is the question mark Windows writes
    instead.
    """
    data = bytearray()
    for char in text:
        code = ord(char)
        if code > 0xFF:
            try:
                code = char.encode('cp1252')[0]
            except UnicodeEncodeError:
                code = 0x3F
        data.append(code)
    return bytes(data)


def wrap_literal(data: Any, must_wrap: bool = False) -> str:
    """
    The spelling the trace gives a value: a number, a boolean, and a text that already carries
    quotes keep their own spelling — the quoted text only while the caller does not insist on
    quotes — and every other text is quoted with its quotes doubled.
    """
    if isinstance(data, bool):
        return 'TRUE' if data else 'FALSE'
    if is_number(data) or (
        isinstance(data, str)
        and len(data) > 1
        and data.startswith('"')
        and data.endswith('"')
        and not must_wrap
    ):
        return str(data)
    if isinstance(data, float) and data.is_integer():
        return str(int(data))
    if isinstance(data, int):
        return str(data)
    return F'"{str(data).replace(chr(34), chr(34) * 2)}"'


def _default_text(value: Any) -> str:
    if value is None:
        return ''
    if isinstance(value, XlmReference):
        return value.a1()
    if isinstance(value, bool):
        return 'TRUE' if value else 'FALSE'
    if isinstance(value, str):
        return wrap_literal(value, must_wrap=True)
    if isinstance(value, float) and value.is_integer():
        return str(int(value))
    return str(value)


def error_value(text: str) -> XlmValue:
    """
    The error value Excel spells with the given text, such as the `#VALUE!` of a coercion that
    failed.
    """
    return XlmValue(value=text, text=text, error=True)


@dataclass(repr=False, eq=False)
class XlmValue:
    """
    One value of the macro language as the emulator computes it: `value` is the Python object
    behind it, `text` the spelling the trace prints, `partial` marks a value whose computation
    the program never finished, `date` marks a value that came from a date cell, `reference`
    names the cell address the value stands for, `cells` holds the elements of an array or the
    corners of a range, and `error` marks an error value such as the `#DIV/0!` of a division
    by zero. A given spelling of a number is normalized unless the value is a text, whose
    spelling is its content.
    """

    value: Any = None
    text: str | None = None
    partial: bool = False
    date: bool = False
    reference: XlmReference | None = None
    cells: tuple[XlmValue, ...] | None = None
    error: bool = False

    def __post_init__(self):
        if self.text is None:
            if isinstance(self.value, bool):
                self.text = 'TRUE' if self.value else 'FALSE'
            elif self.partial:
                self.text = str(self.value)
            else:
                self.text = _default_text(self.value)
        elif not isinstance(self.value, str) and is_number(self.text):
            number = float(self.text)
            self.text = str(int(number) if number.is_integer() else number)

    def unwrap(self) -> str:
        """
        The content of the value as a text. A text the program finished computing is its own
        content, and any other finished value is spelled as itself, whatever spelling the trace
        gives the command that computed it; a value the program never finished is the spelling
        of what it left unfinished, without the quotes of a string literal.
        """
        if self.partial:
            return unwrap_literal(self.text or '')
        if isinstance(self.value, str):
            return self.value
        return _default_text(self.value)

    def unwrap_date(self) -> datetime.datetime | None:
        """
        The moment an ISO-spelled text names, in one of the spellings the macro language
        writes into cells, or `None` for a text that names none.
        """
        for spelling in _DATETIME_FORMATS:
            try:
                return datetime.datetime.strptime(self.unwrap(), spelling)
            except ValueError:
                continue
        return None


_OPERATOR_FUNCTIONS = {
    XlBinaryOperator.ADD: operator.add,
    XlBinaryOperator.SUB: operator.sub,
    XlBinaryOperator.MUL: operator.mul,
    XlBinaryOperator.DIV: operator.truediv,
    XlBinaryOperator.POW: operator.pow,
    XlBinaryOperator.EQ: operator.eq,
    XlBinaryOperator.NE: operator.ne,
    XlBinaryOperator.LT: operator.lt,
    XlBinaryOperator.LE: operator.le,
    XlBinaryOperator.GT: operator.gt,
    XlBinaryOperator.GE: operator.ge,
}

_OPERATOR_SYMBOLS = {
    XlBinaryOperator.ADD: '+',
    XlBinaryOperator.SUB: '-',
    XlBinaryOperator.MUL: '*',
    XlBinaryOperator.DIV: '/',
    XlBinaryOperator.POW: '^',
    XlBinaryOperator.EQ: '=',
    XlBinaryOperator.NE: '<>',
    XlBinaryOperator.LT: '<',
    XlBinaryOperator.LE: '<=',
    XlBinaryOperator.GT: '>',
    XlBinaryOperator.GE: '>=',
}


def is_blank(value: XlmValue) -> bool:
    """
    Whether a value is the blank of a cell that holds nothing: every operator reads it as the
    zero of the other operand's type, unlike the empty text, which the trace spells `""`.
    """
    return value.text == ''


def blank_value(reference: XlmReference | None = None) -> XlmValue:
    """
    The value of a cell that holds nothing: every operator reads it as the zero of the other
    operand's type. The blank of the cell at the given address, or of no cell in particular.
    """
    return XlmValue(value='', text='', reference=reference)


_ARITHMETIC_OPERATORS = frozenset((
    XlBinaryOperator.ADD,
    XlBinaryOperator.SUB,
    XlBinaryOperator.MUL,
    XlBinaryOperator.DIV,
    XlBinaryOperator.POW,
))


def _comparison_rank(value: XlmValue) -> int:
    """
    The rank that orders the three types of a comparison: the numbers order below the texts,
    the texts below the two truth values.
    """
    if isinstance(value.value, bool):
        return 2
    if isinstance(value.value, str):
        return 1
    return 0


def _zero_like(value: XlmValue) -> XlmValue:
    """
    The blank of an empty cell read as the type of the given operand: the zero of a number, the
    empty text of a text, and `FALSE` of a truth value.
    """
    if isinstance(value.value, bool):
        return XlmValue(value=False)
    if isinstance(value.value, str):
        return XlmValue(value='')
    return XlmValue(value=0)


def _arithmetic_operand(value: XlmValue) -> float | None:
    """
    The number an arithmetic operator reads in an operand, or `None` for one that holds none: a
    truth value is one or zero, the blank of an empty cell is zero, and a text is the number it
    spells — every other text, the empty text included, holds none, and so does a number too
    large for a finite double to hold.
    """
    if is_blank(value):
        return 0.0
    data = value.value
    if isinstance(data, bool):
        return 1.0 if data else 0.0
    if isinstance(data, (int, float)):
        try:
            return float(data)
        except OverflowError:
            return None
    if isinstance(data, str) and is_number(data):
        return float(data)
    return None


def _numeric_result(result: Any) -> XlmValue:
    if isinstance(result, bool):
        return XlmValue(value=result)
    if isinstance(result, complex) or (isinstance(result, float) and not math.isfinite(result)):
        return error_value('#NUM!')
    if isinstance(result, float) and result.is_integer():
        return XlmValue(value=int(result))
    return XlmValue(value=result)


def concat(left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The `&` of two values: their joined texts when both are complete, the two texts joined by
    the operator itself when either side is partial, and the error of the first side that is
    an error value.
    """
    if left.partial or right.partial:
        fragment = F'{left.unwrap()}&{right.unwrap()}'
        return XlmValue(value=fragment, partial=True)
    if left.error:
        return left
    if right.error:
        return right
    joined = left.unwrap() + right.unwrap()
    return XlmValue(value=joined)


def _arithmetic(operator: XlBinaryOperator, left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The value of an arithmetic operation. An operand that holds no number degrades to the
    `#VALUE!` error Excel answers for it, the two zeros of an exponentiation to the `#NUM!`
    error Excel answers for `0^0`, a division by zero to `#DIV/0!`, and a result no finite
    number holds to `#NUM!`. An exponentiation of a positive base is the exponential of the
    product of the exponent and the logarithm of the base — the arithmetic Excel itself
    applies, whose result can miss the correctly rounded one in the last digits.
    """
    operand_left = _arithmetic_operand(left)
    operand_right = _arithmetic_operand(right)
    if operand_left is None or operand_right is None:
        return error_value('#VALUE!')
    if operator is XlBinaryOperator.POW:
        if operand_left == 0 and operand_right == 0:
            return error_value('#NUM!')
        if operand_left > 0:
            try:
                return _numeric_result(math.exp(operand_right * math.log(operand_left)))
            except OverflowError:
                return error_value('#NUM!')
    function = _OPERATOR_FUNCTIONS[operator]
    try:
        return _numeric_result(function(operand_left, operand_right))
    except ZeroDivisionError:
        return error_value('#DIV/0!')
    except OverflowError:
        return error_value('#NUM!')


def _compare(operator: XlBinaryOperator, left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The value of a comparison. Two texts that name moments compare the moments they name, the
    blank of an empty cell orders as the zero of the other operand's type, texts compare their
    letters case-insensitively, and the types never cross: a number is never equal to a text or
    a truth value, and the numbers order below the texts, the texts below the two truth values.
    An operand that names a cell address rather than a value leaves the comparison the `#VALUE!`
    error Excel answers for one it cannot make.
    """
    if isinstance(left.value, str) and isinstance(right.value, str):
        moment_left = left.unwrap_date()
        moment_right = right.unwrap_date()
        if moment_left is not None and moment_right is not None:
            return _numeric_result(_OPERATOR_FUNCTIONS[operator](moment_left, moment_right))
    if is_blank(left) and is_blank(right):
        left = XlmValue(value=0)
        right = XlmValue(value=0)
    elif is_blank(left):
        left = _zero_like(right)
    elif is_blank(right):
        right = _zero_like(left)
    rank_left = _comparison_rank(left)
    rank_right = _comparison_rank(right)
    if rank_left != rank_right:
        return _numeric_result(_OPERATOR_FUNCTIONS[operator](rank_left, rank_right))
    if isinstance(left.value, str):
        return _numeric_result(
            _OPERATOR_FUNCTIONS[operator](left.value.lower(), right.value.lower())
        )
    try:
        return _numeric_result(_OPERATOR_FUNCTIONS[operator](left.value, right.value))
    except TypeError:
        return error_value('#VALUE!')


def apply_binary(operator: XlBinaryOperator, left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The value of a binary operation. A partial operand leaves the operation unevaluated,
    spelled from its unfinished operands around the operator — an operand the program finished
    computing is the value the cell shows and spells no fragment — and an operand that is an
    error value is the value of the whole operation. Complete operands compare by the rules
    of `_compare` and compute by the rules of `_arithmetic`.
    """
    if operator is XlBinaryOperator.CONCAT:
        return concat(left, right)
    if operator is XlBinaryOperator.RANGE:
        return XlmValue(
            value=F'{left.unwrap()}:{right.unwrap()}',
            cells=(left, right),
            partial=left.partial or right.partial,
        )
    if operator is XlBinaryOperator.ISECT or operator is XlBinaryOperator.UNION:
        separator = ' ' if operator is XlBinaryOperator.ISECT else ','
        return XlmValue(
            value=F'{left.unwrap()}{separator}{right.unwrap()}',
            partial=True,
        )
    if left.partial or right.partial:
        fragment = _OPERATOR_SYMBOLS[operator].join(
            side.unwrap() for side in (left, right) if side.partial
        )
        return XlmValue(value=fragment, partial=True)
    if left.error:
        return left
    if right.error:
        return right
    if operator in _ARITHMETIC_OPERATORS:
        return _arithmetic(operator, left, right)
    return _compare(operator, left, right)


class XlmOutcome:
    """
    What one macro command produced: the value it computed, the jump it asks the engine to
    take, the status that overrides the one its value derives, whether it halts the program,
    and the arrival the engine lands the jump by, for a jump the command that asks for it
    does not name — the step a macro call jumps to and the cell a return continues at. A
    handler that neither jumps nor overrides a status leaves both empty.
    """

    def __init__(
        self,
        value: XlmValue | None = None,
        jump: XlmCursor | None = None,
        status: XlmStatus | None = None,
        halts: bool = False,
        arrival: XlmArrival | None = None,
    ):
        self.value = XlmValue() if value is None else value
        self.jump = jump
        self.status = status
        self.halts = halts
        self.arrival = arrival

Functions

def is_number(value)

Whether a value reads as a number, as the macro language reads one: a finite number is one, and so is the text of one. The digit separators, the infinities, and the missing number that Python reads in a text spell no number of the macro language.

Expand source code Browse git
def is_number(value: Any) -> bool:
    """
    Whether a value reads as a number, as the macro language reads one: a finite number is one,
    and so is the text of one. The digit separators, the infinities, and the missing number
    that Python reads in a text spell no number of the macro language.
    """
    if isinstance(value, str) and '_' in value:
        return False
    try:
        number = float(value)
    except (ValueError, TypeError, OverflowError):
        return False
    return math.isfinite(number)
def condition(value)

The truth of a value, as every command that reads a condition reads it: the spellings of the two truth values are theirs, a number holds when it is not zero — by its type, so the text of a number holds nothing — a value that holds nothing answers FALSE, and a value that is an error value is that error. Any other text is the #VALUE! error Excel answers for a condition that spells no truth value.

Expand source code Browse git
def condition(value: XlmValue) -> bool | XlmValue:
    """
    The truth of a value, as every command that reads a condition reads it: the spellings of the
    two truth values are theirs, a number holds when it is not zero — by its type, so the text
    of a number holds nothing — a value that holds nothing answers `FALSE`, and a value that is
    an error value is that error. Any other text is the `#VALUE!` error Excel answers for a
    condition that spells no truth value.
    """
    if value.error:
        return value
    data = value.value
    if data is None or data == '':
        return False
    if isinstance(data, bool):
        return data
    if isinstance(data, (int, float)):
        return data != 0
    if isinstance(data, str):
        lowered = data.lower()
        if lowered == 'true':
            return True
        if lowered == 'false':
            return False
    return error_value('#VALUE!')
def error_outcome(truth, spelled=None, halts=False)

The outcome a command that reads a condition answers for one that spells no truth value: the error value the condition is, spelled as the command when it names one, and an error the macro halts on when its grammar makes the condition fatal.

Expand source code Browse git
def error_outcome(
    truth: XlmValue,
    spelled: str | None = None,
    halts: bool = False,
) -> XlmOutcome:
    """
    The outcome a command that reads a condition answers for one that spells no truth value:
    the error value the condition is, spelled as the command when it names one, and an error
    the macro halts on when its grammar makes the condition fatal.
    """
    if spelled is not None:
        truth = replace(truth, text=spelled)
    return XlmOutcome(value=truth, status=XlmStatus.Error if halts else None)
def unwrap_literal(text)

The text of a quoted string literal without its quotes; a text that carries no quotes keeps its spelling, and a doubled quote inside the literal spells one quote.

Expand source code Browse git
def unwrap_literal(text: str) -> str:
    """
    The text of a quoted string literal without its quotes; a text that carries no quotes
    keeps its spelling, and a doubled quote inside the literal spells one quote.
    """
    if len(text) > 1 and text.startswith('"') and text.endswith('"'):
        return text[1:-1].replace('""', '"')
    return text
def ansi_bytes(text)

The bytes a text occupies in the ANSI code page the macro language writes: a character CHAR answers is the byte it was made from, any other character its byte in code page 1252, and a character that code page cannot spell is the question mark Windows writes instead.

Expand source code Browse git
def ansi_bytes(text: str) -> bytes:
    """
    The bytes a text occupies in the ANSI code page the macro language writes: a character
    `CHAR` answers is the byte it was made from, any other character its byte in code page
    1252, and a character that code page cannot spell is the question mark Windows writes
    instead.
    """
    data = bytearray()
    for char in text:
        code = ord(char)
        if code > 0xFF:
            try:
                code = char.encode('cp1252')[0]
            except UnicodeEncodeError:
                code = 0x3F
        data.append(code)
    return bytes(data)
def wrap_literal(data, must_wrap=False)

The spelling the trace gives a value: a number, a boolean, and a text that already carries quotes keep their own spelling — the quoted text only while the caller does not insist on quotes — and every other text is quoted with its quotes doubled.

Expand source code Browse git
def wrap_literal(data: Any, must_wrap: bool = False) -> str:
    """
    The spelling the trace gives a value: a number, a boolean, and a text that already carries
    quotes keep their own spelling — the quoted text only while the caller does not insist on
    quotes — and every other text is quoted with its quotes doubled.
    """
    if isinstance(data, bool):
        return 'TRUE' if data else 'FALSE'
    if is_number(data) or (
        isinstance(data, str)
        and len(data) > 1
        and data.startswith('"')
        and data.endswith('"')
        and not must_wrap
    ):
        return str(data)
    if isinstance(data, float) and data.is_integer():
        return str(int(data))
    if isinstance(data, int):
        return str(data)
    return F'"{str(data).replace(chr(34), chr(34) * 2)}"'
def error_value(text)

The error value Excel spells with the given text, such as the #VALUE! of a coercion that failed.

Expand source code Browse git
def error_value(text: str) -> XlmValue:
    """
    The error value Excel spells with the given text, such as the `#VALUE!` of a coercion that
    failed.
    """
    return XlmValue(value=text, text=text, error=True)
def is_blank(value)

Whether a value is the blank of a cell that holds nothing: every operator reads it as the zero of the other operand's type, unlike the empty text, which the trace spells "".

Expand source code Browse git
def is_blank(value: XlmValue) -> bool:
    """
    Whether a value is the blank of a cell that holds nothing: every operator reads it as the
    zero of the other operand's type, unlike the empty text, which the trace spells `""`.
    """
    return value.text == ''
def blank_value(reference=None)

The value of a cell that holds nothing: every operator reads it as the zero of the other operand's type. The blank of the cell at the given address, or of no cell in particular.

Expand source code Browse git
def blank_value(reference: XlmReference | None = None) -> XlmValue:
    """
    The value of a cell that holds nothing: every operator reads it as the zero of the other
    operand's type. The blank of the cell at the given address, or of no cell in particular.
    """
    return XlmValue(value='', text='', reference=reference)
def concat(left, right)

The & of two values: their joined texts when both are complete, the two texts joined by the operator itself when either side is partial, and the error of the first side that is an error value.

Expand source code Browse git
def concat(left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The `&` of two values: their joined texts when both are complete, the two texts joined by
    the operator itself when either side is partial, and the error of the first side that is
    an error value.
    """
    if left.partial or right.partial:
        fragment = F'{left.unwrap()}&{right.unwrap()}'
        return XlmValue(value=fragment, partial=True)
    if left.error:
        return left
    if right.error:
        return right
    joined = left.unwrap() + right.unwrap()
    return XlmValue(value=joined)
def apply_binary(operator, left, right)

The value of a binary operation. A partial operand leaves the operation unevaluated, spelled from its unfinished operands around the operator — an operand the program finished computing is the value the cell shows and spells no fragment — and an operand that is an error value is the value of the whole operation. Complete operands compare by the rules of _compare and compute by the rules of _arithmetic.

Expand source code Browse git
def apply_binary(operator: XlBinaryOperator, left: XlmValue, right: XlmValue) -> XlmValue:
    """
    The value of a binary operation. A partial operand leaves the operation unevaluated,
    spelled from its unfinished operands around the operator — an operand the program finished
    computing is the value the cell shows and spells no fragment — and an operand that is an
    error value is the value of the whole operation. Complete operands compare by the rules
    of `_compare` and compute by the rules of `_arithmetic`.
    """
    if operator is XlBinaryOperator.CONCAT:
        return concat(left, right)
    if operator is XlBinaryOperator.RANGE:
        return XlmValue(
            value=F'{left.unwrap()}:{right.unwrap()}',
            cells=(left, right),
            partial=left.partial or right.partial,
        )
    if operator is XlBinaryOperator.ISECT or operator is XlBinaryOperator.UNION:
        separator = ' ' if operator is XlBinaryOperator.ISECT else ','
        return XlmValue(
            value=F'{left.unwrap()}{separator}{right.unwrap()}',
            partial=True,
        )
    if left.partial or right.partial:
        fragment = _OPERATOR_SYMBOLS[operator].join(
            side.unwrap() for side in (left, right) if side.partial
        )
        return XlmValue(value=fragment, partial=True)
    if left.error:
        return left
    if right.error:
        return right
    if operator in _ARITHMETIC_OPERATORS:
        return _arithmetic(operator, left, right)
    return _compare(operator, left, right)

Classes

class XlmReference (sheet, row, col)

The address of one cell: the sheet it sits on — None names the sheet the reading formula is on — and its one-based row and column.

Expand source code Browse git
class XlmReference(NamedTuple):
    """
    The address of one cell: the sheet it sits on — `None` names the sheet the reading
    formula is on — and its one-based row and column.
    """

    sheet: str | None
    row: int
    col: int

    def a1(self) -> str:
        """
        The A1 spelling of the address, qualified by its sheet when it names one.
        """
        cell = F'{column_letters(self.col)}{self.row}'
        if self.sheet is None:
            return cell
        return F'{self.sheet}!{cell}'

Ancestors

  • builtins.tuple

Instance variables

var sheet

Alias for field number 0

Expand source code Browse git
class XlmReference(NamedTuple):
    """
    The address of one cell: the sheet it sits on — `None` names the sheet the reading
    formula is on — and its one-based row and column.
    """

    sheet: str | None
    row: int
    col: int

    def a1(self) -> str:
        """
        The A1 spelling of the address, qualified by its sheet when it names one.
        """
        cell = F'{column_letters(self.col)}{self.row}'
        if self.sheet is None:
            return cell
        return F'{self.sheet}!{cell}'
var row

Alias for field number 1

Expand source code Browse git
class XlmReference(NamedTuple):
    """
    The address of one cell: the sheet it sits on — `None` names the sheet the reading
    formula is on — and its one-based row and column.
    """

    sheet: str | None
    row: int
    col: int

    def a1(self) -> str:
        """
        The A1 spelling of the address, qualified by its sheet when it names one.
        """
        cell = F'{column_letters(self.col)}{self.row}'
        if self.sheet is None:
            return cell
        return F'{self.sheet}!{cell}'
var col

Alias for field number 2

Expand source code Browse git
class XlmReference(NamedTuple):
    """
    The address of one cell: the sheet it sits on — `None` names the sheet the reading
    formula is on — and its one-based row and column.
    """

    sheet: str | None
    row: int
    col: int

    def a1(self) -> str:
        """
        The A1 spelling of the address, qualified by its sheet when it names one.
        """
        cell = F'{column_letters(self.col)}{self.row}'
        if self.sheet is None:
            return cell
        return F'{self.sheet}!{cell}'

Methods

def a1(self)

The A1 spelling of the address, qualified by its sheet when it names one.

Expand source code Browse git
def a1(self) -> str:
    """
    The A1 spelling of the address, qualified by its sheet when it names one.
    """
    cell = F'{column_letters(self.col)}{self.row}'
    if self.sheet is None:
        return cell
    return F'{self.sheet}!{cell}'
class XlmValue (value=None, text=None, partial=False, date=False, reference=None, cells=None, error=False)

One value of the macro language as the emulator computes it: value is the Python object behind it, text the spelling the trace prints, partial marks a value whose computation the program never finished, date marks a value that came from a date cell, reference names the cell address the value stands for, cells holds the elements of an array or the corners of a range, and error marks an error value such as the #DIV/0! of a division by zero. A given spelling of a number is normalized unless the value is a text, whose spelling is its content.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlmValue:
    """
    One value of the macro language as the emulator computes it: `value` is the Python object
    behind it, `text` the spelling the trace prints, `partial` marks a value whose computation
    the program never finished, `date` marks a value that came from a date cell, `reference`
    names the cell address the value stands for, `cells` holds the elements of an array or the
    corners of a range, and `error` marks an error value such as the `#DIV/0!` of a division
    by zero. A given spelling of a number is normalized unless the value is a text, whose
    spelling is its content.
    """

    value: Any = None
    text: str | None = None
    partial: bool = False
    date: bool = False
    reference: XlmReference | None = None
    cells: tuple[XlmValue, ...] | None = None
    error: bool = False

    def __post_init__(self):
        if self.text is None:
            if isinstance(self.value, bool):
                self.text = 'TRUE' if self.value else 'FALSE'
            elif self.partial:
                self.text = str(self.value)
            else:
                self.text = _default_text(self.value)
        elif not isinstance(self.value, str) and is_number(self.text):
            number = float(self.text)
            self.text = str(int(number) if number.is_integer() else number)

    def unwrap(self) -> str:
        """
        The content of the value as a text. A text the program finished computing is its own
        content, and any other finished value is spelled as itself, whatever spelling the trace
        gives the command that computed it; a value the program never finished is the spelling
        of what it left unfinished, without the quotes of a string literal.
        """
        if self.partial:
            return unwrap_literal(self.text or '')
        if isinstance(self.value, str):
            return self.value
        return _default_text(self.value)

    def unwrap_date(self) -> datetime.datetime | None:
        """
        The moment an ISO-spelled text names, in one of the spellings the macro language
        writes into cells, or `None` for a text that names none.
        """
        for spelling in _DATETIME_FORMATS:
            try:
                return datetime.datetime.strptime(self.unwrap(), spelling)
            except ValueError:
                continue
        return None

Instance variables

var value

The type of the None singleton.

var text

The type of the None singleton.

var partial

The type of the None singleton.

var date

The type of the None singleton.

var reference

The type of the None singleton.

var cells

The type of the None singleton.

var error

The type of the None singleton.

Methods

def unwrap(self)

The content of the value as a text. A text the program finished computing is its own content, and any other finished value is spelled as itself, whatever spelling the trace gives the command that computed it; a value the program never finished is the spelling of what it left unfinished, without the quotes of a string literal.

Expand source code Browse git
def unwrap(self) -> str:
    """
    The content of the value as a text. A text the program finished computing is its own
    content, and any other finished value is spelled as itself, whatever spelling the trace
    gives the command that computed it; a value the program never finished is the spelling
    of what it left unfinished, without the quotes of a string literal.
    """
    if self.partial:
        return unwrap_literal(self.text or '')
    if isinstance(self.value, str):
        return self.value
    return _default_text(self.value)
def unwrap_date(self)

The moment an ISO-spelled text names, in one of the spellings the macro language writes into cells, or None for a text that names none.

Expand source code Browse git
def unwrap_date(self) -> datetime.datetime | None:
    """
    The moment an ISO-spelled text names, in one of the spellings the macro language
    writes into cells, or `None` for a text that names none.
    """
    for spelling in _DATETIME_FORMATS:
        try:
            return datetime.datetime.strptime(self.unwrap(), spelling)
        except ValueError:
            continue
    return None
class XlmOutcome (value=None, jump=None, status=None, halts=False, arrival=None)

What one macro command produced: the value it computed, the jump it asks the engine to take, the status that overrides the one its value derives, whether it halts the program, and the arrival the engine lands the jump by, for a jump the command that asks for it does not name — the step a macro call jumps to and the cell a return continues at. A handler that neither jumps nor overrides a status leaves both empty.

Expand source code Browse git
class XlmOutcome:
    """
    What one macro command produced: the value it computed, the jump it asks the engine to
    take, the status that overrides the one its value derives, whether it halts the program,
    and the arrival the engine lands the jump by, for a jump the command that asks for it
    does not name — the step a macro call jumps to and the cell a return continues at. A
    handler that neither jumps nor overrides a status leaves both empty.
    """

    def __init__(
        self,
        value: XlmValue | None = None,
        jump: XlmCursor | None = None,
        status: XlmStatus | None = None,
        halts: bool = False,
        arrival: XlmArrival | None = None,
    ):
        self.value = XlmValue() if value is None else value
        self.jump = jump
        self.status = status
        self.halts = halts
        self.arrival = arrival