// Unit conversion + display for garden dimensions. The API and DB are metric // only (centimeters); imperial is purely a display/entry concern here. A minimal // helper set for #8 — #9's geometry lib consolidates/extends it. export type UnitPref = 'metric' | 'imperial' const CM_PER_INCH = 2.54 const CM_PER_METER = 100 const INCHES_PER_FOOT = 12 // Mirrors the server's garden dimension bounds (service/gardens.go): [1cm, 100m]. export const MIN_DIMENSION_CM = 1 export const MAX_DIMENSION_CM = 10_000 // Below this, a garden-scale grid is fine enough to be worth questioning — it // usually means someone reached for plant spacing, which belongs to the bed, not // the garden. Soft: the form hints, it doesn't refuse. export const MIN_GARDEN_GRID_CM = 10 /** Whether a centimeter dimension is within the server's accepted range. */ export function isValidDimensionCm(cm: number): boolean { return Number.isFinite(cm) && cm >= MIN_DIMENSION_CM && cm <= MAX_DIMENSION_CM } /** Round away IEEE-754 dust without rounding away precision: 12 × 2.54 is * 30.479999999999997 in binary floating point, and the 1e-6 cm (10 nm) grain * here is far below anything a garden cares about. Every cm column is REAL, so * entry stays exact — this only stops 30.48 being stored as 30.479999999999997. */ function roundCm(cm: number): number { return Math.round(cm * 1e6) / 1e6 } /** Feet (+ optional inches) → centimeters, exactly (1 ft → 30.48). */ export function cmFromFtIn(feet: number, inches = 0): number { return roundCm((feet * INCHES_PER_FOOT + inches) * CM_PER_INCH) } /** Meters → centimeters, exactly. */ export function cmFromMeters(meters: number): number { return roundCm(meters * CM_PER_METER) } /** A value typed in the given unit (meters, or feet) → centimeters. */ export function cmFromDisplay(value: number, unit: UnitPref): number { return unit === 'imperial' ? cmFromFtIn(value) : cmFromMeters(value) } /** Centimeters → a number in the given unit, for prefilling an input field. * Rounded so cm-quantization doesn't show through (e.g. 244cm shows as 8 ft, * not 8.01): meters to the cm (2 dp), feet to 0.1 ft. */ export function displayFromCm(cm: number, unit: UnitPref): number { if (unit === 'imperial') { const feet = cm / CM_PER_INCH / INCHES_PER_FOOT return Math.round(feet * 10) / 10 } return Math.round((cm / CM_PER_METER) * 100) / 100 } /** Centimeters → a human string in the given unit (e.g. "1.22 m" or "4′ 0″"). */ export function formatCm(cm: number, unit: UnitPref): string { if (unit === 'imperial') { const totalInches = cm / CM_PER_INCH let feet = Math.floor(totalInches / INCHES_PER_FOOT) let inches = Math.round(totalInches - feet * INCHES_PER_FOOT) if (inches === INCHES_PER_FOOT) { feet += 1 inches = 0 } return `${feet}′ ${inches}″` } const meters = Math.round((cm / CM_PER_METER) * 100) / 100 return `${meters} m` } /** Centimeters width × height → a human string in the given unit. */ export function formatDimensions(widthCm: number, heightCm: number, unit: UnitPref): string { return `${formatCm(widthCm, unit)} × ${formatCm(heightCm, unit)}` } /** The input-field label for a single dimension in the given unit. */ export function dimensionUnitLabel(unit: UnitPref): string { return unit === 'imperial' ? 'ft' : 'm' } // --- Plant spacing (small scale) ------------------------------------------- // Plant spacing is a handful of centimeters, so meters/feet read poorly here; // metric shows cm and imperial shows whole inches. /** Centimeters → a spacing string in the given unit (e.g. "15 cm" or "6″"). */ export function formatSpacing(cm: number, unit: UnitPref): string { if (unit === 'imperial') return `${Math.round(cm / CM_PER_INCH)}″` return `${Math.round(cm)} cm` } /** A spacing value typed in the given unit (cm, or inches) → centimeters, * exactly (12 in → 30.48). */ export function cmFromSpacing(value: number, unit: UnitPref): number { return roundCm(unit === 'imperial' ? value * CM_PER_INCH : value) } /** Centimeters → a spacing number in the given unit, for prefilling an input. * Inches to 0.1 so cm-quantization doesn't show through. */ export function spacingFromCm(cm: number, unit: UnitPref): number { return unit === 'imperial' ? Math.round((cm / CM_PER_INCH) * 10) / 10 : Math.round(cm) } /** The spacing input-field unit label. */ export function spacingUnitLabel(unit: UnitPref): string { return unit === 'imperial' ? 'in' : 'cm' }