Ruby / Rails - изменить часовой пояс времени, не меняя значения

109

У меня есть запись fooв базе данных , которая имеет :start_timeи :timezoneатрибуты.

Это :start_timeвремя в формате UTC 2001-01-01 14:20:00, например. Это :timezoneстрока - America/New_Yorkнапример.

Я хочу создать новый объект Time со значением :start_time но чей часовой пояс указан с помощью :timezone. Я не хочу загружать, :start_timeа затем конвертировать в :timezone, потому что Rails будет умным и обновит время от UTC, чтобы оно соответствовало этому часовому поясу.

В настоящее время,

t = foo.start_time
=> 2000-01-01 14:20:00 UTC
t.zone
=> "UTC"
t.in_time_zone("America/New_York")
=> Sat, 01 Jan 2000 09:20:00 EST -05:00

Вместо этого я хочу увидеть

=> Sat, 01 Jan 2000 14:20:00 EST -05:00

т.е. Я хочу делать:

t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST
rwb
источник
1
Может быть, это поможет: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji
3
Я не думаю, что вы правильно используете часовые пояса. Если вы сохраните его в своей базе данных как UTC из локального, что не так с его анализом по местному времени и сохранением через его относительный utc?
Поездка
1
Да ... Я думаю, что для получения помощи вам, возможно, потребуется объяснить, зачем вам это нужно? Зачем вообще нужно сохранять неправильное время в базе данных?
nzifnab
@MrYoshiji согласился. для меня звучит как YAGNI или преждевременная оптимизация.
инженер
1
если бы эти документы помогли, нам бы не понадобился StackOverflow :-) Вот один пример, который не показывает, как что-то было установлено - типично. Мне также нужно сделать это, чтобы обеспечить сравнение яблок с яблоками, которое не прерывается при переходе на летнее время или выходе из него.
JosephK

Ответы:

72

Похоже, вы хотите что-то вроде

ActiveSupport::TimeZone.new('America/New_York').local_to_utc(t)

Это говорит о преобразовании этого местного времени (с использованием зоны) в utc. Если вы Time.zoneустановили, вы, конечно, можете

Time.zone.local_to_utc(t)

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

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

Фредерик Чунг
источник
17
Мне нужна комбинация local_to_utc и Time.use_zone:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
Что вы предлагаете против перехода на летнее время? Предположим, что целевое время для преобразования наступает после перехода D / ST, а Time.now - до изменения. Это сработает?
Сирил Дюшон-Дорис
28

Я только что столкнулся с той же проблемой, и вот что я собираюсь сделать:

t = t.asctime.in_time_zone("America/New_York")

Вот документация по asctime

Женя
источник
2
Он просто делает то, что я ожидал
Kaz
Это здорово, спасибо! Упростил свой ответ на основе этого. Одним из недостатков asctimeявляется то, что он сбрасывает любые субсекундные значения (которые сохраняются в моем ответе).
Хенрик Н.,
22

Если вы используете Rails, вот еще один метод, аналогичный ответу Эрика Уолша:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Брайан
источник
1
А чтобы преобразовать объект DateTime обратно в объект TimeWithZone, просто продвиньтесь .in_time_zoneдо конца.
Брайан
@Brian Murphy-Dye У меня были проблемы с переходом на летнее время при использовании этой функции. Можете ли вы отредактировать свой вопрос, чтобы предоставить решения, работающие с DST? Может быть, Time.zone.nowсработает замена на то, что ближе всего к тому времени, когда вы хотите изменить?
Сирил Дюшон-Дорис
6

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

Самый простой способ сделать это:

t = Foo.start_time.in_time_zone("America/New_York")
t -= t.utc_offset

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

j_mcnally
источник
Это сработало для меня. Это должно оставаться правильным при переходе на летнее время при условии, что значение смещения установлено при использовании. При использовании объектов DateTime можно добавить или вычесть из него "offset.seconds".
JosephK
Если вы находитесь за пределами Rails, вы можете использовать Time.in_time_zone, потребовав правильные части active_support:require 'active_support/core_ext/time'
jevon
Кажется, что это не так, в зависимости от того, в каком направлении вы идете, и не всегда кажется надежным. Например, если я сейчас возьму стокгольмское время и конвертирую его в лондонское время, это сработает, если добавить (а не вычесть) смещение. Но если я переведу Стокгольм в Хельсинки, будет неправильно прибавлять или вычитать.
Хенрик Н.
5

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

1.9.3p194 :042 > utc_time = Time.now.utc
=> 2013-05-29 16:37:36 UTC
1.9.3p194 :043 > local_time = utc_time.in_time_zone('America/New_York')
 => Wed, 29 May 2013 12:37:36 EDT -04:00
1.9.3p194 :044 > desired_time = local_time-local_time.utc_offset
 => Wed, 29 May 2013 16:37:36 EDT -04:00 
Питер Альфвин
источник
3

Зависит от того, где вы собираетесь использовать это время.

Когда ваше время - атрибут

Если время используется как атрибут, вы можете использовать тот же гем date_time_attribute :

class Task
  include DateTimeAttribute
  date_time_attribute :due_at
end

task = Task.new
task.due_at_time_zone = 'Moscow'
task.due_at                      # => Mon, 03 Feb 2013 22:00:00 MSK +04:00
task.due_at_time_zone = 'London'
task.due_at                      # => Mon, 03 Feb 2013 22:00:00 GMT +00:00

Когда вы устанавливаете отдельную переменную

Используйте тот же гем date_time_attribute :

my_date_time = DateTimeAttribute::Container.new(Time.zone.now)
my_date_time.date_time           # => 2001-02-03 22:00:00 KRAT +0700
my_date_time.time_zone = 'Moscow'
my_date_time.date_time           # => 2001-02-03 22:00:00 MSK +0400
Сергей Зинин
источник
1
def relative_time_in_time_zone(time, zone)
   DateTime.parse(time.strftime("%d %b %Y %H:%M:%S #{time.in_time_zone(zone).formatted_offset}"))
end

Я придумал небольшую быструю функцию, чтобы решить эту задачу. Если у кого-то есть более эффективный способ сделать это, опубликуйте его!

Эрик Уолш
источник
1
t.change(zone: 'America/New_York')

Предпосылка OP неверна: «Я не хочу загружать: start_time, а затем конвертировать в: timezone, потому что Rails будет умен и обновит время из UTC, чтобы оно соответствовало этому часовому поясу». Это не обязательно верно, как показывает ответ, представленный здесь.

Куриан
источник
1
Это не дает ответа на вопрос. Чтобы критиковать или запросить разъяснения у автора, оставьте комментарий под его сообщением. - Из обзора
Aksen P
Обновил свой ответ, чтобы прояснить, почему это правильный ответ на вопрос и почему вопрос основан на ошибочном предположении.
kkurian
Кажется, это ничего не делает. Мои тесты показывают, что это не так.
Итай Грудев,
@ItayGrudev Что ты видишь, когда бежишь t.zoneраньше t.change? А что насчет того, когда ты бежишь t.zoneза ним t.change? А какие именно параметры вы переходите t.change?
kkurian
0

Я создал несколько вспомогательных методов, один из которых выполняет то же самое, что и первоначальный автор сообщения на Ruby / Rails - изменение часового пояса Time без изменения значения .

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

  def utc_offset_of_given_time(time, ignore_dst: false)
    # Correcting the utc_offset below
    utc_offset = time.utc_offset

    if !!ignore_dst && time.dst?
      utc_offset_ignoring_dst = utc_offset - 3600 # 3600 seconds = 1 hour
      utc_offset = utc_offset_ignoring_dst
    end

    utc_offset
  end

  def utc_offset_of_given_time_ignoring_dst(time)
    utc_offset_of_given_time(time, ignore_dst: true)
  end

  def change_offset_in_given_time_to_given_utc_offset(time, utc_offset)
    formatted_utc_offset = ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, false)

    # change method accepts :offset option only on DateTime instances.
    # and also offset option works only when given formatted utc_offset
    # like -0500. If giving it number of seconds like -18000 it is not
    # taken into account. This is not mentioned clearly in the documentation
    # , though.
    # Hence the conversion to DateTime instance first using to_datetime.
    datetime_with_changed_offset = time.to_datetime.change(offset: formatted_utc_offset)

    Time.parse(datetime_with_changed_offset.to_s)
  end

  def ignore_dst_in_given_time(time)
    return time unless time.dst?

    utc_offset = time.utc_offset

    if utc_offset < 0
      dst_ignored_time = time - 1.hour
    elsif utc_offset > 0
      dst_ignored_time = time + 1.hour
    end

    utc_offset_ignoring_dst = utc_offset_of_given_time_ignoring_dst(time)

    dst_ignored_time_with_corrected_offset =
      change_offset_in_given_time_to_given_utc_offset(dst_ignored_time, utc_offset_ignoring_dst)

    # A special case for time in timezones observing DST and which are
    # ahead of UTC. For e.g. Tehran city whose timezone is Iran Standard Time
    # and which observes DST and which is UTC +03:30. But when DST is active
    # it becomes UTC +04:30. Thus when a IRDT (Iran Daylight Saving Time)
    # is given to this method say '05-04-2016 4:00pm' then this will convert
    # it to '05-04-2016 5:00pm' and update its offset to +0330 which is incorrect.
    # The updated UTC offset is correct but the hour should retain as 4.
    if utc_offset > 0
      dst_ignored_time_with_corrected_offset -= 1.hour
    end

    dst_ignored_time_with_corrected_offset
  end

Примеры, которые можно попробовать на консоли rails или в сценарии ruby ​​после обертывания вышеуказанных методов в классе или модуле:

dd1 = '05-04-2016 4:00pm'
dd2 = '07-11-2016 4:00pm'

utc_zone = ActiveSupport::TimeZone['UTC']
est_zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
tehran_zone = ActiveSupport::TimeZone['Tehran']

utc_dd1 = utc_zone.parse(dd1)
est_dd1 = est_zone.parse(dd1)
tehran_dd1 = tehran_zone.parse(dd1)

utc_dd1.dst?
est_dd1.dst?
tehran_dd1.dst?

ignore_dst = true
utc_to_est_time = utc_dd1.in_time_zone(est_zone.name)
if utc_to_est_time.dst? && !!ignore_dst
  utc_to_est_time = ignore_dst_in_given_time(utc_to_est_time)
end

puts utc_to_est_time

Надеюсь это поможет.

Джигнеш Гохель
источник
0

Вот еще одна версия, которая сработала для меня лучше, чем текущие ответы:

now = Time.now
# => 2020-04-15 12:07:10 +0200
now.strftime("%F %T.%N").in_time_zone("Europe/London")
# => Wed, 15 Apr 2020 12:07:10 BST +01:00

Он переносит наносекунды с использованием «% N». Если вам нужна другая точность, см. Эту ссылку на strftime .

Хенрик Н
источник
-1

Я также потратил много времени на борьбу с часовыми поясами, и после работы с Ruby 1.9.3 понял, что вам не нужно преобразовывать в именованный символ часового пояса перед преобразованием:

my_time = Time.now
west_coast_time = my_time.in_time_zone(-8) # Pacific Standard Time
east_coast_time = my_time.in_time_zone(-5) # Eastern Standard Time

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

Это также работает для Ruby 2.3.1.

Торри Пейн
источник
1
Иногда восточное смещение составляет -4, я думаю, они хотят обработать его автоматически в обоих случаях.
OpenCoderX