Ruby / Rails - zmień strefę czasową czasu bez zmiany wartości

108

Mam rekord foow bazie danych, który ma :start_timei :timezoneatrybuty.

Na przykład :start_timejest to czas UTC 2001-01-01 14:20:00. Na przykład :timezonejest to ciąg znaków America/New_York.

Chcę utworzyć nowy obiekt Time z wartością, :start_timeale którego strefa czasowa jest określona przez :timezone. Nie chcę ładować, :start_timea następnie konwertować do :timezone, ponieważ Railsy będą sprytne i zaktualizują czas z UTC, aby był zgodny z tą strefą czasową.

Obecnie,

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

Zamiast tego chcę zobaczyć

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

to znaczy. Chcę zrobić:

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
źródło
3
Myślę, że nie używasz poprawnie stref czasowych. Jeśli zapiszesz go w swojej bazie danych jako UTC z lokalnego, co jest złego w analizowaniu go przez jego czas lokalny i zapisywaniu przez jego względny utc?
Wycieczka
1
Tak ... Myślę, że aby otrzymać pomoc, możesz potrzebować wyjaśnienia, dlaczego miałbyś to zrobić? Dlaczego miałbyś przechowywać niewłaściwy czas w bazie danych w pierwszej kolejności?
nzifnab
@MrYoshiji zgodził się. brzmi dla mnie jak YAGNI lub przedwczesna optymalizacja.
inżynier
1
gdyby te dokumenty pomogły, nie potrzebowalibyśmy StackOverflow :-) Jeden przykład, który nie pokazuje, jak cokolwiek zostało ustawione - typowe. Muszę to również zrobić, aby wymusić porównanie jabłek do jabłek, które nie psuje się, gdy zaczyna się lub kończy czas letni.
JosephK

Odpowiedzi:

72

Wygląda na to, że chcesz czegoś podobnego do

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

To mówi, że przekonwertuj ten czas lokalny (używając strefy) na utc. Jeśli masz Time.zoneustawione, możesz oczywiście

Time.zone.local_to_utc(t)

To nie użyje strefy czasowej dołączonej do t - zakłada, że ​​jest lokalna dla strefy czasowej, z której konwertujesz.

Jednym z skrajnych przypadków, przed którym należy się tutaj wystrzegać, są przejścia na czas letni: określony czas lokalny może nie istnieć lub może być niejednoznaczny.

Frederick Cheung
źródło
17
Potrzebowałem kombinacji local_to_utc i Time.use_zone:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
Co sugerujesz przeciwko zmianom na czas letni? Załóżmy, że docelowy czas konwersji jest po przejściu D / ST, podczas gdy Time.now jest przed zmianą. Czy to zadziała?
Cyril Duchon-Doris
28

Właśnie stanąłem przed tym samym problemem i oto, co mam zamiar zrobić:

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

Oto dokumentacja dotycząca asctime

Zhenya
źródło
2
Po prostu robi to, czego się spodziewałem
Kaz,
To świetnie, dzięki! Na tej podstawie uprościłem moją odpowiedź . Jedną wadą asctimejest to, że obniża wszelkie wartości podsekundowe (które zachowuje moja odpowiedź).
Henrik N
22

Jeśli używasz Railsów, oto inna metoda na wzór odpowiedzi Erica Walsha:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Brian
źródło
1
Aby przekonwertować obiekt DateTime z powrotem na obiekt TimeWithZone, po prostu dołącz .in_time_zonedo końca.
Brian
@Brian Murphy-Dye Miałem problemy z czasem letnim używając tej funkcji. Czy możesz edytować swoje pytanie, aby przedstawić rozwiązanie, które działa z czasem letnim? Może zastąpienie Time.zone.nowczegoś, co jest najbliższe czasowi, w którym chcesz zmienić, zadziała?
Cyril Duchon-Doris
6

Po konwersji musisz dodać przesunięcie czasu do swojego czasu.

Najłatwiej to zrobić:

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

Nie jestem pewien, dlaczego chciałbyś to zrobić, chociaż prawdopodobnie najlepiej jest pracować z czasami tak, jak są zbudowane. Wydaje mi się, że pomocne byłoby wyjaśnienie, dlaczego należy przesunąć czas i strefy czasowe.

j_mcnally
źródło
To zadziałało dla mnie. Powinno to pozostać poprawne podczas zmiany czasu letniego, pod warunkiem, że ustawiona jest wartość przesunięcia, gdy jest używana. Używając obiektów DateTime, można dodać lub odjąć od niego "offset.seconds".
JosephK
Jeśli jesteś poza Railsami, możesz użyć Time.in_time_zone, wymagając odpowiednich części active_support:require 'active_support/core_ext/time'
jevon
Wydaje się, że działa to źle w zależności od kierunku, w którym zmierzasz, i nie zawsze wydaje się niezawodne. Np. Jeśli wezmę teraz czas sztokholmski i przekonwertuję na czas londyński, zadziała, jeśli dodam (a nie odejmie) przesunięcie. Ale jeśli zamieniam Sztokholm na Helsinki, to źle, czy dodam, czy odejmuję.
Henrik N
5

Właściwie myślę, że musisz odjąć przesunięcie po konwersji, jak w:

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 
Peter Alfvin
źródło
3

Zależy od tego, gdzie zamierzasz wykorzystać ten czas.

Kiedy twój czas jest atrybutem

Jeśli czas jest używany jako atrybut, możesz użyć tego samego klejnotu 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

Kiedy ustawisz osobną zmienną

Użyj tego samego klejnotu 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
Siergiej Zinin
źródło
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

Szybka funkcja, którą wymyśliłem, aby rozwiązać tę pracę. Jeśli ktoś ma bardziej efektywny sposób na zrobienie tego, opublikuj go!

Eric Walsh
źródło
1
t.change(zone: 'America/New_York')

Założenie OP jest niepoprawne: „Nie chcę ładować: start_time, a następnie konwertować do: timezone, ponieważ Railsy będą sprytne i zaktualizują czas z UTC, aby był zgodny z tą strefą czasową.” Niekoniecznie jest to prawda, na co wskazuje udzielona tutaj odpowiedź.

kkurian
źródło
1
To nie daje odpowiedzi na pytanie. Aby skrytykować lub poprosić autora o wyjaśnienie, zostaw komentarz pod jego postem. - Z recenzji
Aksen P
Zaktualizowałem moją odpowiedź, aby wyjaśnić, dlaczego jest to prawidłowa odpowiedź na pytanie i dlaczego pytanie opiera się na błędnym założeniu.
kkurian
Wydaje się, że to nic nie daje. Moje testy pokazują, że to noop.
Itay Grudev
@ItayGrudev Kiedy biegniesz t.zoneprzed t.changetym, co widzisz? A co po uruchomieniu t.zonepo t.change? A jakie parametry t.changedokładnie przekazujesz ?
kkurian
0

Stworzyłem kilka metod pomocniczych, z których jedna robi to samo, o co prosi pierwotny autor posta w Ruby / Rails - Zmień strefę czasową czasu, bez zmiany wartości .

Udokumentowałem również kilka osobliwości, które zaobserwowałem, a także ci pomocnicy zawierają metody całkowicie ignorujące automatyczne oszczędzanie światła dziennego stosowane podczas konwersji czasu, które nie są dostępne od razu po zainstalowaniu we frameworku 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

Przykłady, które można wypróbować na konsoli railsowej lub w skrypcie ruby ​​po umieszczeniu powyższych metod w klasie lub module:

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

Mam nadzieję że to pomoże.

Jignesh Gohel
źródło
0

Oto inna wersja, która działała lepiej dla mnie niż obecne odpowiedzi:

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

Przenosi przez nanosekundy używając „% N”. Jeśli potrzebujesz innej precyzji, zobacz to odniesienie do strftime .

Henrik N
źródło
-1

Spędziłem również sporo czasu zmagając się z TimeZones i po majstrowaniu przy Rubim 1.9.3 zdałem sobie sprawę, że nie musisz konwertować na nazwany symbol strefy czasowej przed konwersją:

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

Oznacza to, że możesz skupić się na uzyskaniu odpowiedniego ustawienia czasu najpierw w wybranym regionie, tak jak byś o tym pomyślał (przynajmniej w mojej głowie dzielę to w ten sposób), a następnie przekonwertować na koniec do strefy chcesz zweryfikować swoją logikę biznesową.

Działa to również dla Rubiego 2.3.1.

Torrey Payne
źródło
1
Czasami wschodnie przesunięcie wynosi -4, myślę, że chcą sobie z tym poradzić automatycznie w obu przypadkach.
OpenCoderX