From c6a549699acc179de2c7b9c6c96240d70fe5650b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:50:24 +0200 Subject: [PATCH] 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) --- src/lib/server/unit-consolidation.ts | 55 ++++++++++++++++++++ tests/unit/unit-consolidation.test.ts | 74 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/src/lib/server/unit-consolidation.ts b/src/lib/server/unit-consolidation.ts index ce22f28..b5f3aae 100644 --- a/src/lib/server/unit-consolidation.ts +++ b/src/lib/server/unit-consolidation.ts @@ -9,3 +9,58 @@ export function unitFamily(unit: string | null | undefined): UnitFamily { if (VOLUME_UNITS.has(u)) return 'volume'; 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 }; +} diff --git a/tests/unit/unit-consolidation.test.ts b/tests/unit/unit-consolidation.test.ts index 40b6cb5..e411699 100644 --- a/tests/unit/unit-consolidation.test.ts +++ b/tests/unit/unit-consolidation.test.ts @@ -1,5 +1,5 @@ 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', () => { it('maps g and kg to weight', () => { @@ -26,3 +26,75 @@ describe('unitFamily', () => { 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' }); + }); +});