feat(date): add parseDate utility with locale-aware parsing

This commit is contained in:
Roberto
2026-05-13 16:04:44 +02:00
parent 0fc3aa421e
commit 4e89a7a96c

View File

@@ -0,0 +1,146 @@
import type { FormatPrefs } from './date';
type ParseInput = string | null | undefined;
const RE_REL = /^\s*([+-])\s*(\d+)\s*([dwm])\s*$/i;
const RE_NUMERIC = /^(\d{1,4})[/\-.](\d{1,4})(?:[/\-.](\d{1,4}))?$/;
function startOfDay(d: Date): Date {
const out = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return out;
}
function addDays(d: Date, n: number): Date {
const out = new Date(d);
out.setDate(out.getDate() + n);
return out;
}
function addMonths(d: Date, n: number): Date {
const out = new Date(d);
out.setMonth(out.getMonth() + n);
return out;
}
function pivotYear(twoDigit: number): number {
const now = new Date().getFullYear();
const century = Math.floor(now / 100) * 100;
const offset = now % 100;
return twoDigit > (offset + 50) ? century - 100 + twoDigit : century + twoDigit;
}
function parseNumeric(
input: string,
prefs: FormatPrefs,
base: Date,
): Date | null {
const m = input.match(RE_NUMERIC);
if (!m) return null;
let day: number, month: number, year: number;
const a = parseInt(m[1], 10);
const b = parseInt(m[2], 10);
const c = m[3] != null ? parseInt(m[3], 10) : NaN;
if (prefs.dateFormat === 'yyyy-MM-dd') {
if (m[3] == null) return null;
year = a; month = b; day = c;
} else if (prefs.dateFormat === 'MM/dd/yyyy') {
month = a; day = b;
if (m[3] == null) year = base.getFullYear();
else year = c < 100 ? pivotYear(c) : c;
} else {
// default: 'dd/MM/yyyy'
day = a; month = b;
if (m[3] == null) year = base.getFullYear();
else year = c < 100 ? pivotYear(c) : c;
}
if (month < 1 || month > 12) return null;
const result = new Date(year, month - 1, day);
if (result.getFullYear() !== year || result.getMonth() !== month - 1 || result.getDate() !== day) {
return null;
}
if (m[3] == null && result < startOfDay(base)) {
result.setFullYear(year + 1);
}
return result;
}
function parseKeyword(
word: string,
keywords: { today: string[]; tomorrow: string[]; yesterday: string[]; weekdays: string[][] },
base: Date,
): Date | null {
const w = word.trim().toLowerCase();
if (keywords.today.some((k) => k.toLowerCase() === w)) return startOfDay(base);
if (keywords.tomorrow.some((k) => k.toLowerCase() === w)) return addDays(startOfDay(base), 1);
if (keywords.yesterday.some((k) => k.toLowerCase() === w)) return addDays(startOfDay(base), -1);
for (let i = 0; i < keywords.weekdays.length; i++) {
if (keywords.weekdays[i].some((k) => k.toLowerCase() === w)) {
const today = startOfDay(base);
const diff = (i - today.getDay() + 7) % 7 || 7;
return addDays(today, diff);
}
}
return null;
}
export type DateKeywords = {
today: string[];
tomorrow: string[];
yesterday: string[];
/** Index 0=Sunday, 6=Saturday. Each entry: aliases (short + long). */
weekdays: string[][];
};
export function parseDate(
input: ParseInput,
prefs: FormatPrefs,
keywords: DateKeywords,
baseDate: Date = new Date(),
): Date | null {
if (!input) return null;
const trimmed = input.trim();
if (!trimmed) return null;
const rel = trimmed.match(RE_REL);
if (rel) {
const sign = rel[1] === '-' ? -1 : 1;
const n = sign * parseInt(rel[2], 10);
const unit = rel[3].toLowerCase();
const base = startOfDay(baseDate);
if (unit === 'd') return addDays(base, n);
if (unit === 'w') return addDays(base, n * 7);
if (unit === 'm') return addMonths(base, n);
}
const kw = parseKeyword(trimmed, keywords, baseDate);
if (kw) return kw;
const num = parseNumeric(trimmed, prefs, baseDate);
if (num) return num;
return null;
}
export function parseDateRange(
input: ParseInput,
prefs: FormatPrefs,
keywords: DateKeywords,
baseDate: Date = new Date(),
): { from: Date; to?: Date } | null {
if (!input) return null;
const parts = input.split(/\s*(?:-{1,2}||to)\s*/i);
if (parts.length === 1) {
const single = parseDate(parts[0], prefs, keywords, baseDate);
return single ? { from: single } : null;
}
if (parts.length === 2) {
const from = parseDate(parts[0], prefs, keywords, baseDate);
if (!from) return null;
const to = parseDate(parts[1], prefs, keywords, from);
if (!to) return null;
return { from, to };
}
return null;
}