/** * Deterministic thickness-band derivation (TV-9 / QUALITY-95, METADATA-15/25). * * The Woo "Thickness" attribute is a set of named bands (e.g. "2-4 in"). The * owner enters a numeric thickness in 1/8-inch steps; the band is derived by * rounding that thickness UP into the band whose numeric upper bound first * encloses it. The Review UI never exposes a band picker — the band always * follows from thickness_in (issue #88). * * Pure function so it is unit-testable without a browser/backend. */ export interface ThicknessBand { id: number; name: string; } /** Parse the leading numeric bounds ("2-4 in" → {min:2,max:4}). Returns null if the name * carries no usable numeric upper bound (defensive — a band we can't place is skipped). */ function parseBounds(name: string): { min: number; max: number } | null { // Match the first two numbers in the name, in order (handles "2-4 in", "1/8", "12in+"). const nums = name.match(/\d+(?:[./]\d+)?/g); if (!nums) return null; const asNum = (s: string) => Number(s); const min = asNum(nums[0]); const max = nums.length >= 2 ? asNum(nums[1]) : min; if (!Number.isFinite(min) || !Number.isFinite(max)) return null; if (max < min) return null; // malformed "4-2"; not a usable band return { min, max }; } /** * Round `thicknessIn` (inches, 1/8" steps) up into the synced Woo thickness band. * Returns the term id, or '' when no band encloses the value (no thickness known, * empty synced set, or a value above every band — all leave the field empty so the * owner's upstream fix, not a wrong band, resolves it). */ export function deriveThicknessTermId( thicknessIn: number | null | undefined, bands: ThicknessBand[], ): number | '' { if (typeof thicknessIn !== 'number' || !Number.isFinite(thicknessIn) || thicknessIn <= 0) { return ''; } // Sort bands by upper bound so "first enclosing max" is well-defined regardless of // the order Woo returned them in. const ranked = bands .map((b) => ({ b, bounds: parseBounds(b.name) })) .filter((x): x is { b: ThicknessBand; bounds: { min: number; max: number } } => x.bounds !== null) .sort((a, z) => a.bounds.max - z.bounds.max); for (const { b, bounds } of ranked) { // A value exactly on the band's max belongs to that band; above it, fall through. if (thicknessIn <= bounds.max) { return b.id; } } return ''; }