2026-05-06 23:05:37 +03:30
|
|
|
/** Normalize to local midnight; invalid input falls back to today. */
|
|
|
|
|
export function startOfLocalDay(d: Date): Date {
|
|
|
|
|
if (Number.isNaN(d.getTime())) {
|
|
|
|
|
const t = new Date();
|
|
|
|
|
return new Date(t.getFullYear(), t.getMonth(), t.getDate(), 0, 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Local calendar bounds for a date (browser timezone). */
|
|
|
|
|
export function getLocalDayIsoRange(day: Date): { from: string; to: string } {
|
|
|
|
|
const start = startOfLocalDay(day);
|
|
|
|
|
const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 1, 0, 0, 0, 0);
|
|
|
|
|
return { from: start.toISOString(), to: end.toISOString() };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toDateInputValue(d: Date): string {
|
|
|
|
|
const y = d.getFullYear();
|
|
|
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
|
|
|
return `${y}-${m}-${day}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseDateInput(value: string): Date {
|
|
|
|
|
const [y, m, d] = value.split('-').map(Number);
|
|
|
|
|
return new Date(y, m - 1, d, 0, 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function combineLocalDateAndTime(day: Date, timeHHmm: string): Date {
|
|
|
|
|
const [h, min] = timeHHmm.split(':').map(Number);
|
|
|
|
|
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), h, min, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatTimeForInput(d: Date): string {
|
|
|
|
|
const h = String(d.getHours()).padStart(2, '0');
|
|
|
|
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
|
|
|
return `${h}:${m}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSameLocalCalendarDay(a: Date, b: Date): boolean {
|
|
|
|
|
return (
|
|
|
|
|
a.getFullYear() === b.getFullYear() &&
|
|
|
|
|
a.getMonth() === b.getMonth() &&
|
|
|
|
|
a.getDate() === b.getDate()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatHourLabel(hour: number): string {
|
|
|
|
|
const d = new Date(2000, 0, 1, hour, 0, 0, 0);
|
|
|
|
|
return d.toLocaleTimeString(undefined, { hour: 'numeric', hour12: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Local midnight + delta calendar days. */
|
|
|
|
|
export function addCalendarDays(day: Date, delta: number): Date {
|
|
|
|
|
return new Date(day.getFullYear(), day.getMonth(), day.getDate() + delta, 0, 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Compare two calendar days at local midnight (ordering by date only). */
|
|
|
|
|
export function compareLocalDayStart(a: Date, b: Date): number {
|
|
|
|
|
const ta = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime();
|
|
|
|
|
const tb = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime();
|
|
|
|
|
return ta - tb;
|
|
|
|
|
}
|
2026-07-12 22:07:11 +03:30
|
|
|
|
|
|
|
|
export function parseTimeInputToMinutes(time: string): number {
|
|
|
|
|
const [h, m] = time.split(':').map(Number);
|
|
|
|
|
if (!Number.isFinite(h) || !Number.isFinite(m)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return h * 60 + m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function minutesToTimeInput(minutes: number): string {
|
|
|
|
|
const clamped = Math.max(0, Math.min(24 * 60 - 1, minutes));
|
|
|
|
|
const h = Math.floor(clamped / 60);
|
|
|
|
|
const m = clamped % 60;
|
|
|
|
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function adjustTimeInput(time: string, deltaMinutes: number): string {
|
|
|
|
|
return minutesToTimeInput(parseTimeInputToMinutes(time) + deltaMinutes);
|
|
|
|
|
}
|