import test from "node:test"; import assert from "node:assert/strict"; import { officialProfileSections, profileBandForSlope } from "./official-climb-profile-logic.ts"; import type { ProfilePoint } from "./types.js"; test("assigns each slope to its color band", () => { assert.equal(profileBandForSlope(-0.1), "downhill"); assert.equal(profileBandForSlope(0), "downhill"); assert.equal(profileBandForSlope(0.1), "0-3"); assert.equal(profileBandForSlope(2.9), "0-3"); assert.equal(profileBandForSlope(3), "3-6"); assert.equal(profileBandForSlope(6), "6-9"); assert.equal(profileBandForSlope(9), "9-plus"); assert.equal(profileBandForSlope(12), "9-plus"); }); // buildProfile lays out one recorded point every 50 m along the given [length in km, slope in %] stretches. const buildProfile = (stretches: [number, number][]): ProfilePoint[] => { const points: ProfilePoint[] = [{ distanceKm: 0, elevationM: 0 }]; let distanceKm = 0; let elevationM = 0; for (const [lengthKm, slopePercent] of stretches) { const pointCount = Math.round(lengthKm * 20); for (let index = 1; index <= pointCount; index++) { const distanceStepKm = (lengthKm * index) / pointCount; points.push({ distanceKm: distanceKm + distanceStepKm, elevationM: elevationM + distanceStepKm * 1000 * (slopePercent / 100) }); } distanceKm += lengthKm; elevationM += lengthKm * 1000 * (slopePercent / 100); } return points; }; test("keeps a short steep ramp as its own section", () => { const points = buildProfile([ [0.3, 4], [0.3, 12], [0.3, 4], ]); const sections = officialProfileSections(points, 0, points.length - 1); assert.equal(sections.length, 3); assert.ok(Math.abs(sections[1].slopePercent - 12) < 0.01); assert.equal(sections[1].band, "9-plus"); assert.equal(Math.round((sections[1].endDistanceKm - sections[1].startDistanceKm) * 1000), 300); assert.ok(Math.abs(sections[0].slopePercent - 4) < 0.01); assert.ok(Math.abs(sections[2].slopePercent - 4) < 0.01); }); test("collapses a constant slope into a single section", () => { const points = buildProfile([[5, 6]]); const sections = officialProfileSections(points, 0, points.length - 1); assert.equal(sections.length, 1); assert.ok(Math.abs(sections[0].slopePercent - 6) < 0.01); assert.equal(sections[0].startDistanceKm, 0); assert.equal(sections[0].endDistanceKm, 5); }); test("reports a descending stretch as downhill", () => { const points = buildProfile([[2, -5]]); const sections = officialProfileSections(points, 0, points.length - 1); assert.equal(sections.length, 1); assert.equal(sections[0].band, "downhill"); }); test("keeps a noisy profile within the readable section budget", () => { const points: ProfilePoint[] = []; for (let index = 0; index <= 200; index++) { const distanceKm = index * 0.05; points.push({ distanceKm, elevationM: distanceKm * 1000 * 0.05 + Math.sin(index) * 4 }); } const sections = officialProfileSections(points, 0, points.length - 1); assert.ok(sections.length > 10 && sections.length <= 24, `expected 10-24 sections, got ${sections.length}`); assert.equal(sections[0].startDistanceKm, 0); assert.equal(sections[sections.length - 1].endDistanceKm, 10); }); test("reads a slice of a longer profile", () => { const points = buildProfile([ [1, 20], [1, 8], [1, 20], ]); const startIndex = points.findIndex((point) => point.distanceKm >= 1); const endIndex = points.findIndex((point) => point.distanceKm >= 2); const sections = officialProfileSections(points, startIndex, endIndex); assert.equal(sections.length, 1); assert.equal(Math.round(sections[0].slopePercent), 8); assert.equal(sections[0].startDistanceKm, points[startIndex].distanceKm); assert.equal(sections[0].endDistanceKm, points[endIndex].distanceKm); }); test("returns no section for an empty range", () => { const points = buildProfile([[1, 6]]); assert.deepEqual(officialProfileSections(points, 3, 3), []); assert.deepEqual(officialProfileSections(points, 0, points.length), []); });