Natural-language date/time & event parser for Russian (and English)
Project description
TimeSense
Languages: English · Русский
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, ISO2026-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 andrruleexport.
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 viaparse(text, language=...); the chosen language is reported indetected_language. - Time zones: not supported. All
datetimes are naive (notzinfo).TimeConfig.timezoneis stored but never applied. Zone-bearing phrases (в 10 мск,UTC,GMT+3) deliberately returnNone. - 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 hours1–7shift 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 withprefer_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 withis_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 noat/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 likebuy milk and bread tomorroware 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
- GitHub: https://github.com/kruatech
- Telegram: @kruatech
- Email: a@krutilin.pro
Bugs & feature requests: https://github.com/kruatech/timesense/issues
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20a2c38f5b49852aa1455be91f5821f051ef67a96a525283da94d7ba5bc68b80
|
|
| MD5 |
4767617c106c374936fcd94c1a1bb4f0
|
|
| BLAKE2b-256 |
c1c1a526941c8c591382c13d459a366258004671f0c32f3b82309937d72ffbe6
|
Provenance
The following attestation bundles were made for timesense-1.0.0.tar.gz:
Publisher:
release.yml on kruatech/timesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
timesense-1.0.0.tar.gz -
Subject digest:
20a2c38f5b49852aa1455be91f5821f051ef67a96a525283da94d7ba5bc68b80 - Sigstore transparency entry: 2113974553
- Sigstore integration time:
-
Permalink:
kruatech/timesense@dd19d91a9f8583abff55d18e5f8d63cd646f140d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/kruatech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dd19d91a9f8583abff55d18e5f8d63cd646f140d -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc2c173284cfc4a36ae4ffb62c38be705c05c7ae8e0c0a520029a9a487a4a694
|
|
| MD5 |
95e734d27220e898b2b7efcc57dfa965
|
|
| BLAKE2b-256 |
17b4941c613f32fca6b895da84e387754e818fcf2fc57acd2b14bf185d03a2d2
|
Provenance
The following attestation bundles were made for timesense-1.0.0-py3-none-any.whl:
Publisher:
release.yml on kruatech/timesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
timesense-1.0.0-py3-none-any.whl -
Subject digest:
fc2c173284cfc4a36ae4ffb62c38be705c05c7ae8e0c0a520029a9a487a4a694 - Sigstore transparency entry: 2113974706
- Sigstore integration time:
-
Permalink:
kruatech/timesense@dd19d91a9f8583abff55d18e5f8d63cd646f140d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/kruatech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@dd19d91a9f8583abff55d18e5f8d63cd646f140d -
Trigger Event:
release
-
Statement type: