kal-time is a tiny Rust library for parsing human-friendly time and
timespan expressions into chrono::DateTime<FixedOffset>. It supports
relative parsing against a caller-provided reference moment and
gracefully fills missing segments (date, time, or offset) using either
zeroes or the supplied reference.
Use it when you want a CLI or automation tool to accept terse inputs
such as "9h..10h" for “today between 9 and 10” or "30m" to fix the
minute field to 00:30 while reusing the current day/hour, without
forcing users to type full ISO timestamps.
This is more a tiny piece of code I use between many different project. It has no ambition to become anything big, and the quality is alpha level.
The library accepts a variety of date/time formats, listed here from most specific to least specific. Formats are tried in order until one matches.
All examples below use this reference: 2024-03-20T11:45:30+05:00
When the input lacks an explicit timezone, the offset from the reference is used. This makes parsing deterministic regardless of the system’s local timezone.
Full ISO 8601 compliance with explicit timezone offset or UTC marker. The provided timezone is preserved in the output (reference ignored):
2025-01-12T14:30:00+01:00 => 2025-01-12 14:30:00 +01:00 2025-01-12T14:30:00Z => 2025-01-12 14:30:00 +00:00 2025-01-12T14:30+01:00 => 2025-01-12 14:30:00 +01:00 2025-01-12T14:30Z => 2025-01-12 14:30:00 +00:00 2025-01-12 14:30:00+01:00 => 2025-01-12 14:30:00 +01:00 2025-01-12 14:30:00Z => 2025-01-12 14:30:00 +00:00
Standard ISO formats without timezone—offset comes from reference (+05:00):
2025-01-12T14:30:00 => 2025-01-12 14:30:00 +05:00 2025-01-12T14:30 => 2025-01-12 14:30:00 +05:00 2025-01-12 14:30:00 => 2025-01-12 14:30:00 +05:00 2025-01-12 14:30 => 2025-01-12 14:30:00 +05:00 2025-01-12 => 2025-01-12 00:00:00 +05:00
Missing fields filled from reference (year=2024, month=03, day=20, offset=+05:00):
01-12 => 2024-01-12 00:00:00 +05:00 (year, offset from ref) 01/12 => 2024-01-12 00:00:00 +05:00 (year, offset from ref) 01-12 14:30 => 2024-01-12 14:30:00 +05:00 (year, offset from ref) 01-12 14:30:45 => 2024-01-12 14:30:45 +05:00 (year, offset from ref) 15 14:30 => 2024-03-15 14:30:00 +05:00 (year+month, offset from ref)
Date (2024-03-20) and offset (+05:00) come from reference:
14:30:59 => 2024-03-20 14:30:59 +05:00 14:30 => 2024-03-20 14:30:00 +05:00
Missing fields filled from reference (hour=11, minute=45, offset=+05:00):
14h30 => 2024-03-20 14:30:00 +05:00 (date, offset from ref) 9h => 2024-03-20 09:00:00 +05:00 (date, offset from ref) 30m => 2024-03-20 11:30:00 +05:00 (date+hour, offset from ref) 15 14h30 => 2024-03-15 14:30:00 +05:00 (year+month, offset from ref) 15 9h => 2024-03-15 09:00:00 +05:00 (year+month, offset from ref)
Prefix with @ for epoch seconds (always interpreted as UTC, reference ignored):
@1736692200 => 2025-01-12 14:30:00 +00:00
English expressions for relative dates and times (offset +05:00 from ref):
yesterday => 2024-03-19 11:45:30 +05:00 (1 day before ref) tomorrow => 2024-03-21 11:45:30 +05:00 (1 day after ref) friday => 2024-03-22 00:00:00 +05:00 (this friday) last friday => 2024-03-15 00:00:00 +05:00 next monday => 2024-04-01 00:00:00 +05:00 2 days ago => 2024-03-18 11:45:30 +05:00 3 hours ago => 2024-03-20 08:45:30 +05:00 1 hour => 2024-03-20 12:45:30 +05:00 (1 hour from ref) friday 8pm => 2024-03-22 20:00:00 +05:00 april 1 => 2024-04-01 00:00:00 +05:00 1 april => 2024-04-01 00:00:00 +05:00
Shorthand intervals are also supported: 3h, 2d, 15m, 1y.
The library distinguishes between two types of references:
When the reference has an explicit fixed offset, that offset is used for all parsed times (unless the input itself specifies a timezone). No DST checking is performed since the offset is explicit.
use chrono::FixedOffset;
use kal_time::parse_with_reference;
let offset = FixedOffset::east_opt(5 * 3600).unwrap(); // +05:00
let reference = offset.with_ymd_and_hms(2024, 3, 20, 12, 0, 0).unwrap();
let parsed = parse_with_reference("2025-07-15 14:30", &reference)?;
// => 2025-07-15 14:30:00 +05:00 (offset from reference, no DST check)When the reference is a DateTime<Local>, the library uses the system
timezone (TZ environment variable) to determine the correct offset
for the target date. This enables DST-aware parsing.
use chrono::Local;
use kal_time::parse_with_reference;
let reference = Local::now();
let parsed = parse_with_reference("2025-07-15 14:30", &reference)?;
// => 2025-07-15 14:30:00 +02:00 (in Europe/Paris summer time)When using a DateTime<Local> reference (no explicit offset), the
library checks for problematic times during Daylight Saving Time
transitions. Two cases are detected:
In regions that observe DST, clocks “fall back” once a year, causing a range of local times to occur twice.
For example, in Europe/Paris on the last Sunday of October, clocks
move from 03:00 CEST (+02:00) back to 02:00 CET (+01:00). This means
times between 02:00:00 and 03:00:00 are ambiguous—they could refer to
either the CEST or CET instance.
$ TZ=Europe/Paris kt-parse time "2025-10-26 02:30:00"
Failed to parse time: Ambiguous time during DST transition: 2025-10-26 02:30:00 could be 2025-10-26 02:30:00 +0100 or 2025-10-26 02:30:00 +0200
Conversely, clocks “spring forward” once a year, skipping a range of local times entirely.
For example, in Europe/Paris on the last Sunday of March, clocks
jump from 02:00 CET (+01:00) to 03:00 CEST (+02:00). Times between
02:00:00 and 03:00:00 simply do not exist.
$ TZ=Europe/Paris kt-parse time "2025-03-30 02:30:00"
Failed to parse time: Non-existent time during DST transition: 2025-03-30 02:30:00 does not exist (clocks skip forward)
To resolve ambiguity or specify a non-existent time, use an explicit timezone offset:
$ kt-parse time "2025-10-26T02:30:00+02:00"
1761438600 2025-10-26 02:30:00 +02:00
$ kt-parse time "2025-10-26T02:30:00+01:00"
1761442200 2025-10-26 02:30:00 +01:00
Times outside the problematic windows parse normally:
$ TZ=Europe/Paris kt-parse time "2025-10-26 01:59:59"
1761436799 2025-10-26 01:59:59 +02:00
$ TZ=Europe/Paris kt-parse time "2025-10-26 03:00:01"
1761444001 2025-10-26 03:00:01 +01:00
$ TZ=Europe/Paris kt-parse time "2025-03-30 01:59:59"
1743296399 2025-03-30 01:59:59 +01:00
$ TZ=Europe/Paris kt-parse time "2025-03-30 03:00:00"
1743296400 2025-03-30 03:00:00 +02:00
cargo build— compile the library and surface warnings.cargo test— execute unit tests embedded alongside the modules.cargo fmtandcargo clippy— enforce formatting and linting prior to review.
Each snippet shows how a public helper parses input and what kind of timestamp it produces.
Use parse_with_reference to interpret partial or relative
expressions against a known moment.
use chrono::TimeZone;
use chrono::Utc;
use kal_time::parse_with_reference;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let reference = Utc.with_ymd_and_hms(2025, 10, 22, 9, 10, 11).unwrap();
let parsed = parse_with_reference("30m", &reference)?;
println!("{}", parsed);
// => 2025-10-22 09:30:00 +00:00
Ok(())
}parse assumes the local clock when fields are missing; full
timestamps stay in the caller’s local offset.
use kal_time::parse;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let parsed = parse("2025-10-22 14:30")?;
println!("{}", parsed);
// => 2025-10-22 14:30:00 +<local offset>
Ok(())
}parse_utc mirrors parse but always anchors missing pieces to the
current UTC reference.
use kal_time::parse_utc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let parsed = parse_utc("9h")?;
println!("{}", parsed);
// => <today’s date> 09:00:00 +00:00
Ok(())
}parse_timespan expands a range like start..end into start/stop
instants, defaulting to a 1-day window when no end is supplied.
use kal_time::parse_timespan;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (start, stop) = parse_timespan("2025-10-20..2025-10-22 12:00")?;
println!("start: {}", start);
println!("stop: {}", stop);
// => start: 2025-10-20 00:00:00 +<local offset>
// => stop: 2025-10-22 12:00:00 +<local offset>
Ok(())
}parse_timespan returns half-open [start, stop) intervals where
stop is the first instant not in the span. Every operand resolves
to its own natural period; composition with .. then picks edges by
position.
Each operand resolves to a half-open period [L, R):
- Calendar tokens (
today,yesterday,tomorrow,last friday,this week,this month,this year, …) — boundaries from thetwo-timercrate.todayin a Wednesday reference =[Wed 00:00, Thu 00:00). - Explicit / terse formats —
Lis the parsed instant with unspecified fields filled to start-of-unit;RisLplus one unit of the smallest explicitly-specified field. The unit comes from the rightmost%Xspecifier of the matched format:Smallest specifier Unit added to Lto getR%Y1 calendar year %m1 calendar month %d1 day %H1 hour %M1 minute %S/%s1 second So
9has a timespan is[09:00, 10:00)(1 hour);14:30is[14:30, 14:31)(1 minute);2024is the whole year;2024-10is the whole month. - ~now~ (case-insensitive) — the reference instant, treated as a
zero-width period. Recognized only as a
..operand or as the implicit right side ofX...
| Input shape | Result |
|---|---|
bare P | [L(P), R(P)) |
P..Q | [L(P), L(Q)) — right uses LEFT edge |
X.. | [L(X), now) |
..X | rejected |
bare now | rejected |
A timespan with start >= stop is rejected.
All examples use reference 2024-03-20T11:45:30+05:00 (a Wednesday):
$ kt-parse timespan today 2024-03-20T11:45:30+05:00
1710874800 2024-03-20 00:00:00 +05:00
1710961200 2024-03-21 00:00:00 +05:00
$ kt-parse timespan yesterday 2024-03-20T11:45:30+05:00
1710788400 2024-03-19 00:00:00 +05:00
1710874800 2024-03-20 00:00:00 +05:00
$ kt-parse timespan tomorrow 2024-03-20T11:45:30+05:00
1710961200 2024-03-21 00:00:00 +05:00
1711047600 2024-03-22 00:00:00 +05:00
$ kt-parse timespan "this week" 2024-03-20T11:45:30+05:00
1710702000 2024-03-18 00:00:00 +05:00
1711306800 2024-03-25 00:00:00 +05:00
$ kt-parse timespan "last week" 2024-03-20T11:45:30+05:00
1710097200 2024-03-11 00:00:00 +05:00
1710702000 2024-03-18 00:00:00 +05:00
$ kt-parse timespan "this month" 2024-03-20T11:45:30+05:00
1709233200 2024-03-01 00:00:00 +05:00
1711911600 2024-04-01 00:00:00 +05:00
$ kt-parse timespan "last month" 2024-03-20T11:45:30+05:00
1706727600 2024-02-01 00:00:00 +05:00
1709233200 2024-03-01 00:00:00 +05:00
$ kt-parse timespan "this year" 2024-03-20T11:45:30+05:00
1704049200 2024-01-01 00:00:00 +05:00
1735671600 2025-01-01 00:00:00 +05:00
Bare instants follow the precision rule (reference 2026-05-01T15:00:00+02:00):
$ kt-parse timespan 9h 2026-05-01T15:00:00+02:00
1777618800 2026-05-01 09:00:00 +02:00
1777622400 2026-05-01 10:00:00 +02:00
$ kt-parse timespan 14:30 2026-05-01T15:00:00+02:00
1777638600 2026-05-01 14:30:00 +02:00
1777638660 2026-05-01 14:31:00 +02:00
$ kt-parse timespan 2024-10 2026-05-01T15:00:00+02:00
1727733600 2024-10-01 00:00:00 +02:00
1730412000 2024-11-01 00:00:00 +02:00
$ kt-parse timespan 2024 2026-05-01T15:00:00+02:00
1704060000 2024-01-01 00:00:00 +02:00
1735682400 2025-01-01 00:00:00 +02:00
Open-ended ranges and now:
$ kt-parse timespan today.. 2026-05-01T15:00:00+02:00
1777586400 2026-05-01 00:00:00 +02:00
1777640400 2026-05-01 15:00:00 +02:00
$ kt-parse timespan yesterday..now 2026-05-01T15:00:00+02:00
1777500000 2026-04-30 00:00:00 +02:00
1777640400 2026-05-01 15:00:00 +02:00
$ kt-parse timespan yesterday..tomorrow 2026-05-01T15:00:00+02:00
1777500000 2026-04-30 00:00:00 +02:00
1777672800 2026-05-02 00:00:00 +02:00
Rejected forms — each emits a clear error:
$ kt-parse timespan ..today 2026-05-01T15:00:00+02:00
Failed to parse timespan: Invalid timespan "..today": leading-empty (`..X`) is not supported
$ kt-parse timespan now 2026-05-01T15:00:00+02:00
Failed to parse timespan: Invalid timespan "now": bare `now` / empty input is not a period
$ kt-parse timespan today..yesterday 2026-05-01T15:00:00+02:00
Failed to parse timespan: Invalid timespan "today..yesterday": start (2026-05-01 00:00:00 +0200) is not strictly before stop (2026-04-30 00:00:00 +0200)
Note: parse (single time point) is unaffected — "today" in that
context still returns the reference time ± 1 day, preserving the
clock component.
Bare-instant inputs to parse_timespan used to return [t, t + 24h)
unconditionally. They now follow the precision rule: 9h is 1 hour,
14:30 is 1 minute, 2024-10 is 1 month, etc. The single-instant
API (parse) is unchanged.
kt-parse is a thin wrapper around the library, useful in scripts and
shell pipelines.
Run kt-parse --help for a quick usage reminder.
By default, kt-parse outputs both timestamp and human-readable date.
Use the -F (or --format) flag to select a specific format:
full(default):<timestamp> <YYYY-MM-DD> <HH:MM:SS> <+TZ>ts: Unix timestamp onlyiso: ISO 8601 format
$ kt-parse time "2025-01-06 11:45" "2025-01-06T00:00:00+01:00"
1736160300 2025-01-06 11:45:00 +01:00
$ kt-parse -F ts time "2025-01-06 11:45" "2025-01-06T00:00:00+01:00"
1736160300
$ kt-parse -F iso time "2025-01-06 11:45" "2025-01-06T00:00:00+01:00"
2025-01-06T11:45:00+01:00
The format flag works with both time and timespan commands.
When no reference is provided, the current time is used. Output varies based on when you run it:
kt-parse time 9h
You can supply a fully specified reference (RFC3339 or similar) when you need deterministic results regardless of the machine clock.
$ kt-parse time 30m 2025-10-22T09:10:11+00:00
1761125400 2025-10-22 09:30:00 +00:00
Timespans print two lines: start then end. Relative fields reuse the reference on a per-field basis.
$ kt-parse timespan 9h..10h 2025-10-22T09:10:11+00:00
1761123600 2025-10-22 09:00:00 +00:00
1761127200 2025-10-22 10:00:00 +00:00
Missing fields in the end segment now borrow the fully resolved start
instant (commit 1741734), so terse ranges stay on the expected day.
$ kt-parse timespan 10:15..30 2025-10-27T09:00:00+00:00
1761560100 2025-10-27 10:15:00 +00:00
1761561000 2025-10-27 10:30:00 +00:00
$ kt-parse timespan '2025-10-27 10:30..11:30' 2025-10-01T00:00:00+00:00
1761561000 2025-10-27 10:30:00 +00:00
1761564600 2025-10-27 11:30:00 +00:00
$ kt-parse timespan '2025-10-27 10:00:00..01:30' 2025-10-01T00:00:00+00:00
1761559200 2025-10-27 10:00:00 +00:00
1761559290 2025-10-27 10:01:30 +00:00