/** * item-size — the size parser and the line ↔ candidate comparison. * * The descriptions in the "real descriptions" block below are the ones * that came off live data on 2026-10-24, where the POS shortlist * ranked an M22 bolt above the M16 the customer asked for because * they share the words "BOLT 8.8 ZP". */ import { describe, expect, it } from "vitest"; import { compareSizes, describeSize, parseItemSize, SIZE_RANK_EXACT, SIZE_RANK_KIND, SIZE_RANK_NEAR_BASE, SIZE_RANK_OTHER, SIZE_RANK_THREAD, } from "./item-size"; describe("parseItemSize — the real descriptions", () => { it("reads a shorthand bolt", () => { expect(parseItemSize("BOLT HEX M16X60 GR8.8 ZP")).toEqual({ kind: "bolt", variant: null, thread: "M16", length: 60, grade: "8.8", finish: "zp", dims: [], }); }); it("reads a spelled-out bolt", () => { expect(parseItemSize("Hex bolt M16 x 60 grade 8.8 zinc plated")).toEqual({ kind: "bolt", variant: null, thread: "M16", length: 60, grade: "8.8", finish: "zp", dims: [], }); }); it("reads a bare grade and a different size", () => { expect(parseItemSize("M22x65 BOLT 8.8 ZP")).toEqual({ kind: "bolt", variant: null, thread: "M22", length: 65, grade: "8.8", finish: "zp", dims: [], }); }); it("reads a nyloc nut in both word orders", () => { for (const text of ["NUT NYLOC M16 ZINC", "Nyloc nut M16 zinc plated"]) { expect(parseItemSize(text)).toEqual({ kind: "nut", variant: "nyloc", thread: "M16", length: null, grade: null, finish: "zp", dims: [], }); } }); it("reads a flat washer", () => { expect(parseItemSize("WASHER FLAT M16 ZINC PLATED")).toEqual({ kind: "washer", variant: "flat", thread: "M16", length: null, grade: null, finish: "zp", dims: [], }); }); it("reads a spring washer", () => { const s = parseItemSize("SPRING WASHER M16 ZP"); expect(s.kind).toBe("washer"); expect(s.variant).toBe("spring"); }); it("reads a funnel with two dimensions and no kind", () => { expect(parseItemSize('Funnel 150mm PVC round 2 piece Flexible ("6") Round')).toEqual({ kind: null, variant: null, thread: null, length: null, grade: null, finish: null, dims: [ { value: 150, unit: "mm" }, { value: 6, unit: "in" }, ], }); }); it("reads a welding rod", () => { expect(parseItemSize("Welding rod 2.5mm")).toEqual({ kind: null, variant: null, thread: null, length: null, grade: null, finish: null, dims: [{ value: 2.5, unit: "mm" }], }); }); it("reads a U bolt with a bracketed length", () => { expect(parseItemSize("U BOLT M24(400) HDG")).toEqual({ kind: "bolt", variant: null, thread: "M24", length: 400, grade: null, finish: "hdg", dims: [], }); }); }); describe("parseItemSize — the attributes", () => { it("keeps the type word in either order", () => { expect(parseItemSize("hex bolt M16").kind).toBe("bolt"); expect(parseItemSize("bolt hex M16").kind).toBe("bolt"); }); it("knows the other fastener types", () => { expect(parseItemSize("M6x30 SET SCR A2").kind).toBe("setscrew"); expect(parseItemSize("M6x30 set screw A2").kind).toBe("setscrew"); expect(parseItemSize("M8 cap screw 40").kind).toBe("cap_screw"); expect(parseItemSize("M10 threaded stud 100").kind).toBe("stud"); expect(parseItemSize("Chemical anchor 300").kind).toBe("anchor"); expect(parseItemSize("4.8 rivet 16").kind).toBe("rivet"); }); it("leaves an unknown type alone", () => { expect(parseItemSize("Funnel 150mm PVC").kind).toBeNull(); expect(parseItemSize("Welding rod 2.5mm").kind).toBeNull(); }); it("reads the thread from every spelling", () => { expect(parseItemSize("M16").thread).toBe("M16"); expect(parseItemSize("M16X60").thread).toBe("M16"); expect(parseItemSize("M16 x 60").thread).toBe("M16"); expect(parseItemSize("m16x60mm").thread).toBe("M16"); expect(parseItemSize("M 16").thread).toBe("M16"); }); it("reads an imperial thread", () => { expect(parseItemSize('1/2" bolt').thread).toBe('1/2"'); expect(parseItemSize("1/2 UNC bolt").thread).toBe("1/2UNC"); }); it("reads the length that follows the thread", () => { expect(parseItemSize("M16x60").length).toBe(60); expect(parseItemSize("M16 X 60MM").length).toBe(60); expect(parseItemSize("M16 × 60").length).toBe(60); expect(parseItemSize("M16x60mm").length).toBe(60); expect(parseItemSize("M16").length).toBeNull(); }); it("reads the grade in every spelling", () => { expect(parseItemSize("bolt GR8.8").grade).toBe("8.8"); expect(parseItemSize("bolt grade 8.8").grade).toBe("8.8"); expect(parseItemSize("bolt GR 10.9").grade).toBe("10.9"); expect(parseItemSize("bolt 12.9").grade).toBe("12.9"); expect(parseItemSize("bolt 4.6").grade).toBe("4.6"); }); it("does not read a plain number as a grade", () => { expect(parseItemSize("bolt 60mm").grade).toBeNull(); expect(parseItemSize("washer 150").grade).toBeNull(); }); it("reads the finish", () => { expect(parseItemSize("bolt zp").finish).toBe("zp"); expect(parseItemSize("bolt zinc").finish).toBe("zp"); expect(parseItemSize("bolt zinc plated").finish).toBe("zp"); expect(parseItemSize("bolt hdg").finish).toBe("hdg"); expect(parseItemSize("bolt galv").finish).toBe("hdg"); expect(parseItemSize("bolt hot dip galvanised").finish).toBe("hdg"); expect(parseItemSize("bolt ss").finish).toBe("ss"); expect(parseItemSize("bolt stainless").finish).toBe("ss"); expect(parseItemSize("bolt 304").finish).toBe("ss"); expect(parseItemSize("bolt 316").finish).toBe("ss"); expect(parseItemSize("bolt black").finish).toBe("black"); expect(parseItemSize("bolt self colour").finish).toBe("black"); expect(parseItemSize("bolt plain").finish).toBeNull(); }); it("reads a dimension with its unit", () => { expect(parseItemSize("pipe 150mm").dims).toEqual([{ value: 150, unit: "mm" }]); expect(parseItemSize("drum 20L").dims).toEqual([{ value: 20, unit: "l" }]); expect(parseItemSize("plate 2.5mm").dims).toEqual([{ value: 2.5, unit: "mm" }]); expect(parseItemSize('plate 6"').dims).toEqual([{ value: 6, unit: "in" }]); }); it("does not read the thread length as a dimension", () => { expect(parseItemSize("M16x60mm").dims).toEqual([]); expect(parseItemSize("M16 X 60MM").dims).toEqual([]); expect(parseItemSize("U BOLT M24(400) HDG").dims).toEqual([]); }); it("survives rubbish input", () => { expect(parseItemSize("")).toEqual({ kind: null, variant: null, thread: null, length: null, grade: null, finish: null, dims: [], }); expect(parseItemSize("the and of").kind).toBeNull(); }); }); describe("compareSizes", () => { const line = parseItemSize("BOLT HEX M16X60 GR8.8 ZP"); it("is exact when every attribute the line gives matches", () => { const cand = parseItemSize("HEX BOLT M16X60 8.8 ZP"); const cmp = compareSizes(line, cand); expect(cmp.exact).toBe(true); expect(cmp.differs).toEqual([]); expect(cmp.rank).toBe(SIZE_RANK_EXACT); }); it("spells the differences out in plain words", () => { const cmp = compareSizes(line, parseItemSize("M22x65 BOLT 8.8 ZP")); expect(cmp.exact).toBe(false); expect(cmp.differs).toContain("thread M22 vs M16"); expect(cmp.differs).toContain("length 65 vs 60"); }); it("spells a finish difference out", () => { const cmp = compareSizes(line, parseItemSize("M16X60 BOLT 8.8 HDG")); expect(cmp.differs).toContain("finish HDG vs ZP"); }); it("never counts an attribute the line does not mention", () => { // The line says nothing about the finish — a candidate with a // different finish must not be marked down for it. const bare = parseItemSize("BOLT M16X60"); const cmp = compareSizes(bare, parseItemSize("BOLT M16X60 HDG")); expect(cmp.differs).toEqual([]); expect(cmp.exact).toBe(true); }); it("counts an attribute the candidate does not mention", () => { const cmp = compareSizes(line, parseItemSize("HEX BOLT M16")); expect(cmp.exact).toBe(false); expect(cmp.differs).toContain("length ? vs 60"); expect(cmp.differs).toContain("grade ? vs 8.8"); expect(cmp.differs).toContain("finish ? vs zp"); }); it("is never exact when the line says nothing about size", () => { const bare = parseItemSize("Widget"); const cmp = compareSizes(bare, parseItemSize("Widget")); expect(cmp.exact).toBe(false); expect(cmp.rank).toBe(SIZE_RANK_OTHER); }); it("is exact when the line only names a type and the candidate is the same type", () => { const cmp = compareSizes(parseItemSize("Hex bolt"), parseItemSize("HEX BOLT M16")); expect(cmp.exact).toBe(true); }); it("ranks same kind + thread + length first", () => { const cmp = compareSizes(line, parseItemSize("HEX BOLT M16X60 8.8 ZP")); expect(cmp.rank).toBe(SIZE_RANK_EXACT); }); it("ranks same kind + thread by how near the length is", () => { const near = compareSizes(line, parseItemSize("HEX BOLT M16X65 8.8 ZP")); const far = compareSizes(line, parseItemSize("HEX BOLT M16X170 8.8 ZP")); expect(near.rank).toBe(SIZE_RANK_NEAR_BASE + 5); expect(far.rank).toBe(SIZE_RANK_NEAR_BASE + 110); expect(near.rank).toBeLessThan(far.rank); }); it("ranks the same thread above the same kind with a different thread", () => { const sameThread = compareSizes(line, parseItemSize("NUT M16 8.8 ZP")); const sameKind = compareSizes(line, parseItemSize("M22X60 BOLT 8.8 ZP")); expect(sameThread.rank).toBe(SIZE_RANK_THREAD); expect(sameKind.rank).toBe(SIZE_RANK_KIND); expect(sameThread.rank).toBeLessThan(sameKind.rank); }); it("ranks an M16x60 bolt above an M22x65 bolt", () => { const right = compareSizes(line, parseItemSize("HEX BOLT M16X60 8.8 ZP")); const wrong = compareSizes(line, parseItemSize("HEX BOLT M22X65 8.8 ZP")); expect(right.rank).toBeLessThan(wrong.rank); }); it("ranks everything else last", () => { const cmp = compareSizes(line, parseItemSize("Funnel 150mm PVC")); expect(cmp.rank).toBe(SIZE_RANK_OTHER); }); it("compares dimensions", () => { const funnel = parseItemSize('Funnel 150mm PVC round 2 piece Flexible ("6") Round'); const same = parseItemSize("Funnel 150mm PVC"); const other = parseItemSize("Funnel 100mm PVC"); expect(compareSizes(funnel, same).differs).toContain("size ? vs 6in"); expect(compareSizes(funnel, other).differs).toContain("size 100mm vs 150mm"); }); }); describe("describeSize", () => { it("writes one short line", () => { expect(describeSize(parseItemSize("BOLT HEX M16X60 GR8.8 ZP"))).toBe( "bolt, thread M16, length 60, grade 8.8, finish zp", ); }); it("says nothing when there is nothing to say", () => { expect(describeSize(parseItemSize("Widget"))).toBe(""); }); });