GregorianCalendar Проблемы с переходом на летнее время

Если я возьму время в конце осеннего дневного сдвига времени (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 есть некоторые странные соглашения, но я не понимаю, как это может быть правильно.

Как правильно вычесть час и установить минуты на 0, используя класс Calendar?


person Rasmus Faber    schedule 20.03.2014    source источник
comment
Чувак, это странно! только что попробовал и получил то же самое! это похоже на то, что он запоминает первое значение после набора   -  person Hichamov    schedule 20.03.2014
comment
требуется комментарий о времени joda здесь, пока я загружаю свой ide, чтобы проверить это   -  person Denis Tulskiy    schedule 24.03.2014
comment
Я думаю, что это сдвиг значения часа вперед (или вытягивание установленного по умолчанию состояния календаря?. Реверсирование команд (установка минуты, затем добавление отрицательного часа), похоже, работает, но это немного обходной путь для такой очевидной проблемы .   -  person Gorbles    schedule 24.03.2014


Ответы (3)


Вот комментарии к похожей "ошибке" из Java Bug tracker:

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

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

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

cal.add(Calendar.MINUTE, -cal.get(Calendar.MINUTE));
person apangin    schedule 24.03.2014
comment
Еще одно объяснение похожей проблемы. - person apangin; 24.03.2014

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

Если мы проверим 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

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

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

Флаг isTimeSet сбрасывается в false при вызове set. Из источника Григорианский календарь:

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

Поэтому set сбрасывает смещение летнего времени. Эта строка получает смещения:

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

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

internalSet(DST_OFFSET, zoneOffsets[1]); 

И время в миллисекундах изменяется при следующем вызове getTimeInMillis.

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

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

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

person Raul Guiu    schedule 24.03.2014

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 спецификация

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

Он говорит, что вы использовали его неправильно, вызывая 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") (в качестве примера) не имеет смещения летнего времени.

person Sergey Fedorov    schedule 24.03.2014