Validate dates from web pages
Validate calendar dates and explicit-offset timestamps without timezone guesses, with a strict Node.js example, real failure cases and fifteen tests.
By Scraipe editorial. Checked 2026-10-20.
Decide whether the field is a date or an instant
A calendar date and a timestamp answer different questions. A release date such as 2026-10-20 identifies a day. An observation such as 2026-10-20T00:30:00+11:00 identifies an instant, which is 2026-10-19T13:30:00.000Z in UTC. Do not add midnight UTC to a date-only field just to make every field look like a timestamp.
Choose the field's meaning before parsing. Preserve the original text and source evidence, validate every component, then normalize only accepted values. Ambiguous dates, missing offsets and impossible calendar days should produce review cases rather than guessed values.
This guide provides a zero-dependency Node.js validator and fifteen tests. The examples are invented local fixtures, tested with Node 22.23.2 on 20 October 2026. They make no network calls and consume no Scraipe credits. They implement a deliberately narrow date profile, not a complete natural-language or RFC 3339 parser.
Keep the source context with the value
Capture the raw date text, source URL, field or selector, record identity and capture time separately. If the page has a machine-readable datetime attribute, inspect it alongside the visible text. A valid attribute can still describe the wrong event or a stale value. Never confuse a product's release date, an article's update date and your own collection timestamp.
Choose kind: "date" for a day without a time. Choose kind: "instant" only when the input supplies a full time and explicit numeric UTC offset or Z. These are caller decisions; the example does not infer them from page language or a site's country.
| Observed value | Intended field | Result |
|---|---|---|
2026-10-20 | Calendar date | Keep 2026-10-20 |
2026-10-20T00:30:00+11:00 | Instant | Normalize to 2026-10-19T13:30:00.000Z |
03/04/2026 | Either | Review ambiguous order |
2026-02-30 | Calendar date | Review impossible day |
2026-10-20T09:00:00 | Instant | Review missing offset |
| Explicit null | Either | Record missing, without guessing why |
An omitted batch field is an input-contract error; explicit null is a recorded missing value. Neither proves that an event has no date. Check loading, selectors and source changes before treating absence as a business fact. The missing-field validation guide explains this upstream distinction.
Validate before using JavaScript Date
MDN documents that nonstandard Date.parse formats can vary by implementation and machine timezone. Its date-only ISO example implies UTC, while a date-time without a timezone uses local time. Some impossible dates can also roll into a later month. A finite timestamp is therefore not enough to establish that the source date was valid.
The downloadable validator first checks a complete regular-expression match and Gregorian month lengths, including the century leap-year rule: 2000 has a 29 February; 1900 and 2100 do not. It then validates clock and offset ranges before using UTC setters to construct an instant. setUTCFullYear avoids the special 1900-based interpretation that Date.UTC applies to numeric years from 0 through 99.
The accepted profiles are deliberately explicit:
- Dates use exactly
YYYY-MM-DD, with years 0001 through 9999 and a real calendar day. - Instants use uppercase
TandZ, seconds, and eitherZor a signedHH:MMoffset. An optional fraction has one to three digits. Numeric offset hours are 00–23 and minutes 00–59, matching RFC 3339's grammar; acceptance does not prove an offset belongs to a real regional timezone. - Clock hours are 00–23, minutes 00–59 and seconds 00–59. Leap-second notation,
24:00, lowercase separators and more than three fractional digits go to review. RFC 3339 permits some forms this teaching profile excludes. - Whitespace is not silently trimmed. Slashes, named months, epoch numbers and relative phrases such as “yesterday” are unsupported.
RFC 3339 gives -00:00 a special meaning: the UTC time is known but the local offset is unknown. This profile sends it to review so that normalization cannot erase that distinction. Z and +00:00 are accepted. This is a policy choice, not a claim that -00:00 is invalid RFC 3339.
Run the complete validator
Download these three files into one fresh directory:
With Node 22 or later, no package installation is needed:
bash
node --test validate-date.test.mjs
node validate-date.mjs dates.fixture.jsonExpect fifteen passing tests. The fixture report contains three valid values, one missing value and zero review cases. One input is:
json
{ "dateText": "2026-10-20T00:30:00+11:00", "kind": "instant" }The corresponding result, apart from its row index, is:
json
{
"raw": "2026-10-20T00:30:00+11:00",
"kind": "instant",
"policyVersion": "date-profile-v1",
"status": "valid",
"normalized": "2026-10-19T13:30:00.000Z",
"reason": null
}Keep raw and kind alongside the normalized value. The row index identifies a fixture position, not a durable record key. Use your own product or event identity when joining results to stored records.
Exit 0 means no review rows, even if a field is missing. Exit 1 means at least one value needs review and the report is still printed. Exit 2 means malformed JSON, unreadable input, an invalid batch contract or a resource limit. Consumers must check both the exit code and each row's status; null normalized values are not timestamps.
Tests cover month boundaries, leap centuries, year 0099, UTC day/year crossings, unknown offsets, precision, input types, missing fields and all three CLI exit paths. The same fixture runs in subprocesses configured for UTC, Sydney and New York and must produce identical output. That verifies independence from the machine timezone for this fixture; it does not establish correct regional timezone rules for every source.
Do not invent a timezone from a wall clock
An explicit offset locates an instant. A regional zone describes rules that can change over time. The strings 2026-11-01T01:30:00-04:00 and 2026-11-01T01:30:00-05:00 normalize to different instants despite the same wall-clock text. The example accepts both numeric offsets without deciding which one applies to a particular location.
For a source that only supplies local time, obtain an authoritative zone and define how your application handles repeated or nonexistent times. Use a timezone-aware implementation with maintained timezone data and explicit disambiguation tests. Do not append the server's current offset: the event may occur under different rules. A date-only value needs no such conversion.
The validator also rejects normalization outside UTC years 0001–9999. An accepted local year near either boundary can cross it after applying the offset. Fractional precision beyond milliseconds goes to review instead of being silently truncated.
Preserve raw dates through extraction
In your extraction schema, keep an explicit raw string field when you need to audit date parsing. A description requesting an ISO timestamp does not prove the source timezone or calendar validity. Inspect representative output against the actual source, and check the API response's success, truncation and continuation metadata before processing the batch.
The current Scraipe selector interpreter's toDate transform uses JavaScript new Date and returns an ISO string when parsing succeeds. A local source probe under Node 22 converted 2026-02-30 to 2026-03-02T00:00:00.000Z; it also converted the date-only 2026-10-20 to midnight UTC. These observations explain why a generic transform is not this guide's strict validator. They are local source tests, not a completed hosted extraction or a guarantee about every generated extractor.
Apply the reviewed date policy after preserving the raw value. Consult the API reference and request receipt guide for hosted calls and charges. The local validation example is free of API usage; fetching, compilation or repair through Scraipe can incur charges.
Keep review cases actionable
The example limits a batch to 1–1,000 rows, strings to 100 characters and CLI files to 1 MiB after reading them into memory. It is a teaching batch utility, not a streaming importer or hostile-input isolation layer. Duplicate JSON member names are not detected. It does not validate event chronology, age limits, business calendars or a relationship between an offset and a named zone.
Retain the rejected raw value, policy version and reason. Investigate whether the source needs a distinct documented format, the extractor selected the wrong field, or the event genuinely lacks a date. Add a new format only with source evidence and regression cases; broadening parsing until an ambiguous string succeeds hides the problem rather than resolving it.