# -*- coding: utf-8 -*- """Panel Analysis: decimal feet -> feet-inches display text, shared. Owner ruling (`CLAUDE.md` / the pagespec docs): every length this tool displays is feet-inches to 1/8" (`8'-4 1/2"`), never decimal feet -- the same rule `tools/pa_web/static/units.js`'s `Units.fmtFtIn` enforces client-side for plain editable fields. A `kv` block or a read-only table CELL built server- side (Panel Weight & Centroid, the lifter/dunnage summary tables) is plain text by the time it reaches the page -- the client never reformats it -- so the same rounding has to happen here, in CPython/IronPython, before the text is ever sent. Split out of `vibe.pa_extents._feet_str` (2026-09-05) once a second module (`vibe.pa_handling`) needed the identical formatting for its own kv/table cells -- one algorithm, not two copies that can drift on a rounding edge. Standard library only, both Pythons: no f-strings, no `pathlib`, no `dataclasses`/typing/`enum`. """ # Eighths-of-an-inch -> the fraction word this module prints. A precast # dimension finer than 1/8" is not meaningful, so `feet_str` rounds to it # rather than pretending to more precision than the field carries. EIGHTHS = {0: '', 1: '1/8', 2: '1/4', 3: '3/8', 4: '1/2', 5: '5/8', 6: '3/4', 7: '7/8'} def feet_str(value): """Decimal feet -> an architectural dimension string, e.g. 8'-4 1/2". Internal units are decimal feet throughout this codebase (`CLAUDE.md` SS3), so a bare number is read as feet and converted here to the feet-inches form the owner requires everywhere a length is displayed. A value that is already a string is left to the CALLER to decide whether to pass through unreformatted (a sibling module may hand this a value that is already formatted correctly, and reformatting a string that already reads right is how a "2'-11.5\"" becomes a wrong "2'-11 1/2\"" glued onto a stray decimal) -- this function itself always tries to read `value` as a plain number. Returns `None` only when `value` cannot be read as a number at all -- never a fabricated dimension. """ try: feet = float(value) except (TypeError, ValueError): return None negative = feet < 0.0 feet = abs(feet) whole_ft = int(feet) inches = (feet - whole_ft) * 12.0 eighths_total = int(round(inches * 8.0)) whole_in, eighths = divmod(eighths_total, 8) if whole_in >= 12: whole_ft += whole_in // 12 whole_in = whole_in % 12 frac = EIGHTHS[eighths] if frac and whole_in: in_part = '{0} {1}'.format(whole_in, frac) elif frac: in_part = frac else: in_part = str(whole_in) if whole_ft > 0: text = u'{0}\'-{1}"'.format(whole_ft, in_part) else: text = u'{0}"'.format(in_part) return u'-' + text if negative else text