42 lines
1.6 KiB
TypeScript
42 lines
1.6 KiB
TypeScript
/**
|
||
* Unit-aware interval math (issue #59) for the frontend's progress bars and
|
||
* calendar projection stepping. Uses an AVERAGE day-span per unit (below), an
|
||
* intentional approximation — the backend `helpers/dates.add_interval` is
|
||
* calendar-exact (Jan+1mo = 31 days), so display-side spans can differ by a day
|
||
* or two. Cosmetic only. Pure functions — unit-tested in interval.test.ts.
|
||
*/
|
||
|
||
/** Approximate (average) days per interval unit — mean Gregorian month / Julian
|
||
* year. NOT calendar-exact; see the module note above. */
|
||
export const UNIT_DAYS: Record<string, number> = {
|
||
days: 1,
|
||
weeks: 7,
|
||
months: 30.4368,
|
||
years: 365.25,
|
||
};
|
||
|
||
/** Approximate length of `intervalDays` of `unit` in days; 0 when no interval. */
|
||
export function intervalSpanDays(
|
||
intervalDays: number | null | undefined,
|
||
unit?: string | null,
|
||
): number {
|
||
if (!intervalDays || intervalDays <= 0) return 0;
|
||
return intervalDays * (UNIT_DAYS[unit || "days"] ?? 1);
|
||
}
|
||
|
||
/**
|
||
* Progress through the current cycle, unit-aware.
|
||
* Returns a clamped 0–100 % plus `overflow` (raw % > 100, i.e. past due).
|
||
* Falls back to {0, false} when there is no usable interval/countdown.
|
||
*/
|
||
export function daysProgress(
|
||
intervalDays: number | null | undefined,
|
||
daysUntilDue: number | null | undefined,
|
||
unit?: string | null,
|
||
): { pct: number; overflow: boolean } {
|
||
const span = intervalSpanDays(intervalDays, unit);
|
||
if (span <= 0 || daysUntilDue == null) return { pct: 0, overflow: false };
|
||
const raw = ((span - daysUntilDue) / span) * 100;
|
||
return { pct: Math.max(0, Math.min(100, raw)), overflow: raw > 100 };
|
||
}
|