Rails/Ruby: сравнение TimeWithZone необъяснимо не соответствует эквивалентным значениям
У меня есть ужасное время (без каламбура) с сопоставлением DateTime в моем текущем проекте, в частности, сравнение двух экземпляров ActiveSupport:: TimeWithZone. Проблема в том, что оба экземпляра TimeWithZone имеют одинаковое значение, но все сравнения показывают, что они разные.
Приостановка во время выполнения для отладки (с использованием RubyMine), я могу видеть следующую информацию:
timestamp = {ActiveSupport::TimeWithZone} 2014-08-01 10:33:36 UTC
started_at = {ActiveSupport::TimeWithZone} 2014-08-01 10:33:36 UTC
timestamp.inspect = "Fri, 01 Aug 2014 10:33:36 UTC +00:00"
started_at.inspect = "Fri, 01 Aug 2014 10:33:36 UTC +00:00"
Однако сравнение показывает, что значения не равны:
timestamp <=> started_at = -1
Самый близкий ответ, который я нашел в поиске (Сравнение двух объектов ActiveSupport:: TimeWithZone не работает) указывает на ту же проблему здесь, и я пробовал решения, которые были применимы без любой успех (попробованный db: test: подготовьте, и я не запустил Spring).
Кроме того, даже если я пытаюсь преобразовать в явные типы, они все равно не эквивалентны при сравнении.
TO_TIME:
timestamp.to_time = {Time} 2014-08-01 03:33:36 -0700
started_at.to_time = {Time} 2014-08-01 03:33:36 -0700
timestamp.to_time <=> started_at.to_time = -1
to_datetime:
timestamp.to_datetime = {Time} 2014-08-01 03:33:36 -0700
started_at.to_datetime = {Time} 2014-08-01 03:33:36 -0700
timestamp.to_datetime <=> started_at.to_datetime = -1
Решение только ", которое я нашел до сих пор, заключается в том, чтобы преобразовать оба значения с помощью to_i
, а затем сравнить, но это крайне неудобно коду везде, где я хочу делать сравнения (и, кроме того, кажется, что это должно быть ненужным):
timestamp.to_i = 1406889216
started_at.to_i = 1406889216
timestamp.to_i <=> started_at.to_i = 0
Любые советы будут очень оценены!
Ответы
Ответ 1
решаемые
Как показал Джон Скит выше, сравнение не удавалось из-за скрытых миллисекундных различий во времени:
timestamp.strftime('%Y-%m-%d %H:%M:%S.%L') = "2014-08-02 10:23:17.000"
started_at.strftime('%Y-%m-%d %H:%M:%S.%L') = "2014-08-02 10:23:17.679"
Это открытие привело меня к странному пути, чтобы наконец обнаружить, что в конечном итоге вызывает проблему. Это была комбинация этой проблемы, возникающая только во время тестирования и использования MySQL в качестве моей базы данных.
Проблемы показывались только при тестировании, потому что в тесте, где это произошло, я запускаю некоторые тесты против пары связанных моделей, которые содержат указанные выше поля. Один экземпляр модели должен быть сохранен в базе данных во время теста - модель, в которой находится значение timestamp
. Другая модель, однако, выполняла обработку и, таким образом, сама ссылалась на сам экземпляр, созданный в тестовом коде.
Это привело ко второму виновнику, и это факт, что я использую MySQL в качестве базы данных, которая при хранении значений datetime
не хранит миллисекунду (в отличие, скажем, PostgreSQL).
Неизменно это означает, что переменная timestamp
, которая была прочитана после того, как ее ActiveRecord была извлечена из базы данных MySQL, была эффективно округлена и выбрита миллисекундных данных, тогда как переменная started_at
была просто сохранена в памяти во время тестирования и, таким образом, исходные миллисекунды все еще присутствовали.
Мое собственное (субпаративное) решение состоит в том, чтобы по существу заставить обе модели (а не только одну) в моем тесте извлечь из базы данных.
TL;DR; Если это вообще возможно, используйте PostgreSQL, если сможете!
Ответ 2
Похоже, это происходит, если сравнивать время, сгенерированное в Ruby, со временем, загруженным из базы данных.
Например:
time = Time.zone.now
Record.create!(mark: time)
record = Record.last
В этом случае record.mark == time
потерпит неудачу, потому что Ruby сокращает время до наносекунд, в то время как разные базы данных имеют разную точность.
В случае postgres типа DateTime это будет в миллисекундах.
Вы можете видеть это, когда вы проверяете это, пока record.mark.sec == time.msec
- record.mark.nsec != time.nsec