Skip to content

linexcel.values

linexcel.values

What a cell value is, once something has read it.

The engine, the file's own cache and a linked workbook each hand values back in their own shape — a Rust error object, a float that is really a date, a string that is really an error code. This module is the one place that decides what such a thing means: whether it counts as computed, whether two readings of it agree, and how it is written down for the report.

Kept apart from the reading (:mod:linexcel.loader) and from the reasoning (:mod:linexcel.analyzer) because both need these answers and neither owns them.

serial_to_date_text

serial_to_date_text(
    serial: Any, epoch_1904: bool = False
) -> str | None

Excel serial number → YYYY-MM-DD, or None if it is not a date.

Source code in src/linexcel/values.py
def serial_to_date_text(serial: Any, epoch_1904: bool = False) -> str | None:
    """Excel serial number → ``YYYY-MM-DD``, or None if it is not a date."""
    if isinstance(serial, bool) or not isinstance(serial, (int, float)):
        return None
    days = float(serial)
    if days != days or days in (float("inf"), float("-inf")):
        return None
    if epoch_1904:
        if days < 0:
            return None
        base = EXCEL_EPOCH_1904
    else:
        if days < 1 or days == 60:
            return None
        base = EPOCH_EARLY_1900 if days < 60 else EXCEL_EPOCH_1900
    try:
        return (base + datetime.timedelta(days=days)).date().isoformat()
    except (OverflowError, ValueError):
        return None

serial_to_time_text

serial_to_time_text(serial: Any) -> str | None

A fraction of one Excel day as a timezone-free time of day.

Cached times are read at millisecond precision. Keep the date component meaningful: 1.5 days must not silently become the same value as noon.

Source code in src/linexcel/values.py
def serial_to_time_text(serial: Any) -> str | None:
    """A fraction of one Excel day as a timezone-free time of day.

    Cached times are read at millisecond precision. Keep the date component
    meaningful: 1.5 days must not silently become the same value as noon.
    """
    if isinstance(serial, bool) or not isinstance(serial, (int, float)):
        return None
    if not math.isfinite(serial) or not 0 <= serial < 1:
        return None
    milliseconds = round(float(serial) * 86_400_000)
    if milliseconds >= 86_400_000:
        return None
    hours, remainder = divmod(milliseconds, 3_600_000)
    minutes, remainder = divmod(remainder, 60_000)
    seconds, milliseconds = divmod(remainder, 1000)
    return datetime.time(hours, minutes, seconds, milliseconds * 1000).isoformat()

readings_agree

readings_agree(
    recalculated: Any, stored: Any, date_text: str | None
) -> str

How the two readings of one cell relate: same, format or differ.

One rule, in one place. The report used to hold two: Python compared values for the warnings and returned "no difference" for every pair of strings — missing a recalculated non over a stored oui — while the viewer compared the rendered text and called any difference a disagreement, which is what made a French workbook light up red over 6,7 €.

format is the middle answer neither of them had: the same value, spelt with the separators of whatever saved the file. Both readings are still shown; only the verdict softens.

Source code in src/linexcel/values.py
def readings_agree(recalculated: Any, stored: Any, date_text: str | None) -> str:
    """How the two readings of one cell relate: ``same``, ``format`` or ``differ``.

    One rule, in one place. The report used to hold two: Python compared
    values for the warnings and returned "no difference" for every pair of
    strings — missing a recalculated ``non`` over a stored ``oui`` — while the
    viewer compared the rendered text and called any difference a
    disagreement, which is what made a French workbook light up red over
    ``6,7 €``.

    ``format`` is the middle answer neither of them had: the same value, spelt
    with the separators of whatever saved the file. Both readings are still
    shown; only the verdict softens.
    """
    time_agreement = _time_agreement(recalculated, stored, date_text)
    if time_agreement is not None:
        return time_agreement
    if _values_differ(recalculated, stored, date_text):
        return "differ"
    if date_text is not None:
        stored_date = _date_text_of(stored)
        if stored_date is None and isinstance(stored, str):
            try:
                stored_date = datetime.datetime.fromisoformat(stored).date().isoformat()
            except ValueError:
                pass
        if stored_date is not None:
            return "same" if date_text == stored_date else "differ"
    error_text = _excel_error_text(recalculated)
    if error_text is not None:
        return "same" if error_text == stored else "differ"
    if isinstance(recalculated, (int, float)) and isinstance(stored, (int, float)):
        return "same"
    if type(recalculated) is not type(stored):
        return "differ"
    left, right = _fmt_value(recalculated), _fmt_value(stored)
    if left == right:
        return "same"
    if (
        isinstance(recalculated, str)
        and isinstance(stored, str)
        and _separators_only(left, right)
    ):
        return "format"
    return "differ"