Ruby / Rails - Зміна часового поясу за часом, не змінюючи значення


108

У мене 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

1
Можливо, це допоможе: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji

3
Я не думаю, що ви правильно використовуєте часові пояси. Якщо ви збережете його у свій db як UTC від локального, що не так у аналізі його місцевого часу та збереженні через його відносний utc?
Поїздка

1
Я ... Я думаю, щоб найкраще отримати допомогу, можливо, вам потрібно буде пояснити, чому вам потрібно це зробити? Чому б ви зберігали неправильний час для бази даних в першу чергу?
nzifnab

@MrYoshiji погодився. звучить як YAGNI, або передчасна оптимізація для мене.
engineerDave

1
якби ці документи допомогли, нам би не знадобився StackOverflow :-) Один приклад, який не показує, як все було встановлено - типовий. Мені також потрібно зробити це, щоб змусити порівняння "Яблуко-яблучне", яке не порушується, коли літній день запускає або вимикає.
ДжозефК

Відповіді:


72

Звучить, що вам хочеться чогось уздовж

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

Це говорить перетворити цей локальний час (використовуючи зону) у utc. Якщо ви Time.zoneвстановили, то, звичайно, можете

Time.zone.local_to_utc(t)

Це не використовуватиме часовий пояс, приєднаний до t - він передбачає, що він локальний для часового поясу, з якого ви перетворюєтесь.

Один крайній випадок, який слід захистити, - це переходи DST: місцевий час, який ви вказуєте, може не існувати або може бути неоднозначним.


17
Поєднання local_to_utc та Time.use_zone - це те, що мені потрібно:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb

Що ви пропонуєте проти DST переходів? Припустимо, цільовий час для перетворення - це після переходу D / ST, а Time.now - перед зміною. Це би спрацювало?
Кирило Духон-Доріс

28

Я щойно стикався з тією ж проблемою, і ось що я збираюся робити:

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

Ось документація на час


2
Це просто те, що я очікував
Kaz

Це чудово, дякую! Спростив мою відповідь на її основі. Одним із недоліків asctimeє те, що воно скидає будь-які значення, що знаходяться на підсекундах (що моя відповідь зберігає).
Генрік N

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

Я не впевнений, чому ви хотіли б це зробити, хоча, мабуть, найкраще реально працювати в рази, як вони побудовані. Напевно, корисним буде те, чому потрібно змінити час та часові пояси.


Це працювало для мене. Це повинно залишатися правильним під час зміни літнього часу, за умови встановлення значення зміщення при використанні. Якщо ви використовуєте об'єкти DateTime, з нього можна додати або відняти "offset.seconds".
ДжозефК

Якщо ви перебуваєте за межами Rails, ви можете користуватися Time.in_time_zone, вимагаючи правильних частин active_support:require 'active_support/core_ext/time'
jevon

Це, здається, робить неправильну справу залежно від того, в якому напрямку ви рухаєтесь, і не завжди здається надійним. Наприклад, якщо я зараз беру стокгольмський час і переходжу на лондонський час, він працює, якщо додати (не відняти) компенсацію. Але якщо я перетворюю Стокгольм на Гельсінкі, це неправильно, додавати чи віднімати.
Генрік N

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')

Положення ОП є невірним: "Я не хочу завантажувати: start_time, а потім перетворювати на: часовий пояс, тому що Rails буде розумним і оновить час з UTC, щоб він відповідав цьому часовому поясу". Це не обов'язково вірно, про що свідчить відповідь, подана тут.


1
Це не дає відповіді на запитання. Щоб критикувати або вимагати роз'яснення у автора, залиште коментар під їх дописом. - З огляду
Aksen P

Я оновив свою відповідь, щоб уточнити, чому це правильна відповідь на питання, і чому це питання ґрунтується на хибному припущенні.
kkurian

Це, здається, нічого не робить. Мої тести показують, що це нік.
Ітай Грудев

@ItayGrudev Коли ви біжите t.zoneранішеt.change тим, що ти бачиш? А що при запуску t.zoneпісля t.change? І до яких параметрів ви t.changeточно переходите ?
kkurian

0

Я створив кілька допоміжних методів, один з яких робить те саме, що запитує оригінальний автор публікації в Ruby / Rails - Змініть часовий пояс часу, не змінюючи значення .

Крім того, я задокументував декілька особливостей, які я спостерігав, а також ці помічники містять методи, щоб повністю ігнорувати автоматичну економію денного світла, застосовану при перетворенні часу, яка недоступна поза рамками в рамках 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

Приклади, які можна спробувати на консолі рейки або рубіновому скрипті після загортання вищевказаних методів у клас або модуль:

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". Якщо ви бажаєте іншої точності, дивіться це посилання на строповий перегляд .


-1

Я також витратив чималий час на боротьбу з TimeZones, і після повороту з 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.