feat: implement unit conversion utilities and centralize unit definitions for consistency across frontend and backend
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Central enhetsdatabas för Recipe App.
|
||||
*
|
||||
* Alla stödda enheter definieras här med:
|
||||
* - value : kanonisk förkortning (det som lagras i DB)
|
||||
* - labelSv : svensk visningstext
|
||||
* - type : enhetstyp (weight | volume | cooking | piece | other)
|
||||
* - toBaseFactor: multiplicera för att nå SI-basenheten för typen
|
||||
* (g för vikt, ml för volym och cooking)
|
||||
* - aliases : alternativa stavningar/namn som normaliseras hit
|
||||
*
|
||||
* Konvertering sker alltid via basenheten:
|
||||
* result = quantity * from.toBaseFactor / to.toBaseFactor
|
||||
*
|
||||
* Obs: "cooking"-enheter (tsk, msk, krm) är tekniskt sett volym men
|
||||
* behandlas som en separat typ för att matcha befintlig datamodell.
|
||||
* De kan inte konverteras med ml/dl utan vidare konfiguration.
|
||||
*/
|
||||
|
||||
export type UnitType = 'weight' | 'volume' | 'cooking' | 'piece' | 'other';
|
||||
|
||||
export interface UnitDefinition {
|
||||
/** Kanonisk förkortning — det som lagras i databasen */
|
||||
value: string;
|
||||
/** Svensk visningstext */
|
||||
labelSv: string;
|
||||
/** Enhetstyp */
|
||||
type: UnitType;
|
||||
/**
|
||||
* Faktor för att konvertera till SI-basenheten för typen.
|
||||
* - weight → gram (g = 1)
|
||||
* - volume → milliliter (ml = 1)
|
||||
* - cooking → milliliter (krm = 1 ml, tsk = 5 ml, msk = 15 ml)
|
||||
* - piece/other → 1 (ej konverterbara)
|
||||
*/
|
||||
toBaseFactor: number;
|
||||
/** Alternativa stavningar/namn som normaliseras till value */
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export const UNIT_DEFINITIONS: UnitDefinition[] = [
|
||||
// ── Vikt ──────────────────────────────────────────────────
|
||||
{ value: 'g', labelSv: 'g (gram)', type: 'weight', toBaseFactor: 1, aliases: ['gram'] },
|
||||
{ value: 'hg', labelSv: 'hg (hektogram)', type: 'weight', toBaseFactor: 100, aliases: ['hektogram'] },
|
||||
{ value: 'kg', labelSv: 'kg (kilogram)', type: 'weight', toBaseFactor: 1000, aliases: ['kilo', 'kilogram'] },
|
||||
{ value: 'mg', labelSv: 'mg (milligram)', type: 'weight', toBaseFactor: 0.001, aliases: ['milligram'] },
|
||||
|
||||
// ── Volym ─────────────────────────────────────────────────
|
||||
{ value: 'ml', labelSv: 'ml (milliliter)', type: 'volume', toBaseFactor: 1, aliases: ['milliliter'] },
|
||||
{ value: 'cl', labelSv: 'cl (centiliter)', type: 'volume', toBaseFactor: 10, aliases: ['centiliter'] },
|
||||
{ value: 'dl', labelSv: 'dl (deciliter)', type: 'volume', toBaseFactor: 100, aliases: ['deciliter'] },
|
||||
{ value: 'l', labelSv: 'l (liter)', type: 'volume', toBaseFactor: 1000, aliases: ['liter'] },
|
||||
|
||||
// ── Matlagning (svenska köksenheter) ──────────────────────
|
||||
{ value: 'krm', labelSv: 'krm (kryddmått)', type: 'cooking', toBaseFactor: 1, aliases: ['kryddmatt', 'kryddmått'] },
|
||||
{ value: 'tsk', labelSv: 'tsk (tesked)', type: 'cooking', toBaseFactor: 5, aliases: ['tesked', 'test'] },
|
||||
{ value: 'msk', labelSv: 'msk (matsked)', type: 'cooking', toBaseFactor: 15, aliases: ['matsked', 'matsled'] },
|
||||
|
||||
// ── Styckenheter (ej konverterbara) ───────────────────────
|
||||
{ value: 'st', labelSv: 'st (styck)', type: 'piece', toBaseFactor: 1, aliases: ['stycke', 'styck', 'stk'] },
|
||||
{ value: 'port', labelSv: 'port (portioner)', type: 'piece', toBaseFactor: 1, aliases: ['portion', 'portioner'] },
|
||||
{ value: 'förp', labelSv: 'förp (förpackning)',type: 'piece', toBaseFactor: 1, aliases: ['forp', 'förpackning', 'forpackning'] },
|
||||
{ value: 'klyfta',labelSv: 'klyfta', type: 'piece', toBaseFactor: 1, aliases: [] },
|
||||
|
||||
// ── Övrigt ────────────────────────────────────────────────
|
||||
{ value: 'efter smak', labelSv: 'efter smak', type: 'other', toBaseFactor: 1, aliases: ['eftr smak', 'efter smak'] },
|
||||
];
|
||||
|
||||
/** Snabbuppslagning: alias/value → UnitDefinition */
|
||||
const _unitLookup = new Map<string, UnitDefinition>();
|
||||
for (const def of UNIT_DEFINITIONS) {
|
||||
_unitLookup.set(def.value.toLowerCase(), def);
|
||||
for (const alias of def.aliases) {
|
||||
_unitLookup.set(alias.toLowerCase(), def);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisera en enhetssträng till kanonisk förkortning.
|
||||
* T.ex. "Matsked" → "msk", "Kilogram" → "kg".
|
||||
* Om enheten är okänd returneras den trimmad och lowercasad.
|
||||
*/
|
||||
export function normalizeUnit(unit: string): string {
|
||||
const key = unit.trim().toLowerCase();
|
||||
return _unitLookup.get(key)?.value ?? key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hämta UnitDefinition för en enhet (normaliseras automatiskt).
|
||||
* Returnerar undefined om enheten inte finns i UNIT_DEFINITIONS.
|
||||
*/
|
||||
export function getUnitDefinition(unit: string): UnitDefinition | undefined {
|
||||
const key = unit.trim().toLowerCase();
|
||||
return _unitLookup.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hämta enhetstypen för en enhet.
|
||||
* Returnerar null om enheten är okänd.
|
||||
*/
|
||||
export function getUnitType(unit: string): UnitType | null {
|
||||
return getUnitDefinition(unit)?.type ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontrollera om konvertering är möjlig mellan två enheter.
|
||||
* Konvertering kräver att båda tillhör samma typ och inte är 'piece' eller 'other'.
|
||||
*/
|
||||
export function canConvert(fromUnit: string, toUnit: string): boolean {
|
||||
const from = getUnitDefinition(fromUnit);
|
||||
const to = getUnitDefinition(toUnit);
|
||||
if (!from || !to) return false;
|
||||
if (from.type !== to.type) return false;
|
||||
if (from.type === 'piece' || from.type === 'other') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konverterar en mängd från en enhet till en annan.
|
||||
*
|
||||
* @throws Error om quantity ≤ 0
|
||||
* @throws Error om en enhet är okänd ("Unknown unit: ...")
|
||||
* @throws Error om enheterna inte kan konverteras ("Cannot convert between incompatible unit types: ...")
|
||||
*/
|
||||
export function convertUnit(quantity: number, fromUnit: string, toUnit: string): number {
|
||||
if (quantity <= 0) {
|
||||
throw new Error(`Invalid quantity: ${quantity}. Quantity must be positive.`);
|
||||
}
|
||||
|
||||
const normalizedFrom = normalizeUnit(fromUnit);
|
||||
const normalizedTo = normalizeUnit(toUnit);
|
||||
|
||||
if (normalizedFrom === normalizedTo) return quantity;
|
||||
|
||||
const fromDef = getUnitDefinition(normalizedFrom);
|
||||
const toDef = getUnitDefinition(normalizedTo);
|
||||
|
||||
if (!fromDef) throw new Error(`Unknown unit: "${fromUnit}"`);
|
||||
if (!toDef) throw new Error(`Unknown unit: "${toUnit}"`);
|
||||
|
||||
if (fromDef.type !== toDef.type) {
|
||||
throw new Error(
|
||||
`Cannot convert between incompatible unit types: "${fromUnit}" (${fromDef.type}) and "${toUnit}" (${toDef.type})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (fromDef.type === 'piece' || fromDef.type === 'other') {
|
||||
return quantity; // Styckenheter konverteras inte
|
||||
}
|
||||
|
||||
return (quantity * fromDef.toBaseFactor) / toDef.toBaseFactor;
|
||||
}
|
||||
Reference in New Issue
Block a user