From 5ec2d93446ae890e164dd8ad33602a09c0d8814e Mon Sep 17 00:00:00 2001 From: Mica White Date: Sat, 5 Sep 2026 21:29:35 -0400 Subject: First commit --- src/blog/datetime.kuht | 518 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 src/blog/datetime.kuht (limited to 'src/blog/datetime.kuht') 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 @@ + + + + Comparing Date Types Across Languages + + + + +
+ +

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. +

+ +
+ +
+ + + + -- cgit v1.3.1