feat(shopping): consolidate() fuer g/kg + ml/l Summierung
Implementiert consolidate() in unit-consolidation.ts: summiert Mengen innerhalb einer Unit-Family (Gewicht g/kg, Volumen ml/l) mit automatischer Promotion ab Schwellwert; nicht-family-units werden direkt summiert. quantity=null wird als 0 behandelt; alle-null ergibt null-Ergebnis. 9 neue Tests, alle 14 Tests gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,58 @@ export function unitFamily(unit: string | null | undefined): UnitFamily {
|
|||||||
if (VOLUME_UNITS.has(u)) return 'volume';
|
if (VOLUME_UNITS.has(u)) return 'volume';
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuantityInUnit {
|
||||||
|
quantity: number | null;
|
||||||
|
unit: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function round2(n: number): number {
|
||||||
|
return Math.round(n * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konsolidiert mehrere {quantity, unit}-Eintraege derselben Unit-Family
|
||||||
|
* zu einer gemeinsamen Menge + Display-Unit.
|
||||||
|
*
|
||||||
|
* - Gewicht (g, kg): summiert in g, promoted bei >=1000 g auf kg.
|
||||||
|
* - Volumen (ml, l): summiert in ml, promoted bei >=1000 ml auf l.
|
||||||
|
* - Andere: summiert quantity ohne Umrechnung, Display-Unit vom ersten
|
||||||
|
* Eintrag.
|
||||||
|
*
|
||||||
|
* quantity=null wird als 0 behandelt. Wenn ALLE quantities null sind,
|
||||||
|
* ist die Gesamtmenge ebenfalls null.
|
||||||
|
*/
|
||||||
|
export function consolidate(rows: QuantityInUnit[]): QuantityInUnit {
|
||||||
|
if (rows.length === 0) return { quantity: null, unit: null };
|
||||||
|
|
||||||
|
const family = unitFamily(rows[0].unit);
|
||||||
|
const firstUnit = rows[0].unit;
|
||||||
|
|
||||||
|
const allNull = rows.every((r) => r.quantity === null);
|
||||||
|
|
||||||
|
if (family === 'weight') {
|
||||||
|
if (allNull) return { quantity: null, unit: firstUnit };
|
||||||
|
const grams = rows.reduce((sum, r) => {
|
||||||
|
const q = r.quantity ?? 0;
|
||||||
|
return sum + (unitFamily(r.unit) === 'weight' && r.unit?.toLowerCase().trim() === 'kg' ? q * 1000 : q);
|
||||||
|
}, 0);
|
||||||
|
if (grams >= 1000) return { quantity: round2(grams / 1000), unit: 'kg' };
|
||||||
|
return { quantity: round2(grams), unit: 'g' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === 'volume') {
|
||||||
|
if (allNull) return { quantity: null, unit: firstUnit };
|
||||||
|
const ml = rows.reduce((sum, r) => {
|
||||||
|
const q = r.quantity ?? 0;
|
||||||
|
return sum + (r.unit?.toLowerCase().trim() === 'l' ? q * 1000 : q);
|
||||||
|
}, 0);
|
||||||
|
if (ml >= 1000) return { quantity: round2(ml / 1000), unit: 'l' };
|
||||||
|
return { quantity: round2(ml), unit: 'ml' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-family: summiere quantity direkt
|
||||||
|
if (allNull) return { quantity: null, unit: firstUnit };
|
||||||
|
const sum = rows.reduce((acc, r) => acc + (r.quantity ?? 0), 0);
|
||||||
|
return { quantity: round2(sum), unit: firstUnit };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { unitFamily } from '../../src/lib/server/unit-consolidation';
|
import { unitFamily, consolidate } from '../../src/lib/server/unit-consolidation';
|
||||||
|
|
||||||
describe('unitFamily', () => {
|
describe('unitFamily', () => {
|
||||||
it('maps g and kg to weight', () => {
|
it('maps g and kg to weight', () => {
|
||||||
@@ -26,3 +26,75 @@ describe('unitFamily', () => {
|
|||||||
expect(unitFamily(' ')).toBe('');
|
expect(unitFamily(' ')).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('consolidate', () => {
|
||||||
|
it('kombiniert 500 g + 1 kg zu 1,5 kg', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 500, unit: 'g' },
|
||||||
|
{ quantity: 1, unit: 'kg' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 1.5, unit: 'kg' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bleibt bei g wenn Summe < 1 kg', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 200, unit: 'g' },
|
||||||
|
{ quantity: 300, unit: 'g' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 500, unit: 'g' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('kombiniert ml + l analog (400 ml + 0,5 l → 900 ml)', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 400, unit: 'ml' },
|
||||||
|
{ quantity: 0.5, unit: 'l' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 900, unit: 'ml' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promoted zu l ab 1000 ml (0,5 l + 0,8 l → 1,3 l)', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 0.5, unit: 'l' },
|
||||||
|
{ quantity: 0.8, unit: 'l' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 1.3, unit: 'l' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('summiert gleiche nicht-family-units (2 Bund + 1 Bund → 3 Bund)', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 2, unit: 'Bund' },
|
||||||
|
{ quantity: 1, unit: 'Bund' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 3, unit: 'Bund' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('behandelt quantity=null als 0', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: null, unit: 'TL' },
|
||||||
|
{ quantity: 1, unit: 'TL' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: 1, unit: 'TL' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt null zurueck wenn alle quantities null sind', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: null, unit: 'Prise' },
|
||||||
|
{ quantity: null, unit: 'Prise' }
|
||||||
|
]);
|
||||||
|
expect(out).toEqual({ quantity: null, unit: 'Prise' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rundet Float-Artefakte auf 2 Dezimalen (0,1 + 0,2 kg → 0,3 kg)', () => {
|
||||||
|
const out = consolidate([
|
||||||
|
{ quantity: 0.1, unit: 'kg' },
|
||||||
|
{ quantity: 0.2, unit: 'kg' }
|
||||||
|
]);
|
||||||
|
// 0.1 + 0.2 in kg = 0.3 kg, in g = 300 → promoted? 300 < 1000 → 300 g
|
||||||
|
expect(out).toEqual({ quantity: 300, unit: 'g' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nimmt unit vom ersten Eintrag bei unbekannter family', () => {
|
||||||
|
const out = consolidate([{ quantity: 5, unit: 'Stück' }]);
|
||||||
|
expect(out).toEqual({ quantity: 5, unit: 'Stück' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user