feat(date): add parseDate utility with locale-aware parsing
This commit is contained in:
146
src/renderer/lib/parseDate.ts
Normal file
146
src/renderer/lib/parseDate.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user