Автор оригинала: Vlad Mihalcea.
Основные понятия о времени
Большинство веб-приложений должны поддерживать разные часовые пояса, и правильно управлять часовыми поясами непросто. Что еще хуже, вы должны убедиться, что временные метки согласованы на различных языках программирования (например, JavaScript на интерфейсе, Java на среднем уровне и MongoDB в качестве хранилища данных). Цель этого поста-объяснить основные понятия абсолютного и относительного времени.
Эпоха
/| Эпоха – это абсолютная временная привязка. Большинство языков программирования (например, Java, JavaScript, Python) используют эпоху Unix (полночь 1 января 1970 года) при выражении заданной метки времени в виде количества миллисекунд, прошедших с момента привязки к фиксированному моменту времени.
Относительная числовая метка времени
Относительная числовая метка времени выражается как количество миллисекунд, прошедших с начала эпохи.
Часовой пояс
Всемирное координированное время (UTC) является наиболее распространенным стандартом времени. Часовой пояс UTC (эквивалентный GMT ) представляет собой временную привязку, к которой относятся все остальные часовые пояса (через положительное/отрицательное смещение).
Часовой пояс UTC обычно называют временем Зулу (Z) или UTC+0. Часовой пояс Японии-UTC+9, а часовой пояс Гонолулу-UTC-10. Во времена эпохи Unix (1 января 1970 года 00:00 по часовому поясу UTC) это было 1 января 1970 года 09:00 в Токио и 31 декабря 1969 года 14:00 в Гонолулу.
ISO 8601
ISO 8601 является наиболее распространенным стандартом представления даты/времени и использует следующие форматы даты/времени:
1970-01-01T00:00:00.000+00:00 | UTC |
1970-01-01T00:00:00.000 Z | UTC зулусское время |
1970-01-01T09:00:00.000+09:00 | Токио |
1969-12-31T14:00:00.000-10:00 | Гонолулу |
Основы Java time
Основы Java time
java.util.Дата определенно является наиболее распространенным классом, связанным со временем. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с эпохи. java.util.Дата не зависит от часового пояса , за исключением метода toString , который использует локальный часовой пояс для создания строкового представления.
java.util.Дата || определенно является наиболее распространенным классом, связанным со временем. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с эпохи. java.util.Дата || не зависит от часового пояса || , за исключением метода || toString||, который использует || локальный часовой пояс || для создания строкового представления.
Файл java.util.Календарь является одновременно фабрикой даты/времени, а также экземпляром времени с учетом часового пояса. Это один из наименее удобных для работы классов Java API, и мы можем продемонстрировать это в следующем примере:
@Test public void testTimeZonesWithCalendar() throws ParseException { assertEquals( 0L, newCalendarInstanceMillis("GMT").getTimeInMillis() ); assertEquals( TimeUnit.HOURS.toMillis(-9), newCalendarInstanceMillis("Japan").getTimeInMillis() ); assertEquals( TimeUnit.HOURS.toMillis(10), newCalendarInstanceMillis("Pacific/Honolulu").getTimeInMillis() ); Calendar epoch = newCalendarInstanceMillis("GMT"); epoch.setTimeZone(TimeZone.getTimeZone("Japan")); assertEquals( TimeUnit.HOURS.toMillis(-9), epoch.getTimeInMillis() ); } private Calendar newCalendarInstanceMillis( String timeZoneId) { Calendar calendar = new GregorianCalendar(); calendar.set(Calendar.YEAR, 1970); calendar.set(Calendar.MONTH, 0); calendar.set(Calendar.DAY_OF_MONTH, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); calendar.setTimeZone(TimeZone.getTimeZone(timeZoneId)); return calendar; }
Во время эпохи Unix (часовой пояс UTC) токийское время опережало на девять часов, в то время как Гонолулу отставал на десять часов.
Изменение часового пояса календаря сохраняет фактическое время при изменении смещения зоны. Относительная метка времени изменяется вместе со смещением часового пояса календаря.
Joda-Time и API времени даты Java 8 просто создают java.util.Календарь устарел, поэтому вам больше не нужно использовать этот причудливый API.
Joda-Time || и API времени даты Java 8 просто создают || java.util.Календарь || устарел, поэтому вам больше не нужно использовать этот причудливый API.
Joda-Time стремится исправить устаревший API даты и времени, предлагая:
- как неизменяемые, так и изменяемые структуры данных
- свободный API
- улучшенная поддержка стандарта ISO 8601
С Joda-Time именно так выглядит наш предыдущий тестовый случай:
@Test public void testTimeZonesWithDateTime() throws ParseException { assertEquals( 0L, newDateTimeMillis("GMT").toDate().getTime() ); assertEquals( TimeUnit.HOURS.toMillis(-9), newDateTimeMillis("Japan").toDate().getTime() ); assertEquals( TimeUnit.HOURS.toMillis(10), newDateTimeMillis("Pacific/Honolulu").toDate().getTime() ); DateTime epoch = newDateTimeMillis("GMT"); assertEquals( "1970-01-01T00:00:00.000Z", epoch.toString() ); epoch = epoch.toDateTime(DateTimeZone.forID("Japan")); assertEquals( 0, epoch.toDate().getTime() ); assertEquals( "1970-01-01T09:00:00.000+09:00", epoch.toString() ); MutableDateTime mutableDateTime = epoch.toMutableDateTime(); mutableDateTime.setChronology( ISOChronology.getInstance().withZone(DateTimeZone.forID("Japan")) ); assertEquals( "1970-01-01T09:00:00.000+09:00", epoch.toString() ); } private DateTime newDateTimeMillis( String timeZoneId) { return new DateTime(DateTimeZone.forID(timeZoneId)) .withYear(1970) .withMonthOfYear(1) .withDayOfMonth(1) .withTimeAtStartOfDay(); }
API Дата и время fluent намного проще в использовании, чем java.util.Календарь#установлен . Дата-время неизменяемо, но мы можем легко переключиться на MutableDateTime , если это подходит для нашего текущего варианта использования.
По сравнению с нашим тестовым случаем Календаря, при изменении часового пояса относительная метка времени ничуть не меняется, поэтому остается той же исходной точкой во времени.
Меняется только восприятие времени человеком ( 1970-01-01T00:00:00.000 Z и 1970-01-01T09:00:00.000+09:00 указывая на то же самое абсолютное время).
Относительные и абсолютные экземпляры времени
При поддержке часовых поясов у вас в основном есть две основные альтернативы: относительная метка времени и абсолютное время.
Относительная метка времени
Числовое представление метки времени (количество миллисекунд с момента эпохи) является относительной информацией. Это значение соответствует эпохе UTC, но вам все равно нужен часовой пояс, чтобы правильно отображать фактическое время в определенном регионе.
Будучи длительным значением, это наиболее компактное представление времени, и оно идеально подходит для обмена огромными объемами данных.
Если вы не знаете исходный часовой пояс события, вы рискуете отобразить метку времени по сравнению с текущим местным часовым поясом, а это не всегда желательно.
Абсолютная метка времени
Абсолютная метка времени содержит как относительное время, так и информацию о часовом поясе. Довольно часто метки времени выражаются в их строковом представлении ISO 8601.
По сравнению с числовой формой (длиной 64 бита) строковое представление менее компактно и может занимать до 25 символов (200 бит в кодировке UTF-8).
ISO 8601 довольно часто встречается в XML-файлах, поскольку схема XML использует лексический формат, вдохновленный стандартом ISO 8601 .
Абсолютное представление времени намного удобнее, когда мы хотим восстановить экземпляр времени по сравнению с исходным часовым поясом. Клиент электронной почты может захотеть отобразить дату создания электронной почты с использованием часового пояса отправителя, и это может быть достигнуто только с использованием абсолютных меток времени.
Головоломки
Следующее упражнение направлено на демонстрацию того, насколько сложно правильно обрабатывать структуру даты/времени, соответствующую стандарту ISO 8601, с помощью утилит java.text.DateFormat .
Следующее упражнение направлено на демонстрацию того, насколько сложно правильно обрабатывать структуру даты/времени, соответствующую стандарту ISO 8601, с помощью утилит || java.text.DateFormat||.
Сначала мы собираемся протестировать возможности java.text.SimpleDateFormat синтаксического анализа, используя следующую логику тестирования:
private void dateFormatParse( String pattern, String dateTimeString, long expectedNumericTimestamp) { try { Date utcDate = new SimpleDateFormat(pattern).parse(dateTimeString); if(expectedNumericTimestamp != utcDate.getTime()) { LOGGER.warn( "Pattern: {}, date: {} actual epoch {} while expected epoch: {}", new Object[]{ pattern, dateTimeString, utcDate.getTime(), expectedNumericTimestamp } ); } } catch (ParseException e) { LOGGER.warn( "Pattern: {}, date: {} threw {}", new Object[]{ pattern, dateTimeString, e.getClass().getSimpleName() } ); } }
Вариант использования 1
Давайте посмотрим, как различные шаблоны ISO 8601 ведут себя в отношении этого первого анализатора:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "1970-01-01T00:00:00.200Z", 200L );
Что привело к следующему результату:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSS'Z', date: 1970-01-01T00:00:00.200Z actual epoch -7199800 while expected epoch: 200
Этот шаблон не соответствует стандарту ISO 8601. Символ одинарной кавычки является escape-последовательностью, поэтому конечный символ ‘Z’ не рассматривается как директива времени (например, время Зулу). После анализа мы просто получим ссылку на дату местного часового пояса.
Этот тест был выполнен с использованием моего текущего системного часового пояса по умолчанию Европа/Афины
, который на момент написания этого поста на два часа опережает UTC.
Вариант использования 2
Согласно java.util.SimpleDateFormat документации, следующий шаблон: гггг-ММ-дд’т’Ч:мм:сс.SSSZ должен соответствовать строковому значению даты/времени ISO 8601:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200Z", 200L );
Но вместо этого мы получили следующее исключение:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSZ, date: 1970-01-01T00:00:00.200Z threw ParseException
Таким образом, этот шаблон, похоже, не анализирует строковые значения UTC по времени Зулу.
Вариант использования 3
Следующие шаблоны отлично подходят для явных смещений:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0000", 200L );
Вариант использования 4
Этот шаблон также совместим с другими смещениями часовых поясов:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0100", 200L - 1000 * 60 * 60 );
Пример использования 5
Чтобы соответствовать обозначению времени зулу, нам нужно использовать следующий шаблон:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200Z", 200L );
Пример использования 6
К сожалению, этот последний шаблон несовместим с явными смещениями часовых поясов:
dateFormatParse( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200+0000", 200L );
В конечном итоге со следующим исключением:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSXXX, date: 1970-01-01T00:00:00.200+0000 threw ParseException
В конечном итоге со следующим исключением:
В отличие от java.text.SimpleDateFormat , Joda-Time совместим с любым шаблоном ISO 8601. Следующий тестовый случай будет использоваться для предстоящих тестовых случаев:
private void jodaTimeParse( String dateTimeString, long expectedNumericTimestamp) { Date utcDate = DateTime.parse(dateTimeString).toDate(); if(expectedNumericTimestamp != utcDate.getTime()) { LOGGER.warn( "date: {} actual epoch {} while expected epoch: {}", new Object[]{ dateTimeString, utcDate.getTime(), expectedNumericTimestamp } ); } }
Joda-Time совместим со всеми стандартными форматами даты и времени ISO 8601:
jodaTimeParse( "1970-01-01T00:00:00.200Z", 200L ); jodaTimeParse( "1970-01-01T00:00:00.200+0000", 200L ); jodaTimeParse( "1970-01-01T00:00:00.200+0100", 200L - 1000 * 60 * 60 );
Вывод
Как вы можете видеть, с древними утилитами даты и времени Java работать непросто. Joda-Time-гораздо лучшая альтернатива, предлагающая лучшие функции управления временем.
Если вам посчастливилось работать с Java 8, стоит переключиться на Java 8 Date/Time API , который очень сильно основан на Joda-Времени .
Код доступен на GitHub .