Natural Dates
Natural-language dates — 'tomorrow', '10 hours ago', 'next friday at 10am' — are accepted by zdate() as one more string format, a peer to ISO 8601 and RFC 9557. They are parsed by a small handwritten grammar, not keyword matching.
Because natural dates are inherently relative, they are always anchored to now; they never operate on a pre-existing instant.
import {zdate, ZEpoch} from '@sajajs/zdate'
zdate('tomorrow at noon')
zdate('first friday of month 9am')
zdate('2 hours ago')
ZEpoch.parse('next business day') // epoch bigint instead of a ZDateWhat the Grammar Accepts
Filler words (a, an, the, on, at, and, to) are stripped before parsing. All terms match case-insensitively, and common abbreviations (jan, mon, hr) and ordinals (1st, first) are folded in.
| Production | Examples |
|---|---|
| Anchor | 'now' |
| Relative day | 'today', 'tomorrow noon', 'yesterday 10:30pm' |
| Relative weekday | 'this friday', 'next fri at 10am', 'last monday' |
| Relative unit | 'next day', 'previous month', 'next minute' |
| Year selector | 'next leap year', 'last leap yr at midnight' |
| Ordinal weekday | 'first friday of month', 'fifth monday of month 9am' |
| Business day | 'next business day', 'last business day 9am' |
| Absolute date | 'jan 15 9am', 'dec 31 23:59' |
| Time only | 'noon', 'midnight', '10am', '14:30' |
| Duration step | '2 hours ago', 'in 10 minutes', '1 day from now', '2 fridays ago' |
Time forms include 'noon', 'midnight', '10am', '10:30', '10:30:45', and fractional seconds down to nanoseconds.
Direction Semantics
Weekday directions are precise, not fuzzy:
this X— X of the current ISO week (Monday–Sunday)next X— X of the next ISO week, always +7 days fromthis Xprevious X/last X— the most recent past X (1–7 days back)
Formal Grammar
The complete grammar in W3C-EBNF. It is written over normalized tokens: filler words are stripped, terminals match case-insensitively, and abbreviations and ordinals are folded into their terminals.
/* zdate natural date grammar (W3C-EBNF) */
natural-date ::= target | duration-step
target ::= anchor
| absolute-date
| relative-unit
| relative-weekday
| relative-year-selector
| ordinal-weekday
| relative-day
| business-day
| time-only
anchor ::= NOW
absolute-date ::= MONTH DAY time
relative-unit ::= (NEXT | PREVIOUS) date-unit time?
| (NEXT | PREVIOUS) time-unit
relative-day ::= (TODAY | TOMORROW | YESTERDAY) time?
time-only ::= time
relative-weekday ::= direction WEEKDAY time?
relative-year-selector ::= (NEXT | PREVIOUS) year-selector time?
year-selector ::= LEAP YEAR_UNIT
ordinal-weekday ::= ORDINAL WEEKDAY OF (MONTH_UNIT | YEAR_UNIT) time?
business-day ::= (NEXT | PREVIOUS) BUSINESS DAY_UNIT time?
duration-step ::= duration AGO
| duration FROM NOW
| IN duration
| weekday-count AGO
| weekday-count FROM NOW
duration ::= duration-part duration-part*
/* When an amount is omitted, the amount is 1; for example, "a day ago" normalizes to "day ago". */
duration-part ::= NUMBER fractional-duration-unit
| INTEGER integer-duration-unit
| duration-unit
fractional-duration-unit ::= DAY_UNIT
| WEEK_UNIT
| MONTH_UNIT
| YEAR_UNIT
| HOUR_UNIT
| MINUTE_UNIT
| SECOND_UNIT
| MILLISECOND_UNIT
integer-duration-unit ::= MICROSECOND_UNIT
| NANOSECOND_UNIT
weekday-count ::= INTEGER WEEKDAY
direction ::= THIS | NEXT | PREVIOUS
unit ::= date-unit | time-unit
date-unit ::= DAY_UNIT
| WEEK_UNIT
| MONTH_UNIT
| ISO_QUARTER_UNIT
| YEAR_UNIT
time-unit ::= HOUR_UNIT
| MINUTE_UNIT
| SECOND_UNIT
| MILLISECOND_UNIT
| MICROSECOND_UNIT
| NANOSECOND_UNIT
duration-unit ::= DAY_UNIT
| WEEK_UNIT
| MONTH_UNIT
| YEAR_UNIT
| HOUR_UNIT
| MINUTE_UNIT
| SECOND_UNIT
| MILLISECOND_UNIT
| MICROSECOND_UNIT
| NANOSECOND_UNIT
time ::= TIME
TIME ::= NOON | MIDNIGHT | clock-time
clock-time ::= HH (":" MM (":" SS ("." FRACTIONAL)?)?)? MERIDIEM?
HH ::= DIGIT DIGIT?
MM ::= DIGIT DIGIT
SS ::= DIGIT DIGIT
FRACTIONAL ::= DIGIT DIGIT*
NOON ::= "noon"
MIDNIGHT ::= "midnight"
/* When MERIDIEM is omitted, clock-time is 24-hour and interpreted in the selected ZDate time zone (not UTC). */
MERIDIEM ::= "am" | "pm" | "a.m." | "p.m."
ORDINAL_WORD ::= "first" | "second" | "third" | "fourth" | "fifth"
NUMBER ::= INTEGER ("." DIGIT DIGIT*)?
DAY ::= INTEGER ORDINAL_SUFFIX?
ORDINAL ::= INTEGER ORDINAL_SUFFIX | ORDINAL_WORD
ORDINAL_SUFFIX ::= "st" | "nd" | "rd" | "th"
INTEGER ::= DIGIT DIGIT*
DIGIT ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
DAY_UNIT ::= "day" | "days"
WEEK_UNIT ::= "week" | "weeks" | "wk" | "wks"
MONTH_UNIT ::= "month" | "months" | "mo" | "mos"
ISO_QUARTER_UNIT ::= "iso"? ("quarter" | "quarters" | "qtr" | "qtrs")
YEAR_UNIT ::= "year" | "years" | "yr" | "yrs"
HOUR_UNIT ::= "hour" | "hours" | "hr" | "hrs"
MINUTE_UNIT ::= "minute" | "minutes" | "min" | "mins"
SECOND_UNIT ::= "second" | "seconds" | "sec" | "secs"
MILLISECOND_UNIT ::= "millisecond" | "milliseconds" | "ms"
MICROSECOND_UNIT ::= "microsecond" | "microseconds" | "us" | GREEK_MU "s" | MICRO_SIGN "s"
GREEK_MU ::= U+03BC
MICRO_SIGN ::= U+00B5
NANOSECOND_UNIT ::= "nanosecond" | "nanoseconds" | "ns"
MONTH ::= "jan" | "january"
| "feb" | "february"
| "mar" | "march"
| "apr" | "april"
| "may"
| "jun" | "june"
| "jul" | "july"
| "aug" | "august"
| "sep" | "september"
| "oct" | "october"
| "nov" | "november"
| "dec" | "december"
/* singular/plural can be used in any context */
WEEKDAY ::= "mon" | "monday" | "mondays"
| "tue" | "tuesday" | "tuesdays"
| "wed" | "wednesday" | "wednesdays"
| "thu" | "thursday" | "thursdays"
| "fri" | "friday" | "fridays"
| "sat" | "saturday" | "saturdays"
| "sun" | "sunday" | "sundays"
NOW ::= "now"
TODAY ::= "today"
TOMORROW ::= "tomorrow"
YESTERDAY ::= "yesterday"
THIS ::= "this"
NEXT ::= "next"
PREVIOUS ::= "previous" | "last"
LEAP ::= "leap"
BUSINESS ::= "business"
AGO ::= "ago"
FROM ::= "from"
OF ::= "of"
IN ::= "in"