Проблемы с GregorianCalendar DST

Если я возьму время в конце осеннего дневного сдвига времени (2014-10-26 02:00:00 CET в Дании) и вычитаю один час (так что я ожидаю вернуться к 02:00 CEST), а затем установите минуты на ноль, я получаю некоторые странные результаты:

Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("CET"));

cal.set(Calendar.YEAR, 2014);
cal.set(Calendar.MONTH, Calendar.OCTOBER);
cal.set(Calendar.DAY_OF_MONTH, 26);
cal.set(Calendar.HOUR_OF_DAY, 2);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC

cal.add(Calendar.HOUR_OF_DAY, -1);

System.out.println(cal.getTimeInMillis()); // 1414281600000 : 00:00:00 UTC (as expected)

cal.set(Calendar.MINUTE, 0);
// or: cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE));
// both should be NOPs.

System.out.println(cal.getTimeInMillis()); // 1414285200000 : 01:00:00 UTC (Why?!)

Это ошибка? Я знаю, что Java Calendar имеет некоторые странные соглашения, но я не вижу, как это может быть правильно.

Каков правильный способ вычитать час и установить минуты на 0 с помощью класса Calendar?

Ответы

Ответ 1

Вот комментарии к подобной "ошибке" от трекера Java Bug:

В период "спада" календарь не поддерживает значения, и данное локальное время интерпретируется как стандартное время.

Чтобы избежать неожиданного DST для стандартного изменения времени, вызовите add() в reset значение.

Это означает, что вы должны заменить set() на

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));

Ответ 2

Ссылка в ответе apangin объясняет проблему и предлагает решение. Я действительно старался смотреть на это с большим трудом, просматривая код.

Если мы проверим DST_OFFSET до и после нашего набора:

System.out.println(cal.get(Calendar.DST_OFFSET));
cal.set(Calendar.MINUTE, 0);
System.out.println(cal.get(Calendar.DST_OFFSET));

Он печатает:

3600000
0

Посмотрев на methodgetTimeInMillis, мы видим, что они используют флаг (isTimeSet) для проверки при пересчете времени в миллисекундах:

public long getTimeInMillis() {
   if (!isTimeSet) {
       updateTime();
   }
   return time;
}

Флаг isTimeSet reset равен false при вызове set. Из источника GregorianCalendar:

public void set(int field, int value)
{
    if (isLenient() && areFieldsSet && !areAllFieldsSet) {
        computeFields();
    }
    internalSet(field, value);
    isTimeSet = false;             // <-------------- here
    areFieldsSet = false;
    isSet[field] = true;
    stamp[field] = nextStamp++;
    if (nextStamp == Integer.MAX_VALUE) {
        adjustStamp();
    }
}

Итак установите reset смещение DST. Эта строка получает смещения:

zoneOffset = ((ZoneInfo)tz).getOffsets(time, zoneOffsets);

A немного вниз значения устанавливаются:

internalSet(DST_OFFSET, zoneOffsets[1]); 

И время в миллисемене меняется на односторонний вызов getTimeInMillis.

Как предлагать apangin, мы не должны использовать set после инициализации даты, или мы будем форсировать дату reset. Вот как это предлагается в сообщении в отладчике ошибок OpenJDK.

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));

Лучшие альтернативы используют Joda Time. Автор этой библиотеки работает над новым Java 8 date time api, надеюсь, у вас нет таких проблем.

Ответ 3

Calendar.add метод должен действовать следующим образом:

Добавляет или вычитает указанный период времени в заданное поле календаря на основе правил календаря.

Итак, мы столкнулись с таинственными правилами календаря. Каков тип календаря, который у нас есть Calendar.getInstance(TimeZone.getTimeZone("CET"))?

Выполнить

System.out.println(Calendar.getInstance(TimeZone.getTimeZone("CET")).getClass());

У меня есть ответ: class java.util.GregorianCalendar. Попробуйте найти правила GregorianCalendar. Я нашел их в add method (почему здесь?). Один из правил соответствует вашему делу.

Добавить правило 2. Если ожидается, что меньшее поле будет инвариантным, но невозможно, чтобы оно было равно его предыдущему значению из-за изменения его минимума или максимума после изменения поля, тогда его значение настраивается так, чтобы оно как можно ближе к ожидаемому значению

01:00:00 и 23:00:00 имеют одинаковое расстояние, поэтому оба действуют по этим правилам.

Также обратите внимание на следующую строку в GregorianCalendar.add спецификация

Добавляет указанное (подписанное) количество времени

В нем говорится, что вы неправильно использовали вызов cal.add(Calendar.HOUR_OF_DAY, -1); Следовательно, вам некого винить, что ваш add показывает результат только при следующем вызове set. Но это как-то работает; вы можете использовать его!

Что здесь делать? Если вы в порядке, что 2014-10-26 02:00:00 CET минус один час, значит, вы можете добавить глупое set после своего add. Например:

    cal.add(Calendar.HOUR_OF_DAY, -1);
    cal.set(Calendar.HOUR, cal.get(Calendar.HOUR)); //apply changes
    cal.set(Calendar.MINUTE, 0);

Если вы хотите увидеть разницу после операции -1 hour, я бы посоветовал вам изменить TimeZone. TimeZone.getTimeZone("GMT") (как пример) не имеет смещения на летнее время.