Даты и время в RethinkDB
RethinkDB имеет встроенную поддержку времени с точностью до миллисекунд и часовыми поясами. Некоторые моменты:
- Время интегрировано с официальными драйверами, которые автоматически конвертируют его в и из типов времени, используемых в вашем языке.
- Запросы учитывают часовой пояс, поэтому вы можете задавать вопросы, например, «Произошло ли это событие в понедельник в часовом поясе, где оно было записано?»
- Время используется как индексы, поэтому вы можете эффективно получать события по времени их возникновения.
- Операции со временем являются чистым ReQL, что означает, что даже сложные запросы с датами и временем могут быть эффективно распределены по кластеру.

Быстрый пример
Сначала давайте создадим таблицу и добавим некоторые события. Первое событие мы вставим используя объект времени из вашего языка, а второе — с помощью epoch_time конструктора:
r.table_create('events').run(conn)
r.table('events').insert(
[{'id' => 0, 'timestamp' => Time.now},
{'id' => 1, 'timestamp' => r.epoch_time(1376436769.923)}]
).run(conn)
Теперь давайте получим их обратно:
> r.table('events').run(conn).to_a
[{"timestamp"=>2013-08-13 16:32:48 -0700, "id"=>0},
{"timestamp"=>2013-08-13 23:32:49 +0000, "id"=>1}]
Вы заметите, что оба вставленных времени возвращаются как объекты времени из Ruby Time. Они находятся в разных часовых поясах, потому что Time.now создает объект времени в местном часовом поясе, а r.epoch_time создает время в UTC (не зная и не заботясь о часовом поясе клиента). Если бы мы вместо этого вставили Time.now.utc, они оба были бы в UTC, когда мы их извлекли.
Теперь мы можем фильтровать по этому времени:
> r.table('events').filter{|row| row['timestamp'].hours() > 20}.run(conn)
[{"timestamp"=>2013-08-13 23:32:49 +0000, "id"=>1}]
> r.table('events').filter{|row|
row['timestamp'].in_timezone('-02:00').hours() > 20
}.run(conn)
[{"timestamp"=>2013-08-13 16:32:48 -0700, "id"=>0},
{"timestamp"=>2013-08-13 23:32:49 +0000, "id"=>1}]
Или создать вторичный индекс на них:
> r.table('events').index_create('timestamp').run(conn)
{"created"=>1}
> r.table('events').between(r.epoch_time(1376436769.913),
r.epoch_time(1376436769.933),
:index => 'timestamp').run(conn)
[{"timestamp"=>2013-08-13 23:32:49 +0000, "id"=>1}]
Технические подробности
Время хранится на сервере как секунды с момента эпохи (UTC) с точностью до миллисекунд плюс часовой пояс. В настоящее время доступны только часовые пояса с точностью до минуты, но в будущем мы можем добавить поддержку часовых поясов, учитывающих переходы на летнее время. Часовые пояса представлены строками согласно ISO 8601.
Временные значения считаются равными, когда их значения с момента эпохи (UTC) равны, независимо от часового пояса. Это относится как к сравнениям, так и к операциям с индексами. Временные значения сравниваются с плавающей точкой с точностью до миллисекунд.
Большинство операций с датами определены только для годов в диапазоне [1400, 10000] (но обратите внимание, что времена в году 10000 не могут быть напечатаны как даты ISO 8601).
Поправки на високосные секунды не полностью поддерживаются: 2012-06-30T23:59:60 и 2012-07-01T00:00:00 разы интерпретируются как одно и то же время.
Вставка времени
Вы можете вставить время, просто передав объект времени из Ruby Time. Если объект местного времени содержит часовой пояс, вставленное время будет иметь этот часовой пояс; если нет, то вставленное время будет в UTC. (Проверьте это, если вы используете драйвер сторонних разработчиков.)
> r.table('events').insert({'id' => 2, 'timestamp' => Time.now}).run(conn)
{"unchanged"=>0, "skipped"=>0, "replaced"=>0, "inserted"=>1, "errors"=>0, "deleted"=>0}
Вы также можете использовать r.now (которое сервер интерпретирует как время получения запроса в UTC) или создать время с помощью r.time, r.epoch_time, или r.iso8601.
> r.now().to_iso8601().run(conn)
"2013-08-09T18:53:15.012+00:00"
> r.time(2013, r.august, 9, 18, 53, 15.012, '-07:00').to_iso8601().run(conn)
"2013-08-09T18:53:15.012-07:00"
> r.epoch_time(1376074395.012).to_iso8601().run(conn)
"2013-08-09T18:53:15.012+00:00"
> r.iso8601("2013-08-09T18:53:15.012-07:00").to_iso8601().run(conn)
"2013-08-09T18:53:15.012-07:00"
Время может быть использовано в качестве первичного ключа для таблицы. Два времени считаются равными, если у них одинаковое количество миллисекунд с момента эпохи (UTC), независимо от часового пояса.
> r.table('t').insert({'id' => r.iso8601("2013-08-09T11:58:00.1111-07:00")}).run(conn)
{"unchanged"=>0, "skipped"=>0, "replaced"=>0, "inserted"=>1, "errors"=>0, "deleted"=>0}
> r.table('t').insert({'id' => r.iso8601("2013-08-09T10:58:00.1112-08:00")}).run(conn)
{"unchanged"=>0, "skipped"=>0, "replaced"=>0, "inserted"=>0,
"first_error"=>"Duplicate primary key `id`: ...", "errors"=>1, "deleted"=>0}
Вы также можете вставить время, вставив объект литерального псевдотипа. Это полезно, если, например, вы экспортировали строку с использованием :time_format => 'raw' (см. Извлечение времени ниже).
Примечание: Избегайте использования ключей, соответствующих регулярному выражению
^\$reql_.+\$$в ваших объектах. RethinkDB рассматривает их как зарезервированные ключевые слова.
> r.expr({'$reql_type$' => 'TIME',
'epoch_time' => 1376075362.662,
'timezone' => '+00:00'}).to_iso8601().run(conn)
"2013-08-09T19:09:22.662+00:00"
Извлечение времени
По умолчанию время преобразуется в объекты времени языка при извлечении из сервера. Это можно переопределить, передав параметр time_format в run. В настоящее время доступны только native, по умолчанию, и raw.
> r.now().run(conn)
2013-08-09 19:09:11 UTC
> r.now().in_timezone('-07:00').run(conn)
2013-08-09 12:14:56 -0700
> r.now().run(conn, :time_format => 'raw')
{'timezone'=>'+00:00', "epoch_time"=>1376075362.662, "$reql_type$"=>"TIME"}
> r.now().in_timezone('-07:00').run(conn, :time_format => 'raw')
{"timezone"=>"-07:00", "epoch_time"=>1376075702.485, "$reql_type$"=>"TIME"}
Вы также можете преобразовать объект времени на сервере, используя to_epoch_time или to_iso8601.
> r.now().to_epoch_time().run(conn)
1376075986.574
> r.now().to_iso8601().run(conn)
"2013-08-09T19:19:46.574+00:00"
Работа с временем
Существует только три полезных действия с временем: изменить его, сравнить с другим временем или извлечь часть из него.
Изменение времени
Вы можете поместить время в новый часовой пояс:
> r.expr(Time.now).to_iso8601().run(conn)
"2013-08-09T12:48:59.103-07:00"
> r.expr(Time.now).in_timezone('-06:00').to_iso8601().run(conn)
"2013-08-09T13:49:15.503-06:00"
Вы также можете добавить или вычесть продолжительность (в секундах):
> (r.time(2015, 1, 1, 'Z') + 86400).run(conn)
2015-01-02 00:00:00 +0000
Если вы вычтете два времени, вы получите продолжительность:
> (r.time(2015, 1, 2, 'Z') - r.time(2015, 1, 1, 'Z')).run(conn)
86400
Сравнение времени
Определены все обычные операторы сравнения для времени:
> (r.epoch_time(1376081287.982) < Time.now).run(conn)
true
Время сравнивается только с точностью до миллисекунд:
> r.epoch_time(1376081287.9821).eq(r.epoch_time(1376081287.9822)).run(conn)
true
Также существует команда during, которая может проверить, находится ли время в определенном диапазоне времени.
Извлечение частей времени
Если у вас есть время, вы можете извлечь определенную часть (например, месяц или часы) относительно текущего часового пояса. (См. полный список в справочнике API.)
> r.expr(Time.now).run(conn)
2013-08-09 13:53:00 -0700
> r.expr(Time.now).month().run(conn)
8
> r.expr(Time.now).hours().run(conn)
13
> r.expr(Time.now).in_timezone('-06:00').hours().run(conn)
14
Мы используем определение недели ISO 8601, которая начинается с понедельника, представленного как 1.
> r.expr(Time.now).day_of_week().run(conn)
5 # Friday
Мы определяем r.monday...r.sunday и r.january...r.december для удобства:
> r.expr(Time.now).day_of_week().eq(r.friday).run(conn)
true
Мы также позволяем вам разбить время на дату и текущее время суток (время и продолжительность соответственно):
> r.now().to_epoch_time().run(conn)
1376351312.744
> r.now().date().to_epoch_time().run(conn)
1376265600
> r.now().time_of_day().run(conn)
85712.744
Всё вместе
Объединяя эти операции, вы можете писать удивительно полезные запросы в чистом ReQL. Например, предположим, что у вас есть таблица продаж, которые ваша компания совершила, и вы хотите узнать, сколько валовой прибыли приходится на людей, работавших сверхурочно:
r.table('sales').filter {|sale|
# Weekends are overtime.
sale['time'].day_of_week().eq(r.saturday) |
sale['time'].day_of_week().eq(r.sunday) |
# Weekdays outside 9-5 are overtime.
(sale['time'].hours() < 9) |
(sale['time'].hours() >= 17)
}.sum('dollars').run(conn)
Если ваши отметки времени хранятся с часовыми поясами, этот запрос будет работать даже если у вас есть продажи из разных офисов в разных странах (при условии, что они все работают с 9 до 17 по местному времени).
Поскольку этот запрос написан на чистом ReQL, весь запрос будет эффективно распределён по кластеру без каких-либо вычислений на клиенте.
Кроме того, поскольку это ReQL, отдельные части запроса легко комбинируются. Если вы решите получить эти цифры по месяцам, вы можете просто добавить group:
r.table('sales').filter {|sale|
# Weekends are overtime.
sale['time'].day_of_week().eq(r.saturday) |
sale['time'].day_of_week().eq(r.sunday) |
# Weekdays outside 9-5 are overtime.
(sale['time'].hours() < 9) |
(sale['time'].hours() >= 17)
}.group{|sale| sale['time'].month()}.sum('dollars').run(conn)
© RethinkDB contributors
Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
https://rethinkdb.com/docs/dates-and-times/ruby/