/** * Collapses serial / warranty numbers into consecutive runs for display: * SN-001, SN-002 … SN-050, SN-077 → "SN-001 – SN-050" (50) and "SN-077" (1). * * Uses the same "prefix + trailing number" reading as the GRN form's range builder, so a run * entered there as From/adding comes back out as exactly one range here. */ export type SerialRange = { from: string; to: string; count: number } function parseTrailingNumber(s: string): { prefix: string; num: number; width: number } | null { const m = s.match(/^(.*?)(\d+)$/) if (!m) return null return { prefix: m[1], num: parseInt(m[2], 10), width: m[2].length } } /** Natural order: by prefix, then by the trailing number (so SN-9 sorts before SN-10). */ function compareSerials(a: string, b: string): number { const pa = parseTrailingNumber(a) const pb = parseTrailingNumber(b) if (pa && pb && pa.prefix === pb.prefix) return pa.num - pb.num return a.localeCompare(b, undefined, { numeric: true }) } export function toSerialRanges(values: string[]): SerialRange[] { const sorted = [...new Set(values.map((v) => v.trim()).filter(Boolean))].sort(compareSerials) const ranges: SerialRange[] = [] let run: { start: string; prefix: string; width: number; last: number; lastText: string; count: number } | null = null for (const value of sorted) { const parsed = parseTrailingNumber(value) // Consecutive = same prefix, next number, written with the run's zero-padding. const continues = run !== null && parsed !== null && parsed.prefix === run.prefix && parsed.num === run.last + 1 && value === `${run.prefix}${String(parsed.num).padStart(run.width, "0")}` if (continues && run && parsed) { run.last = parsed.num run.lastText = value run.count += 1 continue } if (run) ranges.push({ from: run.start, to: run.lastText, count: run.count }) run = parsed ? { start: value, prefix: parsed.prefix, width: parsed.width, last: parsed.num, lastText: value, count: 1 } : { start: value, prefix: `\u0000${value}`, width: 0, last: Number.NaN, lastText: value, count: 1 } } if (run) ranges.push({ from: run.start, to: run.lastText, count: run.count }) return ranges } /** One-line summary, e.g. "SN-001 – SN-050" or "SN-001 – SN-050, SN-077 +2 more". */ export function formatSerialRanges(ranges: SerialRange[], max = 2): string { const label = (r: SerialRange) => (r.count === 1 ? r.from : `${r.from} – ${r.to}`) const shown = ranges.slice(0, max).map(label).join(", ") return ranges.length > max ? `${shown} +${ranges.length - max} more` : shown }