feat: shared unit-aware formatting helpers

Pure presentation helpers (labels, number parse/format) shared by docks
and dialogs; no Qt imports.
This commit is contained in:
smillmorel 2026-09-16 12:03:07 -04:00
commit f1d58273b7
2 changed files with 148 additions and 0 deletions

View file

@ -0,0 +1,97 @@
# Ported from otko-development (MIT), (c) 2026 OTKO contributors
"""Small presentation helpers shared by docks and dialogs.
Keeps labels, unit hints and numeric parsing in one place so every surface
names a field the same way and rejects bad input with the same message shape.
Pure helpers only — no Qt imports live here.
"""
from __future__ import annotations
from otko.core import UnitLabels, UnitSystem, labels_for
__all__ = [
"DOF_LABELS",
"MASS_LABELS",
"RESTRAINT_LABELS",
"format_number",
"parse_float",
"parse_int",
"unit_label",
]
#: Six translational/rotational DOF labels, indexed 0-5.
DOF_LABELS: tuple[str, ...] = ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz")
#: Six mass-component labels, indexed 0-5.
MASS_LABELS: tuple[str, ...] = ("Mx", "My", "Mz", "Ixx", "Iyy", "Izz")
#: Human-readable restraint component names (restraint + rotation).
RESTRAINT_LABELS: tuple[str, ...] = (
"Ux",
"Uy",
"Uz",
"Rx",
"Ry",
"Rz",
)
#: Map a public quantity name to the :class:`UnitLabels` field that holds it.
_QUANTITY_FIELDS: dict[str, str] = {
"length": "length",
"force": "force",
"moment": "moment",
"stress": "stress",
"curvature": "curvature",
"rotation": "rotation",
}
def format_number(value: float) -> str:
"""Return a compact, round-trippable string for ``value``.
Integral floats lose the decimal point; everything else keeps up to six
significant digits so a round-trip through a table cell is exact enough
for engineering values.
"""
if value == int(value):
return str(int(value))
return f"{value:.6g}"
def parse_float(text: str, field: str, *, allow_empty: bool = False) -> float:
"""Parse ``text`` as a float or raise ``ValueError`` naming ``field``.
An empty string is accepted when ``allow_empty`` is set and yields ``0.0``
so a blank optional cell is not treated as an error.
"""
stripped = text.strip()
if not stripped:
if allow_empty:
return 0.0
raise ValueError(f"{field} is required")
try:
return float(stripped)
except ValueError:
raise ValueError(f"{field} must be a number, got {stripped!r}") from None
def parse_int(text: str, field: str, *, allow_empty: bool = False) -> int:
"""Parse ``text`` as an int or raise ``ValueError`` naming ``field``."""
value = parse_float(text, field, allow_empty=allow_empty)
if value != int(value):
raise ValueError(f"{field} must be a whole number, got {text.strip()!r}")
return int(value)
def unit_label(quantity: str, system: UnitSystem) -> str:
"""Return the display label for ``quantity`` in ``system``.
``quantity`` is one of ``length``, ``force``, ``moment``, ``stress``,
``curvature`` or ``rotation``; anything else raises ``ValueError``.
"""
field = _QUANTITY_FIELDS.get(quantity)
if field is None:
raise ValueError(f"unknown unit quantity: {quantity!r}")
labels: UnitLabels = labels_for(system)
return getattr(labels, field)

View file

@ -0,0 +1,51 @@
"""Unit tests for :mod:`otko.views.formatting` (no Qt required)."""
from __future__ import annotations
import pytest
from otko.core import UnitSystem
from otko.views.formatting import (
DOF_LABELS,
MASS_LABELS,
RESTRAINT_LABELS,
format_number,
parse_float,
parse_int,
unit_label,
)
def test_label_tuples() -> None:
assert DOF_LABELS == ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz")
assert MASS_LABELS == ("Mx", "My", "Mz", "Ixx", "Iyy", "Izz")
assert RESTRAINT_LABELS == ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz")
def test_format_number() -> None:
assert format_number(50.0) == "50"
assert format_number(1.5) == "1.5"
def test_parse_float_required() -> None:
with pytest.raises(ValueError):
parse_float("", "x")
def test_parse_float_allow_empty() -> None:
assert parse_float("", "x", allow_empty=True) == 0.0
def test_parse_int_rejects_fractional() -> None:
with pytest.raises(ValueError):
parse_int("1.5", "x")
def test_unit_label() -> None:
assert unit_label("force", UnitSystem.SI_M_N) == "N"
assert unit_label("moment", UnitSystem.US_FT_KIP) == "kip·ft"
def test_unit_label_unknown_quantity() -> None:
with pytest.raises(ValueError):
unit_label("temperature", UnitSystem.SI_M_N)