diff --git a/frontend/src/__tests__/fixtures/units.ts b/frontend/src/__tests__/fixtures/units.ts new file mode 100644 index 0000000..1ad8e5b --- /dev/null +++ b/frontend/src/__tests__/fixtures/units.ts @@ -0,0 +1,37 @@ +/** TECH-SPEC §8 TV-3 numeric fixture (client-authoritative sqft input). */ +export function tv3Fixture(): { + lengthIn: number; + thicknessIn: number; + sqft: number; + expectedBdft: number; +} { + return { + lengthIn: 96, + thicknessIn: 1.5, + sqft: 0.64, + expectedBdft: 0.96, + }; +} + +/** TECH-SPEC §8 TV-10 rounding vectors. */ +export function tv10Fixture(): { + lengthRaw: number; + lengthExpected: number; + sqftRaw: number; + sqftExpected: number; + bdftRaw: number; + bdftExpected: number; + priceRaw: number; + priceExpected: number; +} { + return { + lengthRaw: 96.04, + lengthExpected: 96, + sqftRaw: 0.641, + sqftExpected: 0.64, + bdftRaw: 11.525, + bdftExpected: 11.53, + priceRaw: 12.345, + priceExpected: 12.35, + }; +} diff --git a/frontend/src/__tests__/tv3-tv10-units.test.ts b/frontend/src/__tests__/tv3-tv10-units.test.ts new file mode 100644 index 0000000..4fa8cfb --- /dev/null +++ b/frontend/src/__tests__/tv3-tv10-units.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { + computeBdft, + roundBdft, + roundLengthIn, + roundPrice, + roundSqft, +} from "../lib/pipeline/units.js"; +import { tv3Fixture, tv10Fixture } from "./fixtures/units.js"; + +describe("TV-3 Scale, sqft, bdft", () => { + it("bdft = sqft × thickness_in with no divide-by-12", () => { + const { lengthIn, thicknessIn, sqft, expectedBdft } = tv3Fixture(); + const bdft = computeBdft(sqft, thicknessIn); + + console.log( + JSON.stringify({ + case: "TV-3", + lengthIn, + thicknessIn, + sqft, + bdft, + expectedBdft, + identity_1sqft_1in: computeBdft(1, 1), + }), + ); + + expect(bdft).toBe(expectedBdft); + expect(bdft).toBe(0.96); + expect(computeBdft(1, 1)).toBe(1); + expect(bdft).not.toBeCloseTo((sqft * thicknessIn) / 12, 5); + }); + + it("bdft is monotonic in thickness and sqft", () => { + const baseSqft = 0.64; + const baseThick = 1.5; + expect(computeBdft(baseSqft, baseThick + 0.125)).toBeGreaterThan( + computeBdft(baseSqft, baseThick), + ); + expect(computeBdft(baseSqft + 0.01, baseThick)).toBeGreaterThan( + computeBdft(baseSqft, baseThick), + ); + }); +}); + +describe("TV-10 Rounding", () => { + it("half-up / nearest-step matches TECH-SPEC vectors", () => { + const fx = tv10Fixture(); + + const length = roundLengthIn(fx.lengthRaw); + const sqft = roundSqft(fx.sqftRaw); + const bdft = roundBdft(fx.bdftRaw); + const price = roundPrice(fx.priceRaw); + + console.log( + JSON.stringify({ + case: "TV-10", + length: { raw: fx.lengthRaw, rounded: length, expected: fx.lengthExpected }, + sqft: { raw: fx.sqftRaw, rounded: sqft, expected: fx.sqftExpected }, + bdft: { raw: fx.bdftRaw, rounded: bdft, expected: fx.bdftExpected }, + price: { raw: fx.priceRaw, rounded: price, expected: fx.priceExpected }, + }), + ); + + expect(length).toBe(fx.lengthExpected); + expect(sqft).toBe(fx.sqftExpected); + expect(bdft).toBe(fx.bdftExpected); + expect(price).toBe(fx.priceExpected); + }); +}); diff --git a/frontend/src/lib/pipeline/index.ts b/frontend/src/lib/pipeline/index.ts index b07ec42..523acae 100644 --- a/frontend/src/lib/pipeline/index.ts +++ b/frontend/src/lib/pipeline/index.ts @@ -1,3 +1,4 @@ export * from "./constants.js"; export * from "./types.js"; export * from "./mask.js"; +export * from "./units.js"; diff --git a/frontend/src/lib/pipeline/units.ts b/frontend/src/lib/pipeline/units.ts new file mode 100644 index 0000000..f0cd5bb --- /dev/null +++ b/frontend/src/lib/pipeline/units.ts @@ -0,0 +1,39 @@ +import { + AREA_DECIMALS, + BDFT_DECIMALS, + INCH_STEP, + PRICE_DECIMALS, +} from "./constants.js"; + +/** Half-up at .5 (away from zero). */ +export function roundHalfUp(value: number, decimals: number): number { + if (!Number.isFinite(value)) { + return value; + } + const factor = 10 ** decimals; + const scaled = Math.abs(value) * factor; + const rounded = Math.floor(scaled + 0.5) / factor; + return value < 0 ? -rounded : rounded; +} + +/** Nearest INCH_STEP (1/8"). */ +export function roundLengthIn(lengthIn: number): number { + return Math.round(lengthIn / INCH_STEP) * INCH_STEP; +} + +export function roundSqft(sqft: number): number { + return roundHalfUp(sqft, AREA_DECIMALS); +} + +export function roundBdft(bdft: number): number { + return roundHalfUp(bdft, BDFT_DECIMALS); +} + +export function roundPrice(price: number): number { + return roundHalfUp(price, PRICE_DECIMALS); +} + +/** bdft = sqft × thickness_in. No divide-by-12. */ +export function computeBdft(sqft: number, thicknessIn: number): number { + return roundBdft(sqft * thicknessIn); +} diff --git a/test-results/tv3-tv10-green.txt b/test-results/tv3-tv10-green.txt new file mode 100644 index 0000000..198bf27 --- /dev/null +++ b/test-results/tv3-tv10-green.txt @@ -0,0 +1,16 @@ + + RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend + +stdout | src/__tests__/tv3-tv10-units.test.ts > TV-3 Scale, sqft, bdft > bdft = sqft × thickness_in with no divide-by-12 +{"case":"TV-3","lengthIn":96,"thicknessIn":1.5,"sqft":0.64,"bdft":0.96,"expectedBdft":0.96,"identity_1sqft_1in":1} + +stdout | src/__tests__/tv3-tv10-units.test.ts > TV-10 Rounding > half-up / nearest-step matches TECH-SPEC vectors +{"case":"TV-10","length":{"raw":96.04,"rounded":96,"expected":96},"sqft":{"raw":0.641,"rounded":0.64,"expected":0.64},"bdft":{"raw":11.525,"rounded":11.53,"expected":11.53},"price":{"raw":12.345,"rounded":12.35,"expected":12.35}} + + ✓ src/__tests__/tv3-tv10-units.test.ts (3 tests) 7ms + + Test Files 1 passed (1) + Tests 3 passed (3) + Start at 06:02:02 + Duration 377ms (transform 61ms, setup 0ms, collect 63ms, tests 7ms, environment 0ms, prepare 67ms) + diff --git a/test-results/tv3-tv10-red.txt b/test-results/tv3-tv10-red.txt new file mode 100644 index 0000000..7f8bb9f --- /dev/null +++ b/test-results/tv3-tv10-red.txt @@ -0,0 +1,18 @@ + + RUN v2.1.9 /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend + + ❯ src/__tests__/tv3-tv10-units.test.ts (0 test) + +⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL src/__tests__/tv3-tv10-units.test.ts [ src/__tests__/tv3-tv10-units.test.ts ] +Error: Failed to load url ../lib/pipeline/units.js (resolved id: ../lib/pipeline/units.js) in /workspace/gitea-atd/Ty_Tech/SlabUploader/frontend/src/__tests__/tv3-tv10-units.test.ts. Does the file exist? + ❯ loadAndTransform node_modules/vite/dist/node/chunks/dep-BK3b2jBa.js:51969:17 + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + Test Files 1 failed (1) + Tests no tests + Start at 06:01:33 + Duration 344ms (transform 42ms, setup 0ms, collect 0ms, tests 0ms, environment 0ms, prepare 69ms) +