Ruby / Rails - Altere o fuso horário de um horário, sem alterar o valor

108

Tenho um cadastro foono banco de dados que possui :start_timee :timezoneatributos.

O :start_timeé um horário em UTC - 2001-01-01 14:20:00, por exemplo. O :timezoneé uma string - America/New_York, por exemplo.

Quero criar um novo objeto Time com o valor de, :start_timemas cujo fuso horário é especificado por :timezone. Não quero carregar o :start_timee depois converter para :timezone, porque o Rails será inteligente e atualizará a hora do UTC para ser consistente com esse fuso horário.

Atualmente,

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

Em vez disso, quero ver

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

ie. Eu quero fazer:

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
fonte
3
Não acho que você esteja usando os fusos horários corretamente. Se você salvá-lo em seu banco de dados como UTC do local, o que há de errado em analisá-lo por meio de sua hora local e salvá-lo por meio de seu utc relativo?
Viagem de
1
Sim ... Eu acho que para melhor receber ajuda você precisa explicar porque você precisa fazer isso? Por que você armazenaria a hora errada no banco de dados em primeiro lugar?
nzifnab
@MrYoshiji concordou. soa como YAGNI ou otimização prematura para mim.
engineerDave
1
se esses documentos ajudassem, não precisaríamos de StackOverflow :-) Um exemplo aí, que não mostra como nada foi configurado - típico. Eu também preciso fazer isso para forçar uma comparação Maçãs com Maçãs que não seja interrompida quando o horário de verão entrar ou sair do ar.
JosephK

Respostas:

72

Parece que você quer algo na linha de

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

Isso diz converter a hora local (usando a zona) para utc. Se você Time.zonedefiniu, então você pode, é claro,

Time.zone.local_to_utc(t)

Isso não usará o fuso horário anexado a t - pressupõe que seja local em relação ao fuso horário do qual você está convertendo.

Um caso extremo a ser evitado aqui são as transições de horário de verão: o horário local especificado pode não existir ou ser ambíguo.

Frederick Cheung
fonte
17
Uma combinação de local_to_utc e Time.use_zone é o que eu precisava:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
O que você sugere contra as transições do horário de verão? Suponha que o tempo desejado para a conversão seja após a transição D / ST, enquanto Time.now é antes da mudança. Isso funcionaria ?
Cyril Duchon-Doris
28

Acabei de enfrentar o mesmo problema e aqui está o que vou fazer:

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

Aqui está a documentação sobre asctime

Zhenya
fonte
2
Ele simplesmente faz o que eu esperava
Kaz
Isso é ótimo, obrigado! Simplifiquei minha resposta com base nisso. Uma desvantagem asctimeé que ele descarta quaisquer valores de subsegundos (que minha resposta mantém).
Henrik N
22

Se você está usando Rails, aqui está outro método na linha da resposta 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
fonte
1
E para converter o objeto DateTime de volta em um objeto TimeWithZone, basta anexar .in_time_zoneao final.
Brian
@Brian Murphy-Dye Tive problemas com o horário de verão ao usar esta função. Você pode editar sua pergunta para fornecer uma solução que funcione com o DST? Talvez a substituição Time.zone.nowpor algo mais próximo do momento que você deseja alterar funcione?
Cyril Duchon-Doris
6

Você precisa adicionar a diferença de tempo ao seu tempo depois de convertê-lo.

A maneira mais fácil de fazer isso é:

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

Não sei por que você faria isso, embora seja provavelmente melhor trabalhar com os tempos da maneira como eles são construídos. Acho que algumas informações básicas sobre por que você precisa mudar o horário e os fusos horários seriam úteis.

j_mcnally
fonte
Isso funcionou para mim. Isso deve permanecer correto durante os turnos de horário de verão, desde que o valor de deslocamento seja definido quando usado. Se estiver usando objetos DateTime, pode-se adicionar ou subtrair "offset.seconds" dele.
JosephK
Se você estiver fora do Rails, você pode usar Time.in_time_zoneexigindo as partes corretas de active_support:require 'active_support/core_ext/time'
jevon
Isso parece fazer a coisa errada dependendo da direção em que você vai e nem sempre parece confiável. Por exemplo, se eu pegar o horário de Estocolmo agora e converter para o horário de Londres, funcionará se adicionar (não subtrair) o deslocamento. Mas se eu converter Estocolmo em Helsinque, é errado adicionar ou subtrair.
Henrik N
5

Na verdade, acho que você precisa subtrair o deslocamento depois de convertê-lo, como em:

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

Depende de onde você vai usar esse tempo.

Quando seu tempo é um atributo

Se o tempo for usado como um atributo, você pode usar a mesma gem 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 você define uma variável separada

Use a mesma gem 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
fonte
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

Uma pequena função rápida que criei para resolver o trabalho. Se alguém tiver uma maneira mais eficiente de fazer isso por favor poste!

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

A premissa do OP está incorreta: “Não quero carregar o: start_time e depois converter para: timezone, porque o Rails será inteligente e atualizará o horário do UTC para ser consistente com aquele fuso horário.” Isso não é necessariamente verdade, conforme demonstrado pela resposta fornecida aqui.

kkurian
fonte
1
Isso não fornece uma resposta para a pergunta. Para criticar ou solicitar esclarecimentos de um autor, deixe um comentário abaixo de sua postagem. - Da avaliação
Aksen P
Atualizei minha resposta para esclarecer por que essa é uma resposta válida para a pergunta e por que a pergunta é baseada em uma suposição falha.
kkurian
Isso não parece fazer nada. Meus testes mostram que é um noop.
Itay Grudev
@ItayGrudev Quando você corre t.zoneantes, t.changeo que você vê? E quando você correr t.zoneatrás t.change? E para quais parâmetros você está passando t.changeexatamente?
kkurian
0

Eu criei alguns métodos auxiliares, um dos quais apenas faz a mesma coisa que o autor original do post em Ruby / Rails - Alterar o fuso horário de um tempo, sem alterar o valor .

Além disso, documentei algumas peculiaridades que observei e também esses helpers contêm métodos para ignorar completamente a economia automática de luz do dia aplicável durante as conversões de tempo, que não estão disponíveis prontamente no 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

Exemplos que podem ser tentados no console Rails ou em um script Ruby após envolver os métodos acima em uma classe ou 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 isto ajude.

Jignesh Gohel
fonte
0

Aqui está outra versão que funcionou melhor para mim do que as respostas atuais:

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

Ele transporta nanossegundos usando "% N". Se você deseja outra precisão, consulte esta referência strftime .

Henrik N
fonte
-1

Passei um tempo significativo lutando com TimeZones também, e depois de mexer no Ruby 1.9.3 percebi que você não precisa converter para um símbolo de fuso horário nomeado antes de converter:

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

O que isso implica é que você pode se concentrar em obter a configuração de tempo apropriada primeiro na região que deseja, da maneira como você pensaria sobre isso (pelo menos na minha cabeça eu particiono desta forma) e, em seguida, converter no final para a zona com o qual deseja verificar sua lógica de negócios.

Isso também funciona para Ruby 2.3.1.

Torrey Payne
fonte
1
Às vezes, o deslocamento oriental é -4, acho que eles querem lidar com isso automaticamente em ambos os casos.
OpenCoderX