Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling getTime changes Calendar value

Tags:

java

calendar

I'm trying to get the sunday of the same week as a given date. During this I ran into this problem:

Calendar calendar = Calendar.getInstance(Locale.GERMANY);
calendar.set(2017, 11, 11);
calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
System.out.println(calendar.getTime().toString());

results in "Sun Jan 07 11:18:42 CET 2018"

but

Calendar calendar2 = Calendar.getInstance(Locale.GERMANY);
calendar2.set(2017, 11, 11);
calendar2.getTime();
calendar2.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
System.out.println(calendar2.getTime().toString());

gives me the correct Date "Sun Dec 17 11:18:42 CET 2017"

Can someone explain why the first exmple is behaving this way? Is this really intended?

Thanks

like image 394
mzl Avatar asked Jan 30 '18 10:01

mzl


2 Answers

Basically, the Calendar API is horrible, and should be avoided. It's not documented terribly clearly, but I think I see where it's going, and it's behaving as intended in this situation. By that I mean it's following the intention of the API authors, not the intention of you or anyone reading your code...

From the documentation:

The calendar field values can be set by calling the set methods. Any field values set in a Calendar will not be interpreted until it needs to calculate its time value (milliseconds from the Epoch) or values of the calendar fields. Calling the get, getTimeInMillis, getTime, add and roll involves such calculation.

And then:

When computing a date and time from the calendar fields, there may be insufficient information for the computation (such as only year and month with no day of month), or there may be inconsistent information (such as Tuesday, July 15, 1996 (Gregorian) -- July 15, 1996 is actually a Monday). Calendar will resolve calendar field values to determine the date and time in the following way.

If there is any conflict in calendar field values, Calendar gives priorities to calendar fields that have been set more recently. The following are the default combinations of the calendar fields. The most recent combination, as determined by the most recently set single field, will be used.

For the date fields:

  • YEAR + MONTH + DAY_OF_MONTH
  • YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK
  • YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK
  • YEAR + DAY_OF_YEAR
  • YEAR + DAY_OF_WEEK + WEEK_OF_YEAR

In the first example, the fact that the last field set was "day of week" means it will then use the YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK calculation (I think). The year and month have been set to December 2017, but the week-of-month is the current week-of-month, which is the week 5 of January 2018... so when you then say to set the day of week to Sunday, it's finding the Sunday in the "week 5" of December 2017. December only had 4 weeks, so it's effectively rolling it forward... I think. It's all messy and you shouldn't have to think about that, basically.

In the second example, calling getTime() "locks in" the year/month/day you've specified, and computes the other fields. When you set the day of week, that's then adjusting it within the existing computed fields.

Basically, avoid this API as far as you possibly can. Use java.time, which is a far cleaner date/time API.

like image 50
Jon Skeet Avatar answered Nov 10 '22 14:11

Jon Skeet


As Jon Skeet said, avoid Calendar. For your case it is truly horrible, and it’s poorly designed in general. Instead do

    WeekFields weekFieldsForLocale = WeekFields.of(Locale.GERMANY);
    // To find out which number Sunday has in the locale,
    // grab any Sunday and get its weekFieldsForLocale.dayOfWeek()
    int dayNumberOfSundayInLocale = LocalDate.now()
            .with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))
            .get(weekFieldsForLocale.dayOfWeek());
    LocalDate date = LocalDate.of(2017, Month.DECEMBER, 11);
    LocalDate sunday
            = date.with(weekFieldsForLocale.dayOfWeek(), dayNumberOfSundayInLocale);

    System.out.println(sunday);

This prints the expected date

2017-12-17

As others have already mentioned, the solution is to use java.time, the modern Java date and time API. Also generally it is so much nicer to work with. One nice feature is the LocalDate class that I am using. It is a date without time of day, which seems to match your requirements more precisely that Calendar did.

If the above looks complicated, it’s because, as I think you are aware, “Sunday of the same week” means different things in different locales. In the international standard that Germany follows, weeks begin on Monday, so Sunday is the last day of the week. In the American standard, for example, Sunday os the first day of the week. WeekFields.dayOfWeek() numbers the days of the week from 1 to 7, so when we want to set the day to Sunday, we first need to find out which number Sunday has got in this numbering (7 in Germany, 1 in the US). So for any Sunday, get its weekFieldsForLocale.dayOfWeek() value and later use this for setting the day of week to Sunday. The reason why this is necessary is that the with() method is so general and therefore has been designed to accept only numeric values; we can’t just pass it a DayOfWeek object.

If I substitute Locale.US into the code, I get 2017-12-10, which is the correct Sunday for a calendar where Sunday is the first day of the week. If you are sure your only want your code to work for Germany, you may of course just hardcode a 7 (please make it a constant with a very explanatory name).

Link: Oracle Tutorial Date Time explaining how to use java.time. There are other resources on the net (just avoid the outdated placed that suggest java.util.Calendar :-)

like image 1
Ole V.V. Avatar answered Nov 10 '22 14:11

Ole V.V.