WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`panellift.units` -- the feet-inches formatter/parser every 前端 and 后端
|
||||
in this repo now goes through (owner's ruling, 2026-09-05: every displayed
|
||||
length is feet-inches, rounded to the nearest 1/8", never a decimal).
|
||||
|
||||
This module is written in the intersection of Python 2 and 3 syntax on
|
||||
purpose (see its own docstring) because the pyRevit/IronPython side is meant
|
||||
to import this exact file later; nothing here exercises anything Python-3
|
||||
-only, so a green run here says nothing about IronPython compatibility that
|
||||
reading the source doesn't already say, but it is still the same file that
|
||||
will make that trip.
|
||||
"""
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from panellift.units import fmt_ft_in, fmt_in, parse_length, parse_length_in
|
||||
|
||||
|
||||
# ------------------------------------------------------------- fmt_ft_in
|
||||
|
||||
def test_canonical_form():
|
||||
assert fmt_ft_in(8 + 4.5 / 12.0) == '8\'-4 1/2"'
|
||||
|
||||
|
||||
def test_zero_feet_still_prints_the_foot_mark():
|
||||
# 6 3/8 in = 0.53125 ft
|
||||
assert fmt_ft_in(0.53125) == '0\'-6 3/8"'
|
||||
|
||||
|
||||
def test_negative_keeps_the_sign_out_front_not_inside():
|
||||
assert fmt_ft_in(-0.5) == '-0\'-6"'
|
||||
|
||||
|
||||
def test_none_is_a_dash():
|
||||
assert fmt_ft_in(None) == '-'
|
||||
|
||||
|
||||
def test_a_whole_number_of_feet_has_no_fraction_cell():
|
||||
assert fmt_ft_in(5.0) == '5\'-0"'
|
||||
|
||||
|
||||
def test_twelve_inches_carries_into_the_foot_count():
|
||||
# 7 ft + 11.96 in rounds to 8 in at 1/8" precision, but 7 ft + 11.98 in
|
||||
# rounds UP to a full 12 in, which must carry to the next foot.
|
||||
almost_eight_feet = 7.0 + 11.98 / 12.0
|
||||
assert fmt_ft_in(almost_eight_feet) == '8\'-0"'
|
||||
|
||||
|
||||
def test_rounds_to_the_nearest_eighth_by_default():
|
||||
# 4 5/16 in -> nearest 1/8" is 4 1/4 or 4 3/8; 5/16 = 0.3125,
|
||||
# which is exactly between 1/4 (0.25) and 3/8 (0.375) at 1/8" steps of
|
||||
# 0.125 -- round(0.3125 / 0.125) = round(2.5) = 2 (round-half-to-even),
|
||||
# i.e. 2/8 = 1/4".
|
||||
four_and_5_16 = 4.3125 / 12.0
|
||||
assert fmt_ft_in(four_and_5_16) == '0\'-4 1/4"'
|
||||
|
||||
|
||||
def test_a_finer_precision_is_honoured():
|
||||
# 1/16" precision: 6 1/16 in.
|
||||
six_and_1_16 = (6.0 + 1.0 / 16.0) / 12.0
|
||||
assert fmt_ft_in(six_and_1_16, precision=16) == '0\'-6 1/16"'
|
||||
# at the default 1/8" precision the same value rounds down to 6".
|
||||
assert fmt_ft_in(six_and_1_16) == '0\'-6"'
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- fmt_in
|
||||
|
||||
def test_fmt_in_basic():
|
||||
assert fmt_in(6.375) == '6 3/8"'
|
||||
|
||||
|
||||
def test_fmt_in_whole_number():
|
||||
assert fmt_in(12.0) == '12"'
|
||||
|
||||
|
||||
def test_fmt_in_none_is_a_dash():
|
||||
assert fmt_in(None) == '-'
|
||||
|
||||
|
||||
def test_fmt_in_carries_a_full_inch():
|
||||
# 5.98 in at 1/8" precision rounds up to a full 6 in, not "5 8/8".
|
||||
assert fmt_in(5.98) == '6"'
|
||||
|
||||
|
||||
def test_fmt_in_negative():
|
||||
assert fmt_in(-0.5) == '-0 1/2"'
|
||||
|
||||
|
||||
# ------------------------------------------------------- rounding boundary
|
||||
|
||||
@pytest.mark.parametrize('eighths, expected', [
|
||||
(0, '0"'), (1, '0 1/8"'), (2, '0 1/4"'), (3, '0 3/8"'),
|
||||
(4, '0 1/2"'), (5, '0 5/8"'), (6, '0 3/4"'), (7, '0 7/8"'),
|
||||
])
|
||||
def test_every_eighth_prints_its_own_reduced_fraction(eighths, expected):
|
||||
assert fmt_in(eighths / 8.0) == expected
|
||||
|
||||
|
||||
def test_exactly_half_a_sixteenth_rounds_to_even_at_default_precision():
|
||||
"""1/16" (0.0625 in) sits exactly halfway between 0 and 1/8" at the
|
||||
default precision -- Python's own `round()` (banker's rounding) sends an
|
||||
exact .5 to the nearest EVEN integer, so 0.5 eighths rounds to 0, not 1.
|
||||
This test pins that behaviour so a future change to the rounding rule
|
||||
shows up here rather than as a silently different printed fraction.
|
||||
"""
|
||||
assert fmt_in(1.0 / 16.0) == '0"'
|
||||
# 3/16" -> 1.5 eighths -> rounds to 2 (even) -> 1/4"
|
||||
assert fmt_in(3.0 / 16.0) == '0 1/4"'
|
||||
|
||||
|
||||
# --------------------------------------------------------------- parsing
|
||||
|
||||
@pytest.mark.parametrize('text, expected_ft', [
|
||||
('8\'-4 1/2"', 8 + 4.5 / 12.0),
|
||||
('8\'4"', 8 + 4.0 / 12.0),
|
||||
("8' 4-1/2\"", 8 + 4.5 / 12.0),
|
||||
('4 1/2"', 4.5 / 12.0),
|
||||
('100"', 100.0 / 12.0),
|
||||
("-0'-6\"", -0.5),
|
||||
('8\'', 8.0),
|
||||
])
|
||||
def test_parse_length_accepts_every_named_form(text, expected_ft):
|
||||
assert parse_length(text) == pytest.approx(expected_ft, abs=1e-9)
|
||||
|
||||
|
||||
def test_a_bare_number_is_feet_by_default():
|
||||
assert parse_length('5') == pytest.approx(5.0)
|
||||
|
||||
|
||||
def test_a_bare_number_can_be_read_as_inches():
|
||||
assert parse_length('6', default_unit='in') == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_parse_length_in_reads_a_bare_number_as_inches_by_default():
|
||||
assert parse_length_in('6') == pytest.approx(6.0)
|
||||
|
||||
|
||||
def test_parse_length_in_can_read_a_bare_number_as_feet():
|
||||
assert parse_length_in('1', default_unit='ft') == pytest.approx(12.0)
|
||||
|
||||
|
||||
def test_parse_length_in_on_a_feet_inches_string():
|
||||
assert parse_length_in('6 3/8"') == pytest.approx(6.375)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad', [
|
||||
'garbage', '', ' ', "8'garbage", '1/0"', None,
|
||||
])
|
||||
def test_unparseable_text_raises_value_error_naming_the_text(bad):
|
||||
with pytest.raises(ValueError):
|
||||
parse_length(bad)
|
||||
|
||||
|
||||
def test_the_error_names_the_offending_text():
|
||||
with pytest.raises(ValueError) as exc:
|
||||
parse_length('nonsense')
|
||||
assert 'nonsense' in str(exc.value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------- negative forms
|
||||
|
||||
def test_negative_feet_and_inches_together():
|
||||
assert parse_length("-8'-4 1/2\"") == pytest.approx(-(8 + 4.5 / 12.0))
|
||||
|
||||
|
||||
def test_negative_bare_inches():
|
||||
assert parse_length('-6"') == pytest.approx(-0.5)
|
||||
|
||||
|
||||
def test_negative_bare_number():
|
||||
assert parse_length('-5') == pytest.approx(-5.0)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- round-trip
|
||||
|
||||
def test_round_trip_property_across_a_range():
|
||||
"""parse_length(fmt_ft_in(x)) == x within 1/16", the max error a value
|
||||
can pick up from rounding to the nearest 1/8" for display.
|
||||
"""
|
||||
rng = random.Random(20260905)
|
||||
max_err_ft = (1.0 / 16.0) / 12.0
|
||||
for _ in range(5000):
|
||||
x = rng.uniform(-60.0, 60.0)
|
||||
rendered = fmt_ft_in(x)
|
||||
back = parse_length(rendered)
|
||||
assert abs(back - x) <= max_err_ft + 1e-12, (x, rendered, back)
|
||||
|
||||
|
||||
def test_round_trip_property_holds_at_zero_and_small_values():
|
||||
for x in (0.0, 1e-9, -1e-9, 1.0 / 12.0, -1.0 / 12.0):
|
||||
back = parse_length(fmt_ft_in(x))
|
||||
assert abs(back - x) <= (1.0 / 16.0) / 12.0 + 1e-9
|
||||
|
||||
|
||||
def test_round_trip_property_for_fmt_in_and_parse_length_in():
|
||||
rng = random.Random(20260905)
|
||||
max_err_in = 1.0 / 16.0
|
||||
for _ in range(5000):
|
||||
x = rng.uniform(-200.0, 200.0)
|
||||
rendered = fmt_in(x)
|
||||
back = parse_length_in(rendered)
|
||||
assert abs(back - x) <= max_err_in + 1e-9, (x, rendered, back)
|
||||
Reference in New Issue
Block a user