diff --git a/src/ocr-extract.js b/src/ocr-extract.js new file mode 100644 index 0000000..6a377ab --- /dev/null +++ b/src/ocr-extract.js @@ -0,0 +1,57 @@ +import { emptySpec, KNOWN, matchKnown, normalizeToken } from './spec.js'; + +// Die Muster sind bewusst grosszuegig: Was sie einsammeln, wird +// anschliessend gegen die bekannten Werte abgeglichen. Ein Treffer, +// der dort nicht besteht, wird verworfen statt geraten. +const CAPACITY_PATTERN = /\b([0-9OQBSIL]{1,3})\s?GB\b/g; +// Kein \b am Ende: auf Etiketten folgt der Geschwindigkeit oft direkt +// ein Buchstabe (PC4-2400T), und dort gibt es keine Wortgrenze. +const SPEED_PATTERN = /\bPC4[-\s]?([0-9OQ]{4})/g; +const RANK_PATTERN = /\b([0-9OQ][DS]?R[Xx][0-9OQ])\b/g; +const PART_NUMBER_PATTERN = /\b([A-Z]{1,3}[0-9]{2,4}[A-Z0-9]{4,}(?:-[A-Z0-9]{2,6})?)\b/g; +const DATE_CODE_PATTERN = /(?:^|\s)([0-9]{4})(?=\s|$)/g; + +function firstMatch(text, pattern, transform) { + pattern.lastIndex = 0; + let match; + while ((match = pattern.exec(text)) !== null) { + const value = transform(match[1]); + if (value !== null) return value; + } + return null; +} + +/** + * Zerlegt den OCR-Rohtext eines Etiketts in Spec-Felder. + * Jeder Kandidat wird gegen die bekannten Werte geprueft; besteht er + * die Pruefung nicht, bleibt das Feld offen. + */ +export function extractFields(rawText) { + const spec = emptySpec(); + const text = normalizeToken(rawText).replace(/\s+/g, ' '); + if (text === '') return spec; + + spec.capacityGb = firstMatch(text, CAPACITY_PATTERN, (raw) => + matchKnown(raw, KNOWN.capacityGb), + ); + spec.speed = firstMatch(text, SPEED_PATTERN, (raw) => + matchKnown(`PC4-${raw}`, KNOWN.speed), + ); + spec.rank = firstMatch(text, RANK_PATTERN, (raw) => matchKnown(raw, KNOWN.rank)); + spec.formFactor = firstMatch(text, /\b(U?L?R?S?O?DIMM)\b/g, (raw) => + matchKnown(raw, KNOWN.formFactor), + ); + + // Teilenummer: der laengste Kandidat, der nicht der Seriennummer entspricht. + PART_NUMBER_PATTERN.lastIndex = 0; + const candidates = [...text.matchAll(PART_NUMBER_PATTERN)].map((m) => m[1]); + const withDash = candidates.filter((c) => c.includes('-')); + spec.partNumber = withDash[0] ?? null; + + // Datumscode: vierstellige Zahl, die fuer sich allein steht. + DATE_CODE_PATTERN.lastIndex = 0; + const dateMatch = [...text.matchAll(DATE_CODE_PATTERN)].map((m) => m[1]); + spec.dateCode = dateMatch[0] ?? null; + + return spec; +} diff --git a/test/ocr-extract.test.js b/test/ocr-extract.test.js new file mode 100644 index 0000000..bf1d552 --- /dev/null +++ b/test/ocr-extract.test.js @@ -0,0 +1,54 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { extractFields } from '../src/ocr-extract.js'; + +const REFERENZ = `SAMSUNG +Made in Philippines +K136000908252DF287 +64GB 4DRx4 PC4-2400T-LD1-11-MC0 +M386A8K40BM1-CRC4Y S +1908`; + +test('liest alle Felder aus dem Referenz-Etikett', () => { + const spec = extractFields(REFERENZ); + assert.equal(spec.capacityGb, 64); + assert.equal(spec.rank, '4DRx4'); + assert.equal(spec.speed, 'PC4-2400'); + assert.equal(spec.partNumber, 'M386A8K40BM1-CRC4Y'); + assert.equal(spec.dateCode, '1908'); +}); + +test('repariert eine Verwechslung in der Geschwindigkeit', () => { + const spec = extractFields('64GB 4DRx4 PC4-24O0T-LD1-11-MC0'); + assert.equal(spec.speed, 'PC4-2400'); +}); + +test('repariert eine Verwechslung in der Kapazitaet', () => { + const spec = extractFields('BGB 1Rx8 PC4-2400'); + assert.equal(spec.capacityGb, 8, 'B wird als 8 gelesen'); + assert.equal(spec.rank, '1Rx8'); +}); + +test('erkennt die Bauform aus dem Klartext, wenn vorhanden', () => { + const spec = extractFields('32GB 2Rx4 PC4-2666 RDIMM'); + assert.equal(spec.formFactor, 'RDIMM'); +}); + +test('laesst Felder offen, die nicht im Text stehen', () => { + const spec = extractFields('Oracle PN: 7325773'); + assert.equal(spec.capacityGb, null); + assert.equal(spec.speed, null); + assert.equal(spec.rank, null); +}); + +test('unbekannte Geschwindigkeit bleibt null statt falsch geraten', () => { + const spec = extractFields('64GB 4DRx4 PC4-9999'); + assert.equal(spec.speed, null); + assert.equal(spec.capacityGb, 64); +}); + +test('leerer Text liefert ein leeres Spec', () => { + const spec = extractFields(''); + assert.equal(spec.capacityGb, null); + assert.equal(spec.partNumber, null); +});