An ergonomic Rust library for parsing, validating, manipulating, and formatting dates, times, and timezones — with guaranteed round-trip safety and unambiguous timezone codes.
- Install
- Quick Start
- Why DTT?
- Features
- Supported Timezone Abbreviations
- API Highlights
- Development
- Troubleshooting
- Documentation
- Contributing
- License
cargo add dttOr add to Cargo.toml:
[dependencies]
dtt = "0.0.10"DTT requires Rust 1.88.0 or later (pinned by time = 0.3.47, which carries the upstream fix for RUSTSEC stack-exhaustion DoS in time < 0.3.47).
| Platform | Setup |
|---|---|
| macOS | brew install rustup-init && rustup-init -y |
| Linux | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y |
| WSL | Same as Linux, run inside your WSL distribution |
| Windows | Download rustup-init.exe from rustup.rs |
After install, verify with rustc --version (must be ≥ 1.88.0). Upgrade an existing toolchain with rustup update stable.
use dtt::prelude::*;
fn main() -> Result<(), AppError> {
// Current UTC time
let now = DateTime::new();
println!("Current time: {}", now);
// Parse — strict, offset-required for time-bearing inputs
let parsed = DateTime::parse("2024-01-15T10:30:00Z")?;
println!("Parsed: {}", parsed);
// Round-trip is guaranteed: parse(format(x)) == x
let s = parsed.format_rfc3339()?;
assert_eq!(parsed, DateTime::parse(&s)?);
// Arithmetic
let next_week = parsed.add_days(7)?;
let next_year = parsed.add_years(1)?;
println!("Next week: {next_week}, next year: {next_year}");
// Timezone conversion (note the explicit USA suffix)
let est = parsed.convert_to_tz("EST_USA")?;
println!("In US Eastern: {est}");
// Validation
assert!(DateTime::is_valid_iso_8601("2024-01-15T10:30:00Z"));
assert!(!DateTime::is_valid_year("10000")); // outside time crate range
Ok(())
}Run the full demo:
cargo run --example dttMost datetime libraries silently produce wrong answers in surprising places. DTT is designed to fail loudly rather than guess:
- Round-trip safety:
DateTime::parse(&dt.format_rfc3339()?)? == dtalways holds. No silent date-only truncation. - Unambiguous timezones:
ISTcould mean Indian (+05:30), Irish (+01:00), or Israel (+02:00). DTT requires explicit suffixes (IST_INDIA,IST_IRELAND,IST_ISRAEL) so you cannot accidentally use the wrong one. - Mixed-sign offsets rejected:
new_with_custom_offset(5, -30)returns an error instead of silently producing+05:30. - Deterministic
Default:DateTime::default()returns the Unix epoch, not wall-clock time, so tests are reproducible. - UTC-normalised equality: Two
DateTimevalues that represent the same instant compare equal regardless of which offset they were stored in. - Strict validation:
is_valid_yearis bounded to the actualtime::Daterange (-9999..=9999), so the validator and the builder always agree.
| Parsing | RFC 3339 with offset, ISO 8601 date-only, custom format strings |
| Formatting | RFC 3339, custom format descriptors |
| Validation | Components, ranges, leap years, ISO 8601, time strings |
| Arithmetic | add_days, add_months, add_years (overflow-checked) |
| Comparisons | Eq, Ord, Hash — all UTC-normalised |
| Calendar helpers | start_of_week, end_of_month, iso_week, iso_year |
| Timezone support | 22 disambiguated abbreviations + custom offsets |
| Serialization | serde round-trip via canonical RFC 3339 strings |
| Cross-platform | macOS, Linux, WSL, Windows |
Common abbreviations are intentionally disambiguated. Bare codes like EST, CST, IST, and WADT are not accepted because they refer to multiple zones in the real world.
| Code | Offset | Region |
|---|---|---|
UTC, GMT |
+00:00 | Coordinated Universal Time |
EST_USA |
−05:00 | US Eastern Standard Time |
EDT |
−04:00 | US Eastern Daylight Time |
CST_USA |
−06:00 | US Central Standard Time |
CDT |
−05:00 | US Central Daylight Time |
MST / MDT |
−07/−06 | US Mountain |
PST / PDT |
−08/−07 | US Pacific |
CET / CEST |
+01/+02 | Central Europe |
EET / EEST |
+02/+03 | Eastern Europe |
IST_IRELAND |
+01:00 | Irish Standard Time |
IST_ISRAEL |
+02:00 | Israel Standard Time |
IST_INDIA |
+05:30 | Indian Standard Time |
JST |
+09:00 | Japan |
HKT |
+08:00 | Hong Kong |
CST_CHINA |
+08:00 | China Standard Time |
EST_AUS / AEST |
+10:00 | Australian Eastern |
AEDT |
+11:00 | Australian Eastern Daylight |
ACWST |
+08:45 | Australian Central Western |
For any other zone, use DateTime::new_with_custom_offset(hours, minutes).
Note: DST is not handled automatically. Pick the appropriate code (e.g.
EDTvsEST_USA) for the date range you care about.
use dtt::prelude::*;
use time::UtcOffset;
let now = DateTime::new(); // current UTC
let utc = DateTime::new_with_tz("UTC")?; // explicit
let mumbai = DateTime::new_with_tz("IST_INDIA")?; // disambiguated
let custom = DateTime::new_with_custom_offset(5, 30)?; // +05:30
let exact = DateTime::from_components(2024, 1, 15, 10, 30, 0, UtcOffset::UTC)?;
let epoch = DateTime::default(); // 1970-01-01T00:00:00Z
// Builder pattern
let dt = DateTimeBuilder::new()
.year(2024).month(1).day(15)
.hour(10).minute(30).second(0)
.offset(UtcOffset::UTC)
.build()?;
# Ok::<(), AppError>(())# use dtt::prelude::*;
let dt1 = DateTime::parse("2024-01-15T10:30:00Z")?;
let dt2 = DateTime::parse("2024-01-15T10:30:00+05:30")?;
let dt3 = DateTime::parse("2024-01-15")?; // date-only OK
let custom = DateTime::parse_custom_format(
"15/01/2024 10:30",
"[day]/[month]/[year] [hour]:[minute]",
)?;
let s: String = dt1.format_rfc3339()?;
let pretty = dt1.format("[year]-[month]-[day]")?;
# Ok::<(), AppError>(())# use dtt::prelude::*;
let dt = DateTime::parse("2024-01-31T00:00:00Z")?;
let next_day = dt.next_day()?;
let prev_day = dt.previous_day()?;
let next_week = dt.add_days(7)?;
let next_feb = dt.add_months(1)?; // → 2024-02-29 (leap year aware)
let next_year = dt.add_years(1)?;
let monday = dt.start_of_week()?;
let sunday = dt.end_of_week()?;
let last_day = dt.end_of_month()?;
# Ok::<(), AppError>(())use dtt::prelude::*;
use dtt::{dtt_now, dtt_parse, dtt_add_days, dtt_diff, dtt_diff_seconds};
let now = dtt_now!();
let dt = dtt_parse!("2024-01-15T10:30:00Z")?;
let later = dtt_add_days!(dt, 7)?;
let secs: Option<i64> = dtt_diff_seconds!("1609459200", "1609459230");
assert_eq!(secs, Some(30));
# Ok::<(), AppError>(())Clone, build, and verify in under a minute on any platform:
git clone https://github.com/sebastienrousseau/dtt.git
cd dtt
make verify # fmt-check + lint + test in one command
make help # full task listThe Makefile is a thin wrapper around the underlying Cargo commands,
so the equivalent direct invocations also work:
cargo build # build the library and binary
cargo test # run all 240+ tests
cargo clippy --all-targets -- -D warnings # lint with strict warnings
cargo fmt --check # verify formatting
cargo doc --no-deps --open # open API docs in your browser
cargo run --example dtt # run the end-to-end demo
cargo bench # run criterion benchmarksAll commands work identically on macOS, Linux, and WSL. CI exercises
the same matrix on Linux, macOS, and Windows on every PR via
.github/workflows/cross-platform.yml.
| Symptom | Likely cause | Fix |
|---|---|---|
feature 'edition2024' is required from time-core |
Rust < 1.88.0 | rustup update stable |
Err(InvalidTimezone) for "EST", "CST", "IST" |
Bare ambiguous codes are rejected by design | Use a suffixed form (e.g. EST_USA, IST_INDIA) |
Err(InvalidFormat) parsing "2024-01-01T12:00:00" |
RFC 3339 requires an offset | Append Z or +HH:MM |
Err(InvalidTimezone) from new_with_custom_offset(5, -30) |
Mixed-sign offsets are rejected | Pass same-sign components, e.g. (4, 30) |
Err(InvalidDate) from from_components(10000, ...) |
time::Date only supports -9999..=9999 |
Use a year inside that range |
| Tests fail with environment-variable race | cargo test runs tests in parallel |
Already mitigated via serial_test; use cargo test -- --test-threads=1 if you have a custom env-var test |
- API reference: https://docs.rs/dtt
- End-to-end example:
examples/dtt.rs - Benchmarks:
benches/criterion.rs— run withcargo bench - Changelog:
CHANGELOG.md - Security policy:
.github/SECURITY.md - Code of conduct:
.github/CODE-OF-CONDUCT.md
Contributions are welcome. Please read CONTRIBUTING.md — in particular, all commits must be cryptographically signed (git commit -S).
Quick checklist before opening a PR:
cargo fmt
cargo clippy --all-targets -- -D warnings
cargo test
git commit -S -m "feat(dtt): your conventional commit message"THE ARCHITECT ᴬ Sebastien Rousseau THE ENGINE ᵞ EUXIS ᴬ Enterprise Unified Execution Intelligence System
Dual-licensed under Apache 2.0 or MIT, at your option.