Работа с датой и временем при разработке под Android

<Пт 08 дек 2023>, обновлено <Ср 10 июл 2024>

В данной статье хотелось бы подробно рассмотреть, как работать с датой и временем при разработке приложений под Android. Статья будет полезна в первую очередь для мобильных разработчиков, но и, в некоторой степени, для начинающих backend-разработчиков. Всё написанное ниже актуально для тех случаев, когда система в целом рассчитана на работу в разных часовых поясах и события, соответственно, могут происходить в разных географических регионах. Это подразумевает, что в приложение реализованы сценарии, где осуществляется фиксация времени события, ввод даты и времени или просто есть сущности, обладающие такими атрибутами, как время создания и модификации. Сразу оговорюсь, что в статье не рассматриваются тонкости чем GMT отличается от UTC, или почему год не всегда содержит одинаковое количество секунд.

В начале разработки, как правило, для работы с датой и временем берут самое простое, что доступно по умолчанию. Это классы Date и Calendar. Класс Date хранит время с точностью до миллисекунды и пытается решать сразу множество задач, связанных с представлением даты и времени. Часть его методов на данный момент помечена как @Deprecated, что намекает на то, что получилось это не очень хорошо. Вероятно, так вышло в виду того, что время, как некоторая линейная величина, и местное время это немного разные, хотя всё же связанные вещи. В Java 8 эти классы были заменены на более продуманную реализацию JSR-310 API, о которой и пойдёт речь.

Поскольку новая реализация стала доступна только начиная с версии 26 Android SDK, то для написания приложений, которые поддерживают устройства работающие на более ранних версиях SDK, лучше использовать бэкпорт ThreeTenABP. Для того чтобы подключить библиотеку к проекту, необходимо добавить в build.gradle:

implementation 'com.jakewharton.threetenabp:threetenabp:1.4.6'

И произвести инициализацию при старте приложения:

override fun onCreate() {
  super.onCreate()
  AndroidThreeTen.init(this)
}

Универсальное время

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

Фактически данный класс хранит два значения, которые определяют, сколько времени прошло с 00:00 01.01.1970 UTC (этот момент называется Unix Epoch, более подробно см. в статье про Unix-время) в наносекундах. Класс позволяет хранить время с данной точностью, но не предполагает, что она обеспечена в исходных данных. В Java 8 для получения текущего времени, в конечном итоге, будет использоваться функция System.currentTimeMillis(), поэтому ожидать наносекундой точности вообще не стоит.

/**
 * The number of seconds from the epoch of 1970-01-01T00:00:00Z.
 */
private final long seconds;
/**
 * The number of nanoseconds, later along the time-line, from the seconds field.
 * This is always positive, and never exceeds 999,999,999.
 */
private final int nanos;

Интерфейс класса сделан таким образом, что это выглядит единым значением. Методы класса позволяют прибавлять и отнимать от этого виртуального значения произвольное количество секунд, миллисекунд или наносекунд, или сравнивать два объекта данного класса (класс реализует интерфейс Comparable, поэтому в Kotlin объекты класса можно легко сравнивать, используя оператор сравнения). Есть и ряд других методов, но основной упор делается на работу с объектом класса как с некоторой скалярной величиной.

Некоторые специфичные для этого класса методы, которые будут использоваться далее:

  • now() — статический метод, возвращающий объект соответствующий текущему моменту;
  • toEpochMilli() — возвращает количество миллисекунд с момента Unix Epoch;
  • ofEpochMilli(long epochMilli) — статический метод, создающий объект из количества миллисекунд с момента Unix Epoch;
  • plusMillis(long millisToAdd) — прибавляет к текущему значению указанное количество миллисекунд;
  • getEpochSecond() — возвращает количество секунд с момента Unix Epoch;
  • ofEpochSecond(long epochSecond) — статический метод, создающий объект из количества секунд с момента Unix Epoch;
  • plusSeconds(long secondsToAdd) — прибавляет к текущему значению указанное количество секунд.

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

>>> import java.time.Instant
>>>
>>> val now = Instant.now()
>>> now
res3: java.time.Instant = 2023-12-20T13:01:51.203Z
>>>
>>> now.toEpochMilli()
res5: kotlin.Long = 1703077311203

2023-12-20T13:01:51.203Z — это дата и время в формате ISO 8601. Значение выводится в UTC, на что намекает буква Z в конце (нулевое смещение часового пояса).

Местное время, дата и часовые пояса

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

/**
 * The hour.
 */
private final byte hour;
/**
 * The minute.
 */
private final byte minute;
/**
 * The second.
 */
private final byte second;
/**
 * The nanosecond.
 */
private final int nano;

Для описания даты есть класс LocalDate, который буквально хранит год, месяц и день:

/**
 * The year.
 */
private final int year;
/**
 * The month-of-year.
 */
private final short month;
/**
 * The day-of-month.
 */
private final short day;

Есть ещё класс LocalDateTime, который агрегирует два этих класса.

Данные классы подойдут для описания времени и дат из документов или для представления данных на UI. Они никак не привязаны к часовому поясу. Тут мы переходим к концепции часовых поясов, которая появилась относительно недавно. Зародилась она в Великобритании в середине XIX века и вначале использовалась расчётной палатой и железнодорожными компаниями. Суть заключается в том, что ориентироваться на световой день в каждом отдельном городе нет смысла и необходимо выбрать некоторую общую точку отсчёта. Этой точкой стал гринвичский меридиан, а среднее время по Гринвичу это почти то же самое, что и UTC. Часовой пояс это то, что связывает универсальное время UTC и местное время. Казалось бы всё просто: добавляем к UTC некоторое значение и получаем местное время, но концепция часовых поясов несколько сложней, так как ещё существует зимнее и летнее время.

Для описания часового пояса существует несколько классов: ZoneOffset и ZoneId. Первый описывает просто фиксированное смещение, а последний позволяет учитывать переходы на летнее и зимнее время. Можно сказать, что ZoneId содержит список правил, которые, в зависимости от времени года, ссылаются на соответствующий ZoneOffset. Оба класса содержат статические методы, которые позволяют создавать объекты класса из текстового описания. Смещение выглядит вот так +03:00, а часовой пояс вот так Europe/Moscow или вот так UTC+03:00.

Если связать LocalDateTime с ZoneOffset то получится OffsetDateTime, а если с ZoneId то ZonedDateTime. Можно заглянуть в код, где оба этих класса, OffsetDateTime и ZonedDateTime, агрегируют LocalDateTime и ZoneOffset, в последнем случае ещё добавляется непосредственно ZoneId, и реализуют ряд интерфейсов, чтобы с ними могли работать другие классы из пакета java.time.

>>> import java.time.LocalDateTime
>>> import java.time.ZoneId
>>> import java.time.ZoneOffset
>>>
>>> val zoneId = ZoneId.of("Europe/Moscow")
>>> val now = LocalDateTime.now()
>>> now
res6: java.time.LocalDateTime = 2023-12-20T16:03:12.972
>>>
>>> now.atZone(zoneId)
res8: java.time.ZonedDateTime = 2023-12-20T16:03:12.972+03:00[Europe/Moscow]
>>>
>>> val offset = ZoneOffset.of("+03:00")
>>> now.atOffset(offset)
res11: java.time.OffsetDateTime = 2023-12-20T16:03:12.972+03:00

В принципе, создать объекты OffsetDateTime и ZonedDateTime можно не только из LocalDateTime, но и из Instant тоже. Сделать это можно следующим образом:

>>> import java.time.Instant
>>> import java.time.ZoneId
>>> import java.time.ZoneOffset
>>>
>>> val now = Instant.now()
>>> now
res5: java.time.Instant = 2023-12-20T13:04:20.812Z
>>>
>>> val zoneId = ZoneId.of("Europe/Moscow")
>>> now.atZone(zoneId)
res9: java.time.ZonedDateTime = 2023-12-20T16:04:20.812+03:00[Europe/Moscow]
>>>
>>> val offset = ZoneOffset.of("+03:00")
>>> now.atOffset(offset)
res12: java.time.OffsetDateTime = 2023-12-20T16:04:20.812+03:00

Возможно обратное преобразование, но тогда теряется информация о временной зоне.

Полезные методы общие для обоих классов:

  • now() — статический метод, возвращающий объект соответствующий текущему моменту;
  • ofInstant(Instant instant, ZoneId zone) — статический метод, возвращающий объект из времени UTC и часового пояса;
  • toInstant() — возвращает момент, то есть Instant;
  • toLocalDate() — возвращает дату;
  • toLocalTime() — возвращает местное время;
  • toLocalDateTime() — возвращает дату и местное время;
  • getYear() — возвращает год;
  • getMonth() — возвращает месяц;
  • getDayOfMonth() — возвращает день месяца;
  • getHour() — возвращает количество часов;
  • getMinute() — возвращает количество минут.

Модификаторы

Вышеперечисленные классы реализуют интерфейсы Temporal и TemporalAdjuster (не во всех случаях), которые, в свою очередь, содержат методы, позволяющие модифицировать объект. Эти методы имеют максимально обобщённый вид (отдельным параметром надо указывать в каких единицах измеряется аргумент), и поэтому их не всегда удобно использовать. Помимо этих методов есть более простые и удобные методы, которые позволяют менять отдельные календарные или временные величины и они принимают всего один аргумент. Эти методы не наследуются от какого-то базового класса или интерфейса, а просто имеют одинаковые названия. Все эти методы возвращают изменённую копию объекта, что позволяет сразу передать объект в другой метод или применить следующий модификатор.

Вот сигнатуры некоторых методов из вышеупомянутых интерфейсов:

public int get (TemporalField field);

public abstract Temporal adjustInto (Temporal temporal);

public abstract Temporal with (TemporalField field, long newValue);

public Temporal with (TemporalAdjuster adjuster);

public Temporal plus (TemporalAmount amount);

public abstract Temporal plus (long amountToAdd, TemporalUnit unit);

public abstract long until (Temporal endExclusive, TemporalUnit unit);

Методы adjustInto и with по сути выполняют одну и ту же функцию — модифицируют копию объекта в соответствии с некоторыми правилами или на основании данных из другого объекта. Рекомендуется использовать последний, потому что так код выглядит понятней.

Помимо этого есть класс TemporalAdjusters, который предоставляет правила (они тоже реализуют интерфейс TemporalAdjuster), модифицирующие объект определённым образом. Вот некоторые из них:

  • firstDayOfMonth() — выбирает первый день месяца;
  • firstDayOfNextMonth() — выбирает первый день следующего месяца;
  • firstInMonth(DayOfWeek dayOfWeek) — выбирает первый день недели в месяце;
  • lastDayOfMonth() — выбирает последний день месяца;
  • lastInMonth(DayOfWeek dayOfWeek) — выбирает последний день недели в месяце;
  • next(DayOfWeek dayOfWeek) — выбирает следующий день недели в месяце;
  • previous(DayOfWeek dayOfWeek) — выбирает предыдущий день недели в месяце.

Вот пример того, как это работает:

>>> import java.time.OffsetDateTime
>>> import java.time.temporal.TemporalAdjusters
>>> import java.time.LocalTime
>>> import java.time.DayOfWeek
>>>
>>> val now = OffsetDateTime.now()
>>> now
res7: java.time.OffsetDateTime = 2023-12-20T16:05:54.733+03:00
>>>
>>> now.with(TemporalAdjusters.lastDayOfMonth())
res9: java.time.OffsetDateTime = 2023-12-31T16:05:54.733+03:00
>>>
>>> now.with(LocalTime.of(0, 0))
res11: java.time.OffsetDateTime = 2023-12-20T00:00+03:00
>>>
>>> TemporalAdjusters.next(DayOfWeek.MONDAY).adjustInto(now)
res13: java.time.temporal.Temporal = 2023-12-25T16:05:54.733+03:00

Методы, принимающие один аргумент:

  • withYear(int year) — меняет год;
  • withMonth(int month) — меняет месяц;
  • withDayOfMonth(int dayOfMonth) — меняет день месяца;
  • withHour(int hour) — меняет час;
  • withMinute(int minute) — меняет минуты;
  • plusYears(long years) — увеличивает год;
  • plusMonths(long months) — увеличивает месяц;
  • plusHours(long hours) — увеличивает час;
  • plusMinutes(long minutes) — увеличивает минуты;
  • truncatedTo(TemporalUnit unit) — «округляет» по календарной или временной единице.

Методы специфичные для OffsetDateTime и ZonedDateTime:

  • withOffsetSameInstant(ZoneOffset offset) — меняет смещение, но оставляет универсальное время прежним;
  • withOffsetSameLocal(ZoneOffset offset) — меняет смещение, но оставляет местное время прежним;
  • withZoneSameInstant(ZoneId zone) — меняет часовой пояс, но оставляет универсальное время прежним;
  • withZoneSameLocal(ZoneId zone) — меняет часовой пояс, но оставляет местное время прежним.

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

И примеры их использования:

>>> import java.time.OffsetDateTime
>>> import java.time.ZoneOffset
>>> import java.time.temporal.ChronoUnit
>>>
>>> val now = OffsetDateTime.now()
>>> now
res5: java.time.OffsetDateTime = 2023-12-20T16:07:08.819+03:00
>>>
>>> now.withMonth(1)
res7: java.time.OffsetDateTime = 2023-01-20T16:07:08.819+03:00
>>>
>>> now.withHour(0).withMinute(0).withSecond(0)
res9: java.time.OffsetDateTime = 2023-12-20T00:00:00.819+03:00
>>>
>>> now.truncatedTo(ChronoUnit.DAYS)
res11: java.time.OffsetDateTime = 2023-12-20T00:00+03:00
>>>
>>> val offset = ZoneOffset.of("+04:00")
>>> now.withOffsetSameInstant(offset)
res14: java.time.OffsetDateTime = 2023-12-20T17:07:08.819+04:00
>>>
>>> now.withOffsetSameLocal(offset)
res16: java.time.OffsetDateTime = 2023-12-20T16:07:08.819+04:00

Пример того, как используя простейший модификатор и метод atStartOfDay, который преобразует дату в ZonedDateTime в определённом часовом поясе, можно продемонстрировать переход на зимнее время:

>>> import java.time.ZoneId
>>> import java.time.LocalDate
>>> val zoneId = ZoneId.of("Europe/Berlin")
>>>
>>> val date = LocalDate.of(2023, 10, 1)
>>> date.atStartOfDay(zoneId)
res4: java.time.ZonedDateTime = 2023-10-01T00:00+02:00[Europe/Berlin]
>>>
>>> date.plusMonths(1).atStartOfDay(zoneId)
res6: java.time.ZonedDateTime = 2023-11-01T00:00+01:00[Europe/Berlin]

Форматирование

Любой из вышеперечисленных классов реализует интерфейс TemporalAccessor, поэтому его можно преобразовывать в удобочитаемую строку используя метод format класса DateTimeFormatter. Если формат предполагает наличие данных, который данный класс не содержит, то будет кинуто исключение.

Для примера можно преобразовать текущую дату и время во что-то более привычное:

>>> import java.time.LocalDateTime
>>> import java.time.format.DateTimeFormatter
>>>
>>> val formatter = DateTimeFormatter.ofPattern("HH:mm, dd MMMM yyyy")
>>> formatter.format(LocalDateTime.now())
res4: kotlin.String = 16:09, 20 December 2023

У этого класса есть ряд статических полей, которые выполняют форматирование в соответствии с широко известными стандартами:

  • ISO_INSTANT — форматирует в соответствии с вышеупомянутым стандартом ISO 8601;
  • ISO_OFFSET_DATE_TIME — форматирует в ISO 8601 и добавляет смещение часового пояса (+03:00);
  • ISO_ZONED_DATE_TIME — форматирует в ISO 8601 и добавляет часовой пояс (+03:00[Europe/Moscow]);
  • RFC_1123_DATE_TIME — форматирует в соответствии с RFC 1123, вернее RFC 822.

Последний, например, можно использовать для формирования значения HTTP хэдера Date или, наоборот, для его парсинга.

Помимо метода format, у DateTimeFormatter есть метод parse, который позволяет парсить строки. Он возвращает некоторый объект, который реализует интерфейс TemporalAccessor, поэтому нужно дополнительное преобразование, чтобы создать объект нужного класса. Для того чтобы не делать это самому, можно использовать готовые методы классов OffsetDateTime и ZonedDateTime.

>>> import java.time.format.DateTimeFormatter
>>> import java.time.OffsetDateTime
>>>
>>> val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("2023-12-31T23:59:59.999+03:00")
>>> temporal
res3: java.time.temporal.TemporalAccessor = {InstantSeconds=1704056399, OffsetSeconds=10800},ISO resolved to 2023-12-31T23:59:59.999
>>>
>>> OffsetDateTime.from(temporal)
res5: java.time.OffsetDateTime = 2023-12-31T23:59:59.999+03:00
>>>
>>> OffsetDateTime.parse("2023-12-31T23:59:59.999+03:00")
res7: java.time.OffsetDateTime = 2023-12-31T23:59:59.999+03:00

Временной промежуток

Помимо ситуаций, когда надо зафиксировать время какого-то события, есть ситуации, когда надо зафиксировать длительность некоторого процесса. Для этого есть специальный класс Duration, который позволяет описать длительность с точностью до наносекунды, как и Instant. У него есть ряд статических методов, которые позволяют создавать объекты класса из секунд, минут, часов и т.д. Также есть методы-модификаторы, которые позволяют прибавить, отнять определенное количество секунд, минут, часов или уменьшить, увеличить значение в определенное количество раз. Класс реализует интерфейс TemporalAmount, поэтому его можно передавать в качестве аргумента в методы-модификаторы других классов.

Пример метода, который позволяет создать объект класса, вычислив разницу между двумя Instant:

fun Instant.diff(instant: Instant): Duration = Duration.ofNanos(until(instant, ChronoUnit.NANOS))

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

>>> import java.time.Instant
>>> import java.time.Duration
>>> import java.time.temporal.ChronoUnit
>>>
>>> val now = Instant.now()
>>> val tenHoursLater = now.plus(Duration.ofHours(1).multipliedBy(10))
>>> fun Instant.diff(instant: Instant): Duration = Duration.ofNanos(until(instant, ChronoUnit.NANOS))
>>> now.diff(tenHoursLater)
res7: java.time.Duration = PT10H

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

>>> import java.time.ZonedDateTime
>>> import java.time.temporal.TemporalAdjusters
>>> import java.time.temporal.ChronoUnit
>>> import java.time.Period
>>>
>>> val now = ZonedDateTime.now()
>>> now
res6: java.time.ZonedDateTime = 2023-12-20T16:11:03.497+03:00[Europe/Moscow]
>>>
>>> val from = now.with(TemporalAdjusters.firstDayOfMonth())
>>> from
res9: java.time.ZonedDateTime = 2023-12-01T16:11:03.497+03:00[Europe/Moscow]
>>>
>>> val to = now.with(TemporalAdjusters.firstDayOfNextMonth())
>>> to
res12: java.time.ZonedDateTime = 2024-01-01T16:11:03.497+03:00[Europe/Moscow]
>>>
>>> val period = Period.ofDays(from.until(to, ChronoUnit.DAYS).toInt())
>>> period
res15: java.time.Period = P31D

Хранение

В данной главе речь пойдёт о том, как сохранить объекты вышеперечисленных классов в Room, самую распространённую реализации БД для Android. Как сохранять сложные типы данных в Room подробно описано вот этой статье. Если кратко, то необходимо через аннотацию @TypeConverters у класса, описывающего БД, указать другой класс, где хранятся методы выполняющие преобразование сложных типов данных в более примитивные.

object Converters {
}

@Database(entities = [Event::class])
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun eventDao(): EventDao
}

Instant, с учётом его точности в Java 8, проще всего преобразовать в миллисекунды и сохранить в виде целочисленного значения:

object Converters {
  @TypeConverter
  fun instantFromMillis(value: Long?) = value?.let { Instant.ofEpochMilli(it) }

  @TypeConverter
  fun instantToMillis(instant: Instant?) = instant?.toEpochMilli()
}

Если есть необходимость сохранить LocalDateTime в БД, то это можно сделать, преобразовав объект в количество секунд UTC и сохранив в виде целочисленного значения:

object Converters {
  @TypeConverter
  fun secondsToLocalDateTime(value: Long?) = value?.let { LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC) }

  @TypeConverter
  fun localDateTimeToSeconds(date: LocalDateTime?) = date?.toEpochSecond(ZoneOffset.UTC)
}

Для сохранение OffsetDateTime и ZonedDateTime проще всего использовать форматирование в строку:

object Converters {
  @TypeConverter
  fun offsetDateTimeToString(value: OffsetDateTime?) = value?.toString()

  @TypeConverter
  fun stringToOffsetDateTime(value: String?) = value?.let { OffsetDateTime.parse(it) }

  @TypeConverter
  fun zonedDateTimeToString(value: ZonedDateTime?) = value?.toString()

  @TypeConverter
  fun stringToZonedDateTime(value: String?) = value?.let { ZonedDateTime.parse(it) }
}

Если надо сохранить ZoneOffset:

object Converters {
  @TypeConverter
  fun offsetToSeconds(offset: ZoneOffset?) = offset?.totalSeconds

  @TypeConverter
  fun secondsToOffset(value: Int?) = value?.let { ZoneOffset.ofTotalSeconds(it) }
}

Применение

В этой главе хотелось бы определиться, в каких случаях следует использовать тот или иной класс:

  • Instant — фиксация времени системных событий;
  • OffsetDateTime — фиксация времени событий, которые будут отображаться в UI;
  • ZonedDateTime — результат ввода даты и времени события в UI;
  • LocalDateTime — даты из документов;
  • Duration — описание длительности процесса.

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

@Entity
data class Event(
  @PrimaryKey
  val id: Long,

  val timestamp: Instant = Instant.now()
)

Instant и OffsetDateTime легко отформатировать в строку ISO 8601 или преобразовать в целочисленное значение Unix time:

{
  "timestamp": "2023-12-31T23:59:59.999Z",
  "epochMillis": 1704056399999,
  "offsetSeconds": 10800
}

При вводе даты и времени события в UI надо учитывать часовой пояс и переход на летнее время, поэтому лучше использовать ZonedDateTime, что позволит вычислить точное время события в UTC (на самом деле это не совсем так, но в каких-то разумных пределах работает). Эта информация, например, позволит запланировать напоминание или будильник при помощи AlarmManager.

val departureDate = MutableStateFlow(ZonedDateTime.now())

fun onDepartureDateChanged(v: DatePicker, y: Int, m: Int, d: Int) {
  departureDate.update { dateTime ->
    dateTime.with(LocalDate.of(y, m + 1, d)
      .also { logger.debug("departure date - $it")})}
}

fun onDepartureTimeChanged(v: TimePicker, h: Int, m: Int) {
  departureDate.update { dateTime ->
    dateTime.with(LocalTime.of(h, m)
      .also { logger.debug("departure time - $it")})}
}

fun onZoneChanged(id: String) {
  departureDate.update { dateTime ->
    dateTime.withZoneSameLocal(ZoneId.of(id)
      .also { logger.debug("time zone - $it")})}
}

fun scheduleAlarm() {
  alarmManager.setAndAllowWhileIdle(RTC_WAKEUP,
    departureDate.value.toInstant().toEpochMilli(),
    getAlarmPendingIntent())
}

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

@Entity
data class VaccinationCertificate(
  @PrimaryKey
  val id: Long,

  val issueDate: LocalDateTime,
  val expiryDate: LocalDateTime
)

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

>>> import java.time.ZoneId
>>> import java.time.format.DateTimeFormatter
>>> import java.time.LocalDate
>>> import java.time.LocalTime
>>>
>>> val zoneId = ZoneId.of("Europe/Moscow")
>>>
>>> val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yy")
>>> val departureDate = LocalDate.parse("08.12.23", dateFormatter).atStartOfDay(zoneId)
>>> departureDate
res9: java.time.ZonedDateTime = 2023-12-08T00:00+03:00[Europe/Moscow]
>>>
>>> val timeFormatter = DateTimeFormatter.ofPattern("HH.mm")
>>> val departureTime = LocalTime.parse("13.00", timeFormatter)
>>> departureTime
res13: java.time.LocalTime = 13:00
>>>
>>> departureDate.plusDays(1).with(departureTime).toInstant()
res15: java.time.Instant = 2023-12-09T10:00:00Z

Заключение

В этой статье, опираясь на собственный опыт разработки приложения для проводников РЖД, я рассмотрел JSR-310 API, который обладает продуманным дизайном и позволяет решать множество задач, связанных с обработкой даты и времени. Упор делался на понимание базовых концепций и обзор основных классов. Были даны примеры готового кода, чтобы облегчить понимание и продемонстрировать возможные сценарии использования, которые позволят быстро применить полученные знания на практике.

Павел Соколов

2025-10-23 Чт 18:24