Show rain in centimetres rather than millimetres on metric pages (#191)

Precipitation on a °C page now reads "4.90 cm" where it read "49.0 mm".

Both units keep two decimals. cm and inches are only 2.54x apart, so cm needs the
same resolution inches have — at one decimal every light-rain day would collapse
to 0.0 or 0.1 cm, and the source itself is hundredths of an inch.

Ingest is untouched: climate.py still converts the upstream mm to inches, which
remains the stored unit and the value carried in data-precip-in.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
This commit is contained in:
Emi Griffith 2026-07-19 12:10:24 -07:00 committed by GitHub
parent 5cf517e8ea
commit 77af680895
2 changed files with 15 additions and 15 deletions

View file

@ -69,9 +69,9 @@ _TEMP_METRICS = {"tmax", "tmin", "feels"}
F_COUNTRIES = frozenset({"US", "PR", "GU", "VI", "AS", "MP", "UM", F_COUNTRIES = frozenset({"US", "PR", "GU", "VI", "AS", "MP", "UM",
"BS", "BZ", "KY", "PW", "FM", "MH", "LR"}) "BS", "BZ", "KY", "PW", "FM", "MH", "LR"})
# One flag drives every measure: a °C page also gets mm and km/h. Mirrors the same # One flag drives every measure: a °C page also gets cm and km/h. Mirrors the same
# decision in frontend/units.js. # decision in frontend/units.js.
MM_PER_IN = 25.4 CM_PER_IN = 2.54
KMH_PER_MPH = 1.609344 KMH_PER_MPH = 1.609344
# The unit the current page renders temperatures in. None = °F, and also means # The unit the current page renders temperatures in. None = °F, and also means
@ -171,11 +171,11 @@ def _precip(v):
def _precip_text(v) -> str: def _precip_text(v) -> str:
"""Millimetres get one decimal and inches two — 0.04 in and 1.0 mm are the same """Two decimals either way — cm and inches are only 2.54x apart, so cm needs
amount, so '1.02 mm' would advertise precision the source doesn't have.""" the same resolution, and the source is hundredths of an inch."""
if v is None: if v is None:
return "" return ""
return f"{v * MM_PER_IN:.1f} mm" if _UNIT.get() == "C" else f"{v:.2f} in" return f"{v * CM_PER_IN:.2f} cm" if _UNIT.get() == "C" else f"{v:.2f} in"
def _wind(v): def _wind(v):

View file

@ -304,11 +304,11 @@ def test_units_default_to_the_citys_country(client):
assert 'data-unit-default="F"' in us assert 'data-unit-default="F"' in us
_PRECIP_SPAN = re.compile(r'<span class="precip" data-precip-in="([\d.]+)">([\d.]+) (in|mm)</span>') _PRECIP_SPAN = re.compile(r'<span class="precip" data-precip-in="([\d.]+)">([\d.]+) (in|cm)</span>')
def test_precip_and_wind_follow_the_citys_unit(client): def test_precip_and_wind_follow_the_citys_unit(client):
# One toggle drives every measure: a °C page shows mm and km/h, not °C mixed # One toggle drives every measure: a °C page shows cm and km/h, not °C mixed
# with inches and mph. The data-* attribute stays imperial in both, so # with inches and mph. The data-* attribute stays imperial in both, so
# climate.js converts from the same source of truth it does for temperature. # climate.js converts from the same source of truth it does for temperature.
gb = client.get(f"{B}/climate/{SLUG}/records").text # London, GB gb = client.get(f"{B}/climate/{SLUG}/records").text # London, GB
@ -317,12 +317,12 @@ def test_precip_and_wind_follow_the_citys_unit(client):
gb_p, us_p = _PRECIP_SPAN.findall(gb), _PRECIP_SPAN.findall(us) gb_p, us_p = _PRECIP_SPAN.findall(gb), _PRECIP_SPAN.findall(us)
assert gb_p and us_p assert gb_p and us_p
assert {u for _raw, _shown, u in gb_p} == {"mm"} assert {u for _raw, _shown, u in gb_p} == {"cm"}
assert {u for _raw, _shown, u in us_p} == {"in"} assert {u for _raw, _shown, u in us_p} == {"in"}
# The conversion is real, not just a relabel. # The conversion is real, not just a relabel.
for raw, shown, _u in gb_p: for raw, shown, _u in gb_p:
assert abs(float(shown) - float(raw) * 25.4) < 0.05, (raw, shown) assert abs(float(shown) - float(raw) * 2.54) < 0.005, (raw, shown)
# On an imperial page the attribute and the text agree. # On an imperial page the attribute and the text agree.
for raw, shown, _u in us_p: for raw, shown, _u in us_p:
assert abs(float(shown) - float(raw)) < 0.005, (raw, shown) assert abs(float(shown) - float(raw)) < 0.005, (raw, shown)
@ -348,14 +348,14 @@ def test_wind_and_humidity_render_per_unit_system():
def test_precip_precision_differs_by_unit(): def test_precip_precision_differs_by_unit():
# 0.04 in and 1.0 mm are the same amount; rendering "1.02 mm" would advertise # cm and inches are only 2.54x apart, so cm keeps two decimals — at one
# precision the source doesn't have. # decimal every light-rain day would collapse to 0.0 or 0.1 cm.
import content import content
with content._unit_scope("F"): with content._unit_scope("F"):
assert content._precip_text(0.04) == "0.04 in" assert content._precip_text(0.04) == "0.04 in"
with content._unit_scope("C"): with content._unit_scope("C"):
assert content._precip_text(0.04) == "1.0 mm" assert content._precip_text(0.04) == "0.10 cm"
assert content._precip_text(1.81) == "46.0 mm" assert content._precip_text(1.81) == "4.60 cm"
def test_measure_conversion_constants_match_the_frontend(): def test_measure_conversion_constants_match_the_frontend():
@ -366,9 +366,9 @@ def test_measure_conversion_constants_match_the_frontend():
os.pardir, "frontend", "units.js") os.pardir, "frontend", "units.js")
with open(units_js, encoding="utf-8") as f: with open(units_js, encoding="utf-8") as f:
src = f.read() src = f.read()
mm = float(re.search(r"MM_PER_IN\s*=\s*([\d.]+)", src).group(1)) cm = float(re.search(r"CM_PER_IN\s*=\s*([\d.]+)", src).group(1))
kmh = float(re.search(r"KMH_PER_MPH\s*=\s*([\d.]+)", src).group(1)) kmh = float(re.search(r"KMH_PER_MPH\s*=\s*([\d.]+)", src).group(1))
assert mm == content.MM_PER_IN assert cm == content.CM_PER_IN
assert kmh == content.KMH_PER_MPH assert kmh == content.KMH_PER_MPH