Skip to content

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.

ts
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 ZDate

What 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.

ProductionExamples
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 from this X
  • previous 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.

bnf
/* 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"