Ruby / Rails: cambia la zona horaria de una hora, sin cambiar el valor

108

Tengo un registro fooen la base de datos que tiene :start_timey :timezoneatributos.

El :start_timees un tiempo en UTC - 2001-01-01 14:20:00, por ejemplo. El :timezonees una cadena - America/New_York, por ejemplo.

Quiero crear un nuevo objeto Time con el valor de :start_timepero cuya zona horaria está especificada por :timezone. No quiero cargar el :start_timey luego convertir a :timezone, porque Rails será inteligente y actualizará la hora de UTC para que sea consistente con esa zona horaria.

Actualmente,

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

En cambio, quiero ver

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

es decir. Quiero hacer:

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
fuente
3
No creo que estés usando las zonas horarias correctamente. Si lo guarda en su base de datos como UTC desde local, ¿qué tiene de malo analizarlo a través de su hora local y guardarlo a través de su utc relativo?
Viaje
1
Sí ... creo que para recibir la mejor ayuda, es posible que deba explicar por qué necesita hacer esto. ¿Por qué almacenaría la hora incorrecta en la base de datos en primer lugar?
nzifnab
@MrYoshiji estuvo de acuerdo. me suena a YAGNI oa una optimización prematura.
ingeniero
1
si esos documentos ayudaran, no necesitaríamos StackOverflow :-) Un ejemplo ahí, que no muestra cómo se configuró nada, típico. También necesito hacer esto para forzar una comparación de manzanas a manzanas que no se rompa cuando el horario de verano entra o se apaga.
JosephK

Respuestas:

72

Parece que quieres algo como

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

Esto dice que convierta esta hora local (usando la zona) a utc. Si lo ha Time.zoneconfigurado, puede, por supuesto,

Time.zone.local_to_utc(t)

Esto no usará la zona horaria adjunta a t, asume que es local a la zona horaria desde la que está convirtiendo.

Un caso extremo para protegerse aquí son las transiciones de horario de verano: la hora local que especifique puede no existir o puede ser ambigua.

Frederick Cheung
fuente
17
Lo que necesitaba es una combinación de local_to_utc y Time.use_zone:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
¿Qué sugieres contra las transiciones DST? Suponga que el tiempo objetivo para convertir es después de la transición D / ST, mientras que Time.now es antes del cambio. Funcionaría eso ?
Cyril Duchon-Doris
28

Acabo de enfrentar el mismo problema y esto es lo que voy a hacer:

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

Aquí está la documentación sobre asctime

Zhenya
fuente
2
Simplemente hace lo que esperaba
Kaz
¡Esto es genial, gracias! Simplificado mi respuesta basándome en eso. Una desventaja asctimees que elimina los valores de subsegundos (que mi respuesta mantiene).
Henrik N
22

Si está utilizando Rails, aquí hay otro método similar a la respuesta de Eric Walsh:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Brian
fuente
1
Y para convertir el objeto DateTime de nuevo en un objeto TimeWithZone, simplemente vire .in_time_zonehasta el final.
Brian
@Brian Murphy-Dye He tenido problemas con el horario de verano al usar esta función. ¿Puede editar su pregunta para proporcionar una solución que funcione con DST? ¿Quizás funcionaría reemplazarlo Time.zone.nowpor algo que esté más cerca de la hora que desea cambiar?
Cyril Duchon-Doris
6

Debe agregar la compensación de tiempo a su tiempo después de convertirlo.

La forma más sencilla de hacerlo es:

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

No estoy seguro de por qué querría hacer esto, aunque probablemente sea mejor trabajar con los tiempos de la forma en que están construidos. Supongo que sería útil contar con algunos antecedentes sobre por qué necesita cambiar la hora y las zonas horarias.

j_mcnally
fuente
Esto funcionó para mí. Esto debe mantenerse correcto durante los turnos de verano, siempre que el valor de compensación se establezca cuando se use. Si se utilizan objetos DateTime, se puede sumar o restar "offset.seconds".
JosephK
Si está fuera de Rails, puede usar Time.in_time_zonerequiriendo las partes correctas de active_support:require 'active_support/core_ext/time'
jevon
Esto parece hacer algo incorrecto dependiendo de la dirección en la que vaya, y no siempre parece confiable. Por ejemplo, si tomo una hora de Estocolmo ahora y la convierto a la hora de Londres, funciona si sumo (no resto) el desplazamiento. Pero si convierto Estocolmo en Helsinki, está mal si sumo o resto.
Henrik N
5

En realidad, creo que debe restar el desplazamiento después de convertirlo, como en:

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

Depende de dónde vaya a utilizar este tiempo.

Cuando tu tiempo es un atributo

Si se usa el tiempo como atributo, puede usar la misma gema 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

Cuando estableces una variable separada

Utilice la misma gema 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
fuente
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

Se me ocurrió una pequeña función rápida para resolver el trabajo. Si alguien tiene una forma más eficiente de hacer esto, ¡publíquelo!

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

La premisa del OP es incorrecta: "No quiero cargar: start_time y luego convertir a: timezone, porque Rails será inteligente y actualizará la hora de UTC para que sea coherente con esa zona horaria". Esto no es necesariamente cierto, como lo demuestra la respuesta proporcionada aquí.

kkurian
fuente
1
Esto no proporciona una respuesta a la pregunta. Para criticar o solicitar una aclaración de un autor, deje un comentario debajo de su publicación. - De la crítica
Aksen P
Actualicé mi respuesta para aclarar por qué esta es una respuesta válida a la pregunta y por qué la pregunta se basa en una suposición errónea.
kkurian
Esto no parece hacer nada. Mis pruebas muestran que es un error.
Itay Grudev
@ItayGrudev Cuando corres t.zoneantes, t.change¿qué ves? ¿Y cuando corres t.zonedetrás t.change? ¿Y a qué parámetros estás pasando t.changeexactamente?
kkurian
0

He creado algunos métodos auxiliares, uno de los cuales hace lo mismo que el autor original de la publicación en Ruby / Rails: cambiar la zona horaria de una hora, sin cambiar el valor .

También he documentado algunas peculiaridades que observé y también estos ayudantes contienen métodos para ignorar por completo el ahorro automático de luz diurna aplicable mientras que las conversiones de tiempo no están disponibles de forma inmediata en el marco de 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

Ejemplos que se pueden probar en la consola de rieles o en un script ruby ​​después de envolver los métodos anteriores en una clase o módulo:

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

Espero que esto ayude.

Jignesh Gohel
fuente
0

Aquí hay otra versión que funcionó mejor para mí que las respuestas actuales:

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

Transfiere nanosegundos usando "% N". Si desea otra precisión, consulte esta referencia de tiempo de ejecución .

Henrik N
fuente
-1

También pasé mucho tiempo luchando con TimeZones, y después de jugar con Ruby 1.9.3 me di cuenta de que no es necesario convertir a un símbolo de zona horaria con nombre antes de convertir:

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

Lo que esto implica es que puede concentrarse en obtener la configuración de tiempo adecuada primero en la región que desea, de la forma en que lo pensaría (al menos en mi cabeza lo particiono de esta manera), y luego convertir al final a la zona con el que desea verificar su lógica empresarial.

Esto también funciona para Ruby 2.3.1.

Torrey Payne
fuente
1
A veces, el desplazamiento del Este es -4, creo que quieren manejarlo automáticamente en ambos casos.
OpenCoderX