Как конвертировать LocalDateTime в UTC и обратно, без загрузки зон?
Фон
Я использую бэкпорт Threetenbp для Android ( здесь) для обработки различных операций с данными, связанными со временем.
Одним из них является преобразование времени в другой часовой пояс (текущий в UTC и обратно).
Я знаю, что это возможно, если вы используете что-то подобное:
LocalDateTime now = LocalDateTime.now();
LocalDateTime nowInUtc = now.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime();
Это работает очень хорошо, и это также довольно легко сделать наоборот.
Эта проблема
Я пытаюсь избежать инициализации библиотеки, которая загружает в нее довольно большой файл зон. Я уже выяснил, как обрабатывать различные операции, связанные с датой и временем, без этого, за исключением случая конвертации в UTC и обратно.
То, что я получил, имеет ошибку целого 1 часа от правильного преобразования.
Что я пробовал
Вот что я нашел и попробовал:
// getting the current time, using current time zone:
Calendar cal = Calendar.getInstance();
LocalDateTime now = LocalDateTime.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), cal.get(Calendar.MILLISECOND) * 1000000);
//the conversion itself, which is wrong by 1 hour in my tests:
LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(TimeZone.getDefault().getRawOffset() / 1000)).withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0))).toLocalDateTime();
Вопрос
Что именно не так с тем, что я написал? Как я могу получить альтернативный код для преобразования времени без инициализации библиотеки?
Учитывая экземпляр LocalDateTime в качестве входных данных, как я могу преобразовать его из текущего часового пояса в UTC и из UTC в текущий часовой пояс?
2 ответа
Вероятно, это происходит потому, что часовой пояс вашего JVM по умолчанию установлен на летнее время (DST).
Чтобы получить правильное смещение, вы должны проверить, находится ли часовой пояс в летнее время, и добавить это к смещению:
Calendar cal = Calendar.getInstance();
TimeZone zone = TimeZone.getDefault();
// if in DST, add the offset, otherwise add zero
int dst = zone.inDaylightTime(cal.getTime()) ? zone.getDSTSavings() : 0;
int offset = (zone.getRawOffset() + dst) / 1000;
LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(offset))
.withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0)))
.toLocalDateTime();
Еще один способ создать nowInUtc
как LocalDateTime
это создать Instant
от Calendar
:
LocalDateTime nowInUtc = Instant.ofEpochMilli(cal.getTimeInMillis())
.atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();
На самом деле, вам не нужно Calendar
вообще, просто используйте Instant.now()
чтобы получить текущий момент:
LocalDateTime nowInUtc = Instant.now().atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();
Или, короче, используйте OffsetDateTime
непосредственно:
LocalDateTime nowInUtc = OffsetDateTime.now(ZoneOffset.ofHours(0)).toLocalDateTime();
Не уверен, загружает ли какой-либо из этих данных часовой пояс, это зависит от вас, чтобы проверить.
И я думаю, что постоянная ZoneOffset.UTC
можно использовать вместо ZoneOffset.ofHours(0)
потому что он не будет загружать данные tz (но я не проверял).
Окончательное решение
Предполагая, что часовой пояс по умолчанию находится в Израиле (TimeZone.getDefault()
является Asia/Jerusalem
):
// April 11th 2018, 3 PM (current date/time in Israel)
LocalDateTime now = LocalDateTime.of(2018, 4, 11, 15, 0, 0);
TimeZone zone = TimeZone.getDefault();
// translate DayOfWeek values to Calendar's
int dayOfWeek;
switch (now.getDayOfWeek().getValue()) {
case 7:
dayOfWeek = 1;
break;
default:
dayOfWeek = now.getDayOfWeek().getValue() + 1;
}
// get the offset used in the timezone, at the specified date
int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1,
now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);
ZoneOffset tzOffset = ZoneOffset.ofTotalSeconds(offset / 1000);
// convert to UTC
LocalDateTime nowInUtc = now
// conver to timezone's offset
.atOffset(tzOffset)
// convert to UTC
.withOffsetSameInstant(ZoneOffset.UTC)
// get LocalDateTime
.toLocalDateTime();
// convert back to timezone
LocalDateTime localTime = nowInUtc
// first convert to UTC
.atOffset(ZoneOffset.UTC)
// then convert to your timezone's offset
.withOffsetSameInstant(tzOffset)
// then convert to LocalDateTime
.toLocalDateTime();
Ответ от carlBjqsd - хорошо, просто неловко и, возможно, должно быть немного яснее.
Почему разница в один час
Смотрите окончательное решение @carlBjqsd: оно использует выражение
int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1, now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);
вместо
getRawOffset()
,
Это вызвало разницу в один час, который вы наблюдали. Приложения обычно не нуждаются только в вычислениях с необработанным смещением, которое не учитывает смещение dst для некоторых периодов года. Это только общее смещение, которое имеет значение при любом преобразовании из локальной временной метки в UTC и обратно. Основная цель тонко-гранулярного дифференцирования частичных смещений, таких как необработанные смещения или смещения dst, заключается в правильном именовании зоны (будем называть это стандартным временем или нет?).
Вводящее в заблуждение название вопроса: "без зон загрузки"
Нет, вы никогда не сможете избежать загрузки зон, если хотите конвертировать между местными временными метками и UTC, используя зоны. Ваш реальный вопрос скорее так: как избежать загрузки зон ThreetenABP и вместо этого использовать / загружать зоны платформы Android. И ваша мотивация выглядит так:
Я пытаюсь избежать инициализации библиотеки, которая загружает в нее довольно большой файл зон
Ну, я не измерил, какие данные зоны оказывают большее влияние на производительность. Я могу только сказать, основываясь на моих исследованиях и знаниях исходного кода задействованных библиотек, что java.time
и ThreetenBP загружают весь файл TZDB.dat
в кэш двоичного массива в памяти (в качестве первого шага), а затем выбрать соответствующую часть для одной зоны (т.е. интерпретировать часть массива двоичных данных путем десериализации в набор правил зоны и, наконец, единый ZoneId
). Старые платформы Java вместо этого работают с набором различных zi-файлов (по одному для каждой зоны), и я подозреваю, что зоны Android ведут себя аналогичным образом (но, пожалуйста, исправьте меня, если вы знаете эту деталь лучше).
Если только ОДНА зона должна использоваться вообще, тогда традиционный подход использования отдельных файлов зон может быть лучше, но как только вы захотите выполнить итерацию по всем доступным зонам, тогда лучше иметь только один файл зоны.
Лично я считаю, что аспект производительности незначителен. Если вы используете зоны Android, у вас также неизбежно будет время загрузки. Если вы действительно хотите ускорить время инициализации ThreetenABP, вам следует рассмотреть возможность загрузки его в фоновом потоке.
Зоны Android и зоны ThreetenABP эквивалентны?
Вообще нет. Оба репозитория часовых поясов могут давать одинаковое смещение для конкретной зоны. И часто они делают это, но иногда будут различия, которые не находятся под вашим контролем. Хотя оба репозитория часовых поясов в конечном итоге используют данные iana.org/tz, различия в основном вызваны возможными различными версиями данных tzdb. И вы не можете контролировать, какая версия данных зоны существует на платформе Android, потому что пользователь мобильного телефона сам решает, как часто он обновляет ОС Android. И это также верно для данных ThreetenABP. Вы можете предложить самую последнюю версию вашего приложения, включая последнюю версию ThreetenABP, но вы не можете контролировать, действительно ли пользователь мобильного устройства обновляет приложение.
Другие причины, почему нужно заботиться о выборе правильного хранилища tz?
Помимо производительности и времени инициализации, действительно существует один особый сценарий, который может быть интересен для выбора. Если ОС Android несколько устарела и использует устаревшую версию правил зоны, то некоторые пользователи мобильных телефонов не обновляют свою операционную систему, а манипулируют часами устройства, чтобы компенсировать неправильные данные часового пояса. Таким образом, они все еще получают правильное местное время на мобильном телефоне (во ВСЕХ приложениях).
В этом сценарии ThreetenABP не предлагает хорошего решения, потому что объединение их правильных данных зоны с неправильными часами устройства приведет к неправильным местным временным меткам (раздражает пользователя). Это было проблемой, например, в Турции, которая недавно изменила dst-правила.
Использование только старого календаря и часового пояса API Android (в пакете java.util
) может принять во внимание проблему, поэтому создаются правильные временные метки. Однако, если приложение сообщает время UTC (например, в виде количества миллисекунд с 1970-01-01T00Z) другим хостам (например, серверам), тогда неправильные часы устройства все еще остаются проблемой.
Мы могли бы сказать, зачем беспокоиться, потому что пользователь сделал "ерунду" с конфигурацией устройства, но мы также живем в реальном мире и должны думать о том, как сделать счастливыми даже таких пользователей. Поэтому, когда я думал о решении, я представил, по крайней мере, в своей библиотеке календаря методы Time4A, такие как SystemClock.inPlatformView(), который использует (вероятно) наиболее актуальные данные зоны и получает правильные часы UTC, исходя из предположения, что пользователь, по крайней мере, будет наблюдать правильное местное время устройства (что бы он / она ни делал для достижения этой цели, либо путем обновления ОС, либо путем настройки часов / зон). Я вполне счастлив, что таким образом отказался от старого API календаря и зоны. Мой API даже позволяет одновременно использовать обе зоны репозиториев:
Timezone.of("java.util.TimeZone~Asia/Jerusalem")
// использует данные AndroidTimezone.of("Asia/Jerusalem")
// использует данные Time4A
Возможно, вы сможете извлечь выгоду из этих идей, когда найдете / разработаете подходящие вспомогательные классы для вашего использования ThreetenABP. Time4A с открытым исходным кодом.