简体   繁体   中英

Strange behavior of the java.util.GregorianCalendar class in different android versions

I am creating a calendar in my Android application. The first day of the calendar is Sunday or Monday. It depends on the locale. Strange behavior of the java.util. GregorianCalendar class in different android versions :

public class CurrentMonth extends AbstractCurrentMonth implements InterfaceCurrentMonth {

    public CurrentMonth(GregorianCalendar calendar, int firstDayOfWeek) {
        super(calendar, firstDayOfWeek);
    }

    @Override
    public List<ContentAbstract> getListContent() {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);

        GregorianCalendar currentCalendar = new GregorianCalendar(year, month, 1);

        List<ContentAbstract> list = new ArrayList<>();
        int weekDay = getDayOfWeek(currentCalendar);
        currentCalendar.add(Calendar.DAY_OF_WEEK, - (weekDay - 1));

        while (currentCalendar.get(Calendar.MONTH) != month) {
            list.add(getContent(currentCalendar));
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
        }

        while (currentCalendar.get(Calendar.MONTH) == month) {
            list.add(getContent(currentCalendar));
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        currentCalendar.add(Calendar.DAY_OF_MONTH, - 1);

        while (getDayOfWeek(currentCalendar) != 7) {
            currentCalendar.add(Calendar.DAY_OF_MONTH, 1);
            list.add(getContent(currentCalendar));
        }

        Log.i("text", "yaer: " + list.get(0).getYear());
        Log.i("text", "month: " + list.get(0).getMonth());
        Log.i("text", "day of month: " + list.get(0).getDay());
        Log.i("text", "day of week: " + list.get(0).getDayOfWeek());

        return list;
    }

    private int getDayOfWeek(GregorianCalendar currentCalendar) {
        int weekDay;
        if (firstDayOfWeek == Calendar.MONDAY) {
            weekDay = 7 - (8 - currentCalendar.get(Calendar.DAY_OF_WEEK)) % 7;
        }
        else weekDay = currentCalendar.get(Calendar.DAY_OF_WEEK);
        return weekDay;
    }

    private GraphicContent getContent(GregorianCalendar cal) {
        GraphicContent content = new GraphicContent();
        content.setYear(cal.get(Calendar.YEAR));
        content.setMonth(cal.get(Calendar.MONTH));
        content.setDay(cal.get(Calendar.DAY_OF_MONTH));
        content.setDayOfWeek(cal.get(Calendar.DAY_OF_WEEK));
        return content;
    }
}

public class GraphicContent extends ContentAbstract {
    private int year;
    private int month;
    private int day;
    private int dayOfWeek;

    @Override
    public int getYear() {
        return year;
    }

    @Override
    public void setYear(int year) {
        this.year = year;
    }

    @Override
    public int getMonth() {
        return month;
    }

    @Override
    public void setMonth(int month) {
        this.month = month;
    }

    @Override
    public int getDay() {
        return day;
    }

    @Override
    public void setDay(int day) {
        this.day = day;
    }

    @Override
    public int getDayOfWeek() {
        return dayOfWeek;
    }

    @Override
    public void setDayOfWeek(int dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
    }
}

Set the class constructor (new GregorianCalendar(1994, 3, 1), Calendar.SUNDAY). In android 4.4, 5.0 Logcat result:

10-12 14:32:28.332 27739-27739/*** I/text: yaer: 1994
10-12 14:32:28.332 27739-27739/*** I/text: month: 2
10-12 14:32:28.332 27739-27739/*** I/text: day of month: 26
10-12 14:32:28.332 27739-27739/*** I/text: day of week: 7

In android 8.0 Logcat result:

2018-10-12 11:50:59.549 6565-6565/*** I/text: yaer: 1994
2018-10-12 11:50:59.549 6565-6565/*** I/text: month: 2
2018-10-12 11:50:59.549 6565-6565/*** I/text: day of month: 27
2018-10-12 11:50:59.549 6565-6565/*** I/text: day of week: 1

As you can see the result - different days (26 and 27), which corresponds to different days of the week. BUT IF YOU CHANGE THE INITIALIZATION of the calendar object:

@Override
    public List<ContentAbstract> getListContent() {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);

        GregorianCalendar currentCalendar = (GregorianCalendar) Calendar.getInstance();
        currentCalendar.set(year, month, 1);

The RESULT WILL BE TRUE on all versions of android:

10-12 15:12:56.400 28914-28914/*** I/text: yaer: 1994
10-12 15:12:56.400 28914-28914/*** I/text: month: 2
10-12 15:12:56.400 28914-28914/*** I/text: day of month: 27
10-12 15:12:56.400 28914-28914/*** I/text: week day: 1

In junit tests the result is correct in all cases (27 and SUNDAY). Delete the logs from the code and check:

 public class TestCurrentMonth {

    @Test
    public void testGetListContent() {
        GregorianCalendar calendar = new GregorianCalendar(1994, 3, 1);
        int firstDay = Calendar.SUNDAY;
        CurrentMonth currentMonth = new CurrentMonth(calendar, firstDay);
        List<ContentAbstract> list = currentMonth.getListContent();
        Assert.assertEquals(27, list.get(0).getDay());
        Assert.assertEquals(Calendar.SUNDAY, list.get(0).getDayOfWeek());
    }
}

Also behavior for April 1993, 1992. Why? I already broke my brains.

java.time

The good solution is to skip the Calendar and GregorianCalendar classes and use LocalDate from java.time, the modern Java date and time API, instead. Calendar and GregorianCalendar are long outdated and poorly designed. The modern API is so much nicer to work with. And LocalDate is a date without time and without time zone, so if the suspicion that I am airing below is correct, it will guarantee to leave your time zone/summer time issue behind. To use it on older Android, see further down.

What went wrong? Speculative explanation

The following explanation is purely theoretical, but the best I have been able to think of. It relies on a couple of assumptions that I have not been able to verify:

  • You are (or one of your devices is) in a time zone where summer time (DST) began in the last days of March 1994.
  • There might be a bug in GregorianCalendar in Android 4.4 and 5.0 so that currentCalendar.add(Calendar.DAY_OF_WEEK, - (weekDay - 1)); just adds that many times 24 hours.

It's pure speculation, but if there is such a bug, your GregorianCalendar will end up at 23:00 on the evening before the target date, which would explain your results. Countries in EU, for example, begin summer time on the last Sunday of March. This was also the case in 1994. This would fit your target date of Sunday, March 27, 1994 very nicely, and would also explain your wrong results for 1992 and 1993. I have made a brief Internet search for mention of such a bug in Android GregorianCalendar and didn't find anything to support it.

For my suspicion to explain your observations, we need a couple of pieces more:

  1. The bug I am suspecting would be only in some Android versions (4.4, 5.0) and fixed in later versions (8.0) (alternatively you Android 8.0 device would be running a different time zone). Also the enviroment where you run your tests either doesn't have the bug or has a different default time zone (either would explain why the tests pass).
  2. The GregorianCalendar you get from getInstance has time of day in it. And keeps it after you set the date. To spell out the difference between the two ways you set the date: Say you run your code at 9:05. new GregorianCalendar(1994, Calendar.APRIL, 1) will give you April 1, 1994 at 00:00. Calendar.getInstance() followed by currentCalendar.set(year, month, 1); gives you April 1, 1994 at 09:05. There's a little over 9 hours difference between the two. In the latter case the suspected bug will cause you to hit 8:05 on 27 March, which is still on the correct date, so you don't see the bug. If you ran your code at, say, 0:35 in the night, you'd hit 23:35 on 26 March, so you'd see the bug at that case too.

As I already said, LocalDate , java.time and the ThreeTenABP would form the good solution. If you choose not to rely on an external library but rather fight your way through with the outdated classes, I believe that the following would help:

    GregorianCalendar currentCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
    currentCalendar.set(year, month, 1);

TimeZone is yet one more old and poorly designed classes, in particular the getTimeZone method that I am using has some nasty surprises to it, but I believe the above works (fingers crossed). The idea is to tell the Calendar to use UTC time. UTC does not have summer time, which evades the problem.

Another and more hacky thing you might try, would be:

    currentCalendar.set(year, month, 1, 6, 0);

This sets the hour of day to 6, meaning that when you go back across the summer time transition, you will hit 5:00 in the morning, which will still be on the correct date (the call above does not set the seconds and milliseconds; in one run I got April 1, 1994 at 06:00:40.213 UTC).

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 new 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.

Links

Nothing strange here. getInstance will return data based on your locale and time zone, unlike conststructor. Not sure about newer Android versions, maybe something changed here or you tested with different time zones/locales?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM