fix(tours): keep a non-canonical sac_scale as written instead of lowercasing it

A value such as Alpine_Hiking is not one of the six SAC values word for
word, so it now reads as an unclear grade with the raw value OpenStreetMap
holds, rather than as a known T4 the planner normalised itself.
This commit is contained in:
Maurice
2026-10-20 14:51:23 +02:00
parent f745ca13c9
commit c80152f41d
2 changed files with 11 additions and 8 deletions
@@ -40,15 +40,16 @@ function written(value: string | undefined): string | null {
}
/**
* MAP-03: a way's `sac_scale` as one of the scale's six values, read as
* OpenStreetMap wrote it. A value that names no single step (`T3`,
* `hiking;mountain_hiking`, a typo) is handed on as written and never read to
* a step, so the planner says unclear rather than picking one.
* MAP-03: a way's `sac_scale` as one of the scale's six values, read exactly
* as OpenStreetMap wrote it (only trimmed). A value that is not one of them
* word for word (`T3`, `Alpine_Hiking`, `hiking;mountain_hiking`, a typo) is
* handed on as written and never read to a step, so the planner says unclear
* and shows what was written rather than a value TREK made up.
*/
function sacOf(value: string | undefined): Pick<TourTrailGrade, 'sac_scale' | 'sac_scale_unclear'> {
const text = written(value);
if (!text) return { sac_scale: null, sac_scale_unclear: null };
const known = sacScaleSchema.safeParse(text.toLowerCase());
const known = sacScaleSchema.safeParse(text);
if (known.success) return { sac_scale: known.data, sac_scale_unclear: null };
return { sac_scale: null, sac_scale_unclear: text };
}
@@ -95,7 +95,7 @@ describe('TourTrailGradeService (BIK-05)', () => {
expect(query).not.toHaveBeenCalled();
});
it('TOUR-GRADE-007: reads sac_scale as OpenStreetMap wrote it and hands an unclear one on as written (MAP-03)', async () => {
it('TOUR-GRADE-007: reads sac_scale exactly as OpenStreetMap wrote it and hands any other value on as written (MAP-03)', async () => {
query.mockResolvedValue([
way(1, { sac_scale: 'demanding_mountain_hiking' }),
way(2, { sac_scale: ' Alpine_Hiking ', 'mtb:scale': '3' }),
@@ -103,14 +103,16 @@ describe('TourTrailGradeService (BIK-05)', () => {
way(4, { sac_scale: 'hiking;mountain_hiking' }),
way(5, { sac_scale: ' ' }),
way(6, { sac_scale: `x${'y'.repeat(80)}` }),
way(7, { sac_scale: ' mountain_hiking ' }),
]);
const answer = await service.grades({ way_ids: [1, 2, 3, 4, 5, 6] });
const answer = await service.grades({ way_ids: [1, 2, 3, 4, 5, 6, 7] });
expect(answer.ways).toEqual([
{ ...NO_MTB, ...NO_WAY, id: 1, sac_scale: 'demanding_mountain_hiking', sac_scale_unclear: null },
{ ...NO_MTB, ...NO_WAY, id: 2, mtb_scale: '3', sac_scale: 'alpine_hiking', sac_scale_unclear: null },
{ ...NO_MTB, ...NO_WAY, id: 2, mtb_scale: '3', sac_scale: null, sac_scale_unclear: 'Alpine_Hiking' },
{ ...NO_MTB, ...NO_WAY, id: 3, sac_scale: null, sac_scale_unclear: 'T3' },
{ ...NO_MTB, ...NO_WAY, id: 4, sac_scale: null, sac_scale_unclear: 'hiking;mountain_hiking' },
{ ...NO_MTB, ...NO_WAY, id: 6, sac_scale: null, sac_scale_unclear: `x${'y'.repeat(63)}` },
{ ...NO_MTB, ...NO_WAY, id: 7, sac_scale: 'mountain_hiking', sac_scale_unclear: null },
]);
});