Основные понятия времени
Большинство веб-приложений должны поддерживать разные часовые пояса, и правильная обработка часовых поясов не так проста. Что еще хуже, вы должны убедиться, что временные метки согласованы для разных языков программирования (например, 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 |
| Tokio | 1970-01-01T00: 00: 00,000 + 09: 00 |
| Гонолулу | 1969-12-31T14: 00: 00.000-10: 00 |
Основы времени Java
java.util.Date
java.util.Date — определенно самый распространенный класс, относящийся ко времени. Он представляет собой фиксированный момент времени, выраженный как относительное количество миллисекунд, прошедших с начала эпохи. java.util.Date не зависит от часового пояса , за исключением метода toString, который использует локальный часовой пояс для генерации представления String.
java.util.Calendar
Java.util.Calendar является одновременно фабрикой даты / времени, а также экземпляром синхронизации с учетом часового пояса. Это один из наименее удобных для работы классов Java API, и мы можем продемонстрировать это в следующем примере:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Testpublic 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 newCalendarInstance(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 и Java 8 Date Time API просто делают java.util.Calandar устаревшим, так что вам больше не придется использовать этот причудливый API.
org.joda.time.DateTime
Joda-Time стремится исправить устаревший API Date / Time, предлагая:
- как неизменяемые, так и изменяемые структуры дат
- свободный API
- лучшая поддержка стандарта ISO 8601
С Joda-Time так выглядит наш предыдущий тестовый пример:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Testpublic 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 DateTime намного проще в использовании, чем набор java.util.Calendar # . DateTime является неизменным, но мы можем легко переключиться на MutableDateTime, если это подходит для нашего текущего варианта использования.
По сравнению с нашим тестовым календарем, при изменении часового пояса относительная временная метка не меняется, поэтому остается прежней исходной точкой времени.
Меняется только восприятие человеческого времени ( 1970-01-01T00: 00: 00.000Z и 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 .
java.text.SimpleDateFormat
Сначала мы протестируем возможности разбора java.text.SimpleDateFormat , используя следующую логику тестирования:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
/** * DateFormat parsing utility * @param pattern date/time pattern * @param dateTimeString date/time string value * @param expectedNumericTimestamp expected millis since epoch */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 ведут себя с этим первым анализатором:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "1970-01-01T00:00:00.200Z", 200L); |
Дает следующий результат:
|
1
|
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 следующий шаблон: гггг-ММ-дд’Т’ХЧ: мм: сс.СССЗ должен соответствовать строковому значению даты / времени ISO 8601:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200Z", 200L); |
Но вместо этого мы получили следующее исключение:
|
1
|
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSZ, date: 1970-01-01T00:00:00.200Z threw ParseException |
Таким образом, этот шаблон, кажется, не анализирует строковые значения UTC времени Зулу.
Вариант использования 3
Следующие шаблоны прекрасно работают для явных смещений:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0000", 200L); |
Вариант использования 4
Этот шаблон также совместим с другими смещениями часового пояса:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0100", 200L - 1000 * 60 * 60); |
Вариант использования 5
Чтобы соответствовать нотации времени зулу, нам нужно использовать следующую схему:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200Z", 200L); |
Вариант использования 6
К сожалению, этот последний шаблон не совместим с явными смещениями часовых поясов:
|
1
|
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200+0000", 200L); |
Окончание со следующим исключением:
|
1
|
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSXXX, date: 1970-01-01T00:00:00.200+0000 threw ParseException |
org.joda.time.DateTime
В отличие от java.text.SimpleDateFormat , Joda-Time совместим с любым шаблоном ISO 8601. Следующий тестовый пример будет использоваться для следующих тестовых случаев:
|
01
02
03
04
05
06
07
08
09
10
11
|
/** * Joda-Time parsing utility * @param dateTimeString date/time string value * @param expectedNumericTimestamp expected millis since epoch */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:
|
1
2
3
|
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 Date / Time работать нелегко. Joda-Time — намного лучшая альтернатива, предлагающая лучшие функции обработки времени.
Если вам доведется работать с Java 8, стоит перейти на API даты / времени Java 8 , разработанный с нуля, но очень вдохновленный Joda-Time .
- Код доступен на GitHub .
| Ссылка: | Руководство для новичков по обработке часовых поясов Java от нашего партнера JCG Влада Михалча в блоге Влада Михалча . |