Files
Calbook/lib/date.ts

130 lines
3.7 KiB
TypeScript

import { de } from "date-fns/locale";
import { format } from "date-fns";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
export const DEFAULT_TIMEZONE = process.env.DEFAULT_TIMEZONE ?? "Europe/Berlin";
export function isValidTimeZone(value: string): boolean {
try {
Intl.DateTimeFormat("de-DE", { timeZone: value }).format(new Date());
return true;
} catch {
return false;
}
}
export function resolveTimeZone(value?: string | null): string {
const trimmed = value?.trim();
if (!trimmed) return DEFAULT_TIMEZONE;
return isValidTimeZone(trimmed) ? trimmed : DEFAULT_TIMEZONE;
}
export function formatDateDE(
date: Date,
withWeekday = false,
timeZone: string = DEFAULT_TIMEZONE
): string {
const zone = resolveTimeZone(timeZone);
return format(toZonedTime(date, zone), withWeekday ? "EEEE, dd.MM.yyyy" : "dd.MM.yyyy", {
locale: de
});
}
export function formatTimeDE(date: Date, timeZone: string = DEFAULT_TIMEZONE): string {
const zone = resolveTimeZone(timeZone);
return format(toZonedTime(date, zone), "HH:mm", { locale: de });
}
export function zonedDateOnlyToUtc(date: string, timeZone: string = DEFAULT_TIMEZONE): Date {
const zone = resolveTimeZone(timeZone);
return fromZonedTime(`${date} 00:00:00`, zone);
}
export function zonedDateFromParts(
date: string,
time: string,
timeZone: string = DEFAULT_TIMEZONE
): Date {
const zone = resolveTimeZone(timeZone);
return fromZonedTime(`${date} ${time}:00`, zone);
}
export function atStartOfDayInZone(date: Date, timeZone: string = DEFAULT_TIMEZONE): Date {
const zone = resolveTimeZone(timeZone);
const zoned = toZonedTime(date, zone);
const yyyy = format(zoned, "yyyy");
const mm = format(zoned, "MM");
const dd = format(zoned, "dd");
return fromZonedTime(`${yyyy}-${mm}-${dd} 00:00:00`, zone);
}
export function atEndOfDayInZone(date: Date, timeZone: string = DEFAULT_TIMEZONE): Date {
const zone = resolveTimeZone(timeZone);
const zoned = toZonedTime(date, zone);
const yyyy = format(zoned, "yyyy");
const mm = format(zoned, "MM");
const dd = format(zoned, "dd");
return fromZonedTime(`${yyyy}-${mm}-${dd} 23:59:59`, zone);
}
export function combineDateAndTime(
date: Date,
hhmm: string,
timeZone: string = DEFAULT_TIMEZONE
): Date {
const zone = resolveTimeZone(timeZone);
const zoned = toZonedTime(date, zone);
const yyyy = format(zoned, "yyyy");
const mm = format(zoned, "MM");
const dd = format(zoned, "dd");
return fromZonedTime(`${yyyy}-${mm}-${dd} ${hhmm}:00`, zone);
}
function parseIsoDateToUtcNoon(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (
!Number.isInteger(year) ||
!Number.isInteger(month) ||
!Number.isInteger(day)
) {
return null;
}
const date = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
if (
date.getUTCFullYear() !== year ||
date.getUTCMonth() !== month - 1 ||
date.getUTCDate() !== day
) {
return null;
}
return date;
}
export function isoDateRangeInclusive(startDateIso: string, endDateIso: string) {
const start = parseIsoDateToUtcNoon(startDateIso);
const end = parseIsoDateToUtcNoon(endDateIso);
if (!start || !end) return [] as string[];
const from = start <= end ? start : end;
const to = start <= end ? end : start;
const values: string[] = [];
for (let ts = from.getTime(); ts <= to.getTime(); ts += 24 * 60 * 60 * 1000) {
values.push(new Date(ts).toISOString().slice(0, 10));
}
return values;
}
export function minutesBetween(start: Date, end: Date): number {
return Math.round((end.getTime() - start.getTime()) / 60000);
}