Comparing Date Types Across Languages
Every language uses a different API to represent types. To put it mildly, some of them are better than others. This post will compare the best and worst APIs, for both informative and entertainment purposes.
C
C has multiple ways of representing time, depending on which version you use,
and what operating system you have. I'm not going to bother to look at the
Win32
or POSIX
APIs. Instead I'll focus on plain, simple <time.h>.
C89 has the `time` function, which returns a
time_t.
The specification doesn't say what the type looks like, but it's usually an
integer counting the number of seconds since the UNIX
epoch1.
On its own, this isn't very useful. How would you get information like the
current year, or the current hour? Luckily, C also provides the
gmtime
and localtime
functions, which convert the time_t into a
tm.
struct tm {
int tm_sec; // seconds after the minute [0, 61?]
int tm_min; // minutes after the hour [0, 59]
int tm_hour; // hours since midnight [0, 23]
int tm_mday; // day of the month [1, 31]
int tm_mon; // months since January [0, 11]
int tm_year; // years since 1900
int tm_wday; // days since Sunday [0, 6]
int tm_yday; // days since January 1st [0, 365]
// positive is Daylight Savings Time is in effect,
// zero if not, negative if unknown
int tm_isdst;
};
There was a mistake in the initial specification where you were allowed to have 62 seconds in a minute. They remembered that leap seconds2 exist, but forgot about inclusive ranges. This was fixed in C11.
There's also no way to represent either just the date or just the time. If you want to do either of those things, you'll either have to set some arbitrary day, or define your own structure.
There's also no time zone information whatsoever here. There's the two
functions to create a tm for the local timezone and UTC, but it's
impossible to tell, just by looking at this structure, which of the two
functions were used to create it. They did, however, include the Daylight
Savings Time information as a nullable boolean.
Overall, I'm not a big fan of this.
JavaScript
JavaScript's date-time class is called Date,
despite also holding information about time. It was copied almost directly from
Java's Date
class, which was almost entirely obsoleted by JDK 1.1 with the
Calendar
type, for good reason.
Let's start with the constructor. Here it is, according to MDN:
new Date() new Date(value) new Date(dateString) new Date(dateObject) new Date(year, monthIndex) new Date(year, monthIndex, day) new Date(year, monthIndex, day, hours) new Date(year, monthIndex, day, hours, minutes) new Date(year, monthIndex, day, hours, minutes, seconds) new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds) Date()
Note that calling `Date()` without the new keyword is equivalent
to new Date().toString(). I wonder how many bugs have been caused
by accidentally passing a string into the constructor instead of a number.
If you pass a year in the range of [0, 99], the year will be
translated into the 20th century. You might think, "Oh, is that because the
Date class doesn't support dates from that long ago?" No. This
class supports flawless millisecond precision between the years from
271,822 BCE to 175,760 CE, but cannot construct an object for 67 CE without a
workaround.
You might also wonder why MDN uses the term, monthIndex instead
of just, you know, month. Well, every other field looks natural.
For example, January 1st uses 1 for the day. But in the case of
month, 1 represents February, and 0 represents
January. So anyone who naively writes the following will have a bug.
`${date.getMonth()}/${day.getDay()}/${day.getFullYear()}`
And now I've just opened another can of worms. What is that
getFullYear method? JavaScript does have a getYear
method, but it's deprecated. It returns the year, minus 1900. The year 2025
will be returned as 125. The year 1812 is returned as -88.
You may be wondering if that constructor with the year, month, day, hours,
minutes, seconds and milliseconds has the ability to select a timezone. It does
not. In fact, Date contains no timezone information whatsoever in
the object itself. It will always be local time. If you want UTC, you can use
new Date(Date.UTC(year, monthIndex, day, hours, minutes, seconds, milliseconds)).
Because of all of these problems, many JavaScript users decide to ignore the
built-in Date API entirely, and use a library like
moment, which will usually add at least 18
KB to your site's download size, which is bigger than
some web frameworks.
More recently, we've been granted the Temporal API, but it's not available in
all browsers yet, and the specification is still a draft. I won't get into the
specifics right now in case some of this information becomes out of date. But
there will probably be multiple classes, including Duration,
Instant, PlainDate, and ZonedDateTime.
These types will support nanosecond precision, and the ZonedDateTime
should support proper timezones like "Asia/Shanghai".
C#
This is the language that inspired me to write this blog post.
In the first versions of .NET, there were two structures related to date and
time: DateTime,
and DateTimeOffset.
These structures, unlike the ones we've seen so far, are pretty well named.
DateTime includes a date and a time. If you need a timezone,
DateTimeOffset includes the date, time, and timezone offset. More
on that later.
There is also a DayOfWeek enum. Perfect! This is probably the best
way of representing days of the week3.
Let's look at some constructors:
DateTime(int year, int month, int day); DateTime(int year, int month, int day, int hour, int minute, int second); DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond); DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, DateTimeKind kind);
Not too bad, but I've omitted some overloads for brevity. Since the language is
statically typed, there's no way to accidentally pass in a string, and there's
no overload for a string. Instead you would use the static method,
DateTime.Parse(string).
Now you might wonder what that
DateTimeKind
is. Remember how I said that you use DateTimeOffset for timezones?
That's not true. DateTime can also use a timezone, but I don't
think anyone uses it this way. Let's look at the DateTimeKind enum.
enum DateTimeKind {
Unspecified,
Utc,
Local
}
Hmm. So it can represent timezones, but only the local timezone and UTC. That's annoying. What if my users are in a different timezone than the server? Tough luck.
Or no? There's the DateTimeOffset struct. Let's just use that!
Except, as you might have suspected, it is insufficient. The constructors for
DateTimeOffset are similar to the constructors for
DateTime, except for no DateTimeKind, and in their
place is a
TimeSpan
struct.
DateTimeOffset(DateTime dateTime, TimeSpan offset); DateTimeOffset(int year, int month, int day, int hours, int minutes, int seconds, TimeSpan offset);
That TimeSpan doesn't seem like a very good timezone, and indeed
it is not. You can't specify a timezone like "America/New_York". Instead, you
specify `UTC-6`. When New York goes into daylight savings time, then you also
need to update the offset4.
Now we're getting back into the problems with JavaScript's Date
class. There's no timezone information whatsoever, and the timezone information
that we can provide is worse than useless. I'm told that most people who have
to work with time in C# use an external library, but this wasn't the case at the
company I worked at. At least in this case, your users aren't forced to
download the library5.
You may have noticed that, unlike with DateTime, there's no way to
create a DateTimeOffset without specifying the time. There's also
no way to create a DateTime without specifying a date. You could
argue that this is a good thing, since a date time should include both
a date and a time. But for a long time, there was no
alternative.
You might look through the documentation and get excited, because of the
Date property, which presumably returns a new structure I hadn't
mentioned yet which only contains the date information. Unfortunately, this
property is completely useless. It returns the same DateTime, but
with the time set to midnight. There's also a TimeOfDay property
which returns a TimeSpan representing the time that has elapsed
since midnight.
Fortunately, in .NET 6, we got the
DateOnly
and
TimeOnly
structs. These do exactly what you think they would do.
DateOnly(int year, int month, int day); TimeOnly(int hour); TimeOnly(int hour, int minute); TimeOnly(int hour, int minute, int second); TimeOnly(int hour, int minute, int second, int millisecond); TimeOnly(int hour, int minute, int second, int millisecond, int microsecond);
There is a caveat here, though. The DateTime.Date property still
doesn't return a DateOnly. A part of me hoped that after these
types were introduced, a breaking change to the language could be made to
replace the completely useless property. Alas, we are stuck with that.
Rust
Of course, I have to talk about Rust. What does Rust do? Let's look at the
time module. It includes three types worth caring about:
Duration,
Instant, and
SystemTIme.
The behavior of Duration should be obvious, but you may wonder
what Instant and SystemTime are. Let's start with
SystemTime.
pub struct SystemTime(/* private fields */);
impl SystemTime {
const UNIX_EPOCH: SystemTime;
fn now() -> SystemTime;
fn duration_since(&self, earlier: Self) -> Result<Duration>;
fn elapsed() -> Result<Duration>;
fn checked_add(&self, duration: Duration) -> Option<Self>;
fn checked_sub(&self, duration: Duration) -> Option<Self>;
}
And that's it! What? You were expecting more? This is every method implemented
on SystemTime outside of traits. No formatting, no figuring out
the current year, just that.
Ok, surely Instant must be more useful, right? Nope. It's actually
the same as SystemTime, except it is monotonically
increasing6. What is this?
The Rust standard library is small, on purpose. They don't include features unless the developers are confident in both the API and its utility. The other languages in this post should make it obvious that this is a difficult feature to make a good API for. So it's better to not include dates and times in the standard library, and just let external libraries handle that.
On the other hand, many low-level system APIs do require some time information.
For example, the Metadata struct contains the time when a file was
last modified. So, there needs to be an Instant struct, but it is
very small, and mostly just a wrapper around the values used by the system
calls.
That being said, I do want to talk about a Rust library that I personally like.
My favorite is chrono.
I mostly just want to talk about the
DateTime
type. Needless to say, it has much more functionality than the
SystemTime type, so I won't go over all of it. But I do want to
show the declaration.
struct DateTime<Tz: TimeZone> {
datetime: NaiveDateTime,
offset: Tz::Offset.
}
That's different. You might correctly guess that
NaiveDateTime
is just a date and a time with no timezone information. But what's that
TimeZone trait?
trait TimeZone: Sized + Clone {
type Offset: Offset;
fn from_offset(offset: &Self::Offset) -> Self;
fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<Self::Offset>;
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<Self::Offset>;
fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset;
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset;
}
trait Offset: Sized + Clone + Debug {
fn fix(&self) -> FixedOffset;
}
enum MappedLocalTime<T> {
Single(T),
Ambiguous(T, T),
None,
}
This is far more complex than the timezone representation in C#. We do see the
Offset
trait in there, but there's more to it than that. The TimeZone
trait includes several methods for getting the offset from UTC for a given date
and time. So, we can have different offsets at different dates. Finally, we can
transparently handle daylight savings time for timezones other than the local
timezone! And since the timezone is a generic type, we can easily infer from
the types what the timezone is going to be, rather than having to look at how
the object was constructed.
The chrono crate by default includes three timezones:
FixedOffset,
Local,
and Utc.
This is already as good as what we were provided in C#. But remember that
TimeZone is a trait that we can implement ourselves. I recommend
importing the chrono-tz
crate, which includes every time zone under the sun. There's also a generic
Tz
enum, which can represent any timezone if you need it. The implementors of the
trait need not be empty structs.
This is, by far, the best implementation of a time API I've seen anywhere. I'm sure there are libraries for other languages which do the same thing, and I recommend trying them out.
The downside to chrono is that, at time of writing, it's
unmaintained. Hopefully a new maintainer will take it over some day soon. For
now, jiff is the
most popular maintained time crate for Rust. It takes heavy inspiration from
JavaScript's new Temporal API that we talked about earlier. It's no chrono,
but it gets the job done, and they're approaching a 1.0 release.
Conclusion
My conclusion is my own opinion. You may have one that differs from mine. But here's what I like to see:
- Handline of timezones
- The timezones cannot be plain offsets from UTC
- Even better: have an implementable `TimeZone` interface that records the timezone information in the type
- Enums for days of the week and months are great
- When passing in numbers as months, use 1 for January
- Higher resolution is better
Hopefully this will inspire you to either go out and see what other time libraries are out there, or make one yourself.