Merge pull request 'fix(admissions): state distance in miles throughout, never mixed' (#105) from fix/standardise-distance-units into main
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 51s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 1m28s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 51s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 1m28s
Reviewed-on: #105
This commit was merged in pull request #105.
This commit is contained in:
commit
228eb214f5
4 files changed
+94
-23
No files matched your search
@@ -1364,6 +1364,17 @@ test('the postcode check answers for the published year, and names it', async ({
|
|||||||
// A verdict without its year is a number a parent cannot place.
|
// A verdict without its year is a number a parent cannot place.
|
||||||
await expect(result).toContainText(new RegExp(`September ${found!.distance.year}`));
|
await expect(result).toContainText(new RegExp(`September ${found!.distance.year}`));
|
||||||
await expect(result).toContainText(/inside|beyond|too close/);
|
await expect(result).toContainText(/inside|beyond|too close/);
|
||||||
|
|
||||||
|
// Both figures in the sentence must be in the same unit. The verdict used
|
||||||
|
// to render the home distance in metres and the cut-off in miles — "69 m
|
||||||
|
// away — inside the September 2026 cut-off of 0.17 miles" — which asks the
|
||||||
|
// reader to convert between units to check a comparison already made for
|
||||||
|
// them. Miles throughout: the unit councils publish and quote.
|
||||||
|
const headline = (await result.textContent()) ?? '';
|
||||||
|
const verdictLine = headline.split('.')[0];
|
||||||
|
expect(verdictLine, `verdict mixes units: ${verdictLine}`).not.toMatch(/\d\s?m\b/);
|
||||||
|
expect((verdictLine.match(/miles/g) ?? []).length,
|
||||||
|
`verdict should quote two figures in miles: ${verdictLine}`).toBe(2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* place — so both are pinned here rather than left to the component.
|
* place — so both are pinned here rather than left to the component.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatCutoffDistance, formatEntryYear } from '@/lib/utils';
|
import { formatCutoffDistance, formatMiles, formatEntryYear } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
describeCutoff, describeCutoffAbsence, compareToCutoff, CUTOFF_UNCERTAINTY_M,
|
describeCutoff, describeCutoffAbsence, compareToCutoff, CUTOFF_UNCERTAINTY_M,
|
||||||
} from '@/components/school/lastDistanceOffered';
|
} from '@/components/school/lastDistanceOffered';
|
||||||
@@ -32,8 +32,19 @@ describe('formatCutoffDistance', () => {
|
|||||||
expect(formatCutoffDistance(3472.96)!.secondary).toBe('3.5 km');
|
expect(formatCutoffDistance(3472.96)!.secondary).toBe('3.5 km');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leads with metres below 100 m, where a miles figure carries nothing', () => {
|
it('stays in miles at short range, where it used to swap to metres', () => {
|
||||||
expect(formatCutoffDistance(27)).toEqual({ primary: '27 m', secondary: '0.02 miles' });
|
// The swap made a single number easier to read and a comparison harder:
|
||||||
|
// "69 m away — inside the cut-off of 0.17 miles" asked the reader to
|
||||||
|
// convert between units to check a claim we had already made for them.
|
||||||
|
expect(formatCutoffDistance(27)).toEqual({ primary: '0.02 miles', secondary: '30 m' });
|
||||||
|
expect(formatCutoffDistance(69)!.primary).toMatch(/miles$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('describes a distance too short for two decimal places', () => {
|
||||||
|
// Rather than a flat "0.00 miles", which reads as no distance at all.
|
||||||
|
expect(formatMiles(5)).toBe('under 0.01 miles');
|
||||||
|
expect(formatMiles(0)).toBe('under 0.01 miles');
|
||||||
|
expect(formatMiles(20)).toBe('0.01 miles');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null rather than a zero cut-off', () => {
|
it('returns null rather than a zero cut-off', () => {
|
||||||
@@ -84,6 +95,36 @@ describe('describeCutoff', () => {
|
|||||||
// ── "Would we have got in?" ────────────────────────────────────────────
|
// ── "Would we have got in?" ────────────────────────────────────────────
|
||||||
|
|
||||||
describe('compareToCutoff', () => {
|
describe('compareToCutoff', () => {
|
||||||
|
it('never states the two figures in different units', () => {
|
||||||
|
/*
|
||||||
|
* The reported defect: "69 m away — inside the September 2026 cut-off of
|
||||||
|
* 0.17 miles". Both numbers are correct and the sentence is still useless,
|
||||||
|
* because checking it means converting one of them.
|
||||||
|
*
|
||||||
|
* Swept across the range where the old formatter switched units, so a
|
||||||
|
* future readability tweak to one figure cannot reintroduce the mismatch
|
||||||
|
* in the other.
|
||||||
|
*/
|
||||||
|
const mixed: string[] = [];
|
||||||
|
for (const homeM of [0, 5, 27, 69, 99, 100, 260, 800, 1609, 5000]) {
|
||||||
|
for (const cutoffM of [30, 69, 100, 270, 1000, 3500]) {
|
||||||
|
const { headline } = compareToCutoff(homeM, cutoffM, 2026);
|
||||||
|
const hasMetres = /\d\s?m\b/.test(headline);
|
||||||
|
const milesCount = (headline.match(/miles/g) ?? []).length;
|
||||||
|
// Two figures, both in miles, and no metric reading anywhere near them.
|
||||||
|
if (hasMetres || milesCount !== 2) {
|
||||||
|
mixed.push(`home=${homeM}m cutoff=${cutoffM}m -> ${headline}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(mixed).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads back the reported case in one unit', () => {
|
||||||
|
expect(compareToCutoff(69, 270, 2026).headline)
|
||||||
|
.toBe('0.04 miles away — inside the September 2026 cut-off of 0.17 miles.');
|
||||||
|
});
|
||||||
|
|
||||||
it('calls a clearly nearer home inside, and names the year', () => {
|
it('calls a clearly nearer home inside, and names the year', () => {
|
||||||
const r = compareToCutoff(300, 800, 2026);
|
const r = compareToCutoff(300, 800, 2026);
|
||||||
expect(r.verdict).toBe('inside');
|
expect(r.verdict).toBe('inside');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SchoolAdmissionDistance } from '@/lib/types';
|
import type { SchoolAdmissionDistance } from '@/lib/types';
|
||||||
import { formatCutoffDistance, formatEntryYear } from '@/lib/utils';
|
import { formatCutoffDistance, formatMiles, formatEntryYear } from '@/lib/utils';
|
||||||
|
|
||||||
export interface CutoffDisplay {
|
export interface CutoffDisplay {
|
||||||
/** Headline figure, e.g. "0.31 miles". */
|
/** Headline figure, e.g. "0.31 miles". */
|
||||||
@@ -108,8 +108,13 @@ export function compareToCutoff(
|
|||||||
const verdict: CutoffVerdict =
|
const verdict: CutoffVerdict =
|
||||||
Math.abs(margin) <= CUTOFF_UNCERTAINTY_M ? 'too-close' : margin > 0 ? 'inside' : 'outside';
|
Math.abs(margin) <= CUTOFF_UNCERTAINTY_M ? 'too-close' : margin > 0 ? 'inside' : 'outside';
|
||||||
|
|
||||||
const label = formatCutoffDistance(distanceM)?.primary ?? `${Math.round(distanceM)} m`;
|
// Both sides through the same formatter, with no fallback that could reach
|
||||||
const cutoffLabel = formatCutoffDistance(cutoffM)?.primary ?? `${Math.round(cutoffM)} m`;
|
// for a different unit: the sentence compares these two numbers directly, so
|
||||||
|
// they have to be in the same one. formatCutoffDistance returns null for a
|
||||||
|
// zero or negative figure, and its old "N m" fallback here was one of the
|
||||||
|
// ways a metres reading used to appear next to a miles one.
|
||||||
|
const label = formatMiles(distanceM);
|
||||||
|
const cutoffLabel = formatMiles(cutoffM);
|
||||||
|
|
||||||
// "the September 2026 cut-off of 0.17 miles" rather than "the 0.17 miles
|
// "the September 2026 cut-off of 0.17 miles" rather than "the 0.17 miles
|
||||||
// cut-off for September 2026": the figure carries its own unit word, which
|
// cut-off for September 2026": the figure carries its own unit word, which
|
||||||
|
|||||||
+31
-17
@@ -505,32 +505,46 @@ export function getPhaseStyle(phase?: string | null): { key: string; label: stri
|
|||||||
const METRES_PER_MILE = 1609.344;
|
const METRES_PER_MILE = 1609.344;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format an admission cut-off distance.
|
* A distance in miles, the unit UK school admissions is conducted in.
|
||||||
*
|
*
|
||||||
* Miles lead because that is the unit councils publish in — 90% of the source
|
* Councils publish cut-offs in miles — 90% of the collected source rows — and
|
||||||
* rows are miles — and because it is the unit a parent has already been quoted
|
* it is the unit a parent has already been quoted in their council's booklet
|
||||||
* if they have read their council's booklet. The metric figure follows as
|
* and offer letter. Everything a reader is asked to compare therefore renders
|
||||||
* support rather than replacing it.
|
* through this one function, so two figures in the same sentence can never
|
||||||
|
* arrive in different units.
|
||||||
*
|
*
|
||||||
* Below 100 m the miles figure stops carrying information ("0.03 miles"), so
|
* That was not previously true. This formatter used to swap to metres below
|
||||||
* the two swap round and metres lead instead.
|
* 100m, on the reasoning that "0.04 miles" carries less for a reader than
|
||||||
|
* "69 m". Taken one figure at a time that holds; taken in a sentence it
|
||||||
|
* produced "69 m away — inside the September 2026 cut-off of 0.17 miles",
|
||||||
|
* which asks the reader to convert between units to understand a comparison we
|
||||||
|
* had already made for them. Legibility of a single number lost to coherence
|
||||||
|
* of the pair.
|
||||||
|
*
|
||||||
|
* Below 0.01 miles the two decimal places run out rather than the unit being
|
||||||
|
* wrong, so the figure is described instead of rounded to a flat "0.00 miles".
|
||||||
|
*/
|
||||||
|
export function formatMiles(metres: number): string {
|
||||||
|
const miles = metres / METRES_PER_MILE;
|
||||||
|
if (miles < 0.01) return 'under 0.01 miles';
|
||||||
|
return `${miles.toFixed(2)} miles`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an admission cut-off distance: the headline figure in miles, and a
|
||||||
|
* metric equivalent to support it.
|
||||||
|
*
|
||||||
|
* The metric figure is support, not an alternative — it appears beside the
|
||||||
|
* miles figure, never instead of it, so nothing a reader compares is ever in
|
||||||
|
* two units at once.
|
||||||
*/
|
*/
|
||||||
export function formatCutoffDistance(
|
export function formatCutoffDistance(
|
||||||
metres: number | null | undefined
|
metres: number | null | undefined
|
||||||
): { primary: string; secondary: string } | null {
|
): { primary: string; secondary: string } | null {
|
||||||
if (metres == null || !Number.isFinite(metres) || metres <= 0) return null;
|
if (metres == null || !Number.isFinite(metres) || metres <= 0) return null;
|
||||||
|
|
||||||
const miles = metres / METRES_PER_MILE;
|
|
||||||
|
|
||||||
if (metres < 100) {
|
|
||||||
return {
|
|
||||||
primary: `${Math.round(metres)} m`,
|
|
||||||
secondary: `${miles.toFixed(2)} miles`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
primary: `${miles.toFixed(2)} miles`,
|
primary: formatMiles(metres),
|
||||||
secondary: metres < 1000
|
secondary: metres < 1000
|
||||||
? `${Math.round(metres / 10) * 10} m`
|
? `${Math.round(metres / 10) * 10} m`
|
||||||
: `${(metres / 1000).toFixed(1)} km`,
|
: `${(metres / 1000).toFixed(1)} km`,
|
||||||
|
|||||||
Reference in new issue
Block a user