ovos-date-parser

September 6, 2026 · View on GitHub

Multilingual parsing, extraction, and formatting of human date, time, and duration expressions. The library is a two-way bridge between machine timestamps and the way people speak and write about time.

  • Text to datetime: pull a datetime out of "next friday at 3pm" or "amanhã às 15h30", and keep the leftover words.
  • Text to duration: turn "two hours and thirty minutes" into a timedelta.
  • Datetime to speech: render 2024-01-05 15:30 as "January fifth twenty twenty four at half past three".
  • Dozens of languages, resolved by BCP-47 code, with an automatic dateparser fallback for the rest.

The library powers date and time understanding in OpenVoiceOS. It is a plain Python library with no voice-assistant dependency at runtime. Use it in an NER pipeline, a TTS front-end, an ASR post-processor, or a scheduling, calendar, or logging tool.

Installation

pip install ovos-date-parser
# or
uv pip install ovos-date-parser

30-second quickstart

from datetime import datetime
from ovos_date_parser import extract_datetime, extract_duration, nice_time, nice_duration

# 1. text -> datetime (+ the words left over)
when, leftover = extract_datetime("lets meet next friday at 8am", "en",
                                  anchorDate=datetime(2024, 1, 5))
print(when)      # 2024-01-12 08:00:00
print(leftover)  # 'lets meet'

# 2. text -> timedelta
delta, leftover = extract_duration("set a timer for 5 minutes", "en")
print(delta)     # 0:05:00

# 3. datetime -> speakable words
print(nice_time(datetime(2024, 1, 5, 15, 30), "en"))   # 'half past three'
print(nice_duration(3690, "en"))                       # 'one hour one minute thirty seconds'

Every snippet above and in examples/ runs with nothing installed but this package.

Use it outside OVOS

The same handful of functions solve everyday text and speech problems that have nothing to do with voice assistants.

Temporal entity extraction (NER)

Tag dates, times, and durations in free text, and keep the non-temporal remainder. This helps with log mining, ticket triage, and note-taking apps.

from datetime import datetime
from ovos_date_parser import extract_datetime

text = "call the supplier next monday at 2pm about the delayed order"
when, rest = extract_datetime(text, "en", anchorDate=datetime(2024, 1, 5))
# when -> 2024-01-08 14:00, rest -> 'call supplier delayed order'

anchorDate is the "now" that relative phrases resolve against. Pass a fixed value for reproducible extraction, or datetime.now() for live use. See examples/ner_temporal.py.

TTS normalization

Speech engines mangle raw digits. Normalize a timestamp to words before synthesis:

from datetime import datetime
from ovos_date_parser import nice_date, nice_time

dt = datetime(2024, 1, 5, 15, 30)
spoken = f"{nice_date(dt, 'en')} at {nice_time(dt, 'en')}"
# 'friday, january fifth, twenty twenty four at half past three'

See examples/tts_normalization.py.

ASR post-processing

Speech-to-text emits words, but downstream logic needs structure. Convert a transcript into a real datetime and an action payload:

from datetime import datetime
from ovos_date_parser import extract_datetime

utterance = "remind me next tuesday at nine thirty to water the plants"
when, action = extract_datetime(utterance, "en", anchorDate=datetime(2024, 1, 5))
# when -> 2024-01-09 09:30, action -> 'remind me to water plants'

See examples/asr_postproc.py.

In an OVOS skill vs. standalone

The library behaves the same way in both settings. Only the caller changes.

# In an OVOS skill: language and anchor come from the session
when, _ = extract_datetime(utterance, self.lang)

# Standalone scheduler / calendar / cron generator: you supply them
when, _ = extract_datetime(user_text, "en", anchorDate=datetime.now())

Core API

FunctionDirectionPurpose
extract_datetime(text, lang, anchorDate=None, default_time=None)text to datetimeDate/time from a phrase, plus leftover text
extract_duration(text, lang, *, resolution=..., replace_token="")text to durationtimedelta/relativedelta/float, plus leftover text
extract_datetime_spans(text, lang, anchor_date=None, default_time=None)text to spansEvery date/time expression with its code-point offsets
extract_duration_spans(text, lang)text to spansEvery duration with its code-point offsets
nice_time(dt, lang, speech=True, use_24hour=False, use_ampm=False, variant=None)datetime to textSpeakable or digit clock time
nice_date(dt, lang, now=None, include_weekday=True)datetime to textSpeakable date, shortened against now
nice_date_time(dt, lang, now=None, use_24hour=False, use_ampm=False)datetime to textDate and time combined
nice_day / nice_weekday / nice_month / nice_yeardatetime to textIndividual date components
nice_span(span, lang="en-us")DateSpan to textLabel for a span, at the granularity its width carries
nice_duration(duration, lang, speech=True)seconds/timedelta to textSpeakable timespan
nice_relative_time(when, relative_to=None, lang="en-us")datetime to textShort "N minutes/days" phrase
get_date_strings(dt, lang, date_format=None, time_format="full")datetime to dictDisplay strings for GUI clients

Full signatures, return shapes, and examples: docs/api.md.

Span-native formatting

A date phrase refers to a stretch of time, not to a single instant. "July 2026" is a whole month, and "the 1980s" is a whole decade. extract_timespan returns that stretch as a DateSpan, a half-open [start, end) interval whose two endpoints are AstroDate points. An AstroDate is a datetime that is not capped at years 1 to 9999, so BC and far-future dates work. The width of a span is its precision.

nice_span is the inverse. Give it a DateSpan, and it picks the label from the width. In English a one-day span reads as a date, a month-wide span as a month, a decade as "the 1980s", a century as "the 19th century", and a BC span by its era year. The decade and coarser labels are English-only, apart from Arabic and Hebrew decades of the 20th century: no other language has a construction for them, so nice_span raises NotImplementedError there instead of answering in English.

In English the label round-trips from the day width up, for years from 1000 AD onward and from 32 BC back: what nice_span writes, extract_timespan reads straight back. Nearer the era boundary the year numeral is ambiguous with a day-of-month, and a sub-day span reads as a spoken date and time. Those get a correct label, but not the inverse.

from ovos_date_parser import extract_timespan, nice_span

span, _ = extract_timespan("the 19th century", "en")   # a DateSpan
print(span.start, "..", span.end)   # 1800-01-01 .. 1900-01-01
print(nice_span(span, "en"))        # 'the 19th century'  (round-trips)

for text in ["July 21st, 2026", "July 2026", "2026", "the 1980s", "300 BC"]:
    span, _ = extract_timespan(text, "en")
    assert nice_span(span, "en") == text

The datetime formatters also accept an AstroDate directly. A point that fits a datetime is projected to one, so you can format extracted points without unwrapping them:

from ovos_date_parser import nice_date, nice_time
from ovos_date_parser.astrodate import AstroDate

when = AstroDate(2026, 7, 21, 15, 30)
print(nice_date(when, "en"))   # 'tuesday, july twenty-first, twenty twenty six'
print(nice_time(when, "en"))   # 'half past three'

Other languages get labels in their own words for the day, week, month, and year widths. Arabic and Hebrew also name decades of the 20th century, the only century their bare decade word can express. The round-trip guarantee is for English only. The span grammars for other languages are in the chronologia reckoning core.

Dialects resolve by prefix. "pt-BR", "pt-PT", and "pt" all reach the Portuguese implementation. A language with no implementation raises NotImplementedError, except extract_datetime, which tries the dateparser fallback first, and the nice_* formatters, which fall back to a generic word table and need a locale resource file for the language.

Language support

Twenty-plus languages have dedicated, idiomatic implementations. Extraction for any other language falls back to dateparser. Formatting falls back to a generic word table.

  • Full: dedicated implementation
  • Partial: partial or generic support (a language-agnostic helper or an external library)
  • None: not available, raises NotImplementedError

Parsing

Languageextract_datetimeextract_duration
ar ArabicFullFull
ast AsturianFullFull
az AzerbaijaniFullFull
ca CatalanFullFull
cs CzechFullFull
da DanishFullFull
de GermanFullFull
en EnglishFullFull
es SpanishFullFull
eu BasqueFullFull
fa PersianFullFull
fr FrenchFullFull
gl GalicianPartialFull
hu HungarianPartialFull
it ItalianFullFull
kab KabyleFullFull
nb Norwegian BokmålFullFull
nl DutchFullFull
nn Norwegian NynorskFullFull
oc OccitanFullFull
pl PolishFullFull
pt PortugueseFullFull
ro RomanianFullFull
ru RussianFullFull
sl SlovenianFullFull
sv SwedishFullFull
uk UkrainianFullFull

Any language not listed uses the dateparser fallback for extract_datetime. This fallback is good at absolute dates and weak at conversational relative phrases. The languages on the shared duration engine (all of the above except ar, ast, fa, kab, sv) also support the resolution and replace_token options of extract_duration. See docs/api.md.

Formatting

Languagenice_date familynice_timenice_durationnice_relative_time
ar ArabicPartialFullFullPartial
ast AsturianFullFullFullPartial
az AzerbaijaniFullFullFullPartial
ca CatalanFullFullFullPartial
cs CzechFullFullFullPartial
da DanishFullFullFullPartial
de GermanFullFullFullPartial
en EnglishFullFullFullPartial
es SpanishFullFullFullPartial
eu BasqueFullFullFullFull
fa PersianFullFullFullPartial
fr FrenchFullFullFullPartial
gl GalicianFullFullFullPartial
hu HungarianFullFullFullPartial
it ItalianFullFullFullPartial
kab KabylePartialFullFullPartial
nb Norwegian BokmålFullFullFullPartial
nl DutchFullFullFullPartial
nn Norwegian NynorskFullFullFullPartial
oc OccitanFullFullFullPartial
pl PolishFullFullFullPartial
pt PortugueseFullFullFullPartial
ro RomanianFullFullFullPartial
ru RussianFullFullFullPartial
sl SlovenianFullNoneFullPartial
sv SwedishFullFullFullPartial
uk UkrainianFullFullFullPartial

nice_relative_time uses a shared implementation for every language except Basque, which has a dedicated one. The shared version is functional but not tuned to each language's idiom.

Per-language quirks (Catalan bell-tower time, Occitan quarter idioms, Romanian "fără un sfert", Kabyle calendar names, Portuguese 15h30 style, and more) are documented in docs/languages.md.

Examples

Runnable, dependency-free scripts live in examples/:

ScriptShows
ner_temporal.pyExtract date/time/duration entities from free text
tts_normalization.pyRender timestamps to speakable words before synthesis
asr_postproc.pyTurn spoken transcripts into structured datetimes
multilingual.pyParse-then-render round trip across many languages
extract.pyMinimal extraction reference
format.pyMinimal formatting reference
python examples/ner_temporal.py

Documentation

License

Apache 2.0.