Ruby / Rails - Cambia il fuso orario di un Time, senza cambiare il valore


108

Ho un record foonel database che ha :start_timee :timezoneattributi.

L' :start_timeora è in UTC - 2001-01-01 14:20:00, per esempio. Il :timezoneè una stringa - America/New_York, per esempio.

Voglio creare un nuovo oggetto Time con il valore di :start_timema il cui fuso orario è specificato da :timezone. Non voglio caricare :start_timee poi convertire in :timezone, perché Rails sarà intelligente e aggiornerà l'ora da UTC per essere coerente con quel fuso orario.

Attualmente,

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

Invece, voglio vedere

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

vale a dire. Voglio fare:

t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST


3
Non penso che tu stia usando correttamente i fusi orari. Se lo salvi nel tuo database come UTC da locale, cosa c'è di sbagliato nell'analizzarlo tramite la sua ora locale e salvarlo tramite il relativo utc?
Viaggio

1
Sì ... penso che per ricevere aiuto al meglio potresti aver bisogno di spiegare perché dovresti farlo? Perché memorizzare l'ora sbagliata nel database in primo luogo?
nzifnab

@MrYoshiji concordato. suona come YAGNI o un'ottimizzazione prematura per me.
ingegnere

1
se quei documenti aiutassero, non avremmo bisogno di StackOverflow :-) Un esempio lì, che non mostra come è stato impostato qualcosa - tipico. Devo anche farlo per forzare un confronto tra mele e mele che non si interrompe quando entra o esce l'ora legale.
JosephK

Risposte:


72

Sembra che tu voglia qualcosa sulla falsariga di

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

Questo dice di convertire questa ora locale (usando la zona) in utc. Se hai Time.zoneimpostato, puoi ovviamente farlo

Time.zone.local_to_utc(t)

Questo non utilizzerà il fuso orario associato a t: si presume che sia locale rispetto al fuso orario da cui si esegue la conversione.

Un caso limite da evitare qui sono le transizioni dell'ora legale: l'ora locale specificata potrebbe non esistere o essere ambigua.


17
Una combinazione di local_to_utc e Time.use_zone è ciò di cui avevo bisogno:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb

Cosa suggerite contro le transizioni dell'ora legale? Supponiamo che l'ora target per la conversione sia dopo la transizione D / ST mentre Time.now è prima della modifica. Funzionerebbe?
Cyril Duchon-Doris

28

Ho appena affrontato lo stesso problema ed ecco cosa farò:

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

Ecco la documentazione su asctime


2
Fa solo quello che mi aspettavo
Kaz

È fantastico, grazie! Ha semplificato la mia risposta basandosi su di esso. Uno svantaggio asctimeè che elimina tutti i valori inferiori al secondo (che la mia risposta mantiene).
Henrik N

22

Se stai usando Rails, ecco un altro metodo sulla falsariga della risposta di Eric Walsh:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end

1
E per riconvertire l'oggetto DateTime in un oggetto TimeWithZone, basta attaccare .in_time_zonealla fine.
Brian

@ Brian Murphy-Dye Ho avuto problemi con l'ora legale usando questa funzione. Puoi modificare la tua domanda per fornire una soluzione che funzioni con l'ora legale? Forse la sostituzione Time.zone.nowcon qualcosa che è più vicino al momento che vuoi cambiare potrebbe funzionare?
Cyril Duchon-Doris

6

È necessario aggiungere la differenza di orario al tempo dopo averlo convertito.

Il modo più semplice per farlo è:

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

Non sono sicuro del motivo per cui vorresti farlo, anche se probabilmente è meglio lavorare con i tempi nel modo in cui sono costruiti. Immagino che alcune informazioni sul motivo per cui è necessario spostare l'ora e i fusi orari sarebbero utili.


Questo ha funzionato per me. Questo dovrebbe rimanere corretto durante i turni di ora legale, a condizione che il valore di offset sia impostato quando viene utilizzato. Se si utilizzano oggetti DateTime, è possibile aggiungere o sottrarre "offset.seconds" da esso.
JosephK

Se sei al di fuori di Rails, puoi usarlo Time.in_time_zonerichiedendo le parti corrette di active_support:require 'active_support/core_ext/time'
jevon

Questo sembra fare la cosa sbagliata a seconda della direzione in cui vai e non sembra sempre affidabile. Ad esempio, se prendo l'ora di Stoccolma adesso e la converto all'ora di Londra, funziona se aggiungo (non sottrai) l'offset. Ma se converto Stoccolma a Helsinki, è sbagliato aggiungere o sottrarre.
Henrik N

5

In realtà, penso che sia necessario sottrarre l'offset dopo averlo convertito, come 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 

3

Dipende da dove utilizzerai questo tempo.

Quando il tuo tempo è un attributo

Se l'ora viene utilizzata come attributo, puoi utilizzare la stessa gemma 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

Quando imposti una variabile separata

Usa la stessa gemma 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

Piccola funzione veloce che ho escogitato per risolvere il lavoro. Se qualcuno ha un modo più efficiente per farlo, pubblicalo!


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

La premessa dell'OP non è corretta: "Non voglio caricare: start_time e poi convertirlo in: timezone, perché Rails sarà intelligente e aggiornerà l'ora da UTC per essere coerente con quel fuso orario." Questo non è necessariamente vero, come dimostrato dalla risposta fornita qui.


1
Questo non fornisce una risposta alla domanda. Per criticare o richiedere chiarimenti a un autore, lascia un commento sotto il suo post. - Dalla recensione
Aksen P

Ho aggiornato la mia risposta per chiarire perché questa è una risposta valida alla domanda e perché la domanda si basa su un presupposto errato.
kkurian

Questo non sembra fare nulla. I miei test dimostrano che è un noop.
Itay Grudev,

@ItayGrudev Quando corri t.zoneprima t.changecosa vedi? E quando corri t.zonedietro t.change? E a quali parametri stai passando t.changeesattamente?
kkurian,

0

Ho creato alcuni metodi di supporto, uno dei quali fa semplicemente la stessa cosa richiesta dall'autore originale del post su Ruby / Rails - Cambia il fuso orario di un orario, senza modificare il valore .

Inoltre ho documentato alcune peculiarità che ho osservato e anche questi helper contengono metodi per ignorare completamente il risparmio automatico di luce diurna applicabile durante le conversioni temporali che non è disponibile out-of-the-box nel framework 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

Esempi che possono essere provati sulla console rails o uno script ruby ​​dopo aver avvolto i metodi precedenti in una classe o modulo:

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

Spero che questo ti aiuti.


0

Ecco un'altra versione che ha funzionato meglio per me rispetto alle risposte attuali:

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

Riporta nanosecondi utilizzando "% N". Se desideri un'altra precisione, consulta questo riferimento strftime .


-1

Ho passato molto tempo a lottare anche con TimeZones e dopo aver armeggiato con Ruby 1.9.3 ho capito che non è necessario convertire in un simbolo di fuso orario con nome prima della conversione:

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

Ciò implica che puoi concentrarti sull'ottenere l'impostazione dell'ora appropriata prima nella regione che desideri, nel modo in cui la penseresti (almeno nella mia testa lo partiziono in questo modo), e poi convertire alla fine nella zona con cui vuoi verificare la tua logica aziendale.

Questo funziona anche per Ruby 2.3.1.


1
A volte l'offset orientale è -4, penso che vogliano gestirlo automaticamente in entrambi i casi.
OpenCoderX
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.