204 lines
5.5 KiB
JavaScript
204 lines
5.5 KiB
JavaScript
// ─── parseDate (C# DateTime.ParseExact style) ───
|
||
// Parses a date string using an exact format pattern.
|
||
// Supported tokens:
|
||
// yyyy — 4-digit year
|
||
// yy — 2-digit year (assumes 2000+)
|
||
// MM — 2-digit month (01–12)
|
||
// M — 1 or 2-digit month
|
||
// dd — 2-digit day (01–31)
|
||
// d — 1 or 2-digit day
|
||
// HH — 2-digit hour 24h (00–23)
|
||
// H — 1 or 2-digit hour 24h
|
||
// hh — 2-digit hour 12h (01–12)
|
||
// h — 1 or 2-digit hour 12h
|
||
// mm — 2-digit minute (00–59)
|
||
// m — 1 or 2-digit minute
|
||
// ss — 2-digit second (00–59)
|
||
// s — 1 or 2-digit second
|
||
// fff — 3-digit milliseconds
|
||
// ff — 2-digit (tenths + hundredths)
|
||
// f — 1-digit (tenths)
|
||
// tt — AM/PM
|
||
// t — A/P
|
||
// Z — literal 'Z' (UTC marker)
|
||
// K — timezone offset (+HH:mm, -HH:mm, or Z)
|
||
//
|
||
// All other characters are matched literally.
|
||
// Returns Date on success, null on failure.
|
||
|
||
const TOKEN_ORDER = [
|
||
"yyyy", "yy",
|
||
"MM", "M",
|
||
"dd", "d",
|
||
"HH", "H", "hh", "h",
|
||
"mm", "m",
|
||
"ss", "s",
|
||
"fff", "ff", "f",
|
||
"tt", "t",
|
||
"K", "Z",
|
||
];
|
||
|
||
const TOKEN_PATTERNS = {
|
||
yyyy: "(\\d{4})",
|
||
yy: "(\\d{2})",
|
||
MM: "(\\d{2})",
|
||
M: "(\\d{1,2})",
|
||
dd: "(\\d{2})",
|
||
d: "(\\d{1,2})",
|
||
HH: "(\\d{2})",
|
||
H: "(\\d{1,2})",
|
||
hh: "(\\d{2})",
|
||
h: "(\\d{1,2})",
|
||
mm: "(\\d{2})",
|
||
m: "(\\d{1,2})",
|
||
ss: "(\\d{2})",
|
||
s: "(\\d{1,2})",
|
||
fff: "(\\d{3})",
|
||
ff: "(\\d{2})",
|
||
f: "(\\d{1})",
|
||
tt: "(AM|PM|am|pm)",
|
||
t: "([AaPp])",
|
||
K: "(Z|[+-]\\d{2}:\\d{2})",
|
||
Z: "(Z)",
|
||
};
|
||
|
||
function buildFormatRegex(format) {
|
||
const groups = [];
|
||
let regexStr = "";
|
||
let i = 0;
|
||
|
||
while (i < format.length) {
|
||
let matched = false;
|
||
for (const token of TOKEN_ORDER) {
|
||
if (format.startsWith(token, i)) {
|
||
groups.push(token);
|
||
regexStr += TOKEN_PATTERNS[token];
|
||
i += token.length;
|
||
matched = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!matched) {
|
||
// Escape literal character
|
||
regexStr += format[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
i++;
|
||
}
|
||
}
|
||
|
||
return { regex: new RegExp("^" + regexStr + "$"), groups };
|
||
}
|
||
|
||
export function parseDate(input, format) {
|
||
if (input == null || format == null) return null;
|
||
if (input instanceof Date) return input;
|
||
|
||
const str = String(input);
|
||
const { regex, groups } = buildFormatRegex(format);
|
||
const match = str.match(regex);
|
||
if (!match) return null;
|
||
|
||
let year = 0, month = 1, day = 1;
|
||
let hour = 0, minute = 0, second = 0, ms = 0;
|
||
let isPM = false, isAM = false;
|
||
let tzOffset = null; // minutes
|
||
|
||
for (let g = 0; g < groups.length; g++) {
|
||
const token = groups[g];
|
||
const val = match[g + 1];
|
||
|
||
switch (token) {
|
||
case "yyyy": year = parseInt(val, 10); break;
|
||
case "yy": year = 2000 + parseInt(val, 10); break;
|
||
case "MM":
|
||
case "M": month = parseInt(val, 10); break;
|
||
case "dd":
|
||
case "d": day = parseInt(val, 10); break;
|
||
case "HH":
|
||
case "H": hour = parseInt(val, 10); break;
|
||
case "hh":
|
||
case "h": hour = parseInt(val, 10); break;
|
||
case "mm":
|
||
case "m": minute = parseInt(val, 10); break;
|
||
case "ss":
|
||
case "s": second = parseInt(val, 10); break;
|
||
case "fff": ms = parseInt(val, 10); break;
|
||
case "ff": ms = parseInt(val, 10) * 10; break;
|
||
case "f": ms = parseInt(val, 10) * 100; break;
|
||
case "tt": isPM = val.toUpperCase() === "PM"; isAM = val.toUpperCase() === "AM"; break;
|
||
case "t": isPM = val.toUpperCase() === "P"; isAM = val.toUpperCase() === "A"; break;
|
||
case "Z": tzOffset = 0; break;
|
||
case "K":
|
||
if (val === "Z") {
|
||
tzOffset = 0;
|
||
} else {
|
||
const sign = val[0] === "+" ? 1 : -1;
|
||
const [h, m] = val.substring(1).split(":").map(Number);
|
||
tzOffset = sign * (h * 60 + m);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 12h → 24h conversion
|
||
if (isPM && hour < 12) hour += 12;
|
||
if (isAM && hour === 12) hour = 0;
|
||
|
||
if (tzOffset !== null) {
|
||
// Build UTC date, apply offset
|
||
const utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
|
||
return new Date(utcMs - tzOffset * 60000);
|
||
}
|
||
|
||
return new Date(year, month - 1, day, hour, minute, second, ms);
|
||
}
|
||
|
||
export function formatDate(date, spec) {
|
||
const pad = (n, l = 2) => String(n).padStart(l, "0");
|
||
const tokens = {
|
||
yyyy: date.getFullYear(),
|
||
yy: String(date.getFullYear()).slice(-2),
|
||
MM: pad(date.getMonth() + 1),
|
||
M: date.getMonth() + 1,
|
||
dd: pad(date.getDate()),
|
||
d: date.getDate(),
|
||
HH: pad(date.getHours()),
|
||
H: date.getHours(),
|
||
hh: pad(date.getHours() % 12 || 12),
|
||
h: date.getHours() % 12 || 12,
|
||
mm: pad(date.getMinutes()),
|
||
m: date.getMinutes(),
|
||
ss: pad(date.getSeconds()),
|
||
s: date.getSeconds(),
|
||
fff: pad(date.getMilliseconds(), 3),
|
||
};
|
||
let result = spec;
|
||
for (const [tok, val] of Object.entries(tokens).sort(
|
||
(a, b) => b[0].length - a[0].length,
|
||
)) {
|
||
result = result.replaceAll(tok, String(val));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ─── Input transform presets ───
|
||
|
||
export const IntInput = { transform: parseInt };
|
||
export const FloatInput = { transform: parseFloat };
|
||
export const BoolInput = {
|
||
transform: (v) => {
|
||
if (typeof v === "boolean") return v;
|
||
if (typeof v === "string") {
|
||
if (v.toLowerCase() === "true") return true;
|
||
if (v.toLowerCase() === "false") return false;
|
||
}
|
||
return Boolean(v);
|
||
},
|
||
};
|
||
|
||
export function DateInput(format) {
|
||
return {
|
||
transform: (v) => parseDate(v, format),
|
||
};
|
||
}
|
||
|
||
export const IsoDateInput = DateInput("yyyy-MM-ddTHH:mm:ssK"); |