98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
/** Returns the ISO date string (YYYY-MM-DD) of the Monday for the week containing `date`. */
|
||||
|
|
export function getWeekStart(date: Date): string {
|
|||
|
|
const d = new Date(date);
|
|||
|
|
const day = d.getDay(); // 0=Sun, 1=Mon, ...
|
|||
|
|
const diff = day === 0 ? -6 : 1 - day;
|
|||
|
|
d.setDate(d.getDate() + diff);
|
|||
|
|
return formatDate(d);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Adds `n` weeks to a week-start date string and returns the new week-start. */
|
|||
|
|
export function addWeeks(weekStart: string, n: number): string {
|
|||
|
|
const d = parseDate(weekStart);
|
|||
|
|
d.setDate(d.getDate() + n * 7);
|
|||
|
|
return formatDate(d);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Adds `n` days to a date string and returns the new date. */
|
|||
|
|
export function addDays(dateStr: string, n: number): string {
|
|||
|
|
const d = parseDate(dateStr);
|
|||
|
|
d.setDate(d.getDate() + n);
|
|||
|
|
return formatDate(d);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Returns an array of 7 ISO date strings for the week starting at `weekStart` (Monday). */
|
|||
|
|
export function getWeekDays(weekStart: string): string[] {
|
|||
|
|
const d = parseDate(weekStart);
|
|||
|
|
const days: string[] = [];
|
|||
|
|
for (let i = 0; i < 7; i++) {
|
|||
|
|
const day = new Date(d);
|
|||
|
|
day.setDate(d.getDate() + i);
|
|||
|
|
days.push(formatDate(day));
|
|||
|
|
}
|
|||
|
|
return days;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Returns a human-readable heading like "Monday, April 7th". */
|
|||
|
|
export function formatDayHeading(dateStr: string): string {
|
|||
|
|
const d = parseDate(dateStr);
|
|||
|
|
const weekday = d.toLocaleDateString(undefined, { weekday: "long" });
|
|||
|
|
const month = d.toLocaleDateString(undefined, { month: "long" });
|
|||
|
|
const day = d.getDate();
|
|||
|
|
return `${weekday}, ${month} ${ordinal(day)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Returns a short day label like "Mon" and day number like "7". */
|
|||
|
|
export function formatShortDay(dateStr: string): { label: string; day: number } {
|
|||
|
|
const d = parseDate(dateStr);
|
|||
|
|
return {
|
|||
|
|
label: d.toLocaleDateString(undefined, { weekday: "short" }),
|
|||
|
|
day: d.getDate(),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Returns the month + year label like "April 2026". */
|
|||
|
|
export function formatMonthYear(dateStr: string): string {
|
|||
|
|
const d = parseDate(dateStr);
|
|||
|
|
return d.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Compact range for a Mon–Sun week, e.g. "Apr 6 – 12, 2026" or spanning two months. */
|
|||
|
|
export function formatWeekRangeLabel(weekStart: string): string {
|
|||
|
|
const start = parseDate(weekStart);
|
|||
|
|
const end = new Date(start);
|
|||
|
|
end.setDate(end.getDate() + 6);
|
|||
|
|
const sameMonth =
|
|||
|
|
start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear();
|
|||
|
|
if (sameMonth) {
|
|||
|
|
const month = start.toLocaleDateString(undefined, { month: "short" });
|
|||
|
|
return `${month} ${start.getDate()} – ${end.getDate()}, ${start.getFullYear()}`;
|
|||
|
|
}
|
|||
|
|
return `${start.toLocaleDateString(undefined, { month: "short", day: "numeric" })} – ${end.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Checks if a date string is today. */
|
|||
|
|
export function isToday(dateStr: string): boolean {
|
|||
|
|
return dateStr === formatDate(new Date());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Formats a Date to YYYY-MM-DD. */
|
|||
|
|
function formatDate(d: Date): string {
|
|||
|
|
const year = d.getFullYear();
|
|||
|
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|||
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|||
|
|
return `${year}-${month}-${day}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Parses a YYYY-MM-DD string to a local Date. */
|
|||
|
|
function parseDate(dateStr: string): Date {
|
|||
|
|
const parts = dateStr.split("-").map(Number);
|
|||
|
|
return new Date(parts[0]!, parts[1]! - 1, parts[2]!);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ordinal(n: number): string {
|
|||
|
|
const s = ["th", "st", "nd", "rd"] as const;
|
|||
|
|
const v = n % 100;
|
|||
|
|
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
|||
|
|
}
|