Skip to main content

Natural-language date/time & event parser for Russian (and English)

Project description

TimeSense

Languages: English · Русский

CI PyPI Python Dependencies No ML License: MIT

Natural-language date/time & event parser for Russian and English.

TimeSense turns phrases like "tomorrow at 10 meeting", "from 9 to 5 work", or "every Monday standup" — and their Russian equivalents — into structured event objects, ready to export as JSON, an iCalendar rrule, or a full .ics file. It is pure Python, has no required dependencies, and is designed for turning voice/ASR transcripts into structured dates.

There is no machine learning inside — the engine is dictionaries and regular expressions. It needs no GPU, downloads no models, and runs comfortably on the cheapest VPS, a Raspberry-Pi-class board, or inside a serverless function: ~0.2 s cold start, a few MB of RAM, and under a millisecond per phrase (~1,000+ phrases/sec on a single modest core). That makes it practical to call inline on every utterance of a live transcription pipeline — speech-to-text output goes in, calendar-ready events come out.

Footprint
Required dependencies 0 — pure Python 3.9–3.13, stdlib only
ML models / GPU none — dictionaries & regular expressions
Cold start ~0.2 s (interpreter + import)
Memory a few MB
Latency <1 ms per phrase — 1,000+ phrases/sec on a single modest core
Deployment cheapest VPS · Raspberry-Pi-class boards · serverless
from datetime import datetime
from timesense import TimeSenseParser

parser = TimeSenseParser()
r = parser.parse("tomorrow at 10 meeting", now=datetime(2026, 2, 14, 14, 0))

type(r).__name__     # 'ReminderResult'
r.title              # 'meeting'
r.human_readable()   # '15.02.2026 10:00'
r.to_dict()
# {'type': 'reminder', 'title': 'meeting', 'source': 'tomorrow at 10 meeting',
#  'confidence': 0.9, 'is_past': False, 'language': 'en',
#  'datetime': '2026-02-15T10:00:00'}

Russian works out of the box too — the language is auto-detected:

parser.parse("каждый понедельник в 10 планёрка").to_dict()["recurrence"]["rrule"]
# 'FREQ=WEEKLY;BYDAY=MO'

Why TimeSense?

TimeSense Classic date parsers¹ LLM / ML pipelines
Dates & times from free text
Event classification (reminder / calendar / task) 🟡 prompt-dependent
Recurrence → RFC 5545 RRULE 🟡
Ready-to-import .ics export
Event title & location extraction
Zero dependencies
Deterministic (same input → same output)
No GPU, no model downloads, fully offline
Runs on Raspberry-Pi-class hardware

¹ e.g. dateparser, parsedatetime — great at extracting a datetime, but they stop there.

Contents: Features · Installation · Quick start · Showcase · Result types · Recurrence · .ics export · Configuration · Multiple events · Patterns · Parsing policy · Development

Features

  • Times: at 5pm, 9:30am, at 17:00, noon, half past ten, quarter to eight · RU: в 15:30, в 10 утра, без пяти десять.
  • Relative: in 2 hours, in an hour and a half, in forty five minutes, tomorrow, next Friday · RU: через 2 часа, через час, через полтора часа, через 2,5 часа (decimal comma), через два с половиной часа, через неделю, 5 минут назад.
  • Dates: Feb 17, March 3rd, 3/17, ISO 2026-02-17 · RU: 10 января, первого сентября, 15 числа, в пятницу.
  • Ranges → CalendarResult: from 9 to 5, 9am-11am, between 10 and 12 · RU: с 9 до 10, с десяти до одиннадцати, с понедельника по среду.
  • Recurrence → rrule: every day, every weekday, every 2 weeks on Monday, every month on the 15th, N times (COUNT), until end of March (UNTIL), except weekends (BYDAY) · RU: каждый понедельник, по будням, каждые 2 недели.
  • Deadlines: by Friday, by 5pm, by end of month, by end of March · RU: до пятницы, до конца марта, не позднее 20-го, в течение часа.
  • Duration & location: for 2 hours, на 2 часа, at the office, в офисе.
  • Result classification: reminder / calendar event / task, with is_past, detected_language, JSON and rrule export.

Installation

pip install timesense

No dependencies — pure Python (3.9–3.13). Morphology is optional; without it the parser uses built-in word-form dictionaries. For improved Russian lemmatization:

pip install "timesense[morph]"   # pulls in pymorphy3

Quick start

parse() returns a single object (ReminderResult / CalendarResult / TaskResult) or None when no date/time is found. Pin the reference time with now, and force a language with language= (otherwise it is auto-detected):

from datetime import datetime
from timesense import TimeSenseParser

parser = TimeSenseParser()
now = datetime(2026, 2, 14, 14, 0)

parser.parse("in forty five minutes call", now=now)     # ReminderResult 14:45
parser.parse("from 9 to 5 work", now=now)               # CalendarResult 09:00–17:00
parser.parse("every weekday at 9 standup", now=now)     # rrule FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
parser.parse("в 10 мск созвон", now=now)                # None (timezones unsupported)
parser.parse("tomorrow at 10", now=now, language="en")  # explicit language

Showcase: complex phrases

One phrase can carry a recurrence rule, a time, a location and a title at once — and the result type is picked automatically. All outputs below are real (now = 2026-07-06 15:41, a Monday):

# Recurrence + time + location → ReminderResult with an RRULE
p.parse("every second Friday of the month at 6:30pm retro in the meeting room")
# ReminderResult · title='retro' · location='in the meeting room'
# datetime=2026-07-10 18:30 · rrule='FREQ=MONTHLY;BYDAY=2FR'

# Range crossing midnight → CalendarResult
p.parse("next Tuesday from 23:15 to 00:45 check the maintenance window")
# CalendarResult · 2026-07-14 23:15 → 2026-07-15 00:45 · duration_minutes=90

# Business-day arithmetic → TaskResult (deadline)
p.parse("two business days before end of month remind about closing documents")
# TaskResult · task_type=deadline · deadline=2026-07-29 23:59

# Season → TaskResult (fuzzy period)
p.parse("in winter run the security audit")
# TaskResult · 2026-12-01 → 2027-02-28

# Mixed types in one line → parse_multi
p.parse_multi("today by 18 close the contract, tomorrow at 9 show the demo, "
              "Friday at 16 collect retro notes")
# TaskResult(deadline 2026-07-06 18:00) · ReminderResult(2026-07-07 09:00)
# · ReminderResult(2026-07-10 16:00)

Result types

All results share a common interface: .title, .source, .confidence, .is_past, .location, .duration_minutes, .recurrence, .exclusions, .detected_language, .event_type (an EventType enum), plus .to_dict() and .human_readable().

ReminderResult — a point in time

r = parser.parse("at 3:30pm sync")
r.datetime_at        # datetime(..., 15, 30)

CalendarResult — an event with start and end

r = parser.parse("from 10 to 11 sync")
r.start_at           # datetime(..., 10, 0)
r.end_at             # datetime(..., 11, 0)
r.duration_minutes   # 60

TaskResult — a task (period / deadline / open-start / fuzzy)

from timesense import TaskType

r = parser.parse("by Friday submit report")
r.task_type          # TaskType.PERIOD | DEADLINE | OPEN_START | FUZZY
r.deadline           # deadline datetime (for TaskType.DEADLINE)
r.start_at, r.end_at # start/end when applicable
r.fuzzy              # bool

Recurring events (rrule)

r = parser.parse("every Monday standup")
r.recurrence.to_rrule()      # 'FREQ=WEEKLY;BYDAY=MO'
r.to_dict()["recurrence"]
# {'frequency': 'WEEKLY', 'interval': 1, 'by_day': ['MO'],
#  'until': None, 'count': None, 'rrule': 'FREQ=WEEKLY;BYDAY=MO'}

parser.parse("every Friday until end of March sync").recurrence.to_rrule()
# 'FREQ=WEEKLY;BYDAY=FR;UNTIL=20260331T235900'   (naive: no trailing Z)

The start date always satisfies its own rule: a BYDAY/BYMONTHDAY recurrence snaps DTSTART to the first valid occurrence.

Calendar export (.ics)

Export any result — or a whole list — to an iCalendar file that opens in Google Calendar, Apple Calendar and Outlook:

from timesense import TimeSenseParser, to_ics_calendar

p = TimeSenseParser()
events = [
    p.parse("every Monday at 10 standup", language="en"),
    p.parse("meeting Friday 18:00 to 20:00", language="en"),
]
with open("events.ics", "w", encoding="utf-8") as f:
    f.write(to_ics_calendar(events))

# single event:
p.parse("tomorrow at 10 call the client", language="en").to_ics()

RFC 5545 VEVENTs carry DTSTART/DTEND/RRULE/SUMMARY/LOCATION. Periods and deadlines are emitted as all-day (VALUE=DATE) events; text is escaped and long lines folded per the spec.

Configuration

from timesense import TimeSenseParser, TimeConfig

config = TimeConfig(
    prefer_nearest_future=True,   # shift past times into the nearest future
    default_hour_for_one=13,      # "at one" → 13:00
    default_language="auto",      # "auto" | "ru" | "en"
    timezone="Europe/Amsterdam",  # stored but NOT applied — datetimes are naive
    working_hours={"start": 9, "end": 18},
    custom_times={"briefing": (11, 30)},  # custom word → (hour, minute)
)

parser = TimeSenseParser(config)

Presets: TimeConfig.default() and TimeConfig.strict() (prefer_nearest_future=False, default_hour_for_one=1). Invalid language values raise ValueError.

Production / working calendar

Load a working calendar so that all "business day" logic honours public holidays (excluded) and transferred working weekends (included):

from timesense import TimeSenseParser, TimeConfig, WorkingCalendar

cal = WorkingCalendar(
    holidays=["2026-02-26", "2026-02-27"],   # non-working days
    working_weekends=["2026-02-28"],          # a Saturday declared working
    holiday_names={"new year": "2026-01-01"},
)
parser = TimeSenseParser(TimeConfig(calendar=cal))
parser.parse("last business day of the month report")  # → 2026-02-28 (skips the holidays)

WorkingCalendar accepts date, datetime or "YYYY-MM-DD" strings, loads from WorkingCalendar.from_dict(...) / from_json(path_or_str), and exposes is_working_day, is_holiday, next_working_day, prev_working_day, resolve_holiday. With no calendar, a business day is simply Mon–Fri.

Multiple events in one phrase

parser.parse_multi("meeting tomorrow at 10 and lunch at 12")
# [ReminderResult('meeting'), ReminderResult('lunch')]

parse_multi() splits heuristically on , ; and / then (en) and « и » (ru). A separator inside an event title may cause a false split; between X and Y is not split, and a decimal comma (через 2,5 часа) is never treated as a separator.

A time-only segment inherits the date of the previous event: "завтра в 10 встреча и в 15 созвон" puts both events on tomorrow. Segments with their own date words (послезавтра, weekdays, today/сегодня, …) keep their own anchor.

Supported patterns (excerpt)

Category Examples (EN)
Times at 5pm, 9:30am, at 17:00, noon, midnight
Word times at ten thirty, half past ten, quarter to eight
Relative in 2 hours, in forty five minutes, in 3 days
Dates Feb 17, March 3rd, 3/17, 2026-02-17
Weekdays next Friday, this Monday, Friday
Ranges from 9 to 5, 9am-11am, between 10 and 12
Recurrence every day, every weekday, every 2 weeks on Monday, every month on the 15th
Modifiers 5 times (COUNT), until end of March (UNTIL), except weekends (BYDAY)
Deadlines by Friday, by 5pm, by end of month

Russian examples are listed in the Russian README.

Status, languages & parsing policy

  • Status: stable (1.0.0). The public API (parse, parse_multi, TimeConfig, result types, detected_language, to_dict()) is frozen; breaking changes only in the next major (semver).
  • Python: 3.9–3.13.
  • Languages: Russian is the primary, most complete locale; English is a dedicated locale (timesense/locales/en.py) covering the core grammar, including spelled-out numbers (in forty five minutes, at ten thirty, half past ten, quarter to eight, from nine to five, every forty five minutes). The language is auto-detected (default_language="auto") or set explicitly via parse(text, language=...); the chosen language is reported in detected_language.
  • Time zones: not supported. All datetimes are naive (no tzinfo). TimeConfig.timezone is stored but never applied. Zone-bearing phrases (в 10 мск, UTC, GMT+3) deliberately return None.
  • Smart-hour: a bare hour after the current time is read as the nearest future. Example: with now=14:00, "at 10"22:00 today (not 10:00 tomorrow); bare hours 1–7 shift to PM. Explicit minutes disable this: "at 10:00" → 10:00 sharp (tomorrow if already past). An explicit day-part marker always wins over the heuristics: "завтра в 7 утра" → 07:00, "завтра в 3 ночи" → 03:00, "tomorrow at 3am" → 03:00. Turn smart-hour off with prefer_nearest_future=False.
  • "Next" weekday: plain weekday means the nearest upcoming one; next / следующий means the nearest one plus a week — the same convention in both languages. On Saturday, "Friday" → the coming Friday and "next Friday" / "в следующую пятницу" → a week after it.
  • Reminder / Calendar / Task: a point in time → ReminderResult; an interval with start and end → CalendarResult; period / deadline / open-start / fuzzy → TaskResult. Past events are flagged with is_past=True.

Known limitations

  • Time zones are not applied (naive datetimes).
  • Spelled-out numbers are supported, including compounds and verbose forms (in forty five minutes, from nine to five, every forty five minutes; RU в десять часов пятнадцать минут, час пятнадцать, с двух до половины четвёртого). A few rare colloquialisms (e.g. English "ten fifteen" with no at/idiom) still need explicit forms.
  • parse_multi() splits into multiple events only when every segment is self-contained (parses on its own); otherwise it returns a single result, so plain lists like buy milk and bread tomorrow are not broken apart.
  • Holidays and typo correction are out of scope (they return None).

Development & tests

Tests are not shipped in the wheel (pip install timesense does not include them), but they are included in the source distribution. They live in the repository; run them from a clone:

git clone https://github.com/kruatech/timesense
cd timesense
pip install -e ".[dev]"

python -m timesense.tests.test_fixes --fail-only     # RU runner
python -m timesense.tests.test_en_fixes --fail-only  # EN runner
pytest -q                                            # wrappers + EN pytest suite

# release checks
python -m build
python -m twine check dist/*

Examples run as a module from the repo root:

python -m examples.basic_usage

Contributing

Pull requests are welcome. For larger changes, please open an issue first. See CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md.

License

MIT — see LICENSE.

Author & contact

Anton Krutilin

Bugs & feature requests: https://github.com/kruatech/timesense/issues

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

timesense-1.0.0.tar.gz (156.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

timesense-1.0.0-py3-none-any.whl (109.1 kB view details)

Uploaded Python 3

File details

Details for the file timesense-1.0.0.tar.gz.

File metadata

  • Download URL: timesense-1.0.0.tar.gz
  • Upload date:
  • Size: 156.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timesense-1.0.0.tar.gz
Algorithm Hash digest
SHA256 20a2c38f5b49852aa1455be91f5821f051ef67a96a525283da94d7ba5bc68b80
MD5 4767617c106c374936fcd94c1a1bb4f0
BLAKE2b-256 c1c1a526941c8c591382c13d459a366258004671f0c32f3b82309937d72ffbe6

See more details on using hashes here.

Provenance

The following attestation bundles were made for timesense-1.0.0.tar.gz:

Publisher: release.yml on kruatech/timesense

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file timesense-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: timesense-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 109.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timesense-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fc2c173284cfc4a36ae4ffb62c38be705c05c7ae8e0c0a520029a9a487a4a694
MD5 95e734d27220e898b2b7efcc57dfa965
BLAKE2b-256 17b4941c613f32fca6b895da84e387754e818fcf2fc57acd2b14bf185d03a2d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for timesense-1.0.0-py3-none-any.whl:

Publisher: release.yml on kruatech/timesense

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page