On Java, dates, calendars and time zones
Today marks the second time I write about my job. It won’t be as big an article as last time, when I wrote about our use of Agile processes at one of my jobs. This time it is about the wonders a small selection of the wonders of the Java Date API. Not the nice-ish one from Java 8, but the regular old one. In particular, this post will be about the early nineteen hundreds, Hitler actually doing something right, and my innate tendency to do a one-eighty and run like hell whenever the word timezone is mentioned when I am near.
This article I should say is about Dutch dates in particular, but I’m sure each country/timezone has its own little quirks like the ones mentioned here. My current job requires me to process birth dates. It also involves a chunk of legacy software, or software I have to use that is built and maintained by other people. One such library contains a bunch of POJO’s, with properties, getters, setters, the occasional constructor and some utility methods. Someone actually took the time to put all this stuff in a separate artifact to promote reuse. Neat, or so you’d think. But I’ll get to that later.
Now we all know how to parse a String to a Date in Java, and modify it a bit. And we also know the greatness of the Java Calendar API. But in case you do not, let me give you an example of this epic fail of an API:
String valueToParse = "1985-11-23 11:38:43";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date result = df.parse(valueToParse);
Calendar calendar = Calendar.getInstance();
calendar.setTime(result);
calendar.add(Calendar.HOUR, -1);
// at this point the time is one hour earlier,
Date oneHourBack = calendar.getTime();
So what is wrong here? Well for one the getInstance() implies singleton, yet the Calendar class is an abstract one, and one can create as many extension class instances (e.g. GregorianCalendar) as one would like. Second, using a setTime() method to set the time AND DATE. And third of course is the mindbogglingly idiotic call of add(-1) to subtract – in this case – one hour. Finally, to get back a Date object, we call getTime(). This whole API makes no sense at all, but that’s really not what this post is supposed to be about.
Back to the code base and our wonderful setBirthDate(Date birthDate) method. Now when dealing with a birthday, one generally does not care about time information, it is the day that matters. Unfortunately, Java has no Date/Time/DateTime classes, just Date which contains everything. Our lovely setter decided to fix all this by calling DateUtil.removeTimeData(birthDate); before setting the value in the POJO. This util method looked something along the lines of:
Calendar calendar = Calendar.getInstance();
// so February 30th triggers an exception
calendar.setLenient(false);
calendar.setTime(birthDate);
calendar.set(Calendar.HOUR, 0);
// same for minutes, seconds, millis
return calendar.getTime();
Okay, well… okay. Not something I would have done, but what could possibly go wrong, right? Right?! Well nothing really ever went wrong, until one day the system said kaboom with this wonderfully descriptive error message: IllegalArgumentException: HOUR: 0 -> 1. Then another day it said something similar: IllegalArgumentException: SECOND: 0 -> 28
What the heck is going on here? To understand the problem, here is a short history lesson. Back to May first, 1908. Someone must have thought, wouldn’t it be great to create a new timezone? Yes, the imaginary crowd shouted! And so Amsterdam Time (AMT) was born *. Only applicable to the Netherlands, and twenty minutes ahead of GMT. Oh the horror… Which is what Hitler must have thought back in 1940, just after occupying my little country. This is when the man actually had a decent idea. Get rid of AMT and join the CET club. He also gave us daylight saving time that same day, May 16th. Can’t do everything right…
* to be fair (but only a little), before that day, times differed from town to town, and that was even worse
What does this mean though? Well, on May 16th 1940, the day did not start at 00:00:00, but at 01:40:00. This means that day did not have midnight and some time after. So, when you try to tell your code to set the time to 00:00:00 it finds that said time does not exist and throws the exception. Do note that this only happens when lenient is set to false. If it is set to true, it will pick the first available moment after 23:59:59, being 01:40:00. After the war was won, we decided not to switch back to AMT, so that timezone sort of got lost in time.
That explains the HOUR 0 -> 1 thing. The other one is similar, but on a smaller scale. On Juli 1, 1937 00:00:00 to 00:00:27 do not exist. Fuck knows why, but they don’t. So a similar exception occurs, and the code breaks. Unfortunately we were dealing with shared legacy code, so several decent solutions were just not feasible. One could have opted to deal only with UTC dates, or introduce a proper API like Jodatime, or remove the stupid time reset. We ended up writing a hacky method that set lenient to true on both of the problematic dates. Ugh..
If you would like to see this thing in action, simply put the following code in a test case or main method:
System.setProperty("user.timezone", "Europe/Amsterdam");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
// or"1940-05-16 10:10:10 CET"
String valueToParse = "1937-07-01 10:10:10 CET";
Date date = df.parse(valueToParse);
Calendar calendar = Calendar.getInstance();
calendar.setLenient(false);
calendar.setTime(date);
calendar.set(Calendar.HOUR, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
System.out.println(calendar.getTime());
Afterthought
Now wouldn’t it be great if we just got rid of time zones altogether. It would take some getting used to, but wouldn’t it be nice if it were the same time everywhere? Sure, in some places it would mean it would be pitch dark outside at 2 PM, but who cares really? Imagine being able to tell someone on the other side of the planet to call you at 18:00, and that that person could pick up the phone at 18:00 and make the call. No thinking involved. Easy, right?