iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Dates

Date validation by regex is famously tricky — February has fewer days, leap years exist, formats vary by region. The practical answer mirrors emails and URLs: regex for shape, a real date parser (Date.parse, dayjs, chrono) for semantics, and explicit storage in ISO 8601.

Patterns + Date parsers + validation

EXAMPLE
// 1) Why pure regex is the wrong tool
// Regex can't tell you that 2024-02-30 is invalid (Feb has 29 days max).
// Use regex to check SHAPE; use a date library to check SEMANTICS.

// 2) Shape patterns — common formats
const ISO_DATE_RE        = /^\d{4}-\d{2}-\d{2}$/;                          // 2024-01-15
const ISO_DATETIME_RE    = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
const SLASH_DATE_RE      = /^\d{1,2}\/\d{1,2}\/\d{2,4}$/;                  // 1/15/24, 15/1/2024
const DOT_DATE_RE        = /^\d{1,2}\.\d{1,2}\.\d{4}$/;                    // German style 15.1.2024
const US_DATE_RE         = /^(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[12]\d|3[01])\/\d{4}$/;
const EU_DATE_RE         = /^(?:0?[1-9]|[12]\d|3[01])\/(?:0?[1-9]|1[0-2])\/\d{4}$/;

// 3) Validating + parsing — combined helper
function parseIsoDate(input) {
    if (!ISO_DATE_RE.test(input)) return null;
    const d = new Date(input + 'T00:00:00Z');
    if (isNaN(d.getTime())) return null;
    // Confirm round-trip — rejects 2024-02-30 etc.
    if (d.toISOString().slice(0, 10) !== input) return null;
    return d;
}

parseIsoDate('2024-01-15');        // Date
parseIsoDate('2024-02-30');         // null — Feb doesn't have 30 days
parseIsoDate('2024-02-29');         // Date — 2024 is a leap year
parseIsoDate('2023-02-29');         // null — not a leap year

// 4) Leap-year-aware regex (academic exercise — DON'T ship this)
// It's HUGE and still doesn't handle every calendar rule.
//
// const VALID_DATE_PURE_RE = /^(?:(?:(?:1[6-9]|[2-9]\d)\d{2})-(?:(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:02-(?:0[1-9]|1\d|2[0-8])))|(?:(?:(?:1[6-9]|[2-9]\d)(?:0[48]|[2468][048]|[13579][26])|(?:16|[2468][048]|[3579][26])00)-02-29))$/;
//
// Notice the awkward 'is leap year' logic. The lesson: use a parser.

// 5) Practical approach — Date.parse + verification
function parseDateFlexible(input) {
    // dayjs handles many formats; install via npm install dayjs
    // import dayjs from 'dayjs';
    // const d = dayjs(input);
    // if (!d.isValid()) return null;
    // return d.toDate();

    // Vanilla
    const d = new Date(input);
    return isNaN(d.getTime()) ? null : d;
}

// 6) Library choices
// • Day.js — small, immutable, modern
// • date-fns — modular, tree-shakeable
// • Luxon — comprehensive, timezone-aware
// • Moment.js — legacy; avoid for new code
// • chrono-node — natural-language parsing ('next Friday')
// • Native Date — limited but free

// 7) Extracting dates from text
const FIND_ISO_DATES = /\d{4}-\d{2}-\d{2}/g;
'Meet at 2024-01-15 or 2024-02-30 (invalid)'.match(FIND_ISO_DATES);
// ['2024-01-15', '2024-02-30']    — match doesn't validate

// Filter using a real parser
const valid = [];
for (const s of 'Meet at 2024-01-15 or 2024-02-30'.match(FIND_ISO_DATES) ?? []) {
    const d = parseIsoDate(s);
    if (d) valid.push(d);
}

// 8) Common formats reference
// ISO 8601 date:        2024-01-15
// ISO 8601 datetime:    2024-01-15T03:21:00Z   (always UTC with Z)
// RFC 3339 datetime:    2024-01-15T03:21:00+10:00
// RFC 2822 (email):     Mon, 15 Jan 2024 03:21:00 +0000
// ANSI SQL:             2024-01-15 03:21:00
// Unix timestamp:       1705291260

// 9) Time-only patterns
const TIME_24_RE = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/;        // 23:59 or 23:59:59
const TIME_12_RE = /^(?:0?[1-9]|1[0-2]):[0-5]\d(?:\s?[ap]m)?$/i;       // 1:30, 11:59 pm
const HM_24      = /^([01]\d|2[0-3]):([0-5]\d)$/;

TIME_24_RE.test('23:59');         // true
TIME_24_RE.test('24:00');         // false
TIME_12_RE.test('11:59 PM');      // true

// 10) Duration patterns (ISO 8601 duration)
const DURATION_RE = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
DURATION_RE.test('P1Y2M3DT4H5M6S');   // true (1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds)
DURATION_RE.test('PT30M');             // true (30 minutes)
DURATION_RE.test('P3D');                // true (3 days)

// 11) Detection patterns (multiple formats)
function normaliseDate(input) {
    const fmts = [
        { re: /^(\d{4})-(\d{2})-(\d{2})$/,         build: (m) => `${m[1]}-${m[2]}-${m[3]}` },
        { re: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/,    build: (m) => `${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}` },    // d/m/y
        { re: /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/,    build: (m) => `${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}` },
    ];
    for (const f of fmts) {
        const m = input.match(f.re);
        if (m) return parseIsoDate(f.build(m));
    }
    return null;
}

normaliseDate('15/01/2024');    // Date for 2024-01-15
normaliseDate('15.01.2024');
normaliseDate('2024-01-15');

// 12) Timezone handling
// Storing dates as ISO 8601 with timezone (Z or +offset) is unambiguous.
// Naive dates (no timezone) are ambiguous — same string interpreted differently per server.
// For 'date only' (birthday, deadline), store as YYYY-MM-DD; compare without time.

// 13) Real-world tips
// • Accept many formats from users; store ONE canonical (ISO 8601)
// • Display in the user's locale on the client; use Intl.DateTimeFormat
// • Server-side: always work in UTC; convert at the edges
// • For 'date pickers', use a real component (HTML5 input type=date or a library)
// • Don't parse 'free text' dates without a real parser

// 14) UI feedback
function validateDate(input, options = { format: 'iso', minYear: 1900, maxYear: 2100 }) {
    const d = parseIsoDate(input);
    if (!d) return { ok: false, error: 'Use YYYY-MM-DD format' };
    const y = d.getUTCFullYear();
    if (y < options.minYear) return { ok: false, error: `Year must be >= ${options.minYear}` };
    if (y > options.maxYear) return { ok: false, error: `Year must be <= ${options.maxYear}` };
    return { ok: true, value: d };
}

// 15) Common bugs
// • Regex passing 2024-02-30 → silently invalid; round-trip with toISOString
// • US vs EU format ambiguity (1/2/2024 = Jan 2 or Feb 1) → use ISO
// • Year 2-digit (24 vs 1924) → ambiguous; require 4-digit
// • Time zone confusion — store UTC, display local
// • Date arithmetic with new Date(2024, 0, 32) → silently becomes Feb 1; check explicitly
// • Native Date.parse on non-standard strings — works on Chrome, fails on Safari/Firefox
// • Leap year regex DOESN'T cover century rule (1900 not a leap year, 2000 was)
// • Storing dates without timezone → 'this happened on a different day in Sydney vs LA'

Why it matters

Use regex to enforce the shape (^\d{4}-\d{2}-\d{2}$), then a real date parser to verify the day actually exists (round-trip with toISOString). Store ISO 8601 (with timezone where applicable), display in the user’s locale, and reach for Day.js / date-fns / Luxon instead of custom leap-year regex.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/   // ISO YYYY-MM-DD
Try it Yourself »

Discussion

Loading…