Parse natural-language dates and times in English, Persian, Arabic, and Chinese - each with its own digit script and calendar (Solar Hijri, lunar Hijri). Locales are data: adding a language is adding tables, never forking a parser.
0
stars
10
commits
Go
primary language
Aug 24, 2026
updated
Natural-language date and time parsing for the whole world. One Go engine parses English, Persian, Arabic, and Chinese - each with its own digit script and calendar (Solar Hijri, lunar Hijri). Adding a language is adding tables (words, numerals, calendar, handler patterns), never forking a parser.
r, err := zaman.Parse("tomorrow at 10:30", nil)
r.Time // 2024-01-16 10:30:00 UTC
zaman takes a natural-language phrase and resolves it to a concrete time range, anchored to an optional reference instant (default: now). It handles the things that break most "English date parsers with a translation layer":
فردا ساعت ۱۰:۳۰ -> tomorrow 10:30, on the Solar Hijri calendar,
with Persian digits and a Saturday week start.بعد أسبوع -> one week from now, on the lunar Hijri calendar, with
Arabic-Indic digits.明天下午三点 -> tomorrow 3pm, from a single maximal-munch pass
over a composed-numeral system with no spaces in sight.next monday, in 2 weeks, december 25th, 10:30 pm,
2024-12-25.Each parse returns the matched substring and its byte offsets, so callers can strip the recognized phrase out of a sentence:
r, _ := zaman.Parse("meet me tomorrow at 10:30 in the lobby", nil)
r.Text // "tomorrow at 10:30"
r.Start // 8
r.End // 25
go get github.com/fishman/zaman
Go 1.25+. The single runtime dependency is golang.org/x/text (Unicode
normalization; see Architecture).
package main
import (
"fmt"
"time"
"github.com/fishman/zaman"
)
func main() {
now := time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC)
// Auto-detect the locale by Unicode script.
r, err := zaman.Parse("فردا ساعت ۱۰:۳۰", &zaman.Options{Now: now})
fmt.Println(r.Time) // 2024-01-16 10:30:00 +0000 UTC
// Force a locale.
r, _ = zaman.ParseIn("下周三", "zh", &zaman.Options{Now: now})
fmt.Println(r.Time) // 2024-01-24 12:00:00 +0000 UTC
}
| Call | Purpose |
|---|---|
Parse(s, opts) | auto-detect the locale, return the best match |
ParseIn(s, lang, opts) | parse with a named locale, no detection |
List() | registered locale names |
Register(l) | add a locale |
Options tune parsing: Now (reference instant), Locale (parse as a
specific locale; empty auto-detects), Location (result timezone; nil = UTC
time.LoadLocation("Asia/Tokyo") so "next tuesday" resolves in that
zone), Guess (middle/begin/end within a span), WeekStart,
AmbiguousTimeRange (when a bare clock like "10:30" may mean PM), Endian
(how 1/2/2024 reads).Result carries Time (guessed point), Span (Begin/End range),
Text/Start/End (matched substring and byte offsets), and Locale
(which locale matched, for the auto-detect path).
| Locale | Script | Digits | Calendar | Week starts | Notes |
|---|---|---|---|---|---|
en | Latin | western | Gregorian | Sunday | reference locale |
fa | Arabic (Persian variant) | Persian + western | Solar Hijri | Saturday | ZWNJ-safe compound words (نیمهشب), noun-first grabbers (هفته آینده) |
ar | Arabic | Arabic-Indic + western | Lunar Hijri (tabular) | Sunday | pointer-first prepositional phrases (بعد أسبوع), compound month names (رمضان) |
zh | CJK | western + composed numerals (三十二) | Gregorian | Monday | space-free trie lexing, 下午三点 portion-first clocks |
ja is designed (same CJK lexer, 万/億 numeral bases, KindEra era
years) but not yet built.
The core is the chronic model (tokenize, tag, handle) with one deliberate change: the locale is a table, not a fork. The pipeline is:
input string
-> normalize locale fold: NFKC, diacritic/tatweel strip, script-variant letters
-> lex space-splitting (Latin/Arabic) OR maximal-munch trie (CJK)
-> tag lexicon tries + number systems -> Token{Kind, Value, Pos, Text}
-> handle ordered kind-pattern handlers mutate a Span
-> resolve Span + Options -> time.Time
KindMonth,
KindClock, KindGrabber, KindScalar, ...) every locale maps into.
Handlers match kinds, so one handler serves any locale that emits the same
kinds. English's [Grabber, Week] handler is Persian's [Week, Grabber]
with the word order swapped as data.星期一 lexes whole rather than as 一 + 月 + 一.三十二 = 3*10+2), and longest-match Any
combos so a locale accepts several digit scripts.time.Date with calendar space. Calendar (Gregorian, Solar Hijri, lunar
Hijri) does all year/month/day conversion, including span arithmetic like
"next month" and year rollover. Persian and Arabic parse correctly on their
own calendars, not on a Gregorian mask.golang.org/x/text NFKC folds Arabic
Presentation Forms A/B and fullwidth forms; tashkeel/tatweel are dropped;
per-locale letter tables handle script conventions NFKC does not (Arabic
ي/ة vs Persian ی/ه).The parse pipeline is pure and immutable: locales compile once at init,
parsing is goroutine-safe, and there is no mutable parser state.
A contributor supplies, in order of effort:
NumberSystem (one digit table, or one
ComposedNumerals table).Gregorian or pick SolarHijri/Islamic.Each locale ships a corpus test (TestFaCorpus, TestZhCorpus, ...) of
fabricated fixtures. Non-trivial logic in the engine carries its own check
(TestIslamicRoundTrip walks every day across a century; the digit tables
and numerals are covered by the corpus cases).
DESIGN.md walks through the architecture; the reference
implementations that shaped it are olebedev/when
(simple public surface, regex-per-language ceiling) and
ruby chronic (the tokenize/tag/handle
grammar model, its English/whitespace/Gregorian assumptions). Both are
advisory; the decision to be a data-driven Go engine is not inherited from
either.
Working: en, fa, ar, zh. ja next. Deferred: lunisolar calendars, relative
repeaters ("every Tuesday"), timezone-name tables.
10 commits
Go
100.0%
Parse natural-language dates and times in English, Persian, Arabic, and Chinese - each with its own digit script and calendar (Solar Hijri, lunar Hijri). Locales are data: adding a language is adding tables, never forking a parser.
0
stars
10
commits
Go
primary language
Aug 24, 2026
updated
Natural-language date and time parsing for the whole world. One Go engine parses English, Persian, Arabic, and Chinese - each with its own digit script and calendar (Solar Hijri, lunar Hijri). Adding a language is adding tables (words, numerals, calendar, handler patterns), never forking a parser.
r, err := zaman.Parse("tomorrow at 10:30", nil)
r.Time // 2024-01-16 10:30:00 UTC
zaman takes a natural-language phrase and resolves it to a concrete time range, anchored to an optional reference instant (default: now). It handles the things that break most "English date parsers with a translation layer":
فردا ساعت ۱۰:۳۰ -> tomorrow 10:30, on the Solar Hijri calendar,
with Persian digits and a Saturday week start.بعد أسبوع -> one week from now, on the lunar Hijri calendar, with
Arabic-Indic digits.明天下午三点 -> tomorrow 3pm, from a single maximal-munch pass
over a composed-numeral system with no spaces in sight.next monday, in 2 weeks, december 25th, 10:30 pm,
2024-12-25.Each parse returns the matched substring and its byte offsets, so callers can strip the recognized phrase out of a sentence:
r, _ := zaman.Parse("meet me tomorrow at 10:30 in the lobby", nil)
r.Text // "tomorrow at 10:30"
r.Start // 8
r.End // 25
go get github.com/fishman/zaman
Go 1.25+. The single runtime dependency is golang.org/x/text (Unicode
normalization; see Architecture).
package main
import (
"fmt"
"time"
"github.com/fishman/zaman"
)
func main() {
now := time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC)
// Auto-detect the locale by Unicode script.
r, err := zaman.Parse("فردا ساعت ۱۰:۳۰", &zaman.Options{Now: now})
fmt.Println(r.Time) // 2024-01-16 10:30:00 +0000 UTC
// Force a locale.
r, _ = zaman.ParseIn("下周三", "zh", &zaman.Options{Now: now})
fmt.Println(r.Time) // 2024-01-24 12:00:00 +0000 UTC
}
| Call | Purpose |
|---|---|
Parse(s, opts) | auto-detect the locale, return the best match |
ParseIn(s, lang, opts) | parse with a named locale, no detection |
List() | registered locale names |
Register(l) | add a locale |
Options tune parsing: Now (reference instant), Locale (parse as a
specific locale; empty auto-detects), Location (result timezone; nil = UTC
time.LoadLocation("Asia/Tokyo") so "next tuesday" resolves in that
zone), Guess (middle/begin/end within a span), WeekStart,
AmbiguousTimeRange (when a bare clock like "10:30" may mean PM), Endian
(how 1/2/2024 reads).Result carries Time (guessed point), Span (Begin/End range),
Text/Start/End (matched substring and byte offsets), and Locale
(which locale matched, for the auto-detect path).
| Locale | Script | Digits | Calendar | Week starts | Notes |
|---|---|---|---|---|---|
en | Latin | western | Gregorian | Sunday | reference locale |
fa | Arabic (Persian variant) | Persian + western | Solar Hijri | Saturday | ZWNJ-safe compound words (نیمهشب), noun-first grabbers (هفته آینده) |
ar | Arabic | Arabic-Indic + western | Lunar Hijri (tabular) | Sunday | pointer-first prepositional phrases (بعد أسبوع), compound month names (رمضان) |
zh | CJK | western + composed numerals (三十二) | Gregorian | Monday | space-free trie lexing, 下午三点 portion-first clocks |
ja is designed (same CJK lexer, 万/億 numeral bases, KindEra era
years) but not yet built.
The core is the chronic model (tokenize, tag, handle) with one deliberate change: the locale is a table, not a fork. The pipeline is:
input string
-> normalize locale fold: NFKC, diacritic/tatweel strip, script-variant letters
-> lex space-splitting (Latin/Arabic) OR maximal-munch trie (CJK)
-> tag lexicon tries + number systems -> Token{Kind, Value, Pos, Text}
-> handle ordered kind-pattern handlers mutate a Span
-> resolve Span + Options -> time.Time
KindMonth,
KindClock, KindGrabber, KindScalar, ...) every locale maps into.
Handlers match kinds, so one handler serves any locale that emits the same
kinds. English's [Grabber, Week] handler is Persian's [Week, Grabber]
with the word order swapped as data.星期一 lexes whole rather than as 一 + 月 + 一.三十二 = 3*10+2), and longest-match Any
combos so a locale accepts several digit scripts.time.Date with calendar space. Calendar (Gregorian, Solar Hijri, lunar
Hijri) does all year/month/day conversion, including span arithmetic like
"next month" and year rollover. Persian and Arabic parse correctly on their
own calendars, not on a Gregorian mask.golang.org/x/text NFKC folds Arabic
Presentation Forms A/B and fullwidth forms; tashkeel/tatweel are dropped;
per-locale letter tables handle script conventions NFKC does not (Arabic
ي/ة vs Persian ی/ه).The parse pipeline is pure and immutable: locales compile once at init,
parsing is goroutine-safe, and there is no mutable parser state.
A contributor supplies, in order of effort:
NumberSystem (one digit table, or one
ComposedNumerals table).Gregorian or pick SolarHijri/Islamic.Each locale ships a corpus test (TestFaCorpus, TestZhCorpus, ...) of
fabricated fixtures. Non-trivial logic in the engine carries its own check
(TestIslamicRoundTrip walks every day across a century; the digit tables
and numerals are covered by the corpus cases).
DESIGN.md walks through the architecture; the reference
implementations that shaped it are olebedev/when
(simple public surface, regex-per-language ceiling) and
ruby chronic (the tokenize/tag/handle
grammar model, its English/whitespace/Gregorian assumptions). Both are
advisory; the decision to be a data-driven Go engine is not inherited from
either.
Working: en, fa, ar, zh. ja next. Deferred: lunisolar calendars, relative
repeaters ("every Tuesday"), timezone-name tables.
10 commits
Go
100.0%