Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange result when adding one year to java.util.Calendar

Tags:

java

date

kotlin

Initialize java.util.Calendar with May, 31 1900. Then add one year to it twenty times.

Here's code:

import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*

fun main(args : Array<String>) {
    val f = SimpleDateFormat("yyyy.dd.MM")
    val cal = Calendar.getInstance()
    cal.set(1900, Calendar.MAY, 31)
    for(i in 1..20) {
        println(f.format(cal.time))
        cal.add(Calendar.YEAR, 1)
    }
}

The output is following:

1900.31.05
1901.31.05
1902.31.05
1903.31.05
1904.31.05
1905.31.05
1906.31.05
1907.31.05
1908.31.05
1909.31.05
1910.31.05
1911.31.05
1912.31.05
1913.31.05
1914.31.05
1915.31.05
1916.31.05
1917.31.05
1918.01.06
1919.01.06

Why I get June, 1 instead of May, 31 since 1918?

UPD: with time information

1917.31.05 23:38:50.611
1918.01.06 01:38:50.611

If this is DST invention, how do I prevent that?

like image 575
Dmitriy Kornilov Avatar asked Dec 17 '22 23:12

Dmitriy Kornilov


2 Answers

You seem to be running your code in a timezone that changed its offset by two hours in 1917 or 1918. That is, the number of hours ahead or behind UTC changed. I've no idea why your timezone would have done that, but I'm sure there's a good historical reason for it.

If you're only interested in dates, without the Time component, use the java.time.LocalDate class, which effectively represents a day, month and year only. It's not subject to any daylight savings anomalies.

LocalDate today = LocalDate.now(); 

or

LocalDate moonLanding = LocalDate.of(1969, 7, 20);
like image 200
Dawood ibn Kareem Avatar answered Dec 20 '22 13:12

Dawood ibn Kareem


I am assuming that you are in Europe/Moscow time zone. Turing85 in a comment correctly spotted the cause of the behaviour you observed: In 1918 summer time (DST) in your time zone began on May 31. The clock was moved forward from 22:00 to 24:00, that is, by two hours. Your Calendar object is aware of this and therefore refuses to give 23:38:50.611 on this date. Instead it picks the time 2 hours later, 1918.01.06 01:38:50.611. Now the month and day-of-month have changed to 1st of June.

Unfortunately this change is kept in the Calendar and carried on to the following year.

If this is DST invention, how do I prevent that?

Thomas Kläger in a comment gave the right solution: If you only need the dates, use LocalDate from java.time:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("uuuu.dd.MM");
    LocalDate date = LocalDate.of(1900, Month.MAY, 31);
    for (int i = 1; i <= 20; i++) {
        System.out.println(date.format(dateFormatter));
        date = date.plusYears(1);
    }

Output (abbreviated):

1900.31.05
1901.31.05
…
1917.31.05
1918.31.05
1919.31.05

The “local” in LocalDate means “without timezone” in java.time jargon, so this is guaranteed to keep you free of surprises from time zone anomalies.

If you need a time, you may consider LocalDateTime, but since this is without time zone too, it will give you the non-existing time of 1918.31.05 23:38:50.611, so maybe not.

An alternative thing you may consider is adding the right number of years to your origin of 1900.31.05 23:38:50.611. Then at least you will only have surprises in years where you hit a non-existing time. I am using ZonedDateTime for this demonstration:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu.dd.MM HH:mm:ss.SSS");
    ZonedDateTime originalDateTime = ZonedDateTime.of(1900, Month.MAY.getValue(), 31,
            23, 30, 50, 611000000, ZoneId.of("Europe/Moscow"));
    for (int i = 0; i < 25; i++) {
        System.out.println(originalDateTime.plusYears(i).format(formatter));
    }

Output:

1900.31.05 23:30:50.611
1901.31.05 23:30:50.611
…
1917.31.05 23:30:50.611
1918.01.06 01:30:50.611
1919.01.06 00:30:50.611
1920.31.05 23:30:50.611
…
1924.31.05 23:30:50.611

Again in 1919 summer time began on May 31. This time the clock was only advanced by 1 hour, from 23 to 24, so you get only 1 hour later than the imaginary time of 23:30:50.611.

I am recommending java.time for date and time work, not least when doing math on dates like you do. The Calendar class is considered long outmoded. java.time was designed acknowledging that Calendar and the other old classes were poorly designed. The modern ones are so much nicer to work with.

How could I be sure it was Moscow?

In no other time zone than Europe/Moscow is the time of 1918.31.05 23:38:50.611 nonexistent. I checked:

    LocalDateTime dateTime = LocalDateTime.of(1918, Month.MAY, 31, 23, 38, 50, 611000000);
    for (String zid : ZoneId.getAvailableZoneIds()) {
        ZonedDateTime zdt = dateTime.atZone(ZoneId.of(zid));
        LocalDateTime newDateTime = zdt.toLocalDateTime();
        if (! newDateTime.equals(dateTime)) {
            System.out.println(zid + ": -> " + zdt + " -> " + newDateTime);
        }
    }

Output:

Europe/Moscow: -> 1918-06-01T01:38:50.611+04:31:19[Europe/Moscow] -> 1918-06-01T01:38:50.611
W-SU: -> 1918-06-01T01:38:50.611+04:31:19[W-SU] -> 1918-06-01T01:38:50.611

“W-SU” is a deprecated name for the same time zone, it stands for Western Soviet Union.

Links

  • Oracle tutorial: Date Time explaining how to use java.time.
  • Time Changes in Moscow Over the Years
  • List of tz database time zones showing W-SU as deprecated.
  • An old message on an IANA mailing list stating “But long ago … we based the name on some more-political entity than a city name. For example, we used "W-SU" for the western Soviet Union…”
like image 38
Ole V.V. Avatar answered Dec 20 '22 12:12

Ole V.V.