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:

Hopefully this will inspire you to either go out and see what other time libraries are out there, or make one yourself.