Announcing Timetraveler

One of my current projects at work is to gather data from several different platforms and combine them together to form a holistic view.
Since I am a lazy developer I tend to use available client projects whenever I can. This time this means that I'm using both the Azure DevOps Rust API and the Octocrab crates to communicate with Azure DevOps and GitHub respectively.
Using client libraries that someone else has already written is very convenient, however they usually make their own technology choices. As an example, Azure DevOps Rust API is built with TypeSpec, which uses Reqwest for HTTP calls, while Octocrab uses Hyper.
Apart from increasing the number of dependencies of the project and having to compile both Reqwest and Hyper, this doesn't actually mean that much to me.
What means something though, is their respective choices of date and time handling.
TypeSpec has chosen Time for representing dates and times in the response structs, whereas Octocrab has chosen Chrono.
Azure DevOps was the first service I integrated with, so since that client uses Time, I went with that for the internal model as well.
This means that I can't use the Chrono values from Octocrab directly, but have to convert them to Time first.
Chrono vs Time
I'm not going to go into that much details here; if you're here because you can't choose between the libraries, have a look at their respective API documentations and pick the one you like the best—they both cover more or less the same functionality, are actively maintained, and you probably can't go wrong with either.
That said, here are some differences that I find significant:
- Time has a macro for quickly creating OffsetDateTimes. This is a godsend when writing tests. However, I don't like that it doesn't accept "full" ISO8601-formatted strings (e.g. 2025-10-07T07:24:35.325+02:00)
- Chrono has support for actual time zones whereas Time only supports fixed offsets (i.e. no automatic adjustment of daylight savings time).
- Chrono uses strftime-compatible/inspired parsing and formatting syntax. Time uses its own custom format—which is arguably nicer, but also less familiar.
Conversion
Anyways, I was stuck with using both of them and found myself needing to convert between Chrono's DateTime<Utc> and Time's OffsetDateTime. And I need to do this in both directions since I need to pass arguments to the client where the source data is OffsetDateTimes and then convert DateTime<Utc> in the response to OffsetDateTime so I can store it in the internal model.
After searching around a bit and determining that neither crate has support for the other (which I didn't expect them to have), I concluded that I had to write my own conversion functions.
Seeing as I'm probably not alone in having this issue, I decided to do it properly as a utility crate and publish it for others to use.
And this brings us here.
Announcing Timetraveler
Timetraveler is a crate for converting between the different equivalent structs in Chrono and Time while retaining as much information as possible.
Here's a quick example:
use timetraveler::{chrono::AsDateTime, time::AsOffsetDateTime};
use time::{macros::datetime, OffsetDateTime};
use chrono::{DateTime, Utc, FixedOffset};
use chrono_tz::{Tz, Europe::Stockholm};
use rxpect::{expect, expectations::EqualityExpectations};
// A time::OffsetDateTime
let dt = datetime!(2024-01-01 12:00:00 +02:00);
// Convert to chrono::DateTime<Stockholm>
let utc: DateTime<Utc> = dt.as_date_time_utc();
expect(utc.to_rfc3339().as_ref()).to_equal("2024-01-01T10:00:00+00:00");
// Convert to chrono::DateTime<FixedOffset>
let offset: DateTime<FixedOffset> = dt.as_date_time_offset();
expect(offset.to_rfc3339().as_ref()).to_equal("2024-01-01T12:00:00+02:00");
// Convert to chrono::DateTime<Tz> - i.e. a specific timezone
let stockholm: DateTime<Tz> = dt.as_date_time(Stockholm);
expect(stockholm.to_rfc3339().as_ref()).to_equal("2024-01-01T11:00:00+01:00");
// Convert back to time::OffsetDateTime
let roundtrip: OffsetDateTime = stockholm.as_offset_date_time();
expect(roundtrip).to_equal(dt);
This starts out with an OffsetDateTime from Time and then converts it to Chrono's DateTime with three different time zones: UTC, fixed offset (e.g. east 2 hours), and the actual time zone Europe/Stockholm. Finally it is converted from DateTime<Tz> back into an OffsetDateTime.
Note that since we convert to the Europe/Stockholm time zone, the final OffsetDateTime will have a different offset than the one we started with—1 hour east instead of 2 hours east—the time it represents will still be the same though. If we would have converted the DateTime<FixedOffset> instead, the offset would remain intact.
There are conversions for "naive" dates, times and date times as well (i.e. without time zones and offsets).
At the moment Timetraveler only supports conversion between Chrono and Time, but in the future more crates can definitely be added.
The crate itself only has two dependencies: Chrono and Time, so if you're already using both of them, adding Timetraveler should not bloat your project noticeably.
$ cargo add timetraveler
Enjoy!