web-dev-qa-db-fra.com

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

J'ai un enregistrement foo dans la base de données qui a les attributs :start_time et :timezone.

Le :start_time est une heure en UTC - 2001-01-01 14:20:00, par exemple . Le :timezone est une chaîne - America/New_York, par exemple.

Je souhaite créer un nouvel objet Time avec la valeur :start_time mais dont le fuseau horaire est spécifié par :timezone. Je ne veux pas charger le :start_time et ensuite le convertir en :timezone, car Rails sera intelligent et mettra à jour l'heure à partir de l'heure UTC pour qu'elle soit 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
87
rwb

On dirait que vous voulez quelque chose du genre 

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

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

Time.zone.local_to_utc(t)

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

Les transitions DST sont un cas à éviter ici: l’heure locale spécifiée peut ne pas exister ou être ambiguë.

58
Frederick Cheung

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
18
Brian

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

13
Ievgen

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 le faire, bien qu'il soit probablement préférable de travailler avec les époques telles qu'elles sont construites. J'imagine qu'il serait utile de connaître les raisons pour lesquelles vous devez changer l'heure et les fuseaux horaires.

7
j_mcnally

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 
4
Peter Alfvin

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

Quand votre temps est un attribut

Si le temps est utilisé comme attribut, vous pouvez utiliser le même date_time_attribute gem :

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 la même date_time_attribute gem :

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
3
Sergei Zinin

J'ai passé beaucoup de temps à lutter avec TimeZones également, et après avoir bricolé Ruby 1.9.3, j'ai réalisé qu'il n'était pas nécessaire de convertir 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

Cela signifie que vous pouvez vous concentrer sur la configuration de l'heure appropriée en premier dans la région de votre choix, comme vous le pensez (du moins dans ma tête, je la partitionne de cette façon), puis à la fin, la conversion en zone. vous voulez vérifier votre logique métier avec.

Cela fonctionne aussi pour Ruby 2.3.1.

1
Torrey Payne
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, je suis venu avec pour résoudre le travail. Si quelqu'un a un moyen plus efficace de le faire, postez-le!

1
Eric Walsh

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

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

0
Jignesh Gohel