Меню

Как сравнить время java



Дата и время

Содержание

Java предоставляет класс Date, который доступен в пакете java.util, этот класс заключает в себе текущую дату и время.

Конструкторы

Класс Date поддерживает два конструктора, которые показаны ниже.

Конструктор и описание
1 Date()
Этот конструктор инициализирует объект с текущей датой и временем.
2 Date(long millisec)
Этот конструктор принимает аргумент равный числу миллисекунд, истекших с полуночи 1 января 1970 г.

Методы класса Date

Ниже представлены методы класса Date.

Методы с описанием
1 boolean after(Date date)
Возвращает значение true, если вызывающий объект date содержит дату, которая раньше заданной даты, в противном случае он возвращает значение false.
2 boolean before(Date date)
Возвращает значение true, если вызывающий объект date содержит дату, более раннюю, чем заданная дата, в противном случае он возвращает значение false.
3 Object clone()
Дублирование вызывающего объекта date.
4 int compareTo(Date date)
Сравнивает значение вызывающего объекта с этой датой. Возвращает 0, если значения равны. Возвращает отрицательное значение, если объект вызова является более ранним, чем дата. Возвращает положительное значение, если объект вызова позже даты.
5 int compareTo(Object obj)
Работает точно так же compareTo(Date), если объект вызова имеет класс Date. В противном случае вызывает ClassCastException.
6 boolean equals(Object date)
Возвращает значение true, если вызывающий объект date содержит то же время и дату, которая указана в date, в противном случае он возвращает значение false.
7 long getTime()
Возвращает количество миллисекунд, истекших с 1 января 1970 года.
8 int hashCode()
Возвращает хэш-код для вызывающего объекта.
9 void setTime(long time)
Задает дату и время, соответствующие моменту времени, что представляет собой общее затраченное время в миллисекундах от полуночи 1 января 1970 года.
10 String toString()
Преобразует вызывающий объект date в строку и возвращает результат.

Текущая дата и время в Java

Получить текущую дату и время в Java достаточно не трудно. Вы можете использовать простой объект date вместе с методом toString(), чтобы вывести текущую дату и время следующим образом:

Получим следующий результат:

Сравнение дат

Существуют три способа в Java сравнить даты:

  • Можно использовать функцию getTime(), чтобы получить количество миллисекунд, прошедших с момента полуночи 1 января 1970, для обоих объектов, а затем сравнить эти два значения.
  • Вы можете использовать методы before(), after() и equals(). Поскольку 12 число месяца раньше 18 числа, например, new Date(99, 2, 12).before(new Date (99, 2, 18)) возвращает значение true.
  • Можно использовать метод compareTo(), который определяется сопоставимым интерфейсом и реализуется по дате.

Форматирование даты с помощью SimpleDateFormat

SimpleDateFormat — это конкретный класс для парсинга и форматирования даты в Java. SimpleDateFormat позволяет начать с выбора любых пользовательских шаблонов для форматирования даты и времени. Например:

Получим следующий результат:

Формат-коды SimpleDateFormat

Для того, чтобы задать в Java формат даты и времени, используйте строковый шаблон (регулярные выражения) с датой и временем. В этой модели все буквы ASCII зарезервированы как шаблон письма, которые определяются следующим образом:

Символ Описание Пример
G Обозначение эры н.э.
y Год из четырех цифр 2016
M Номер месяца года 11
d Число месяца 13
h Формат часа в A.M./P.M.(1

12)

7
H Формат часа(0

23)

19
m Минуты 30
s Секунды 45
S Миллисекунды 511
E День недели Вс
D Номер дня в году 318
F Номер дня недели в месяце 2 (второе воскресение в этом месяце)
w Номер неделя в году 46
W Номер недели в месяце 2
a Маркер A.M./P.M. AM
k Формат часа(1

24)

24
K Формат часа в A.M./P.M.(0

11)

z Часовой пояс FET (Дальневосточноевропейское время)
Выделение для текста Текст
» Одинарная кавычка

Форматирование даты с помощью printf

Формат даты и времени можно сделать без труда с помощью метода printf. Вы используете формат двух букв, начиная с t и заканчивая одним из символов в таблице, приведенных ниже. Например:

Получим следующий результат:

Было бы немного глупо, если Вы должны были бы поставить дату несколько раз для форматирования каждой части. По этой причине формат строки может указывать индекс аргумента для форматирования.

Индекс должен непосредственно следовать за % и завершаться $. Например:

Получим следующий результат:

Получим следующий результат:

Символы преобразования даты и времени

В Java вывод даты в нужном формате можно реализовать с помощью следующих символов преобразования:

Символ Описание Пример
c Текущее время и дата Вс ноя 13 01:19:27 FET 2016
F Формат даты ISO 8601 (год-месяц-день) 2016-11-13
D Американский формат даты (месяц/день/год) 11/13/16
T 24-часовой формат времени 01:26:09
r 12-часовой формат времени 01:26:51 AM
R 24-часовой формат времени без секунд 01:27
Y Текущий год из четырех цифр (с ведущими нулями) 2016
y Последние две цифры года (с ведущими нулями) 16
C Первые две цифры года (с ведущими нулями) 20
B Полное название месяца ноября
b Сокращенное название месяца ноя
m Номер текущего месяца (с ведущими нулями) 11
d Номер текущего дня месяца (с ведущими нулями) 09
e Номер текущего дня месяца (без ведущих нулей) 9
A Полное название текущего дня недели воскресенье
a Сокращенное название дня недели Вс
j Количество дней с начала года (с ведущими нулями) 318
H Формат часа (с ведущими нулями), от 00 до 23 01
k Формат часа (без ведущих нулей), от 0 до 23 1
I Формат часа (с ведущими нулями), от 01 до 12 01
l Формат часа (без ведущих нулей), от 1 до 12 1
M Минуты (с ведущими нулями) 38
S Секунды (с ведущими нулями) 50
L Миллисекунды (с ведущими нулями) 382
N Наносекунды (с ведущими нулями) 775000000
p (%Tp) Верхний регистр маркера A.M./P.M. AM
p (%tp) Нижний регистр маркера A.M./P.M. am
z Часовое смещение RFC 822 по GMT +0300
Z Часовой пояс FET
s Секунды, начиная с 1970-01-01 00:00:00 GMT 1478991147
Q Миллисекунды, начиная с 1970-01-01 00:00:00 GMT 1478991172134

Есть и другие полезные классы, связанные с датой и временем. Для получения более подробной информации обратитесь к стандартной документации Java.

Преобразование строки в дату

Класс SimpleDateFormat имеет некоторые дополнительные методы, в частности parse(), который в Java поможет нам перевести строку в дату соответствии с форматом, хранящимся в данном объекте SimpleDateFormat. Например:

Получим следующий результат:

Задержка по времени

Вы можете увеличить или уменьшить время работы программы на любой период времени, от одной миллисекунды до срока службы вашего компьютера. Например, следующая программа будет находится в режиме ожидания в течение 10 секунд:

Получим следующий результат:

Вместо Thread.sleep() рекомендуется использовать TimeUnit(): TimeUnit.NANOSECONDS.sleep(), TimeUnit.MICROSECONDS.sleep(), TimeUnit.MILLISECONDS.sleep(), TimeUnit.SECONDS.sleep(), TimeUnit.MINUTES.sleep(), TimeUnit.HOURS.sleep() или TimeUnit.DAYS.sleep().

Время выполнения программы

Довольно просто можно узнать время выполнения кода вашей программы с помощью System.currentTimeMillis(). Для этого необходимо в начале программы записать в переменную значение System.currentTimeMillis(), а в конце вычесть из текущего значения System.currentTimeMillis() переменную, записанную вначале. Рассмотрим пример, в котором измерим скорость работы кода программы, которая выводит 10 случайных чисел на экран.

Получим следующий результат:

Разница дат в Java

Иногда Вам может понадобиться рассчитать разницу между датами, измерить точку во времени в миллисекундах. Поэтому давайте перепишем еще раз пример, находящийся выше:

Получим следующий результат:

Количество дней между датами

А иногда Вам может понадобиться в Java узнать количество дней, часов, минут и т.п. между датами. Рассмотрим один из способов нахождения дней между двумя датами ниже в примере:

Получим следующий результат:

Класс GregorianCalendar

GregorianCalendar является конкретной реализацией класса Calendar, который отображает обычный григорианский календарь, с которым Вы знакомы. Мы не обсуждали класс Calendar в этом учебнике, для этого Вы можете посмотреть стандартную документацию Java.

Метод getInstance() Calendar возвращает GregorianCalendar, который инициализирован по умолчанию текущей датой и временем, локализацией и часовым поясом. GregorianCalendar определяет два поля: н. э и до н. э. Они представляют собой две эпохи, которые определяются по григорианскому календарю.

Существует несколько конструкторов для объектов GregorianCalendar:

Конструктор и его описание
1 GregorianCalendar()
Создает значение GregorianCalendar, используя по умолчанию текущей датой и временем, локализацией и часовым поясом.
2 GregorianCalendar(int year, int month, int date)
Создает GregorianCalendar в соответствии с заданной датой в часовом поясе и локализацией по умолчанию.
3 GregorianCalendar(int year, int month, int date, int hour, int minute)
Создает GregorianCalendar в соответствии с заданной датой и временем в часовом поясе и локализацией по умолчанию.
4 GregorianCalendar(int year, int month, int date, int hour, int minute, int second)
Создает GregorianCalendar в соответствии с заданной датой и временем в часовом поясе и локализацией по умолчанию.
5 GregorianCalendar(Locale aLocale)
Создает GregorianCalendar в соответствии с текущим временем в часовом поясе по умолчанию в рамках заданной локализации.
6 GregorianCalendar(TimeZone zone)
Конструирует GregorianCalendar, основанный на текущем времени в данной зоне времени с локализацией по умолчанию.
7 GregorianCalendar(TimeZone zone, Locale aLocale)
Конструирует GregorianCalendar, основанный на текущем времени в заданном часовом поясе и локализации.

Список нескольких полезных методов, предоставляемых классом GregorianCalendar:

Методы с описанием
1 void add(int field, int amount)
Добавляет указанное количество времени в данное временное поле в соответствии с правилами календаря.
2 protected void computeFields()
Преобразует время по Гринвичу в миллисекунды до значения полей времени.
3 protected void computeTime()
Преобразует значения временного поля Календаря в UTC формате в миллисекундах.
4 boolean equals(Object obj)
Сравнивает этот GregorianCalendar эталонным объектом.
5 int get(int field)
Получает значение для поля заданного времени.
6 int getActualMaximum(int field)
Возвращает максимальное значение, которое это поле может иметь, учитывая текущую дату.
7 int getActualMinimum(int field)
Возвращает минимальное значение, которое это поле может иметь, учитывая текущую дату.
8 int getGreatestMinimum(int field)
Возвращает наибольшее минимальное значение для данного поля, если изменяется.
9 Date getGregorianChange()
Получает изменения даты по григорианскому календарю.
10 int getLeastMaximum(int field)
Возвращает минимально максимальное значение для данного поля, если изменяется.
11 int getMaximum(int field)
Возвращает максимальное значение для данного поля.
12 Date getTime()
Определяет текущее время в соответствии с календарем.
13 long getTimeInMillis()
Получает текущее время по Календарю как длительное.
14 TimeZone getTimeZone()
Возвращает часовой пояс.
15 int getMinimum(int field)
Возвращает минимальное значение для данного поля.
16 int hashCode()
Переопределите хэш-код.
17 boolean isLeapYear(int year)
Определяет, является ли год високосным.
18 void roll(int field, boolean up)
Добавление или вычитание (вверх/вниз) одной единицы времени в данном временном поле без изменений в больших полях.
19 void set(int field, int value)
Устанавливает временное поле с заданным значением.
20 void set(int year, int month, int date)
Задает значения для поля год, месяц и дата.
21 void set(int year, int month, int date, int hour, int minute)
Задает значения для поля год, месяц, дату, час и минуту.
22 void set(int year, int month, int date, int hour, int minute, int second)
Задает значения для поля год, месяц, дату, час, минуту и секунду.
23 void setGregorianChange(Date date)
Устанавливает дату изменения грегорианского календаря.
24 void setTime(Date date)
Устанавливает в соответствии с данным календарем текущее время с заданной датой.
25 void setTimeInMillis(long millis)
Устанавливает в соответствии с данным календарем текущее время от заданного long значения.
26 void setTimeZone(TimeZone value)
Задает часовой пояс со значением заданного часового пояса.
27 String toString()
Возвращает строковое представление календаря.

Пример 1: вывод текущей даты и времени, високосный год

Получим следующий результат:

Для изучения полного списка констант в классе календаря обратитесь к стандартной документации Java.

Пример 2: получить день недели по дате

Получим следующий результат:

Источник

ТЛ; др

Вы должны подумать об обратном: как превратить эту строку в значение времени . Вы не будете пытаться математически, превращая свои числа в строки. Так же и со значениями даты и времени.

Избегайте старых связанных классов, java.util.Date и .Calendar, поскольку они общеизвестно хлопотны, имеют недостатки как в дизайне, так и в реализации. Они вытесняются новым пакетом java.time в Java 8 . И java.time был вдохновлен Joda-Time .

Оба java.time и Joda время предлагают класс , чтобы захватить время- дня без даты к временной зоне: LocalTime .

java.time

Использование классов java.time, встроенных в Java, в частности LocalTime . Получить текущее время суток в вашем местном часовом поясе. Создайте время суток для вашей входной строки. Сравните с isBefore , isAfter или isEqual методами.

Лучше указать желаемый / ожидаемый часовой пояс, чем неявно полагаться на текущий часовой пояс JVM по умолчанию.

Joda времени

Код в этом случае с использованием библиотеки Joda-Time оказывается почти таким же, как и код, приведенный выше для java.time.

Имейте в виду, что проект Joda-Time сейчас находится в режиме обслуживания , и группа рекомендует переход на классы java.time .

О java.time

Java.time каркас встроен в Java 8 и более поздних версий. Эти классы вытеснять неприятные старые устаревшие классы даты и времени , такие как java.util.Date , Calendar , и SimpleDateFormat .

Проект Joda-Time , находящийся сейчас в режиме обслуживания , рекомендует перейти на классы java.time .

Чтобы узнать больше, см. Учебник по Oracle . И поиск переполнения стека для многих примеров и объяснений. Спецификация JSR 310 .

Где взять классы java.time?

Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является полигоном для возможных будущих дополнений к java.time. Вы можете найти некоторые полезные классы здесь , такие как Interval , YearWeek , YearQuarter , и более .

6 плюса

Просто сравните строки как обычно:

Если времена одинаковы, то x будет, true если они не будут, x будет false

Автор: James Размещён: 24.07.2014 07:42

3 плюса

После получу время. Вы можете использовать его для сравнения

плюса

тогда вы можете сравнить с вашей строкой

плюса

У меня похожая ситуация, когда мне нужно сравнить строку времени, например «15:30», с текущим временем. Я использую Java 7, поэтому я не могу использовать решение Бэзила Бурка. Поэтому я реализовал один метод, который принимает одну строку, например «15:30», и проверяет текущее время и возвращает истину, если текущее время больше времени ввода .

В календаре есть другие методы, такие как after(Object when)

Возвращает, представляет ли этот Календарь время после времени, представленного указанным объектом. Вы также можете использовать этот метод для сравнения строки времени с текущим временем.

плюса

Вот мой пример для сравнения до и после текущего времени:

плюса

Таким образом, мое решение немного отличается, оно начинается с создания часов с текущим временем и проверки часа, заданного пользователем int.

плюса

Этот код проверяет введенное пользователем время с текущим системным временем и возвращает разницу во времени в длинном значении.

Пользователь должен передать время в формате «гггг-мм-дд чч: мм: сс» .

Источник

Java и время: часть первая

Восемь лет назад я принимал участие в проектировании и разработке сервиса, который был должен обслуживать запросы пользователей со всех уголков земного шара и координировать их действия. Работая над проектом я понял, что очень часто многие важные аспекты работы со временем просто игнорируются. Иногда это действительно не очень критично: если сервис локален и им пользуются только на определенной территории, либо пользователи естественным образом разделены на почти не взаимодействующие между собой географические кластеры. Однако же, если сервис объединяет пользователей по всему миру, то без четкого понимания принципов работы со временем уже не обойтись. Представим сервис, в котором общие события (совещания например) начинаются в какое-то строго определенное время, а пользователи рассчитывают на это. Какое время им показывать, в какой момент их беспокоить уведомлениями, что такое день рождения и когда можно поздравить человека — в статье я попробую это осмыслить.

Статья не претендует на глубину и/или академичность. Это попытка систематизировать опыт и обратить внимание разработчиков на не очень очевидные аспекты.

В JDK8 появилось новое Date Time API, в котором появилось множество новых полезных классов, но я не буду его упоминать, также как и об отличных сторонних библиотеках. Это отдельная, большая тема, которая заслуживает отдельной статьи; плюс, как я уже упомянул, я стараюсь рассказывать не о какой-то конкретной технологии, а о принципах работы в целом, а в этом плане новое API принципиально ничем не отличается от старого.

Временная ось

Начнем сильно издалека. Прямо очень издалека. Нарисуем ось времени.

Прямо тут начинаются разные вопросы. Идет ли время всегда в одном направлении? Идет ли оно равномерно? Непрерывно ли оно? Что принять за единичный вектор? Едина ли эта ось для всех? Зависит ли от положения в пространстве? От скорости движения? Каков минимально измеряемый отрезок?

Собственно тут я готов только задавать вопросы, но никак не отвечать на них. Есть мнение, что никакого времени нет, но я также пока не готов к обсуждению этого вопроса.

Но есть и уже решенные моменты.

Про единицу измерения все ясно — она четко и однозначно специфицирована.

Расстояние от Москвы до Вашингтона составляет примерно 7840000 метров и свет проходит это расстояние по поверхности земли минимум за 0.026 секунды, что совсем немало. Запрос на создание учетной записи пользователем во Владивостоке, будет отработан на московском сервере только через некоторое время. Таким образом информация о происходящих событиях доступна совсем не сразу и зависит от расстояния между точками в пространстве.

Кроме того, сама скорость течения времени зависит от скорости перемещения объекта, причем даже для вполне рядовых около-земных технологий вроде GPS.

Текущая стандартная библиотека обработки времени на Java считает, что никаких релятивистских эффектов не существует и никто не движется на около-световых скоростях, а ось времени одна и едина для всех (по крайней мере в масштабах одной планеты) — и это вполне всех нас устраивает. Возможно впоследствии в JDK #6543 будет реализован новый Java Date Time API, который позволит написать сервис для внутренней офисной системы «Сокола Тысячелетия» с учетом скорости его движения и наличия/отсутствия кротовых нор рядом.

Теперь отметим на временной оси некий момент. Вот, например, прямо сейчас я нажму на кнопку «точка». (Нажал)

Теперь нужно придумать способ, с помощью которого я смог бы сообщить вам о том, в какой именно момент я нажал эту кнопку. Самый простой способ сделать это — обозначить какой-то момент времени, общий для нас всех, с которого мы все постоянно отсчитываем временные отсчеты. Если этот момент времени обозначен (тот-самый-момент), то я смогу передавать вам число своих отсчетов с этого общего момента, а вы сможете понять отношение между временем нажатия кнопки и своим текущим временем при получении моего значения.

В примитивном физическом мире мы могли бы встретиться и одновременно запустить одинаковые песочные часы. После чего спокойно разойтись по своим делам, а времена событий сообщать в виде высоты песочного столба на нашем экземпляре часов (вероятно часы должны быть очень большими и громоздкими).

Используемый нами тот-самый-момент, в свою очередь, может быть также измерением — но уже относительно какого-то более общего и важного события. В нашем случае так и происходит, тот-самый-момент в системе Unix-time (числовое значение 0), которая используется в Java — это временная точка с меткой 00:00:00 1 января 1970 от Р.Х. по UTC уже по другой шкале — Григорианскому календарю.

При чем тут Java

Для задания временной точки на временной оси в Java существует класс java.util.Date. Вообще, java.util.Date — это позор Java, начиная с самых ранних версий. Во-первых, у него название не отражает суть; а во-вторых, он mutable. Однако жизнь будет проще, если вы будете воспринимать его как простую обертку над внутренним полем типа long в котором хранится количество миллисекунд с того-самого-момента — и ничего более. Все остальные методы помечены как устаревшие и использовать их не нужно ни в коем случае. Просто запомните, что java.utl.Date тождественен (по своей сути) простому числовому long-значению Unix-time в миллисекундах.

Если немного подумать, то становится понятно, что в любом языке есть ограничения точности представления времени. Поэтому java.util.Date (как и любые другие подобные типы) представляет собой вовсе не точку на временной оси, а отрезок. В нашем случае — отрезок миллисекундной длительности. Но с практической точки зрения такая точность нас устраивает, поэтому и дальше будем называть это точкой.

Поскольку представление в Java с самого начала 64-битное, то на наш век хватит точно:

Для различных операций с временем таких как чтение/установка/модификация отдельных календарных полей (год, месяц, день, часы, минуты, секунды и прочее) существует класс java.util.Calendar. Он также не без греха — при операциях помните, что месяцы идут с 0 (лучше использовать константы), а дни идут с 1.

Другой мутный момент в java.util.Calendar состоит в том, что при установке в нем полной даты (yyyy,MM,dd,HH,mm,ss) количество миллисекунд не сбрасывается в 0, а остается равным количеству миллисекунд от предыдущего установленного момента (текущего времени, если календарь не менялся). Поэтому, если по условиям задачи в миллисекундах должно быть 0, то это поле нужно сбросить еще одним дополнительным вызовом:

Первое число гуляет в первых трех разрядах в зависимости от времени вызова. Это поведение может быть достаточно критичным в тестах.

Для перевода временных меток в точки на оси и обратно существует класс java.text.DateFormat и его наследники.

Про java.text.DateFormat и java.util.Calendar обязательно нужно сказать следующее:

  • У обоих классов есть метод setTimezone() для явной установки временной зоны. Крайне желательно всегда его использовать для того, чтобы обозначить, что вы полностью контролируете процесс, а не полагаетесь на временную зону по-умолчанию.
  • У обоих классов есть метод setLenient() для установки «мягкого» режима. В таком режиме оба класса будут снисходительно относиться к ошибкам в календарных метках, пытаясь угадать что же вы имели в виду на самом деле. Тут зависит от ситуации, но я бы рекомендовал угадывание отключать (по умолчанию «мягкий» режим включен).
  • Оба класса потоко-небезопасны. И, если для java.util.Calendar это совершенно ожидаемо (поскольку мы понимаем что у него есть внутреннее состояние), то, в случае java.text.DateFormat, это для многих оказывается сюрпризом.

Также есть (в старом АПИ) еще несколько классов:
java.sql.Timestamp — расширение (subclass) java.util.Date с наносекундной точностью для работы с типом TIMESTAMP в БД
java.sql.Date — расширение (subclass) java.util.Date для работы с типом DATE в БД.
java.sql.Time — расширение (subclass) java.util.Date для работы с типом TIME в БД.

Кроме того, любую временную точку можно хранить в виде обычного long-значения Unix-time в миллисекундах.

Временные зоны

Любой, кто работал в глобальной компании со множеством офисов по всему миру (или даже просто по России) знает, что информация которая содержится во фразе «совещание будет в 01 января 2016 в 14:00:00» практически бесполезна. Метка «14:00» не соответствует какой-то конкретной точке на временной оси, а вернее сказать — соответствует сразу нескольким. Для того, чтобы все собрались в видео-переговорках в одно и то же время, организатору совещания нужно указать кое-что еще, а именно временную зону, в которой мы будем интерпретировать метку «14:00». Часто временная зона подразумевается по главенству головного офиса («работаем по московскому времени»), в противном же случае, если временная зона по-умолчанию вообще никак не подразумевается, то нужно задать ее в явном виде, например «01 января 2016 в 14:00:00 MSK» — в этом случае точка на временной оси задается совершенно однозначно и все соберутся на совещание в одно и тоже время.

Для устранения неоднозначности при операциях с временными метками в формате ЧЧ:MM:CC временная зона должна быть указана как при выводе временной метки, так и при вводе.

Можно не указывать временную зону явно, в случаях когда ее можно каким-либо образом подразумевать неявно:

  • временная зона подразумевается одной и той же по умолчанию для всего сервиса;
  • временная зона явно указана в профиле самим пользователем;
  • временную зону пользователя можно вычислить по его положению через гео-координаты;
  • временную зону пользователя можно вычислить по его положению через IP адрес;
  • временную зону пользователя можно вычислить по его положению через косвенные признаки (анализ поведения);
  • текущее смещение временной зоны пользователя можно вычислить через JavaScript;

Наверное стоит отметить, что временная зона сервиса (по умолчанию) и временная зона сервера (по умолчанию) — это в общем совсем не одно и то же. BIOS, cистема, приложение и утилиты могут, например, работать во временной зоне UTC, но при всем этом временной зоной сервиса будет временная зона Москвы. Хотя конечно сильно проще когда они совпадают — в таком случае админы настраивают временные зоны на серверах, а программисты о них не думают вообще. Если это как раз ваш случай — дальше можете не читать.

Одну и ту же временную метку можно выводить по-разному для пользователей — с использованием той временной зоны, которая наиболее привычна для каждого. Например, следующие временные метки указывают на одну и ту же временную точку на временной оси:

В Java информация о временной зоне представлена классом java.util.TimeZone.

Нужно сказать, что временная зона — это не смещение. Нельзя сказать что GMT+3 — это Europe/Moscow. Но можно сказать, что в течение всего 2016-го года временная зона Europe/Moscow будет соответствовать смещению GMT+3. Временная зона — это вся история смещений полностью за весь исторический период, а также другие данные, которые позволяют нам правильно вычислять смещения в разные исторические моменты, а также производить правильные временные расчеты.

Поисследуем временные зоны — для начала посмотрим на Europe/Moscow:

Видим, что у зоны Europe/Moscow текущее базовое смещение составляет +3 часа относительно UTC, а общее смещение в данный момент составляет также +3 часа. Перевод стрелок на летнее время в перспективе отсутствует. У зоны есть отдельные имена для летнего и зимнего времени.

Теперь посмотрим на парижское время:

Базовое смещение составляет +1 час относительно UTC, общее смещение в данный момент составляет также +1 час. Переход на летнее время в перспективе у зоны есть. Также присвоено два имени — для зимнего и для летнего времени отдельно.

Теперь посмотрим на зону «GMT+5». Это фактически не совсем временная зона — у нее нет истории, нет летнего времени, а смещение постоянно.

Так и есть, смещение постоянно, составляет +5 часов относительно GMT и никогда не меняется.

Примеры

Продолжим рассказывать примерами то, что слишком долго объяснять на словах. Для начала посмотрим, что происходило в 2005 году во временной зоне «Europe/Moscow»:

Отлично, мы видим перевод стрелок на летнее и обратно на зимнее время. Посмотрим на то, что происходит в эти моменты с временными метками. Для начала — переход на зимнее время:

Видим, что после 02:59:00 MSD стрелки сдвигаются на час назад и следующей меткой идет уже 02:00:00 MSK — зимнее время. Также временная зона говорит о том, что летнее время закончилось, а смещение изменилось с GMT+4 на GMT+3.

В примере есть интересный нюанс: c помощью зоны «Europe/Moscow» совершенно невозможно установить в календаре точку соответствующую метке 02:00:00 MSD — устанавливается точка 02:00:00 MSK, что на час позже чем нужно нам. Чтобы задать эту точку как начало отсчета, приходится прибегать к услугам временной зоны UTC, в которой можно установить все. Другим вариантом может быть установка точки 01:00:00 MSD в зоне «Europe/Moscow» и прибавление часа.

Теперь — переход на летнее время:

Видно, что после 01:59:00 MSK сразу следует 03:00:00 MSD — то есть перевод стрелок на час вперед. Временная зона сигнализирует, что в этот момент смещение меняется с GMT+3 на GMT+4, а также появляется флаг летнего времени.

Но что будет если мы попробуем обработать метку «2005-03-27 02:30:00» в зоне «Europe/Moscow» — в теории такой метки существовать не должно?

Все верно — в строгом режиме мы получаем исключение.

Посчитаем длительность дня в день перевода стрелок на зимнее время:

С 2005-10-30 00:00:00 MSD до 2005-10-31 00:00:00 MSK прошло 25 часов, а не 24.

Теперь проверим день перехода на летнее время:

C 2005-03-27 00:00:00 MSK до 2005-03-28 00:00:00 MSD прошли 23 часа, а не 24.

Эти два последних примера посвящены тем, кто прибавляет 24*60*60*1000 миллисекунд не как 24 часа, а как календарный день. Вы можете сказать, теперь такой проблемы нет, так как больше нет и переводов на летнее/зимнее время. На это я могу ответить следующее:

  • ваша программа должна работать корректно в любой временной зоне, а не только зоне «Europe/Moscow»;
  • расчеты «назад» (в прошлое) все равно требуют корректного подхода;
  • в 2016 году у нас будут выборы госдумы, а в 2018 будут выборы президента — так что я думаю, что история еще не закончена.

java.sql.Time, java.sql.Date

Типы предназначаются для работы с SQL типами TIME и DATE соответственно. Подразумевается, что оба значения от временной зоны не зависят, но к сожалению это не совсем так. Поскольку оба типа являются наследниками java.util.Date — интерпретация дней-часов зависит от временной зоны:

В принципе оба типа справляются с задачей переноса информации от бизнес-логики в JDBC-драйвер поскольку обычно код и там и там работает в одной временной зоне, но в более продвинутых случаях, включая сериализацию, надо быть очень аккуратным при использовании этих классов.

В новом API для соответствующих типов подобные проблемы решены.

UTC, GMT

Большинство знают, что GMT и UTC — это особые обозначения, относительно которых оформляются смещения в других временных зонах. Но не все знают, что UTC и GMT — это не совсем одно и тоже (формально). Я имею в виду то, что метки «2015-12-01 00:00:00 GMT» и «2015-12-01 00:00:00 UTC» обозначают различные (хоть и близкие) точки на временной оси.

GMT вычисляется астрономически по положению земли относительно других объектов. GMT также напрямую используется в качестве временной зоны в некоторых странах.

Поскольку вращение земного шара хаотично замедляется, земля оказывается в одном и том же положении через все увеличивающиеся промежутки времени. Таким образом расстояние между временными точками по соседним меткам по GMT (например «10:00:01» и «10:00:02») может точно не равняться одной секунде.

UTC введен на замену GMT и рассчитывается по атомным часам. Непосредственно в качестве временной зоны не используется (только как опора для смещения).

В UTC расстояние между временными метками (например «10:00:01» и «10:00:02») совершенно одинаковое и строго равно одной секунде. Замедление земного вращения и накапливающееся отличие от GMT решается вводом лишней секунды в году (или даже двух) — а именно секунды координации (leap second).

Таким образом разница между точками с одинаковыми метками в GMT и UTC никогда не превышает одной секунды.

Пишут, что время UTC практически повсюду вытеснило GMT, и что использовать обозначения смещений в виде GMT+3 уже давно моветон — правильно использовать обозначение UTC+3.

Ни GMT ни UTC летнего времени не имеют.

Надо сказать что Unix-time, который используется в Java, ни UTC ни GMT напрямую не соответствует. С одной стороны в Unix-time разница между соседними метками составляет всегда равно 1 секунду, с другой стороны наличие leap second в Unix-time не предполагается.

Временная зона по умолчанию

Отображаете ли вы временную зону при выводе явно или не отображаете, запрашиваете ли вы временную зону при вводе или не запрашиваете, указываете ли вы временную зону при операциях над временем или не указываете — какая-то временная зона все равно присутствует в этих операциях неявно. Если вы не указали свою — будет использована временная зона по-умолчанию.

Термин временная зона по-умолчанию уже был упомянут несколько раз по тексту выше. Все потому что без этого понятия ничего и объяснить толком нельзя. Все операции с временем, вывод и ввод временных меток требуют временную зону. То что вы ее не указываете, не означает что ее нет — просто она берется по-умолчанию.

Но все снова не так-то просто — по умолчанию для кого и чего?

Начнем с ядра. В мануале к hwclock сказано, что в ядре есть внутренняя концепция временной зоны, но ее почти никто не использует, кроме некоторых редких модулей — вроде драйвера файловой системы FAT. Проинформировать ядро о смене временной зоны можно этой же командой hwclock.

Прикладные приложения определяют временную зону по-умолчанию несколькими способами.

Во-первых, общесистемная временная зона (полная информация о ней) в Ubuntu хранится в файле (может быть симлинком) /etc/localtime, а имя этой временной зоны — в файле /etc/timezone:

Установить временную зону для системы можно специальной командой для вашего дистрибутива, для Ubuntu это:

А также есть вежливая утилита tzselect:

Вторым способом указания временной зоны является переменная окружения TZ, в которой можно указать идентификатор временной зоны индивидуально для каждой программы и/или пользователя.

Некоторые программы можно попросить о специфической временной зоне в настройках и/или аргументах командной строки:

Кстати, можно попросить date вывести только текущую временную зону без времени:

Но это для обычных программ под libc, а у нас целая платформа Java. Поэтому кроме этих двух перечисленных возможностей у нас есть еще две.

Можно указать аргумент для запуска JVM.

А можно прямо в коде установить временную зону по умолчанию через метод TimeZone.setDefault(TimeZone timeZone):

Или даже все сразу:

База временных зон

Законодатели и правительства различных стран и даже регионов не сидят сложа руки, регулярно включая/отключая летнее время или даже перемещая регионы между часовыми поясами. Критически важно иметь на системах всю последнюю информацию о подобных изменениях — в противном случае время будет вводиться и выводиться неправильно, люди будут получать СМС-ки во время своего сна, а расчеты вроде «+2 календарных дня» будут неправильными.

В Linux обычные программы на libc используют базу временных зон состоящую из файлов в директории /usr/share/zoneinfo. Эти файлы принадлежат пакету tzdata, за которым активно присматривают разработчики каждого из дистрибутивов. Этот пакет своевременно обновляется и проблем с ним я не помню. В крайнем случае всю информацию можно обновить вручную, если ваша развернутая версия Linux уже больше никем не поддерживается.

К счастью, мне не придется расписывать тут формат содержимого этих файлов, ни историю их возникновения — поскольку на Хабре уже есть отличная статья на эту тему. Не менее отличная статья есть в википедии.

Но не все так просто.

Java использует свою собственную базу с временными зонами. И, если, для OpenJDK как правило можно просто и легко обновить пакет tzdata-java штатным пакетным менеджером, то для Oracle JDK придется либо апгрейдить всю JDK целиком на новую версию, либо пользоваться отдельной специальной утилитой для обновления базы временных зон в уже установленной JDK.

Кстати, упомянутая выше библиотеке Joda-time не использует ни системную базу tzdata, ни базу из JVM — да, у нее есть еще одна своя внутренняя база временных зон, которую нужно также обновлять отдельным и неповторимым способом.

Для python нужно ставить (и затем также не забыть обновлять) отдельную библиотеку.

Для javascript есть куча каких-то сторонних библиотек, как минимум я точно помню что поддержка есть в Google Closure.

Вообще тема того, что тот или иной софт использует свои личные базы с временными зонами, всплывает регулярно. Например, модуль календаря Lightning для почтового клиента Thunderbird хранит свою личную sqlite-базу с часовыми зонами, и поэтому при последних изменениях в нашем государстве мне приходилось делать прямые интервенции в эту базу для корректировки. Иначе все митинги просто плыли по времени.

Вообще, есть ощущение, что в основной массе разработчики не страдают паранойей (как я), про временные зоны никто не думает и в базовые поставки своих платформ tzdata никто не включает — кроме разработчиков JVM.

Отдельное слово я хотел бы сказать про Android. Буду краток — временные зоны в Android это боль. При разработке платформы никто не подумал про отдельный механизм обновления tzdata, как и про то, что у законодателей по всему миру есть страшный зуд к переменам (кто бы мог подумать). Базы с временными зонами меняются только в случае, если вендор прошивки этого захочет. Учитывая то, что некоторые вендоры перестают узнавать свои собственные телефоны уже через полгода, то можно сказать, что на многих аппаратах tzdata просто не обновляется никогда. Продвинутые пользователи меняют текущую временную зону в своих аппаратах на другую, более-менее подходящую текущим условиям (например Europe/Minsk вместо Europe/Moscow). Непродвинутые пользователи все также живут в Europe/Moscow (GMT+4) и просто переводят стрелки — в результате чего временные метки событий во всех программах сдвигаются на час назад. Есть конечно вариант с рутированием и использованием сторонних решений для обновления, но всех пользователей рутировать телефоны не заставишь.

Календари

Про необходимость указания временной зоны вместе с меткой уже было сказано. Однако настоящие параноики должны бы указывать еще и систему летоисчисления. Мы не делаем этого, потому как наиболее развитая часть населения земного шара уже договорилась использовать григорианский календарь, хотя мы до сих пор в удовольствием празднуем новый год по юлианскому календарю, а некоторые из нас высказывают другие, отличающиеся точки зрения на то, как именно правильно считать даты.

Есть совершенно другие, порой достаточно странные системы счисления, в которых одно и ту же временную точку можно отобразить совершенно по-иному. Например — календарь Чучхе. Вообще таких систем оказывается достаточно много, а все мы просто не задумываемся, что наш календарь лишь один из многих, возможно самый используемый, но не единственный. Поиграться с некоторыми можно тут.

Leap year

Високосный год — год в котором 366 дней, а не 365 дней как в обычном году. В високосном году добавляется один день к февралю — 29 февраля.

Формула определения того, что год високосный проста и описана в википедии

Leap second

А вот с лишней секундой (секунда координации) все сильно сложнее. Суть процесса в том, что земля постоянно немножко замедляется и ее положение относительно звезд по одним и тем же меткам времени постоянно меняется. Если не производить коррекцию то время дня и ночи будет постоянно сдвигаться. Чтобы этого не происходило ученые отслеживают положение земли, вычисляют необходимую коррекцию и вносят ее в план корректировки. Поскольку процесс замедления хаотичен, долгосрочный план по коррекции составить невозможно — определение необходимости ввода секунды коррекции происходит по текущей ситуации. Также теоретически возможен ввод отрицательной секунды координации — в случае если земной шар вдруг наоборот ускорится.

Подразумевается, что при наличии секунды координации, время по UTC течет с появлением 60-й секунды:

В концепции Unix-time не существует понятия секунды с номером 60: «Because it does not handle leap seconds, it is neither a linear representation of time nor a true representation of UTC.»

Для того, чтобы хоть как-то соответствовать времени по UTC используется трюк с переводом времени на секунду назад в полночь:

Трюк проводится либо через сервисы точного времени, либо самим ядром автономно на основании данных в файле таймзоны.

Это именно хак, который имеет свои негативные последствия:

  • Количество секунд между 23:59:00 и 00:01:00 следующего дня равно 120, а не 121 как должно быть
  • Поскольку одну секунду мы съедаем, все прошлое смещается на секунду вперед

В Java, поскольку все время завязано на концепцию Unix-time, также нет никакого учета leap-second. Нет ни в старом API, ни в новом API, ни в библиотеке Joda-time. При этом сама информация о leap-second в базах tzdata есть, а в JavaDoc к методу java.util.Date#getSeconds говорится о том, что в неких, пока несуществующих, гипотетических Java-машинах значение поля секунд может быть равно 60 или даже 61.

Сначала проверим то, что в момент leap second классы Java эту секунду не учитывают.

Результат — 120 секунд, а не 121 как должно быть.

Теперь проверим новое API:

Ровно 3600 секунд, а должно быть 3601.

Выясним, сколько именно секунд координации было за все время. Самое простое — проверить это на странице в вики.

Проверим и другими способами. Информация о секундах координации есть в дубликатах временных зон в директории /usr/share/zoneinfo/right.

В основной директории /usr/share/zoneinfo файлы таймзон информацию о секундах координации не содержат.

Как бы мы не смотрели — всего получается 26 таких секунд.

Теперь посчитаем сколько секунд прошло между 1970-01-01 00:00:00 UTC и 2016-01-01 00:00:00 UTC. Посчитаем двумя способами: в Java (по Unix-time) и каким-нибудь другим, более высокоточным способом.

Получилось 1451606400, перепроверим:

Все сходится — также 1451606400 секунд, теперь натравим высокоточное оружие:

Вот оно, теперь 1451606425 секунд. Мне не очень понятно почему разница составляет 25, а не 26 секунд, но других точных калькуляторов я пока что не нашел.

Становится понятно, что с путешествиями во времени есть серьезные проблемы — по крайней мере если мы делаем движок управления с использованием стандартной Java. Установив точное время, мы не сможем точно рассчитать количество секунд на которые мы должны откатиться — ошибка составит до полуминуты. Точные путешествия в будущее невозможны вообще в принципе — поскольку количество секунд координации заранее предопределить невозможно.

currentTimeMillis(), nanoTime()

Как практически на любой другой платформе в Java существует два источника времени: первый отображает текущую метку на общей временной оси, второй — считает время с момента подачи питания на процессор.

  • java.lang.System#currentTimeMillis — возвращает количество миллисекунд прошедших с полночи 1 января 1970 (по временной зоне UTC). Именно этот метод используется при создании новых экземпляров java.util.Date и java.util.Calendar. Несмотря на то, что возвращаемое значение измеряется в миллисекундах, реальная гранулярность может быть сильно выше — до десятков миллисекунд. Никаких гарантий на монотонность нет, значение может скакать вперед и назад — в результате перевода системных часов оператором или сервисами точного времени.
  • java.lang.System#nanoTime — возвращает некоторое абстрактное количество наносекундных «тиков». Значение тиков не обязательно берется с процессорных счетчиков — во-первых, на современном железе есть множество других источников точных сигналов; во-вторых — на многопроцессорных системах есть проблема с тем, что у каждого из процессора счетчик свой и поэтому последовательные вызовы метода могут возвращать скачущие значения в разных потоках. Конкретная реализация зависит от железа, типа и версии операционной системы. Также, несмотря на наносекундную точность результата, никаких гарантий на реальную гранулярность не дается — точность также может снижаться до десятков миллисекунд, но никак не хуже гранулярности java.lang.System#currentTimeMillis

Измерение длительности операций

Исходя из из предыдущей главы, казалось бы можно сделать только один-единственный вывод о правильном измерении времени — нужно использовать только метод java.lang.System#nanoTime. Метод java.lang.System#currentTimeMillis не подходит, поскольку его значение может скакать при изменении системного времени. Этот вывод подтверждается также чтением JavaDoc к обоим методам.

Тем не менее, мы смотрим в методы класса java.lang.Thread и видим нечто очень странное:

По всей видимости, это что-то очень древнее, поскольку, например в java.util.concurrent.ExecutorService, используется уже System.nanoTime.

Этот же вопрос также очень актуален для различных сторонних библиотек, а уже сколько измерений длительности на базе System.currentTimeMillis() реализовано в самописном коде — просто не сосчитать.

Время при тестировании

Не буду растекаться по древу в этом вопросе — просто расскажу про свой успешный опыт в уже упомянутом вначале проекте восьмилетней давности.

Проект был достаточно ответственным — деньги, баллы, сложная бизнес-логика, откаты операция по таймаутам, смена статусов по истечении времени и все подобное.

Разработчиков было два: я и фронтендер, тестировщиков не было вообще. На сервере у нас был фреймворк для IoC, инжекции, принципы low-coupling и high-cohesion. Не то, что бы это был необходимый запас для разработки. Но если начал читать соответствующие книги, становится трудно остановиться. Единственное что вызывало у меня опасение — это тесты. Нет ничего более беспомощного, безответственного и испорченного, чем разработчик начавший писать тесты. Но я знал, что рано или поздно мы перейдем и на эту дрянь.

В общем, я сразу же решительно завел интерфейс:

Этот интерфейс инжектится практически везде, где нужно текущее время. Класс для отслеживания длительности (org.myproject.Timer) получает этот интерфейс в конструктор.

Самое тяжелое при таком подходе — помнить, что можно, а что нельзя:

Полностью отследить такие куски конечно можно только в своем личном коде, но нам важно чтобы этот правильный подход реализовывался в коде отвечающем за персистентность и бизнес-логику.

При обсуждении вопросов тестирования никак невозможно не вспомнить про базы данных.

При таком подходе, если он корректно реализован везде, все автотесты можно отправить в любое время — весь код можно легко выполнить в 1953 году или в 2312:

Если вы уже ткнули пальцем в монитор и сказали мне кг/ам за то, что я не поставил временную зону в вызовах MockChronometer и TimeUtils — то тогда вот мой вам респект. Оставлять их в тестах на откуп текущей временной зоне — значит сделать тест хрупким. На самом же деле оба класса по-умолчанию оперируют в временной зоне UTC всегда, когда зона не указана специально в аргументах методов.

В Java 8 в новом Date Time API появился интерфейс java.time.Clock, который введен ровно для тех же целей — но я не уверен, что общественность это уже оценила.

Альтернативный подход — запускать тесты в отдельной JVM с указанием агента, который будет производить инструментацию кода с целью перехвата вызовов к System.nanoTime() и System.currentTimeMillis(). Такой подход я не пробовал, а беглый поиск готовых решений не предлагает. Более здравым вариантом кажется простой препроцессинг исходного кода в процессе сборки — замена вызовов System.nanoTime(), System.currentTimeMillis(), new Date(), Calendar.getInstance() на вызовы к своему классу.

Spring Framework

8 лет назад в Spring был LocaleResolver, но не было TimezoneResolver (что, как мне кажется, вполне характеризует общее отношение к проблеме). Пришлось написать свой комплект, а заодно сделать сабкласс DispatcherServlet.

После не очень многочисленных, но достаточно настойчивых просьб сообщества (моих в том числе), штатный резолвер временной зоны запроса был введен в 4-й версии фреймворка.

Отдельный вопрос — как, зная временную зону в контроллере, правильно установить ее для шаблонизатора.

В FreeMarker предусмотрена специальная настройка для текущего рендеринга:

Для JSP также можно указать временную зоны индивидуально для одиночного вывода или сразу для всего блока:

В Velocity тоже что-то есть, но я лично не пробовал.

Хранение времени в БД

Самый бронебойный способ хранения временной точки в БД — эта передача значения java.util.Date#getTime() в базу в виде простого числового long-значения Unix-time. Соответственно при чтении мы преобразуем long в java.util.Date с помощью конструктора. Это можно сделать в конверторах Hibernate или RowMapper’ах. База данных в этом случае ничего не знает про время, поэтому никаких внезапных эффектов мы получить не сможем. Если очень надо вывести временную точку в виде метки, то, например в MySQL, всегда можно вызвать метод FROM_UNIXTIME.

Такой способ подходит, если в запросах в БД и/или в хранимых процедурах нет операций с временем. Если же такие операции есть (чего, для простоты разработки, лучше конечно бы избегать), то вы уже в курсе, что без временной зоны они не проходят. В этом случае, надо понять какая именно временная зона действует в ходе операции преобразования или ввода/вывода:

  • временная зона указанная по-умолчанию для сервера;
  • временная зона указанная по-умолчанию для СУБД;
  • временная зона указанная по-умолчанию для базы данных;
  • временная зона указанная по-умолчанию для таблицы;
  • временная зона указанная по-умолчанию для соединения;
  • временная зона хранящаяся в ячейке вместе с временной меткой.

Я ни в коем случае не хочу сказать, что не надо хранить время в специально предназначенных для этого типах. Просто вариант с прямым хранением long очень сложно как-либо сломать (я не смог придумать как) и такой способ никак не зависит от типа хранилища.

При хранении времени в специально предназначенных для этого типах нужно обязательно проверить, что при указанных настройках БД и ее драйвера указанное значение времени пишется, читается и выводится в родной консоли управления этой БД консистентно даже при смене часовых поясов на ходу.

Неконсистентность может быть вызвана тем, что БД на самом деле может хранить временную метку, а не точку. Например в случае MySQL для хранения времени существуют два стандартных типа: TIMESTAMP and DATETIME.

Судя по официальной документации, TIMESTAMP хранит именно временную точку и, получив значение «2015-01-01 12:00:00 MSK» от клиента в московской временной зоне, вернет «2015-01-01 09:00:00 UTC» для другого клиента во временной зоне UTC, что соответствует одной временной точке и совершенно правильно по своей сути. А с типом DATETIME, получив от MSK-клиента «2015-01-01 12:00:00 MSK», сервер MySQL вернет UTC-клиенту «2015-01-01 12:00:00 UTC», что соответствует уже другой временной точке и все дальнейшие расчеты будут неверными.

Проверим MySQL. Сначала все подготовим:

Устанавливаем в сессии зону «Europe/Moscow» и создаем запись:

Меняем временную зону сессии на «UTC» и читаем запись снова:

Видим, что временная метка поля t1 изменилась — как и должно быть, а временная метка для поля t2 при смене временной зоны не изменилась и теперь соответствует другой точке на числовой оси.

Для большинства БД существуют специфичные типы, которые хранят только дату и только время. Такие типы невозможно привести к временной точке, не указав временную зону (о чем мы уже знаем), но также необходимо дополнить эти значения недостающей частью (временем или датой соответственно). На самом деле, можно и не приводить такие типы к временным точкам, если в этом нет никакой необходимости — например в случае простого вывода дня исторического события. Просто нужно помнить, что это метка (или даже ее часть), а не временная точка на оси.

Многие БД имеют разновидности типов, которые кроме самой временной точки хранят дополнительную и информацию о временной зоне в которой она была введена. Например, это может быть полезно, если нужно знать какая именно временная зона была первична для этого значения.

Вне зависимости от причин, по которым локальное время в системе отклоняется от реального, на помощь приходят сервисы поставки точного времени с различными протоколами, самый популярный из которых — это протокол NTP.

Атомные часы — достаточно дорогое удовольствие, поэтому чтобы не перегрузить связанные с ними сервера, в NTP выстраивается целая иерархия для обслуживания запросов рядовых пользователей.

Клиент регулярно опрашивает сразу несколько сервисов точного времени в разных подсетях одновременно, производит компенсацию времени потраченного на отправку и прием UDP датаграмм, откидывает значения являющиеся выбросами и затем усредняет их специальным алгоритмом.

Заявляется, что при использовании в публичных сетях возможная точность синхронизации может достигать 10мс.

Уведомления

В большинстве сервисов мобильные уведомления (пуши и СМС) можно поделить на два класса: срочные (сообщения, новые дружбы, платежи) и несрочные (промо, реклама, предложения, изменения регламента). Первые пользователь скорее всего ожидает получить сразу же при наступлении события, для вторых же события как такового может и не быть, и пользователя лучше беспокоить в комфортное для него время. Комфортным временем по-умолчанию можно считать например период с 10 утра до 20 вечера. Если это критично, комфортное время можно позволить указывать вручную индивидуально.

Так или иначе, этот период мы обязаны трактовать в какой-то временной зоне и вполне очевидно, что это будет временная зона пользователя. В принципе, если сервис работает только для какой-то ограниченной территории (например для одного города) все это можно не учитывать и считать, что временная зона всех пользователей совпадает с временной зоной сервера, но для распределенных сервисов этого недостаточно.

Поэтому временную зону необходимо сохранять для фоновых задач — как минимум индивидуально для пользователя, а еще лучше отдельно для каждого устройства пользователя. В современных реалиях одна и та же учетная запись приложения может использоваться сразу на нескольких устройствах: например на стационарном Android TV, который всегда находится дома в Москве; и на Android-планшете, которые уезжает вместе с пользователем в отпуск в Таиланд. Поэтому возможно, что уведомления на оба устройства придется отсылать в разное время. Имея временную зону пользователя и комфортное для него время, всегда можно рассчитать точку на временной оси, когда мы можем начать его спамить.

A.M. / P.M.

Я предполагаю, что тут нет людей, которые бы уже не поняли, что речь идет о 12-часовом представлении временных меток.

Но не только лишь все знают, как именно переводятся эти аббревиатуры: a.m. (лат. ante meridiem дословно — «до полудня») и p.m. (лат. post meridiem дословно — «после полудня»).

А сюрпризом для многих будет то, что полночь — это не 12pm и даже не 0am, а очень даже 12am. Аналогично, полдень — это не 12am и не 0pm, а 12pm. В таких обозначениях путаются даже жители привычных к такому формату стран, поэтому придумывают различные трюки.

Часы реального времени

На любой железке с любой архитектурой есть специальный чип с подключенной батарейкой — это часы реального времени или в терминологии Linux «The Hardware Clock».

Они бесшумно тикают своими электронами вне зависимости от внешних условий: подано ли основное питание на системный блок, установлена ли операционная система, а также от наличия или отсутствия каких-либо ваших действий. Их ход прерывается только после того как энергия в батарейке иссякнет, что на самом деле произойдет с вашим оборудованием достаточно нескоро.

Для управления часами Hardware Clock (фактически для отправки команд в этот чип) в Linux существует специальная команда hwclock. Поскольку она напрямую общается с оборудованием (как правило это /dev/rtc), выполнять ее нужно с правами root.

Команда автоматически приводит время полученное из Hardware Clock к временной зоне системы. Но что это за странное смещение в конце вывода?

Дело в том, что на самом деле в ядре Linux есть еще другие, свои собственные часы (The System Time). Ядро Linux считывает показания с чипа один раз, в начале загрузки ядра, после чего тикает уже само по себе в ходе прерываний. Скорее всего это сделано по одной простой причине — читать данные с железного чипа Hardware Clock по последовательной шине на каждый пользовательский запрос относительно долго, поэтому проще вести свой локальный, исключительно программный счетчик. После того как ядро Linux прочитало значение с Hardware Clock, с последними теоретически можно производить любые операции — на время в приложениях это никак не повлияет.

Таким образом в системе параллельно тикают двое часов — настоящие электронные часы и программные часы в ядре. Между ними неизбежно возникает разница, которая и отображена в выводе команды hwclock

Есть два варианта решения этой проблемы (а для кого-то это и вовсе не проблема).

Рассмотрим первый вариант, предположив также что система не подключена к сети. Поскольку железные часы Hardware Clock работают круглыми сутками без каких либо перерывов — то и ошибаются они в среднем на одну и ту же величину каждый день. Утилита hwclock может нивелировать это через специальный механизм коррекции при чтении значения из чипа. Пользователю достаточно два раза установить Hardware Clock с каким-то существенным промежутком времени между установками. Утилита hwclock сама посчитает на сколько именно ошибаются Hardware Clock в течении суток, после чего сохранит эту величину в файле /etc/adjtime. После этого мы можем периодически читать значение из Hardware Clock и устанавливать значение System Time по нему, при этом утилита сама произведет коррекцию накопленной ошибки на сохраненную ранее величину дневной ошибки.

Второй вариант предполагает, что у нас есть какой-то способ периодической правильной установки System Time в ядре. Скорее всего, это какой-то внешний источник точного времени, с помощью которого мы можем приводить System Time в актуальное состояние. Все что остается после этого — попросить ядро периодически (раз в 11 секунд) скидывать верное значение ядра System Time в чип Hardware Time.

Подробнее обо всем этом можно прочитать в мануале команды hwclock.

Также наверное стоит рассказать о том, что система Linux может интерпретировать значения из Hardware Clock двумя способами. Связано это с тем, что Hardware Clock хранит время в виде счетчиков yyyy, MM, dd, HH, mm, ss. А как было уже сказано выше, без временной зоны эти счетчики нельзя привязать к точке на временной оси.

На самом деле тут всего два варианта: это будет или UTC, или локальная временная зона (та, что установлена в операционной системе по-умолчанию).

Для начала зайдите в BIOS и посмотрите на часы в главном меню. Сравните показания часов в BIOS и своих наручных часов, если они совпадают, то у вас часы BIOS идут по локальному времени (поздравляю — скорее всего у вас Windows компьютера); а если не совпадают, то часы BIOS установлены во временной зоне UTC (или другой вариант — они идут неправильно).

Windows по умолчанию требует, чтобы время в чипе Hardware Clock соответствовало локальному времени. А Linux легко может работать как с локальным временем в Hardware Clock, так и временем по UTC (последнее предпочтительнее). Поэтому, как правило, при двойной загрузке системы часы в BIOS идут по локальному времени специально для Windows, а Linux к этому приспосабливается.

Посмотреть текущий режим в Debian/Ubuntu можно так:

Хаки времени

Если нужно заставить какую-то программу думать, что в данный момент для нее время отличается от времени всей системы, то в репозиториях Ubuntu/Debian уже есть утилита faketime, которая перехватывает и модифицирует системные вызовы.

День рождения

Если кто-то скажет, что родился «15 апреля», мы получим возможность поздравлять этого человека с днем рождения каждый год. Если он скажет, что родился «15 апреля 2001 года», мы получим возможность узнать еще и его возраст. Но и в том, и другом случае это никак не соответствует никакой временной точке на оси. Во-первых, не указано время рождения. Во-вторых, не указана временная зона рождения. Хотя, теоретически, временную зону даты рождения можно узнать, если указано точное место рождения.

Как именно нам рассчитывать временную точку в которую мы можем отправить поздравления самому пользователю, а в какую временную точку отправлять уведомления о его дне рождения его друзьям? Как вариант, можно предложить следующее:

  • Отправка нотификации пользователю — t(u). Поскольку временная зона рождения нам неизвестна, используем текущую временную зону пользователя. Поскольку время рождения неизвестно, используем комфортное для пользователя время.
  • Отправка нотификации другу — t(f). Также комбинируем дату рождения пользователя, временную зону друга и комфортное время друга.

Время t(f) может быть сильно больше чем время t(u) — например, если пользователь находится в Японии, а его друг — в Европе. В этом случае получается, что к моменту, когда другу в Европе рано утром придет уведомление, сам пользователь в Японии уже возможно закончит праздновать свое ДР. В таком случае можно сдвинуть время уведомления друга на день назад, а во фразу уведомления добавить слово «завтра».

Пограничный возраст

Есть еще один, крайне интересный для меня момент, уже юридического характера. Есть целый ряд граничных возрастов: возраст совершеннолетия, возраст сексуального согласия, возраст уголовной ответственности.

Предположим, что некто родился в 2000.01.10 00:00:01 во Владивостоке. Совершил нетяжкое преступление 2016.01.09 23:59:59 в Москве. По записи дня рождения в паспорте (2000.01.10) и дня преступления в протоколе (2016.01.09) получается что 16 лет человеку еще не исполнилось. Однако 2016.01.09 23:59:59 в Москве — это уже будет 2016.01.10 во Владивостоке (где он родился) и тогда 16 лет ему уже есть. В обратной ситуации, человеку родившемуся в Москве и совершившему преступление во Владивостоке уже исполнится 16, а вот если посчитать по московскому времени — получается что еще нет.

Для устранения этого казуса в судебной практике используется норма, при которой активация прав/ответственности наступает с 00:00:00 в день, следующий за ожидаемым. То есть уголовная ответственность наступит в 2016.01.11 00:00:00 по месту совершения события — в этом случае человеку точно будет 16 лет, где бы он не находился.

Выводы

  • Не игнорируйте таймзону
  • Если у вас почти готова машина времени, то я не рекомендую использовать стандартную библиотеку Java для точного расчета смещения в прошлое. Точные путешествия в будущее по временным меткам невозможны в принципе.
  • Также текущая версия JDK не подходит для написания внутренних систем планирования на космических кораблях Галактической империи.

Вторая часть статьи — про новое Date Time API в Java 8.

Источник

Читайте также:  Таблица по истории линии сравнения 1 крестовый поход 3 4