Skip to content

Rust

dtrexp-rs is the Rust implementation of DTRExp. Its scope is the spec’s core interface: parsing, validation and coverage evaluation. Rendering, description and RRULE export are out of scope; the reference implementation has them. Zero dependencies, including the zone handling; IANA zones are read straight from the system TZif database, and the test suite is driven by the shared conformance vectors.

Terminal window
cargo add dtrexp

Rust 2021 edition.

use dtrexp::{parse, Tz};
let dtr = parse("T0900:1800 E1:5").unwrap(); // business hours, Mon–Fri
// 2026-07-07 (a Tuesday) 10:00:00Z, in ms since the Unix epoch:
let t: i64 = 1_783_418_400_000;
dtr.covers(t, "Europe/Berlin").unwrap();
// —> true; a weekday, 09:00–18:00 Berlin local time
// preloaded zone; cannot fail:
let berlin = Tz::load("Europe/Berlin").unwrap();
dtr.covers_in(t, &berlin);
// —> true

Instants are milliseconds since the Unix epoch (UTC); the time zone is passed at evaluation, and the default is Tz::utc(). You parse once (at write/config time) and evaluate many; a Dtrexp value is immutable after parse. covers is a single calendar-field extraction followed by integer comparisons; no occurrence iteration, ever.

An access-control grant that only applies during business hours; the expression lives in the grant as data, and the check runs on every request. Load the zone once and evaluate with covers_in, which cannot fail:

use dtrexp::{parse, Dtrexp, Tz};
// grant.scope = "T0900:1800 E1:5" — stored in your DB / ACL
let scope: Dtrexp = parse(&grant.scope).unwrap();
let berlin = Tz::load("Europe/Berlin").unwrap();
fn authorize(scope: &Dtrexp, tz: &Tz, now_ms: i64) -> Result<(), Forbidden> {
if !scope.covers_in(now_ms, tz) {
return Err(Forbidden("outside the permitted window"));
}
// … proceed
Ok(())
}

Validation with the spec’s unsatisfiability lint; D30 M2 parses cleanly but can never match, and the library tells you so instead of failing silently:

let warnings = dtrexp::validate("D30 M2").unwrap(); // parses cleanly
// —> one warning; no February has 30 days
warnings[0].pos; // —> 0
warnings[0].message; // —> "unsatisfiable — day never exists …"

A syntactically invalid expression returns a positioned ParseError rather than a warning:

let err = dtrexp::parse("T2500").unwrap_err(); // hour out of range
err.pos; // —> 1; points at the offending token
err.message; // —> "hour out of range — 24 exists only as the exact token '2400'"
FunctionDescription
parse(input)Parses a DTRExp string into an immutable Dtrexp. Returns a positioned ParseError { pos, message } on syntactically or statically invalid input. The only way to construct a Dtrexp. Warnings from a clean parse are available via Dtrexp::warnings().
validate(input)Returns Result<Vec<Warning>, ParseError>: the warnings on a clean parse, or the same ParseError when the input does not parse. Same warnings as Dtrexp::warnings().
MemberDescription
covers(instant_ms, tz_id)Whether the expression covers the instant, evaluated in IANA zone tz_id. Returns Result<bool, UnknownTimeZone>; an empty identifier or "UTC" selects UTC. One field extraction followed by integer tests.
covers_in(instant_ms, &tz)The preloaded-zone variant; evaluate against an already-loaded Tz, infallibly. Returns bool.
warnings()The §9.1 unsatisfiability warnings of the parsed expression; the same content validate() returns. Empty for a clean expression.
MemberDescription
Tz::load(id)Loads an IANA zone from the system TZif database. Returns Result<Tz, UnknownTimeZone>; a non-resolving identifier is the one runtime failure.
Tz::utc()The UTC zone; the default when none is given.
  • Instants: milliseconds since the Unix epoch (UTC), as i64.
  • Time zone: an IANA identifier passed to covers, or an already-loaded Tz passed to covers_in. The zone is always an evaluation parameter, never part of the expression; an empty identifier or "UTC" means UTC. DST is resolved per the spec’s local-time rules (§9.3).
  • Errors: both ParseError and Warning carry a pos; the byte offset into the source where the problem was detected. UnknownTimeZone { id, message } is the only failure at evaluation time, and covers_in avoids it entirely by taking a Tz you already loaded.

The test suite is driven by the shared vectors.json from the spec repo (draft 2.8), vendored at tests/vectors.json: every coverage, rejection, warning and quiet vector, including the calendar traps (Feb 29 across 2000/2024/2100, W53 existence, DST gap/overlap in Europe/Berlin). Run cargo test; see VECTORS for how the suite works. Tz::load reads the system zoneinfo database directly, with no time-zone crate underneath, and the crate has zero dependencies.