Ruby / Rails - Changer le fuseau horaire d'une heure, sans changer la valeur

108

J'ai un record foo dans la base de données qui a :start_timeet des :timezoneattributs.

Le :start_timeest un temps en UTC -2001-01-01 14:20:00 , par exemple. Le :timezoneest une chaîne - America/New_York, par exemple.

Je souhaite créer un nouvel objet Time avec la valeur de :start_timemais dont le fuseau horaire est spécifié par:timezone . Je ne veux pas charger :start_timepuis convertir en :timezone, car Rails sera intelligent et mettra à jour l'heure UTC pour être cohérente avec ce fuseau horaire.

Actuellement,

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

Au lieu de cela, je veux voir

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

c'est à dire. Je veux faire:

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
la source
1
Peut-être que cela aidera: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji
3
Je ne pense pas que vous utilisez correctement les fuseaux horaires. Si vous l'enregistrez dans votre base de données en tant que UTC à partir de local, quel est le problème avec son analyse via son heure locale et son enregistrement via son utc relatif?
Voyage
1
Ya ... Je pense que pour mieux recevoir de l'aide, vous devrez peut-être expliquer pourquoi vous auriez besoin de faire cela? Pourquoi stockeriez-vous le mauvais moment dans la base de données en premier lieu?
nzifnab
@MrYoshiji était d'accord. Cela ressemble à YAGNI ou à une optimisation prématurée pour moi.
ingénieurDave
1
si ces documents aidaient, nous n'aurions pas besoin de StackOverflow :-) Un exemple là-bas, qui ne montre pas comment quelque chose a été défini - typique. Je dois également le faire pour forcer une comparaison Apples-To-Apples qui ne se rompt pas lorsque l'heure d'été commence ou disparaît.
JosephK

Réponses:

72

On dirait que vous voulez quelque chose du genre

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

Cela dit convertir cette heure locale (en utilisant la zone) en utc. Si vous avez Time.zonedéfini, vous pouvez bien sûr

Time.zone.local_to_utc(t)

Cela n'utilisera pas le fuseau horaire associé à t - cela suppose qu'il est local par rapport au fuseau horaire à partir duquel vous effectuez la conversion.

Les transitions DST constituent un cas de pointe à éviter: l'heure locale que vous spécifiez peut ne pas exister ou être ambiguë.

Frederick Cheung
la source
17
Une combinaison de local_to_utc et Time.use_zone est ce dont j'avais besoin:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb
Que suggérez-vous contre les transitions DST? Supposons que l'heure cible pour la conversion soit après la transition D / ST tandis que Time.now est avant le changement. Cela fonctionnerait-il?
Cyril Duchon-Doris
28

Je viens de faire face au même problème et voici ce que je vais faire:

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

Voici la documentation sur asctime

Zhenya
la source
2
Il fait juste ce que j'attendais
Kaz
C'est génial, merci! Simplifié ma réponse en fonction de cela. Un inconvénient asctimeest qu'il supprime toutes les valeurs en sous-secondes (que ma réponse conserve).
Henrik N
22

Si vous utilisez Rails, voici une autre méthode dans le sens de la réponse d'Eric Walsh:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Brian
la source
1
Et pour reconvertir l'objet DateTime en objet TimeWithZone, il suffit de clouer .in_time_zoneà la fin.
Brian
@Brian Murphy-Dye J'ai eu des problèmes avec l'heure d'été en utilisant cette fonction. Pouvez-vous modifier votre question pour fournir une solution qui fonctionne avec DST? Peut-être que le remplacement Time.zone.nowpar quelque chose qui est le plus proche de l'heure à laquelle vous souhaitez changer fonctionnerait?
Cyril Duchon-Doris
6

Vous devez ajouter le décalage horaire à votre temps après l'avoir converti.

Le moyen le plus simple de procéder est:

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

Je ne sais pas pourquoi vous voudriez faire cela, bien qu'il soit probablement préférable de travailler avec les temps tels qu'ils sont construits. Je suppose que des informations sur les raisons pour lesquelles vous devez changer l'heure et les fuseaux horaires seraient utiles.

j_mcnally
la source
Cela a fonctionné pour moi. Cela devrait rester correct pendant les changements d'heure d'été, à condition que la valeur de décalage soit définie lorsqu'elle est utilisée. Si vous utilisez des objets DateTime, on peut y ajouter ou soustraire "offset.seconds".
JosephK
Si vous êtes en dehors de Rails, vous pouvez utiliser Time.in_time_zoneen exigeant les parties correctes de active_support:require 'active_support/core_ext/time'
jevon
Cela ne semble pas faire la bonne chose en fonction de la direction dans laquelle vous allez et ne semble pas toujours fiable. Par exemple, si je prends une heure de Stockholm maintenant et que je la convertis en heure de Londres, cela fonctionne si si j'ajoute (sans soustraire) le décalage. Mais si je convertis Stockholm en Helsinki, je ne peux pas ajouter ou soustraire.
Henrik N
5

En fait, je pense que vous devez soustraire le décalage après l'avoir converti, comme dans:

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
la source
3

Cela dépend de l'endroit où vous allez utiliser ce temps.

Quand ton temps est un attribut

Si l'heure est utilisée comme attribut, vous pouvez utiliser la même gemme 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

Lorsque vous définissez une variable distincte

Utilisez le même gemme 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
la source
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

Petite fonction rapide que j'ai inventée pour résoudre le travail. Si quelqu'un a un moyen plus efficace de faire cela, veuillez le poster!

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

La prémisse de l'OP est incorrecte: "Je ne veux pas charger le: start_time puis le convertir en: timezone, car Rails sera intelligent et mettra à jour l'heure UTC pour être cohérente avec ce fuseau horaire." Ce n'est pas nécessairement vrai, comme le montre la réponse fournie ici.

kkurian
la source
1
Cela ne répond pas à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous sa publication. - De l'avis
Aksen P
J'ai mis à jour ma réponse pour clarifier pourquoi c'est une réponse valide à la question et pourquoi la question est basée sur une hypothèse erronée.
kkurian
Cela ne semble rien faire. Mes tests montrent que c'est un noop.
Itay Grudev
@ItayGrudev Quand tu cours t.zoneavant t.changeque vois-tu? Et quand vous courez t.zoneaprès t.change? Et à quels paramètres passez-vous t.changeexactement?
kkurian le
0

J'ai créé quelques méthodes d'aide dont l'une fait la même chose que celle demandée par l'auteur original du message sur Ruby / Rails - Changer le fuseau horaire d'une heure, sans changer la valeur .

J'ai également documenté quelques particularités que j'ai observées et ces aides contiennent également des méthodes pour ignorer complètement les économies automatiques de lumière du jour applicables lors des conversions de temps qui ne sont pas disponibles prêtes à l'emploi dans le cadre 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

Exemples qui peuvent être essayés sur la console rails ou un script ruby ​​après avoir encapsulé les méthodes ci-dessus dans une classe ou un 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

J'espère que cela t'aides.

Jignesh Gohel
la source
0

Voici une autre version qui a mieux fonctionné pour moi que les réponses actuelles:

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

Il reporte les nanosecondes en utilisant "% N". Si vous désirez une autre précision, consultez cette référence strftime .

Henrik N
la source
-1

J'ai également passé beaucoup de temps à me débattre avec TimeZones, et après avoir bricolé Ruby 1.9.3, j'ai réalisé que vous n'aviez pas besoin de convertir en un symbole de fuseau horaire nommé avant 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

Ce que cela implique, c'est que vous pouvez vous concentrer sur l'obtention de la configuration de l'heure appropriée en premier dans la région que vous voulez, la façon dont vous y penseriez (au moins dans ma tête, je la partitionne de cette façon), puis convertissez à la fin de la zone vous souhaitez vérifier votre logique métier avec.

Cela fonctionne également pour Ruby 2.3.1.

Torrey Payne
la source
1
Parfois, le décalage oriental est de -4, je pense qu'ils veulent le gérer automatiquement dans les deux cas.
OpenCoderX