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"
|