From edc8a774b066114a22745547916ccebaf9f4d6ea Mon Sep 17 00:00:00 2001 From: btrippcsci Date: Tue, 15 Sep 2026 06:04:19 -0400 Subject: [PATCH] feat(units): port imperial/metric conversion from the kiosk Mirror the kiosk web UI's unit helpers (ui/src/utils/units.ts in open-flight/openflight) so the mobile app can report speeds and distances in the same units, with the same constants and rounding, as the kiosk. The copies are hand-mirrored rather than shared: this repository has no build-time dependency on the server repository. The kiosk's vitest suite is ported to jest with every assertion unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QvMGAzMMWRuxRht8aGciAv --- __tests__/units.test.ts | 46 +++++++++++++++++++++++++++++++++ utils/units.ts | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 __tests__/units.test.ts create mode 100644 utils/units.ts diff --git a/__tests__/units.test.ts b/__tests__/units.test.ts new file mode 100644 index 0000000..73b73ae --- /dev/null +++ b/__tests__/units.test.ts @@ -0,0 +1,46 @@ +import { + convertDistanceFromYards, + convertSpeedFromMph, + formatCarryRange, + formatDistance, + formatSpeed, + getDistanceUnit, + getSpeedUnit, + getUnitsLabel, +} from '../utils/units'; + +describe('units helpers', () => { + it('preserves imperial speed values', () => { + expect(convertSpeedFromMph(150, 'imperial')).toBe(150); + expect(formatSpeed(150, 'imperial', 1)).toBe('150.0'); + expect(getSpeedUnit('imperial')).toBe('mph'); + }); + + it('converts mph to km/h with stable rounding', () => { + expect(convertSpeedFromMph(100, 'metric')).toBeCloseTo(160.934, 3); + expect(formatSpeed(100, 'metric', 1)).toBe('160.9'); + expect(getSpeedUnit('metric')).toBe('km/h'); + }); + + it('preserves imperial distance values', () => { + expect(convertDistanceFromYards(250, 'imperial')).toBe(250); + expect(formatDistance(250, 'imperial', 0)).toBe('250'); + expect(getDistanceUnit('imperial')).toBe('yds'); + }); + + it('converts yards to meters with stable rounding', () => { + expect(convertDistanceFromYards(100, 'metric')).toBeCloseTo(91.44, 2); + expect(formatDistance(100, 'metric', 0)).toBe('91'); + expect(getDistanceUnit('metric')).toBe('m'); + }); + + it('joins speed and distance units for chrome labels', () => { + expect(getUnitsLabel('imperial')).toBe('mph / yds'); + expect(getUnitsLabel('metric')).toBe('km/h / m'); + }); + + it('formats carry ranges in the selected unit system', () => { + expect(formatCarryRange([200, 220], 'imperial')).toBe('200-220 yds'); + expect(formatCarryRange([200, 220], 'metric')).toBe('183-201 m'); + }); +}); diff --git a/utils/units.ts b/utils/units.ts new file mode 100644 index 0000000..ec1ae57 --- /dev/null +++ b/utils/units.ts @@ -0,0 +1,56 @@ +// Imperial/metric conversion mirrored from the OpenFlight kiosk web UI +// (`ui/src/utils/units.ts` in open-flight/openflight). The two copies are +// hand-mirrored, not shared: this repository has no build-time dependency on +// the server repository (see AGENTS.md). Keep the constants and rounding here +// identical to the kiosk so both clients report the same numbers. + +export type UnitSystem = 'imperial' | 'metric'; + +const MPH_TO_KMH = 1.60934; +const YARDS_TO_METERS = 0.9144; +const IMPERIAL_SPEED_UNIT = 'mph'; +const METRIC_SPEED_UNIT = 'km/h'; +const IMPERIAL_DISTANCE_UNIT = 'yds'; +const METRIC_DISTANCE_UNIT = 'm'; + +export function convertSpeedFromMph(speedMph: number, unitSystem: UnitSystem): number { + if (unitSystem === 'metric') { + return speedMph * MPH_TO_KMH; + } + + return speedMph; +} + +export function convertDistanceFromYards(distanceYards: number, unitSystem: UnitSystem): number { + if (unitSystem === 'metric') { + return distanceYards * YARDS_TO_METERS; + } + + return distanceYards; +} + +export function formatSpeed(speedMph: number, unitSystem: UnitSystem, digits = 1): string { + return convertSpeedFromMph(speedMph, unitSystem).toFixed(digits); +} + +export function formatDistance(distanceYards: number, unitSystem: UnitSystem, digits = 0): string { + return convertDistanceFromYards(distanceYards, unitSystem).toFixed(digits); +} + +export function getSpeedUnit(unitSystem: UnitSystem): string { + return unitSystem === 'metric' ? METRIC_SPEED_UNIT : IMPERIAL_SPEED_UNIT; +} + +export function getDistanceUnit(unitSystem: UnitSystem): string { + return unitSystem === 'metric' ? METRIC_DISTANCE_UNIT : IMPERIAL_DISTANCE_UNIT; +} + +export function getUnitsLabel(unitSystem: UnitSystem): string { + return `${getSpeedUnit(unitSystem)} / ${getDistanceUnit(unitSystem)}`; +} + +export function formatCarryRange(carryRange: [number, number], unitSystem: UnitSystem): string { + const min = formatDistance(carryRange[0], unitSystem, 0); + const max = formatDistance(carryRange[1], unitSystem, 0); + return `${min}-${max} ${getDistanceUnit(unitSystem)}`; +}