Странное поведение класса java.util.GregorianCalendar в разных версиях Android - PullRequest
0 голосов
/ 12 октября 2018

Я создаю календарь в своем приложении для Android.Первый день календаря - воскресенье или понедельник.Это зависит от локали.Странное поведение java.util. GregorianCalendar класс в разных версиях Android :

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;
    }
}

Установить конструктор класса (новый GregorianCalendar (1994, 3, 1)Календарь.Воскресенье). В Android 4.4, 5.0 Результат Logcat:

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

В Android 8.0 Результат Logcat:

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

Как вы видите результат- разные дни (26 и 27), что соответствует разным дням недели. НО ЕСЛИ ВЫ ИЗМЕНИТЕ ИНИЦИАЛИЗАЦИЮ объекта календаря:

@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);

РЕЗУЛЬТАТ БУДЕТ ИСТИНА на всех версиях 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

В тестах junit результат верен во всех случаях (27 и воскресенье).Удалите журналы из кода и проверьте:

 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());
    }
}

Также поведение за апрель 1993, 1992. Почему?Я уже сломал себе мозги.

Ответы [ 2 ]

0 голосов
/ 14 октября 2018

java.time

Хорошее решение - пропустить классы Calendar и GregorianCalendar и использовать вместо него LocalDate из java.time, современного Java-API даты и времени.Calendar и GregorianCalendar давно устарели и плохо спроектированы.Современный API намного приятнее работать.И LocalDate - это дата без времени и без часового пояса, поэтому, если подозрение, что я транслирую ниже, верное, это гарантирует, что проблема вашего часового пояса / летнего времени останется позади.Чтобы использовать его на старых Android, см. Ниже.

Что пошло не так?Спекулятивное объяснение

Следующее объяснение чисто теоретическое, но лучшее, что я смог придумать.Он основан на нескольких предположениях, которые я не смог проверить:

  • Вы находитесь (или одно из ваших устройств) в часовом поясе, где в последние дни началось летнее время (DST).от марта 1994 года.
  • В Android 101 и 5.0 может быть ошибка в GregorianCalendar, так что currentCalendar.add(Calendar.DAY_OF_WEEK, - (weekDay - 1)); просто добавляет это много раз за 24 часа.

Это чистое предположение,но если есть такая ошибка, ваш GregorianCalendar закончится в 23:00 вечера перед целевой датой, что объяснит ваши результаты.Например, страны в ЕС начинают летнее время в последнее воскресенье марта.Это также имело место в 1994 году. Это очень хорошо подошло бы к вашей целевой дате воскресенья, 27 марта 1994 года, и также объяснило бы ваши неверные результаты за 1992 и 1993 годы. Я сделал краткий поиск в Интернете для упоминания такой ошибки вAndroid GregorianCalendar и не нашел ничего, что могло бы его поддержать.

Для того, чтобы мои подозрения объяснили ваши наблюдения, нам понадобится еще пара деталей:

  1. Ошибка, которую я подозреваюбудет только в некоторых версиях Android (4.4, 5.0) и исправлена ​​в более поздних версиях (8.0) (в противном случае ваше устройство Android 8.0 будет работать в другом часовом поясе).Кроме того, среда, в которой вы запускаете свои тесты, либо не содержит ошибок, либо имеет другой часовой пояс по умолчанию (либо объясняет, почему тесты проходят).
  2. GregorianCalendar, который вы получаете от getInstance, имеет времядня в этом.И держит его после того, как вы установите дату.Чтобы объяснить разницу между двумя способами установки даты: допустим, вы запускаете код в 9:05.new GregorianCalendar(1994, Calendar.APRIL, 1) даст вам 1 апреля 1994 года в 00:00.Calendar.getInstance(), за которым следует currentCalendar.set(year, month, 1);, дает вам 1 апреля 1994 года в 09:05.Разница между ними чуть больше 9 часов.В последнем случае предполагаемая ошибка приведет к тому, что вы наберете 8:05 27 марта, который все еще находится на правильной дате, поэтому вы не увидите ошибку.Если вы запустили свой код, скажем, в 0:35 ночью, вы попали в 23:35 26 марта, так что вы увидите ошибку и в этом случае.

Как яуже сказано, LocalDate, java.time и ThreeTenABP сформируют хорошее решение.Если вы решите не полагаться на внешнюю библиотеку, а скорее пробиться через устаревшие классы, я думаю, что поможет следующее:

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

TimeZone - это еще один старый и плохо разработанный классВ частности, метод getTimeZone, который я использую, имеет несколько неприятных сюрпризов, но я считаю, что вышеприведенное работает (скрестив пальцы).Идея состоит в том, чтобы указать Calendar использовать время UTC.UTC не имеет летнего времени, что уклоняется от проблемы.

Другая и более хакерская вещь, которую вы могли бы попробовать, была бы:

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

Это устанавливает час дня на 6, что означает, чтоКогда вы вернетесь назад к переходу на летнее время, вы нажмете 5 часов утра, который все равно будет в правильную дату (вышеупомянутый вызов не устанавливает секунды и миллисекунды; за один прогон Iполучено 1 апреля 1994 года в 06:00: 40,213 UTC).

Вопрос: Можно ли использовать java.time на Android?

Да, java.time прекрасно работает на старых и новых устройствах Android,Для этого требуется как минимум Java 6 .

  • В Java 8 и более поздних версиях и на более новых устройствах Android (от уровня API 26) современный API поставляется встроенным.
  • В Java 6 и 7 получить ThreeTen Backport, бэкпорт новых классов (ThreeTen для JSR 310; см. Ссылки внизу).
  • On (older) Android использует Android-версию ThreeTen Backport.Это называется ThreeTenABP.И убедитесь, что вы импортируете классы даты и времени из org.threeten.bp с подпакетами.

Ссылки

0 голосов
/ 12 октября 2018

Ничего странного здесь нет.getInstance вернет данные, основанные на вашей локали и часовом поясе, в отличие от conststructor.Не уверен насчет новых версий Android, может быть, что-то здесь изменилось или вы тестировали с другими часовыми поясами / локалями?

...