Skip to content

0k/kal-time

Repository files navigation

kal-time Repository

Overview

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.

Supported Formats

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.

ISO 8601 formats (with 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

ISO 8601 formats (no timezone)

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

Partial date formats

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)

Time-only formats

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

Terse/human-friendly formats

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)

Unix timestamp

Prefix with @ for epoch seconds (always interpreted as UTC, reference ignored):

@1736692200                 => 2025-01-12 14:30:00 +00:00

Natural language

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.

Reference Types and Offset Behavior

The library distinguishes between two types of references:

Fixed offset reference (DateTime<FixedOffset> or DateTime<Utc>)

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)

Local reference (DateTime<Local>)

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)

DST Transition Handling

When using a DateTime<Local> reference (no explicit offset), the library checks for problematic times during Daylight Saving Time transitions. Two cases are detected:

Ambiguous times (fall back)

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

Non-existent times (spring forward)

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)

Resolving DST issues

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

Build & Test

  • cargo build — compile the library and surface warnings.
  • cargo test — execute unit tests embedded alongside the modules.
  • cargo fmt and cargo clippy — enforce formatting and linting prior to review.

Usage Examples

Each snippet shows how a public helper parses input and what kind of timestamp it produces.

Relative offset from a reference

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(())
}

Parsing absolute time with local defaults

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(())
}

UTC parsing shortcut

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(())
}

Parsing timespans

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(())
}

Natural language timespans

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.

Operand resolution

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 the two-timer crate. today in a Wednesday reference = [Wed 00:00, Thu 00:00).
  • Explicit / terse formatsL is the parsed instant with unspecified fields filled to start-of-unit; R is L plus one unit of the smallest explicitly-specified field. The unit comes from the rightmost %X specifier of the matched format:
    Smallest specifierUnit added to L to get R
    %Y1 calendar year
    %m1 calendar month
    %d1 day
    %H1 hour
    %M1 minute
    %S / %s1 second

    So 9h as a timespan is [09:00, 10:00) (1 hour); 14:30 is [14:30, 14:31) (1 minute); 2024 is the whole year; 2024-10 is 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 of X...

Composition rule

Input shapeResult
bare P[L(P), R(P))
P..Q[L(P), L(Q))right uses LEFT edge
X..[L(X), now)
..Xrejected
bare nowrejected

A timespan with start >= stop is rejected.

Examples

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.

Breaking change (vs. previous behavior)

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.

Command-line Utility

kt-parse is a thin wrapper around the library, useful in scripts and shell pipelines.

Run kt-parse --help for a quick usage reminder.

Output formats

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 only
  • iso: 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.

Parse a time with the current clock

When no reference is provided, the current time is used. Output varies based on when you run it:

kt-parse time 9h

Parse a time with an explicit reference

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

Parse a timespan

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

About

Small rust time parsing helper

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors