Ruby / Rails - Ändern Sie die Zeitzone einer Zeit, ohne den Wert zu ändern

108

Ich habe einen Datensatz fooin der Datenbank, der :start_timeund :timezoneAttribute hat.

Das :start_timeist eine Zeit in UTC - 2001-01-01 14:20:00zum Beispiel. Das :timezoneist eine Zeichenfolge - America/New_Yorkzum Beispiel.

Ich möchte ein neues Zeitobjekt mit dem Wert erstellen :start_time, dessen Zeitzone jedoch durch angegeben ist :timezone. Ich möchte das nicht laden :start_timeund dann konvertieren :timezone, da Rails clever ist und die Zeit von UTC aktualisiert, um mit dieser Zeitzone übereinzustimmen.

Zur Zeit,

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

Stattdessen möchte ich sehen

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

dh. Ich will das tun:

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
quelle
1
Vielleicht hilft dies: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji
3
Ich glaube nicht, dass Sie Zeitzonen richtig verwenden. Wenn Sie es als UTC von local in Ihrer Datenbank speichern, was ist falsch daran, es über seine lokale Zeit zu analysieren und über seine relative utc zu speichern?
Reise
1
Ja ... Ich denke, um am besten Hilfe zu erhalten, müssen Sie möglicherweise erklären, warum Sie dies tun müssen. Warum würden Sie überhaupt die falsche Zeit in der Datenbank speichern?
Nzifnab
@ MrYoshiji stimmte zu. klingt für mich nach YAGNI oder vorzeitiger Optimierung.
Ingenieur
1
Wenn diese Dokumente helfen würden, würden wir StackOverflow nicht benötigen :-) Ein Beispiel dort, das nicht zeigt, wie etwas eingestellt wurde - typisch. Ich muss dies auch tun, um einen Vergleich von Äpfeln zu Äpfeln zu erzwingen, der nicht unterbrochen wird, wenn die Sommerzeit ein- oder ausgeht.
JosephK

Antworten:

72

Klingt so, als ob Sie etwas in der Art von wollen

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

Dies besagt, dass diese Ortszeit (unter Verwendung der Zone) in utc konvertiert wird. Wenn Sie eingestellt haben Time.zone, können Sie das natürlich tun

Time.zone.local_to_utc(t)

Dabei wird die an t angehängte Zeitzone nicht verwendet. Es wird davon ausgegangen, dass sie lokal für die Zeitzone ist, aus der Sie konvertieren.

Ein Randfall, vor dem Sie sich schützen sollten, sind Sommerzeitübergänge: Die von Ihnen angegebene Ortszeit ist möglicherweise nicht vorhanden oder nicht eindeutig.

Frederick Cheung
quelle
17
Eine Kombination von local_to_utc und Time.use_zone ist das, was ich brauchte:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
Was schlagen Sie gegen Sommerzeitübergänge vor? Angenommen, die zu konvertierende Zielzeit liegt nach dem D / ST-Übergang, während Time.now vor der Änderung liegt. Funktioniert das ?
Cyril Duchon-Doris
28

Ich habe gerade das gleiche Problem gehabt und hier ist, was ich tun werde:

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

Hier ist die Dokumentation zu asctime

Zhenya
quelle
2
Es macht einfach das, was ich erwartet hatte
Kaz
Das ist großartig, danke! Vereinfachte meine Antwort darauf basierend. Ein Nachteil von asctimeist, dass es alle Subsekundenwerte fallen lässt (was meine Antwort behält).
Henrik N
22

Wenn Sie Rails verwenden, ist hier eine andere Methode in Anlehnung an Eric Walshs Antwort:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Brian
quelle
1
Und um das DateTime-Objekt wieder in ein TimeWithZone-Objekt zu konvertieren, gehen Sie einfach .in_time_zonebis zum Ende.
Brian
@ Brian Murphy-Dye Ich hatte Probleme mit der Sommerzeit mit dieser Funktion. Können Sie Ihre Frage bearbeiten, um eine Lösung bereitzustellen, die mit der Sommerzeit funktioniert? Vielleicht Time.zone.nowwürde es funktionieren, wenn Sie etwas ersetzen , das der Zeit am nächsten kommt, die Sie ändern möchten?
Cyril Duchon-Doris
6

Sie müssen den Zeitversatz zu Ihrer Zeit hinzufügen, nachdem Sie ihn konvertiert haben.

Der einfachste Weg, dies zu tun, ist:

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

Ich bin mir nicht sicher, warum Sie dies tun möchten, obwohl es wahrscheinlich am besten ist, tatsächlich mit Zeiten zu arbeiten, wie sie gebaut sind. Ich denke, einige Hintergrundinformationen darüber, warum Sie die Zeit verschieben müssen, und Zeitzonen wären hilfreich.

j_mcnally
quelle
Das hat bei mir funktioniert. Dies sollte während der Sommerzeitschichten korrekt bleiben, vorausgesetzt, der Versatzwert wird bei Verwendung eingestellt. Wenn Sie DateTime-Objekte verwenden, können Sie "offset.seconds" hinzufügen oder davon abziehen.
JosephK
Wenn Sie sich außerhalb von Rails befinden, können Sie Time.in_time_zonedie richtigen Teile von active_support benötigen:require 'active_support/core_ext/time'
jevon
Dies scheint das Falsche zu tun, je nachdem, in welche Richtung Sie gehen, und scheint nicht immer zuverlässig zu sein. Wenn ich jetzt beispielsweise eine Stockholm-Zeit nehme und in die London-Zeit konvertiere, funktioniert dies, wenn der Offset addiert (nicht subtrahiert) wird. Aber wenn ich Stockholm nach Helsinki konvertiere, ist es falsch, ob ich addiere oder subtrahiere.
Henrik N
5

Eigentlich denke ich, dass Sie den Offset subtrahieren müssen, nachdem Sie ihn konvertiert haben, wie in:

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
quelle
3

Hängt davon ab, wo Sie diese Zeit verwenden werden.

Wenn Ihre Zeit ein Attribut ist

Wenn time als Attribut verwendet wird, können Sie dasselbe Juwel date_time_attribute verwenden :

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

Wenn Sie eine separate Variable festlegen

Verwenden Sie dasselbe Juwel 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
Sergei Zinin
quelle
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

Schnelle kleine Funktion, die ich mir ausgedacht habe, um den Job zu lösen. Wenn jemand eine effizientere Möglichkeit hat, dies zu posten, posten Sie es bitte!

Eric Walsh
quelle
1
t.change(zone: 'America/New_York')

Die Prämisse des OP ist falsch: "Ich möchte die: start_time nicht laden und dann in: timezone konvertieren, da Rails clever ist und die Zeit von UTC aktualisiert, um mit dieser Zeitzone übereinzustimmen." Dies ist nicht unbedingt der Fall, wie die hier gegebene Antwort zeigt.

kkurian
quelle
1
Dies gibt keine Antwort auf die Frage. Um einen Autor zu kritisieren oder um Klarstellung zu bitten, hinterlassen Sie einen Kommentar unter seinem Beitrag. - Aus dem Rückblick
Aksen P
Meine Antwort wurde aktualisiert, um zu verdeutlichen, warum dies eine gültige Antwort auf die Frage ist und warum die Frage auf einer fehlerhaften Annahme basiert.
kkurian
Das scheint nichts zu bewirken. Meine Tests zeigen, dass es ein Noop ist.
Itay Grudev
@ItayGrudev Wenn du t.zonevorher rennst, t.changewas siehst du? Und was ist, wenn du hinterher t.zonerennst t.change? Und an welche Parameter übergeben Sie t.changegenau?
kkurian
0

Ich habe einige Hilfsmethoden erstellt, von denen eine genau das tut, was der ursprüngliche Autor des Beitrags bei Ruby / Rails verlangt - Ändern Sie die Zeitzone einer Zeit, ohne den Wert zu ändern .

Außerdem habe ich einige Besonderheiten dokumentiert, die ich beobachtet habe, und diese Helfer enthalten Methoden zum vollständigen Ignorieren der automatischen Sommerzeiteinsparungen, die bei Zeitumrechnungen anfallen, die im Rails-Framework nicht sofort verfügbar sind:

  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

Beispiele, die auf der Rails-Konsole oder einem Ruby-Skript ausprobiert werden können, nachdem die oben genannten Methoden in eine Klasse oder ein Modul eingeschlossen wurden:

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

Hoffe das hilft.

Jignesh Gohel
quelle
0

Hier ist eine andere Version, die für mich besser funktioniert hat als die aktuellen Antworten:

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

Es überträgt Nanosekunden mit "% N". Wenn Sie eine andere Präzision wünschen, lesen Sie diese Strftime-Referenz .

Henrik N.
quelle
-1

Ich habe auch viel Zeit mit TimeZones verbracht und nach dem Basteln mit Ruby 1.9.3 festgestellt, dass Sie vor dem Konvertieren nicht in ein benanntes Zeitzonensymbol konvertieren müssen:

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

Dies bedeutet, dass Sie sich darauf konzentrieren können, zuerst in der gewünschten Region die richtige Zeiteinstellung zu erhalten, wie Sie es sich vorstellen würden (zumindest in meinem Kopf partitioniere ich es auf diese Weise) und dann am Ende in die Zone zu konvertieren Sie möchten Ihre Geschäftslogik mit überprüfen.

Dies funktioniert auch für Ruby 2.3.1.

Torrey Payne
quelle
1
Manchmal ist der östliche Versatz -4, ich denke, sie wollen in beiden Fällen automatisch damit umgehen.
OpenCoderX