04a17d2775
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
/**
|
||
* 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]}`;
|
||
}
|
||
}
|