diff options
Diffstat (limited to 'src/blog/datetime.kuht')
| -rw-r--r-- | src/blog/datetime.kuht | 518 |
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><time.h></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(&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>; +} +</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<Tz: TimeZone> { + 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: &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, +} +</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> |
