feat: toleranter Spec-Vergleich gegen OCR-Verwechslungen
This commit is contained in:
+59
@@ -53,3 +53,62 @@ export function fingerprint(spec) {
|
||||
export function isUsable(spec) {
|
||||
return typeof spec.capacityGb === 'number' && Number.isFinite(spec.capacityGb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeichen, die OCR auf glaenzenden Etiketten regelmaessig verwechselt,
|
||||
* werden auf einen gemeinsamen Vertreter abgebildet. D bleibt bewusst
|
||||
* unangetastet, sonst kollidiert 4DRx4 mit 40Rx4.
|
||||
*/
|
||||
const CONFUSIONS = {
|
||||
O: '0', Q: '0',
|
||||
I: '1', L: '1',
|
||||
B: '8',
|
||||
S: '5',
|
||||
Z: '2',
|
||||
G: '6',
|
||||
};
|
||||
|
||||
/** Vergleichsform eines Tokens: normalisiert, ohne Trennzeichen, verwechslungsfrei. */
|
||||
export function canonical(raw) {
|
||||
const normalized = normalizeToken(raw).replace(/[^A-Z0-9]/g, '');
|
||||
let out = '';
|
||||
for (const char of normalized) {
|
||||
out += CONFUSIONS[char] ?? char;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gleicht einen gelesenen Wert gegen eine Liste bekannter Werte ab.
|
||||
* Erst exakt, dann ueber die Vergleichsform. Mehrdeutigkeit gilt als Treffer-los.
|
||||
*/
|
||||
export function matchKnown(raw, list) {
|
||||
const normalized = normalizeToken(raw);
|
||||
if (normalized === '') return null;
|
||||
|
||||
for (const candidate of list) {
|
||||
if (normalizeToken(String(candidate)) === normalized) return candidate;
|
||||
}
|
||||
|
||||
const target = canonical(normalized);
|
||||
const hits = list.filter((candidate) => canonical(String(candidate)) === target);
|
||||
return hits.length === 1 ? hits[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwei Specs sind vertraeglich, wenn alle beidseitig gesetzten Felder
|
||||
* in ihrer Vergleichsform uebereinstimmen. Felder, die auf einer Seite
|
||||
* fehlen, verhindern die Vertraeglichkeit nicht - die Entscheidung
|
||||
* darueber faellt in session.js.
|
||||
*/
|
||||
export function specsCompatible(a, b) {
|
||||
const fields = ['capacityGb', 'formFactor', 'rank', 'speed', 'partNumber'];
|
||||
for (const field of fields) {
|
||||
const left = a[field];
|
||||
const right = b[field];
|
||||
if (left === null || left === undefined) continue;
|
||||
if (right === null || right === undefined) continue;
|
||||
if (canonical(String(left)) !== canonical(String(right))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { KNOWN, emptySpec, canonical, matchKnown, specsCompatible } from '../src/spec.js';
|
||||
|
||||
test('canonical bildet verwechselbare Zeichen auf einen Vertreter ab', () => {
|
||||
assert.equal(canonical('PC4-24O0'), canonical('PC4-2400'));
|
||||
assert.equal(canonical('1RX8'), canonical('IRX8'));
|
||||
assert.equal(canonical('BGB'), canonical('868'));
|
||||
assert.equal(canonical('LRDIMM'), canonical('1RD1MM'));
|
||||
});
|
||||
|
||||
test('canonical laesst D unangetastet, damit 4DRx4 erhalten bleibt', () => {
|
||||
assert.notEqual(canonical('4DRX4'), canonical('40RX4'));
|
||||
});
|
||||
|
||||
test('matchKnown findet den exakten Wert', () => {
|
||||
assert.equal(matchKnown('PC4-2400', KNOWN.speed), 'PC4-2400');
|
||||
assert.equal(matchKnown('lrdimm', KNOWN.formFactor), 'LRDIMM');
|
||||
});
|
||||
|
||||
test('matchKnown repariert eine Verwechslung', () => {
|
||||
assert.equal(matchKnown('PC4-24O0', KNOWN.speed), 'PC4-2400');
|
||||
assert.equal(matchKnown('4DRX4', KNOWN.rank), '4DRx4');
|
||||
});
|
||||
|
||||
test('matchKnown liefert null bei unbekanntem Wert', () => {
|
||||
assert.equal(matchKnown('PC4-9999', KNOWN.speed), null);
|
||||
assert.equal(matchKnown('', KNOWN.speed), null);
|
||||
});
|
||||
|
||||
test('matchKnown funktioniert auch mit Zahlenlisten', () => {
|
||||
assert.equal(matchKnown('64', KNOWN.capacityGb), 64);
|
||||
assert.equal(matchKnown('6A', KNOWN.capacityGb), null);
|
||||
});
|
||||
|
||||
test('specsCompatible vergleicht nur beidseitig gesetzte Felder', () => {
|
||||
const voll = { capacityGb: 64, formFactor: 'LRDIMM', rank: '4DRx4', speed: 'PC4-2400', partNumber: 'M386A8K40BM1-CRC4Y', dateCode: '1908' };
|
||||
const ohnePn = { ...voll, partNumber: null, dateCode: null };
|
||||
assert.equal(specsCompatible(voll, ohnePn), true);
|
||||
});
|
||||
|
||||
test('specsCompatible erkennt einen echten Unterschied', () => {
|
||||
const a = { ...emptySpec(), capacityGb: 64, speed: 'PC4-2400' };
|
||||
const b = { ...emptySpec(), capacityGb: 32, speed: 'PC4-2400' };
|
||||
assert.equal(specsCompatible(a, b), false);
|
||||
});
|
||||
|
||||
test('specsCompatible trennt gleiche Spec mit unterschiedlicher Teilenummer', () => {
|
||||
const samsung = { ...emptySpec(), capacityGb: 64, speed: 'PC4-2400', partNumber: 'M386A8K40BM1-CRC4Y' };
|
||||
const hynix = { ...emptySpec(), capacityGb: 64, speed: 'PC4-2400', partNumber: 'HMAA8GL7AMR4N-UH' };
|
||||
assert.equal(specsCompatible(samsung, hynix), false);
|
||||
});
|
||||
|
||||
test('specsCompatible toleriert eine Verwechslung in der Teilenummer', () => {
|
||||
const a = { ...emptySpec(), capacityGb: 64, partNumber: 'M386A8K40BM1-CRC4Y' };
|
||||
const b = { ...emptySpec(), capacityGb: 64, partNumber: 'M386A8K4OBM1-CRC4Y' };
|
||||
assert.equal(specsCompatible(a, b), true);
|
||||
});
|
||||
Reference in New Issue
Block a user