Проблемы с 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")
(как пример) не имеет смещения на летнее время.