functional
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Date helpers that work in whole calendar days at local midnight, so day
|
||||
* counting is not affected by time-of-day or DST shifts.
|
||||
*/
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Strip time; return a new Date at local midnight. */
|
||||
export function atMidnight(d: Date): Date {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
}
|
||||
|
||||
/** Parse an ISO YYYY-MM-DD string as a local-midnight Date. Returns null if invalid. */
|
||||
export function parseISODate(value: string): Date | null {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const [, y, m, d] = match;
|
||||
const year = Number(y);
|
||||
const month = Number(m);
|
||||
const day = Number(d);
|
||||
const date = new Date(year, month - 1, day);
|
||||
// Reject overflow (e.g. 2026-02-30 rolling into March).
|
||||
if (
|
||||
date.getFullYear() !== year ||
|
||||
date.getMonth() !== month - 1 ||
|
||||
date.getDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/** Format a Date as ISO YYYY-MM-DD (local). */
|
||||
export function toISODate(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}`;
|
||||
}
|
||||
|
||||
/** Whole days from a to b (b - a). Negative if b is before a. */
|
||||
export function daysBetween(a: Date, b: Date): number {
|
||||
const diff = atMidnight(b).getTime() - atMidnight(a).getTime();
|
||||
return Math.round(diff / MS_PER_DAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inclusive day count of [start, end]. Zero if end < start.
|
||||
* e.g. same day => 1.
|
||||
*/
|
||||
export function inclusiveDayCount(start: Date, end: Date): number {
|
||||
const n = daysBetween(start, end) + 1;
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
/** Return the later of two dates. */
|
||||
export function maxDate(a: Date, b: Date): Date {
|
||||
return a.getTime() >= b.getTime() ? a : b;
|
||||
}
|
||||
|
||||
/** Return the earlier of two dates. */
|
||||
export function minDate(a: Date, b: Date): Date {
|
||||
return a.getTime() <= b.getTime() ? a : b;
|
||||
}
|
||||
|
||||
/** Clamp d into [lo, hi]. */
|
||||
export function clampDate(d: Date, lo: Date, hi: Date): Date {
|
||||
return maxDate(lo, minDate(d, hi));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count days of the inclusive window [winStart, winEnd] that are covered by the
|
||||
* inclusive period [perStart, perEnd] — i.e. the size of the overlap.
|
||||
*/
|
||||
export function overlapDays(
|
||||
winStart: Date,
|
||||
winEnd: Date,
|
||||
perStart: Date,
|
||||
perEnd: Date
|
||||
): number {
|
||||
const start = maxDate(winStart, perStart);
|
||||
const end = minDate(winEnd, perEnd);
|
||||
return inclusiveDayCount(start, end);
|
||||
}
|
||||
Reference in New Issue
Block a user