From 2794835590fadba119ace2f45742ecd69dbfeeef Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 8 Mar 2026 13:41:50 -0700 Subject: [PATCH] feat: Add Python consistency tests for calculator definitions and refactor QuickConversionTable to explicitly pass calculator configuration to its row builder. --- .../components/QuickConversionTable.svelte | 8 +- hdyc-svelte/src/routes/[slug]/+page.svelte | 4 +- tests/test_consistency.py | 106 ++++++++++++++++++ 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 tests/test_consistency.py diff --git a/hdyc-svelte/src/lib/components/QuickConversionTable.svelte b/hdyc-svelte/src/lib/components/QuickConversionTable.svelte index eac68d7..2ef5f9d 100644 --- a/hdyc-svelte/src/lib/components/QuickConversionTable.svelte +++ b/hdyc-svelte/src/lib/components/QuickConversionTable.svelte @@ -8,8 +8,8 @@ type Row = { input: number; output: string }; - const buildRow = (value: number): Row => { - const formatted = solve(config, 1, value.toString(), '', ''); + const buildRow = (value: number, c: CalculatorDef): Row => { + const formatted = solve(c, 1, value.toString(), '', ''); return { input: value, output: formatted.val2 || '—', @@ -22,8 +22,8 @@ let outputLabel = 'target units'; $: supportsTable = ['standard', 'inverse'].includes(config.type); - $: rows = supportsTable - ? numericSamples.map(buildRow) + $: rows = (config && supportsTable) + ? numericSamples.map(v => buildRow(v, config)) : []; $: inputLabel = config.labels?.in1 ?? 'source units'; $: outputLabel = config.labels?.in2 ?? 'target units'; diff --git a/hdyc-svelte/src/routes/[slug]/+page.svelte b/hdyc-svelte/src/routes/[slug]/+page.svelte index ad15cd0..15d837c 100644 --- a/hdyc-svelte/src/routes/[slug]/+page.svelte +++ b/hdyc-svelte/src/routes/[slug]/+page.svelte @@ -82,7 +82,9 @@

{calc.name}

- +{#key calc.slug} + +{/key}
{#if calc.descriptionHTML} diff --git a/tests/test_consistency.py b/tests/test_consistency.py new file mode 100644 index 0000000..178547b --- /dev/null +++ b/tests/test_consistency.py @@ -0,0 +1,106 @@ + +import math +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CALCULATORS_TS = ROOT / "hdyc-svelte" / "src" / "lib" / "data" / "calculators.ts" + +def _js_fmt(n: float) -> str: + """Mimics the fmt() function in engine.ts""" + if not math.isfinite(n): + return str(n) + if n == 0: + return "0" + if abs(n) < 1e-6: + return f"{n:.6e}".replace("e-0", "e-").replace("e+0", "e+") + + # engine.ts uses parseFloat(n.toFixed(6)).toString() + rounded = round(n, 6) + if rounded == 0: # Handle -0.0 + return "0" + if rounded == int(rounded): + return str(int(rounded)) + return str(rounded) + +def _js_fmt_precise(n: float) -> str: + """Mimics formatExampleValue in QuickConversionExample.svelte""" + if n is None or math.isnan(n): + return "—" + if not math.isfinite(n): + return str(n) + if n == 0: + return "0" + + rounded = round(n, 6) + if rounded != 0: + if rounded == int(rounded): + return str(int(rounded)) + return str(rounded) + + # Precise version for very small numbers + precise = f"{n:.12f}".rstrip('0').rstrip('.') + return precise if precise else "0" + +class TestCalculatorsConsistency(unittest.TestCase): + def test_standard_calculators_consistency(self): + text = CALCULATORS_TS.read_text(encoding="utf-8") + + # Extract the calculators array content + match = re.search(r"export const calculators: CalculatorDef\[\] = \[(.*?)\];", text, re.S) + self.assertTrue(match, "Could not find calculators array in calculators.ts") + + body = match.group(1) + # Split by '{"slug":' to avoid splitting on nested braces + raw_entries = body.split('{"slug":') + + errors = [] + for raw_entry in raw_entries: + if not raw_entry.strip(): + continue + entry = '{"slug":' + raw_entry + slug_match = re.search(r'"slug": "(.*?)"', entry) + if not slug_match: + continue + slug = slug_match.group(1) + + type_match = re.search(r'"type": "(.*?)"', entry) + if not type_match or type_match.group(1) != "standard": + continue + + # Use non-greedy search for factor/offset and handle potential whitespace + factor_match = re.search(r'"factor":\s*([0-9.eE+-]+)', entry) + offset_match = re.search(r'"offset":\s*([0-9.eE+-]+)', entry) + + factor = float(factor_match.group(1)) if factor_match else 1.0 + offset = float(offset_match.group(1)) if offset_match else 0.0 + + # 1. Formula Hint vs Chart Row 1 Consistency + # Logic: solve(config, 1, "1") -> val2 = fmt(1 * factor + offset) + row_one_output = _js_fmt(1.0 * factor + offset) + + # 2. How to convert Examples Consistency (Inverse) + if factor != 0: + reverse_val = (1.0 - offset) / factor + formatted_reverse = _js_fmt_precise(reverse_val) + + # Specific check for Réaumur to Kelvin (User request) + if slug == "reaumur-to-kelvin": + if formatted_reverse != "-217.72": + errors.append(f"[{slug}] Reverse example mismatch: expected -217.72, got {formatted_reverse}") + if row_one_output != "274.4": + errors.append(f"[{slug}] Chart row 1 mismatch: expected 274.4, got {row_one_output}") + + # Specific check for Feet per minute to Knots (Previous bug fix) + if slug == "feet-per-minute-to-knots": + if row_one_output != "0.009875": + errors.append(f"[{slug}] Chart row 1 mismatch: expected 0.009875, got {row_one_output}") + if formatted_reverse != "101.268504": + errors.append(f"[{slug}] Reverse example mismatch: expected 101.268504, got {formatted_reverse}") + + if errors: + self.fail("\n" + "\n".join(errors)) + +if __name__ == "__main__": + unittest.main()