import { Request } from 'express'; /** Regulární výraz pro datum ve formátu YYYY-MM-DD. */ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; /** Chyba způsobená neplatným vstupem uživatele — mapuje se na HTTP 400. */ export class BadRequestError extends Error { } /** Chyba hledaného, ale neexistujícího záznamu — mapuje se na HTTP 404. */ export class NotFoundError extends Error { } /** Vrátí datum v ISO formátu (YYYY-MM-DD) v lokální časové zóně. */ export function formatDate(date: Date): string { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = String(date.getFullYear()); return `${year}-${month}-${day}`; } /** Vrátí dnešní datum s vynulovaným časem. */ export function getToday(): Date { const today = new Date(); today.setHours(0, 0, 0, 0); return today; } /** * Ověří, že řetězec je datum ve formátu YYYY-MM-DD a odpovídá reálnému dni. * Zachytí i přetečení typu 2024-02-31, které by `new Date` tiše posunul na březen. */ export function isValidIsoDate(value: unknown): value is string { if (typeof value !== 'string' || !ISO_DATE_PATTERN.test(value)) { return false; } const [year, month, day] = value.split('-').map(Number); const date = new Date(year, month - 1, day); return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; } /** * Vrátí datum z query parametru, nebo vyhodí {@link BadRequestError}. * * @param value hodnota query parametru * @param name název parametru pro chybovou hlášku */ export function requireIsoDate(value: unknown, name: string): string { if (!isValidIsoDate(value)) { throw new BadRequestError(`Parametr '${name}' není platné datum ve formátu YYYY-MM-DD`); } return value; } /** Vrátí JWT token z hlavičky Authorization, pokud tam je. */ export function parseToken(req: Request): string | undefined { return req.headers?.authorization?.split(' ')[1]; } /** * Vrátí všechna data v rozsahu (včetně obou krajů) jako ISO řetězce. * * @param from první den rozsahu (YYYY-MM-DD) * @param to poslední den rozsahu (YYYY-MM-DD) */ export function listDatesInRange(from: string, to: string): string[] { const dates: string[] = []; const [fromYear, fromMonth, fromDay] = from.split('-').map(Number); const [toYear, toMonth, toDay] = to.split('-').map(Number); const cursor = new Date(fromYear, fromMonth - 1, fromDay); const end = new Date(toYear, toMonth - 1, toDay); while (cursor <= end) { dates.push(formatDate(cursor)); cursor.setDate(cursor.getDate() + 1); } return dates; } /** Nejvyšší povolený násobek porce nebo aktivity — pojistka proti překlepu. */ const MAX_MULTIPLIER = 100; /** * Ověří násobek porce nebo aktivity a zaokrouhlí ho na setiny. * * Násobek 1 je výchozí stav, proto se vrací jako undefined a neukládá se — * stejně jako jedna série u aktivit. * * @param value vstupní hodnota */ export const normalizeMultiplier = (value: unknown): number | undefined => { if (value == null) return undefined; if (typeof value !== 'number' || !Number.isFinite(value)) { throw new BadRequestError("Násobek musí být číslo"); } const rounded = Math.round(value * 100) / 100; if (rounded <= 0) { throw new BadRequestError("Násobek musí být kladné číslo"); } if (rounded > MAX_MULTIPLIER) { throw new BadRequestError(`Násobek může být nejvýše ${MAX_MULTIPLIER}`); } return rounded === 1 ? undefined : rounded; }; /** * Vrátí kalorie záznamu po započtení násobku. * * Uložené kalorie jsou vždy za jednu porci (jedno provedení), aby šlo násobek * měnit tam a zpět bez ztráty přesnosti zaokrouhlováním. Klient má stejnou * funkci v `Utils.tsx` — musí dávat stejné výsledky. * * @param calories kcal jedné porce * @param multiplier násobek, nevyplněný znamená 1 */ export const applyMultiplier = (calories: number | undefined, multiplier?: number): number | undefined => calories == null ? undefined : Math.round(calories * (multiplier ?? 1));