Files
firefly-ui/src/routes/recurring/recurrence.ts
T

102 lines
2.9 KiB
TypeScript

import type { Recurrence } from "#lib/api/types.js";
import { dateOnly } from "#lib/components/misc/queries.js";
import { parse, today } from "#lib/dates.js";
import { currentLocale, formatDate } from "#lib/format.js";
export const TYPES: Record<string, string> = {
deposit: "Deposit",
transfer: "Transfer",
withdrawal: "Withdrawal",
};
export const TONES = {
deposit: "income",
transfer: "transfer",
withdrawal: "expense",
} as const;
export const toneOf = (type?: string) =>
TONES[(type ?? "withdrawal") as keyof typeof TONES] ?? "neutral";
export function endsText(r: Recurrence): string {
if (r.repeat_until) {
return `Repeats until ${formatDate(dateOnly(r.repeat_until))}`;
}
if (r.nr_of_repetitions) {
return r.nr_of_repetitions === 1
? "Repeats 1 time"
: `Repeats ${r.nr_of_repetitions} times`;
}
return "Repeats forever";
}
export function stopped(r: Recurrence): boolean {
return Boolean(r.repeat_until && r.repeat_until.slice(0, 10) < today());
}
export function upcoming(r: Recurrence, count = 3): string[] {
const now = today();
const dates = (r.repetitions ?? [])
.flatMap((rep) => rep.occurrences ?? [])
.map((date) => date.slice(0, 10))
.filter((date) => date >= now);
return [...new Set(dates)].sort().slice(0, count);
}
type Repetition = NonNullable<Recurrence["repetitions"]>[number];
const DAYS = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const WEEKEND: Record<number, string> = {
2: ", skipped on weekends",
3: ", moved to Friday on weekends",
4: ", moved to Monday on weekends",
};
const SUFFIX = ["th", "st", "nd", "rd"];
export function ordinal(n: number): string {
const teen = n % 100 >= 11 && n % 100 <= 13;
return `${n}${teen ? "th" : (SUFFIX[n % 10] ?? "th")}`;
}
function every(unit: string, skip = 0): string {
if (skip === 0) {
return `Every ${unit}`;
}
return skip === 1
? `Every other ${unit}`
: `Every ${ordinal(skip + 1)} ${unit}`;
}
function base(rep: Repetition): string {
const day = (value: string | number) =>
DAYS[Number(value) - 1] ?? String(value);
switch (rep.type) {
case "daily":
return every("day", rep.skip);
case "weekly":
return `${every("week", rep.skip)} on ${day(rep.moment)}`;
case "monthly":
return `${every("month", rep.skip)} on the ${ordinal(Number(rep.moment))}`;
case "ndom": {
const [week, weekday] = rep.moment.split(",");
return `${every("month", rep.skip)} on the ${ordinal(Number(week))} ${day(weekday)}`;
}
case "yearly":
return `${every("year", rep.skip)} on ${new Intl.DateTimeFormat(currentLocale(), { day: "numeric", month: "long" }).format(parse(rep.moment))}`;
default:
return rep.description ?? rep.type;
}
}
export function describe(rep: Repetition): string {
return `${base(rep)}${WEEKEND[rep.weekend ?? 1] ?? ""}`;
}