bekkopen / NoCommons

The NoCommons library is a collection of helper classes for manipulation and validation of data specific to Norway and Norwegian citizens.
http://github.com/bekkopen/NoCommons/wiki/NoCommons
MIT License
71 stars 31 forks source link

NorwegianDateUtil.isHoliday does not handle non-Europe/Oslo timezones (e.g. jvm default timezone) #30

Open torgeir opened 5 years ago

torgeir commented 5 years ago

A test similar to the following fails on my end:

    @Test
    public void midnight_may17_is_a_norwegian_holiday_even_if_the_jvm_timezone_is_gmt() {
        System.setProperty("user.timezone", ZoneId.of("GMT").getId());

        ZonedDateTime may17 = 
            ZonedDateTime.parse("2018-05-17T00:00:00+02:00")
                .withZoneSameInstant(ZoneId.of("Europe/Oslo"));

        assertTrue(NorwegianDateUtil.isHoliday(Date.from(may17.toInstant())));
    }

I would argue that the 17th of may is a norwegian holiday even if the jvm timezone is GMT 😉

eivinhb commented 5 years ago

But you ask isHoliday with an date at 16.05.2018 22:00. How would you argue that its the date utils fault when you give it a date for the previous day?

eivinhb commented 5 years ago

I would be great to rework the api in NoCommons to use java.time.

torgeir commented 5 years ago

Because its the first hour of the 17th of may in norwegian time! as shown in the created ZonedDateTime in the test (i.e. when the clock is 16.05.2018 22:00 in London, norwegians would be celebrating the 17 of May)

eivinhb commented 5 years ago

But you enter what is equal to new Date(1526508000000L) and that is may 16th. So if you sendt that to the Calendar du get, it will say that its May 16.

If you can spot a code change in NoCommons, please do not hesitate to create a PR. :)

eivinhb commented 5 years ago
ZonedDateTime may17 = ZonedDateTime.parse("2018-05-17T00:00:00+02:00").withZoneSameInstant(ZoneId.of("Europe/Oslo")); 
// 2018-05-17T00:00+02:00[Europe/Oslo]

Instant instant = may17.toInstant(); // 2018-05-16T22:00:00Z
Date from = Date.from(instant); // Wed May 16 22:00:00 GMT 2018
assertTrue(NorwegianDateUtil.isHoliday(from)); // false
runeflobakk commented 5 years ago

new Date(1526508000000L) is not May 16 in Norway. The problem is that Date is only a container for millis since epoch, similar to Instant, and when converting it to some "human readable form", one must be very careful to provide the TimeZone.

To resolve anything involving the years, days, and so on from milliseconds from epoch, one must provide a TimeZone. If the API permits you to omit the TimeZone, some assumption must be made. The toString() you see in the comments in https://github.com/bekkopen/NoCommons/issues/30#issuecomment-461056231 must assume a time zone, and probably reaches for the JVM time zone user.timezone.

Here is a little more involved code example:

        ZoneId norwayZone = ZoneId.of("Europe/Oslo");
        long epochMillis = 1526508000000L;

        ZonedDateTime java8DateTime = Instant.ofEpochMilli(epochMillis).atZone(norwayZone);
        System.out.println(java8DateTime);  // 2018-05-17T00:00+02:00[Europe/Oslo]

        Date crapDate = new Date(epochMillis);
        Calendar crapCalendar = Calendar.getInstance(TimeZone.getTimeZone(norwayZone));
        crapCalendar.setTime(crapDate);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        format.setTimeZone(TimeZone.getTimeZone(norwayZone));
        System.out.println(format.format(crapCalendar.getTime()));  // 2018-05-17 00:00

Damn, I hate that old Date/Calendar API with a passion!

torgeir commented 5 years ago

My expectations here seem to be off with how this is actually implemented 😄

If this was a lib you queried for if a date was the 17th of may I would agree!

As this is a lib you query for whether a given date is a norwegian holiday or not, I expected the test I provided to pass, with the argument being that even if I bring my program over to the UK where the default jvm timezone would be GMT, the same point in linear time is still a norwegian holiday. But I see now that the code does not really take time zones into consideration. This is a pain with the old Date apis, and would probably be a lot easier to handle with the new java.time apis.

runeflobakk commented 5 years ago

I would argue that if I am in UK, it is 10PM on the 16th of May, and I query "is now a holiday in Norway", I would expect the answer to be "yes". So I agree with @torgeir's expectations.

Ignoring any Einsteinian relativistic effects 😄

eivinhb commented 5 years ago

Exactly, if you run a user.timezone on jvm as GMT, then new Date(1526508000000L) IS may 16. And that is all that is checked. The api needs to have more than a long to work with. NoCommons need pre adjusted dates.

runeflobakk commented 5 years ago

Yes, 1526508000000L is May 16 in GMT, but that is not relevant, because you are asking if 1526508000000L is a Norwegian holiday. I would expect the time zone to be implicitly "Europe/Oslo", or any equivalent, since the library is about Norwegian temporal events.

If I am calling someone in Norway from UK on May 16 10PM, and asks her if now is a holiday, I would get "yes" back.

If I have some kind of application with a rule that says "never bother Norwegians in their holidays", and I run that application on the other side of the planet, I would expect that application not to send any SMSs during when Norway celebrates their constitution day.

torgeir commented 5 years ago

Put another way;

As someone living abroad, you are of course entitled to celebrate the norwegian constitution day whenever your country reaches the 17th of the month, but that does not change when Norwegians at home celebrate their holiday.

eivinhb commented 5 years ago

I do see your point. But its not designed that way. Put simply: if the toString of the date you ask with is 16th of may, then it probably is read as that and not as any thing else. 🤷‍♂️

edit: I think you both demand too much into what this is as it sits to day. I'm not sure how this is supposed to be used, but I guess that you can make a calendar and given a date can check if that is a holiday or not. It can for instance count working days between two dates. But I think that this is designed naive and use only one timesone.

Sidenote: I have never actually worked at a shop that have full control over timezones and need to have. And java.util.Date, sql DATETIME, and javascript Date has to be a good reason why this is a problem.

Then lets write an api that is capable of doing that. But then we need to ditch Date from util and sql and use java.time.