summaryrefslogtreecommitdiff
path: root/src/blog/datetime.kuht
diff options
context:
space:
mode:
Diffstat (limited to 'src/blog/datetime.kuht')
-rw-r--r--src/blog/datetime.kuht518
1 files changed, 518 insertions, 0 deletions
diff --git a/src/blog/datetime.kuht b/src/blog/datetime.kuht
new file mode 100644
index 0000000..38bceff
--- /dev/null
+++ b/src/blog/datetime.kuht
@@ -0,0 +1,518 @@
+<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>