Files
butcem/src/shared/recurrence.ts
T

137 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Düzenli işlem tekrarı (Bölüm 6.7). Saf fonksiyonlar: tarihler "YYYY-MM-DD" (İstanbul günü).
* Aylık/yıllık tekrarda gün, başlangıç tarihinin gününden alınır ve ayda yoksa ayın son günü
* kullanılır (31 Ocak → 28/29 Şubat → 31 Mart). "Ayın son günü" modu her zaman son günü verir.
*/
export const FREQUENCIES = ["daily", "weekly", "monthly", "yearly"] as const;
export type Frequency = (typeof FREQUENCIES)[number];
export const MONTHLY_MODES = ["day", "last"] as const;
export type MonthlyMode = (typeof MONTHLY_MODES)[number];
/** Planlı kayıtların üretileceği ufuk (ay). */
export const HORIZON_MONTHS = 12;
export const MAX_OCCURRENCES = 600;
export type Recurrence = {
frequency: Frequency;
/** Her N gün/hafta/ay/yılda bir (1–12). */
interval: number;
monthlyMode: MonthlyMode;
startDate: string;
endDate: string | null;
/** Toplam tekrar sayısı (ör. 12) — endDate ile birlikte verilirse hangisi önce gelirse. */
count: number | null;
};
const parse = (ymd: string) => {
const [y, m, d] = ymd.split("-").map(Number) as [number, number, number];
return { y, m, d };
};
const fmt = (y: number, m: number, d: number) =>
`${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
export function daysInMonth(y: number, m: number): number {
return new Date(Date.UTC(y, m, 0)).getUTCDate();
}
export function addDays(ymd: string, days: number): string {
const { y, m, d } = parse(ymd);
const t = new Date(Date.UTC(y, m - 1, d + days));
return fmt(t.getUTCFullYear(), t.getUTCMonth() + 1, t.getUTCDate());
}
/** Ay ekler; gün ayda yoksa ayın son günü. `day` verilirse o gün hedeflenir. */
export function addMonths(ymd: string, months: number, day?: number | "last"): string {
const { y, m, d } = parse(ymd);
const index = y * 12 + (m - 1) + months;
const ny = Math.floor(index / 12);
const nm = (index % 12) + 1;
const last = daysInMonth(ny, nm);
const target = day === "last" ? last : Math.min(day ?? d, last);
return fmt(ny, nm, target);
}
/** index. tekrarın tarihi (0 = başlangıç; "son gün" modunda başlangıç ayının son günü). */
export function occurrenceDate(r: Recurrence, index: number): string {
const n = index * r.interval;
switch (r.frequency) {
case "daily":
return addDays(r.startDate, n);
case "weekly":
return addDays(r.startDate, n * 7);
case "monthly":
return addMonths(r.startDate, n, r.monthlyMode === "last" ? "last" : parse(r.startDate).d);
case "yearly":
return addMonths(r.startDate, n * 12, parse(r.startDate).d);
}
}
/** Ufuk tarihine (dahil) kadar, `fromIndex`ten başlayarak tekrarlar. */
export function listOccurrences(
r: Recurrence,
until: string,
fromIndex = 0,
): { index: number; date: string }[] {
const out: { index: number; date: string }[] = [];
for (let i = fromIndex; i < MAX_OCCURRENCES; i++) {
if (r.count !== null && i >= r.count) break;
const date = occurrenceDate(r, i);
if (r.endDate && date > r.endDate) break;
if (date > until) break;
out.push({ index: i, date });
}
return out;
}
/** Seri bitti mi (bir sonraki tekrar yok)? */
export function isFinished(r: Recurrence, nextIndex: number): boolean {
if (r.count !== null && nextIndex >= r.count) return true;
return Boolean(r.endDate && occurrenceDate(r, nextIndex) > r.endDate);
}
export function horizonDate(today: string): string {
return addMonths(today, HORIZON_MONTHS);
}
const MONTHS = [
"Ocak",
"Şubat",
"Mart",
"Nisan",
"Mayıs",
"Haziran",
"Temmuz",
"Ağustos",
"Eylül",
"Ekim",
"Kasım",
"Aralık",
];
const WEEKDAYS = ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"];
/** "Her ay 3. gün", "2 haftada bir Pazartesi", "Her ayın son günü", "Her yıl 15 Mart". */
export function describeRecurrence(
r: Pick<Recurrence, "frequency" | "interval" | "monthlyMode" | "startDate">,
): string {
const { y, m, d } = parse(r.startDate);
const every = (unit: string) =>
r.interval === 1 ? `Her ${unit}` : `${r.interval} ${unit}da bir`;
switch (r.frequency) {
case "daily":
return r.interval === 1 ? "Her gün" : `${r.interval} günde bir`;
case "weekly": {
const wd = WEEKDAYS[new Date(Date.UTC(y, m - 1, d)).getUTCDay()]!;
return r.interval === 1 ? `Her hafta ${wd}` : `${r.interval} haftada bir ${wd}`;
}
case "monthly":
if (r.monthlyMode === "last")
return r.interval === 1 ? "Her ayın son günü" : `${r.interval} ayda bir, ayın son günü`;
return r.interval === 1 ? `Her ay ${d}. gün` : `${every("ay")}, ${d}. gün`;
case "yearly":
return r.interval === 1
? `Her yıl ${d} ${MONTHS[m - 1]}`
: `${r.interval} yılda bir, ${d} ${MONTHS[m - 1]}`;
}
}