Search code examples
androidcalendarjava.util.calendar

Need to query Calendar instance for Calendar setting to work


I have a Calendar instance that starts with the first day in December. I am trying to set that Calendar instance to the last Tuesday of the year. Somewhere between setting the last day of the year and the day of week, the Calendar instance reverts back to the original time:

Calendar cal = ....;
Log.d(TAG, String.format("Starting here %d-%d-%d", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)+1, cal.get(Calendar.DAY_OF_MONTH)));

cal.set(Calendar.DAY_OF_YEAR, cal.getActualMaximum(Calendar.DAY_OF_YEAR));
cal.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);

Log.d(TAG, String.format("Ending here %d-%d-%d", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)+1, cal.get(Calendar.DAY_OF_MONTH)));
/* Other stuff */

Gives me this output for 2017 and 2018:

2017:

Starting here 2017-12-1 
Ending here 2017-11-28 

2018:

Starting here 2018-12-1 
Ending here 2018-11-27 

An Unsatisfying Fix

However, if I retrieve data from the Calendar instance, it works fine:

Calendar cal = ....;
Log.d(TAG, String.format("Starting here %d-%d-%d", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)+1, cal.get(Calendar.DAY_OF_MONTH)));

cal.set(Calendar.DAY_OF_YEAR, cal.getActualMaximum(Calendar.DAY_OF_YEAR));

// Ask the Calendar instance for information
long dummyValue = cal.getTimeInMillis();

cal.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);

Log.d(TAG, String.format("Ending here %d-%d-%d", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH)+1, cal.get(Calendar.DAY_OF_MONTH)));
/* Other stuff */

Gives me this output for 2017 and 2018:

2017:

Starting here 2017-12-1 
Ending here 2018-1-2 

2018:

Starting here 2018-12-1
Ending here 2019-1-1 

And yes, I know the dates for the second set of values are not the last Tuesday. That's what /* Other stuff */ is for.


Solution

  • java.time and ThreeTenABP

        LocalDate myLocalDate = LocalDate.of(2017, Month.DECEMBER, 1);
    
        System.out.format("Starting here: %s%n", myLocalDate);
        LocalDate lastTuesdayOfYear = myLocalDate
                .with(TemporalAdjusters.lastDayOfYear())
                .with(TemporalAdjusters.previousOrSame(DayOfWeek.TUESDAY));
        System.out.format("Ending here:   %s%n", lastTuesdayOfYear);
    

    Output for 2017:

    Starting here: 2017-12-01
    Ending here:   2017-12-26
    

    For 2018:

    Starting here: 2018-12-01
    Ending here:   2018-12-25
    

    This is giving you the last Tuesday of the year. Nothing further is needed for that.

    Question: Can I use java.time on Android?

    Yes, java.time works nicely on older and newer Android devices. It just requires at least Java 6.

    • In Java 8 and later and on newer Android devices (from API level 26) the modern API comes built-in.
    • In Java 6 and 7 get the ThreeTen Backport, the backport of the modern classes (ThreeTen for JSR 310; see the links at the bottom).
    • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

    What went wrong in your code?

    The Calendar class is just confusing. In your case it is behaving according to the documentation. Allow me to quote:

    Calendar Fields Resolution

    When computing a date and time from the calendar fields, … 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 your case DAY_OF_WEEK was set most recently. DAY_OF_WEEK is in three of the five combinations above. Which one it picks I don’t know. None of them also include DAY_OF_YEAR, the other field you set, so DAY_OF_YEAR is not being used. Which explains the behaviour you saw. If you insist on using Calendar, you should be able to get somewhere with cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, -1); since negative values count backward from the end of the month.

    However, Calendar is not only poorly and confusingly designed, it is also long outdated. So consider using java.time instead.

    Links