summaryrefslogtreecommitdiff
path: root/src/blog/datetime.kuht
blob: 352708bc6c6035a7035b7e4e48d61fb1388be6b1 (plain)
<import "base.kuht" as "base" />

<head>
	<title>Comparing Date Types Across Languages</title>
	<meta name="description" content="Every language has some way of representing time. Some of them are better than others." />
</head>

<body>
<article>

<h1>Comparing Date Types Across Languages</h1>

<p>
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.
</p>

<h2>C</h2>

<p>
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
<a href="https://learn.microsoft.com/en-us/windows/win32/api/windows.foundation/ns-windows-foundation-datetime">Win32</a>
or <a href="https://www.man7.org/linux/man-pages/man2/gettimeofday.2.html">POSIX</a>
APIs. Instead I'll focus on plain, simple <code>&lt;time.h&gt;</code>.
</p>

<p>
C89 has the `time` function, which returns a
<a href="https://cppreference.com/w/c/chrono/time_t.html"><code>time_t</code></a>.
The specification doesn't say what the type looks like, but it's usually an
integer counting the number of seconds since the UNIX
epoch<a id="af-1" href="#footnote-1"><sup>1</sup></a>.
</p>

<p>
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
<a href="https://cppreference.com/w/c/chrono/gmtime.html"><code>gmtime</code></a>
and <a href="https://cppreference.com/w/c/chrono/localtime.html"><code>localtime</code></a>
functions, which convert the <code>time_t</code> into a
<a href="https://cppreference.com/w/c/chrono/tm.html"><code>tm</code></a>.
</p>

<pre>
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;
};
</pre>

<p>
There was a mistake in the initial specification where you were allowed to have
62 seconds in a minute. They remembered that leap
seconds<a id="af-2" href="#footnote-2"><sup>2</sup></a> exist, but forgot
about inclusive ranges. This was fixed in C11.
</p>

<p>
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.
</p>

<p>
There's also no time zone information whatsoever here. There's the two
functions to create a <code>tm</code> 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.
</p>

<p>
Overall, I'm not a big fan of this.
</p>

<h2>JavaScript</h2>

<p>
JavaScript's date-time class is called <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date"><code>Date</code></a>,
despite also holding information about time. It was copied almost directly from
Java's <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Date.html"><code>Date</code></a>
class, which was almost entirely obsoleted by JDK 1.1 with the
<a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Calendar.html"><code>Calendar</code></a>
type, for good reason.
</p>

<p>
Let's start with the constructor. Here it is, according to MDN:
</p>

<pre>
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()
</pre>

<p>
Note that calling `Date()` without the <code>new</code> keyword is equivalent
to <code>new Date().toString()</code>. I wonder how many bugs have been caused
by accidentally passing a string into the constructor instead of a number.
</p>

<p>
If you pass a year in the range of <code>[0, 99]</code>, the year will be
translated into the 20th century. You might think, "Oh, is that because the
<code>Date</code> 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.
</p>

<p>
You might also wonder why MDN uses the term, <code>monthIndex</code> instead
of just, you know, <code>month</code>. Well, every other field looks natural.
For example, January 1st uses <code>1</code> for the day. But in the case of
month, <code>1</code> represents February, and <code>0</code> represents
January. So anyone who naively writes the following will have a bug.
</p>

<pre>
`${date.getMonth()}/${day.getDay()}/${day.getFullYear()}`
</pre>

<p>
And now I've just opened another can of worms. What is that
<code>getFullYear</code> method? JavaScript does have a <code>getYear</code>
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.
</p>

<p>
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, <code>Date</code> contains no timezone information whatsoever in
the object itself. It will always be local time. If you want UTC, you can use
<code>new Date(Date.UTC(year, monthIndex, day, hours, minutes, seconds, milliseconds))</code>.
</p>

<p>
Because of all of these problems, many JavaScript users decide to ignore the
built-in <code>Date</code> API entirely, and use a library like
<a href="https://momentjs.com/">moment</a>, which will usually add at least 18
KB to your site's download size, which is bigger than
<a href="https://www.solidjs.com/">some web frameworks</a>.
</p>

<p>
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 <code>Duration</code>,
<code>Instant</code>, <code>PlainDate</code>, and <code>ZonedDateTime</code>.
These types will support nanosecond precision, and the <code>ZonedDateTime</code>
should support proper timezones like <code>"Asia/Shanghai"</code>.
</p>

<h2>C#</h2>

<p>
This is the language that inspired me to write this blog post.
</p>

<p>
In the first versions of .NET, there were two structures related to date and
time: <a href="https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-10.0"><code>DateTime</code></a>,
and <a href="https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset?view=net-10.0"><code>DateTimeOffset</code></a>.
These structures, unlike the ones we've seen so far, are pretty well named.
<code>DateTime</code> includes a date and a time. If you need a timezone,
<code>DateTimeOffset</code> includes the date, time, and timezone offset. More
on that later.
</p>

<p>
There is also a <code>DayOfWeek</code> enum. Perfect! This is probably the best
way of representing days of the week<a id="af-3" href="#footnote-3"><sup>3</sup></a>.
</p>

<p>
Let's look at some constructors:
</p>

<pre>
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);
</pre>

<p>
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,
<code>DateTime.Parse(string)</code>.
</p>

<p>
Now you might wonder what that
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.datetimekind?view=net-10.0"><code>DateTimeKind</code></a>
is. Remember how I said that you use <code>DateTimeOffset</code> for timezones?
That's not true. <code>DateTime</code> can also use a timezone, but I don't
think anyone uses it this way. Let's look at the <code>DateTimeKind</code> enum.

<pre>
enum DateTimeKind {
	Unspecified,
	Utc,
	Local
}
</pre>

<p>
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.
</p>

<p>
Or no? There's the <code>DateTimeOffset</code> struct. Let's just use that!
Except, as you might have suspected, it is insufficient. The constructors for
<code>DateTimeOffset</code> are similar to the constructors for
<code>DateTime</code>, except for no <code>DateTimeKind</code>, and in their
place is a
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.timespan?view=net-10.0"><code>TimeSpan</code></a>
struct.
</p>

<pre>
DateTimeOffset(DateTime dateTime, TimeSpan offset);
DateTimeOffset(int year, int month, int day, int hours, int minutes, int seconds, TimeSpan offset);
</pre>

<p>
That <code>TimeSpan</code> 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 offset<a id="af-4" href="#footnote-4"><sup>4</sup></a>.
</p>

<p>
Now we're getting back into the problems with JavaScript's <code>Date</code>
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 library<a id="af-5" href="#footnote-5"><sup>5</sup></a>.
</p>

<p>
You may have noticed that, unlike with <code>DateTime</code>, there's no way to
create a <code>DateTimeOffset</code> without specifying the time. There's also
no way to create a <code>DateTime</code> without specifying a date. You could
argue that this is a good thing, since a <em>date time</em> should include both
a <em>date</em> and a <em>time</em>. But for a long time, there was no
alternative.
</p>

<p>
You might look through the documentation and get excited, because of the
<code>Date</code> 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 <code>DateTime</code>, but
with the time set to midnight. There's also a <code>TimeOfDay</code> property
which returns a <code>TimeSpan</code> representing the time that has elapsed
since midnight.
</p>

<p>
Fortunately, in .NET 6, we got the
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.dateonly?view=net-10.0"><code>DateOnly</code></a>
and
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.timeonly?view=net-10.0"><code>TimeOnly</code></a>
structs. These do exactly what you think they would do.
</p>

<pre>
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);
</pre>

<p>
There is a caveat here, though. The <code>DateTime.Date</code> property still
doesn't return a <code>DateOnly</code>. 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.
</p>

<h2>Rust</h2>

<p>
Of course, I have to talk about Rust. What does Rust do? Let's look at the
<code>time</code> module. It includes three types worth caring about:
<a href="https://doc.rust-lang.org/stable/std/time/struct.Duration.html"><code>Duration</code></a>,
<a href="https://doc.rust-lang.org/stable/std/time/struct.Instant.html"><code>Instant</code></a>, and
<a href="https://doc.rust-lang.org/stable/std/time/struct.SystemTime.html"><code>SystemTIme</code></a>.
The behavior of <code>Duration</code> should be obvious, but you may wonder
what <code>Instant</code> and <code>SystemTime</code> are. Let's start with
<code>SystemTime</code>.
</p>

<pre>
pub struct SystemTime(/* private fields */);

impl SystemTime {
	const UNIX_EPOCH: SystemTime;
	
	fn now() -> SystemTime;
	fn duration_since(&amp;self, earlier: Self) -> Result&lt;Duration&gt;;
	fn elapsed() -> Result&lt;Duration&gt;;
	fn checked_add(&amp;self, duration: Duration) -> Option&lt;Self&gt;;
	fn checked_sub(&amp;self, duration: Duration) -> Option&lt;Self&gt;;
}
</pre>

<p>
And that's it! What? You were expecting more? This is every method implemented
on <code>SystemTime</code> outside of traits. No formatting, no figuring out
the current year, just that.
</p>

<p>
Ok, surely <code>Instant</code> must be more useful, right? Nope. It's actually
the same as <code>SystemTime</code>, except it is monotonically
increasing<a id="af-6" href="#footnote-6"><sup>6</sup></a>. What is this?
</p>

<p>
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.
</p>

<p>
On the other hand, many low-level system APIs do require some time information.
For example, the <code>Metadata</code> struct contains the time when a file was
last modified. So, there needs to be an <code>Instant</code> struct, but it is
very small, and mostly just a wrapper around the values used by the system
calls.
</p>

<p>
That being said, I do want to talk about a Rust library that I personally like.
My favorite is <a href="https://docs.rs/chrono/latest/chrono/index.html"><code>chrono</code></a>.
I mostly just want to talk about the
<a href="https://docs.rs/chrono/latest/chrono/struct.DateTime.html"><code>DateTime</code></a>
type. Needless to say, it has much more functionality than the
<code>SystemTime</code> type, so I won't go over all of it. But I do want to
show the declaration.
</p>

<pre>
struct DateTime&lt;Tz: TimeZone&gt; {
	datetime: NaiveDateTime,
	offset: Tz::Offset.
}
</pre>

<p>
That's different. You might correctly guess that
<a href="https://docs.rs/chrono/latest/chrono/trait.Offset.html"><code>NaiveDateTime</code></a>
is just a date and a time with no timezone information. But what's that
<a href="https://docs.rs/chrono/latest/chrono/trait.TimeZone.html"><code>TimeZone</code></a> trait?
</p>

<pre>
trait TimeZone: Sized + Clone {
	type Offset: Offset;
	
	fn from_offset(offset: &amp;Self::Offset) -> Self;
	fn offset_from_local_date(&amp;self, local: &amp;NaiveDate) -> MappedLocalTime&lt;Self::Offset&gt;;
	fn offset_from_local_datetime(&amp;self, local: &amp;NaiveDateTime) -> MappedLocalTime&lt;Self::Offset&gt;;
	fn offset_from_utc_date(&amp;self, utc: &amp;NaiveDate) -> Self::Offset;
	fn offset_from_utc_datetime(&amp;self, utc: &amp;NaiveDateTime) -> Self::Offset;
}

trait Offset: Sized + Clone + Debug {
	fn fix(&amp;self) -> FixedOffset;
}

enum MappedLocalTime&lt;T&gt; {
	Single(T),
	Ambiguous(T, T),
	None,
}
</pre>

<p>
This is far more complex than the timezone representation in C#. We do see the
<a href="https://docs.rs/chrono/latest/chrono/trait.Offset.html"><code>Offset</code></a>
trait in there, but there's more to it than that. The <code>TimeZone</code>
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.
</p>

<p>
The <code>chrono</code> crate by default includes three timezones:
<a href="https://docs.rs/chrono/latest/chrono/struct.FixedOffset.html"><code>FixedOffset</code></a>,
<a href="https://docs.rs/chrono/latest/chrono/struct.Local.html"><code>Local</code></a>,
and <a href="https://docs.rs/chrono/latest/chrono/struct.Utc.html"><code>Utc</code></a>.
This is already as good as what we were provided in C#. But remember that
<code>TimeZone</code> is a trait that we can implement ourselves. I recommend
importing the <a href="https://docs.rs/chrono-tz/0.10.4/chrono_tz/"><code>chrono-tz</code></a>
crate, which includes every time zone under the sun. There's also a generic
<a href="https://docs.rs/chrono-tz/0.10.4/chrono_tz/enum.Tz.html"><code>Tz</code></a>
enum, which can represent any timezone if you need it. The implementors of the
trait need not be empty structs.
</p>

<p>
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.
</p>

<p>
The downside to <code>chrono</code> is that, at time of writing, it's
unmaintained. Hopefully a new maintainer will take it over some day soon. For
now, <a href="https://docs.rs/jiff/latest/jiff"><code>jiff</code></a> 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.
</p>

<h2>Conclusion</h2>

<p>
My conclusion is my own opinion. You may have one that differs from mine. But
here's what I like to see:
</p>
 
<ul>
	<li>Handline of timezones</li>
	<li>The timezones cannot be plain offsets from UTC</li>
	<li>Even better: have an implementable `TimeZone` interface that records the timezone information in the type</li>
	<li>Enums for days of the week and months are great</li>
	<li>When passing in numbers as months, use 1 for January</li>
	<li>Higher resolution is better</li>
</ul>
 
<p>
Hopefully this will inspire you to either go out and see what other time
libraries are out there, or make one yourself.
</p>

</article>

<hr />

<footer>
	<ol>
		<li id="footnote-1">
			The <a href="https://en.wikipedia.org/wiki/Unix_time">UNIX epoch</a> is
			midnight, January 1st, 1970. <a class="return" href="#af-1">return</a>
		</li>
		<li id="footnote-2">
			A <a href="https://en.wikipedia.org/wiki/Leap_second">leap second</a> is when
			a minute contains 61 seconds. This is done from time to time to account for
			the slowing down of the rotation of the Earth. Unlike leap years, which
			happen at predictable times, leap seconds are decided by commitee. This
			makes them difficult to work with on computers, and most timestamps ignore
			them, even though the time format internally allows them to be represented.
			<a class="return" href="#af-2">return</a>
		</li>
		<li id="footnote-3">
			It's better if you use a language with good enums. C# enums are aliases for
			integers, which makes them somewhat less useful.
			<a class="return" href="#af-3">return</a>
		</li>
		<li id="footnote-4">
			I have discovered a bug caused by this before. There were other confounding
			factors in that case, but the bug occurred when daylight savings time
			changed. <a class="return" href="#af-4">return</a>
		</li>
		<li id="footnote-5">
			Assuming you're not converting your C# to WebAssembly and sending it to the
			browser, which is probably more common with C# than most other languages,
			but most people just use JavaScript.
			<a class="return" href="#af-5">return</a>
		</li>
		<li id="footnote-6">
			Monotonic time just means that the time never ever decreases. You might
			think that this should always be the case, but computers are complicated.
			<a class="return" href="#af-6">return</a>
		</li>
	</ol>
</footer>

</body>