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"
Error: Ambiguous time during DST transition: 2025-10-26 02:30:00 could be
2025-10-26 02:30:00 +0200 or 2025-10-26 02:30:00 +0100
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"
Error: 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" # CEST (before fallback) $ kt-parse time "2025-10-26T02:30:00+01:00" # CET (after fallback)
Times outside the problematic windows parse normally:
$ TZ=Europe/Paris kt-parse time "2025-10-26 01:59:59" # Before fall-back window $ TZ=Europe/Paris kt-parse time "2025-10-26 03:00:01" # After fall-back window $ TZ=Europe/Paris kt-parse time "2025-03-30 01:59:59" # Before spring-forward gap $ TZ=Europe/Paris kt-parse time "2025-03-30 03:00:00" # After spring-forward gap
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(())
}kt-parse is a thin wrapper around the library, useful in scripts and
shell pipelines.
Run kt-parse --help for a quick usage reminder.
$ kt-parse time 9h 1761104400 2025-10-22 09:00:00 +00:00
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 1761105011 2025-10-22 09:30:11 +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 1761104400 2025-10-22 09:00:00 +00:00 1761108000 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