Ruby / Rails - Mengubah zona waktu suatu Waktu, tanpa mengubah nilainya


108

Saya memiliki catatan foodalam database yang memiliki :start_timedan :timezoneatribut.

Ini :start_timeadalah Waktu dalam UTC - 2001-01-01 14:20:00, misalnya. Ini :timezoneadalah string - America/New_York, misalnya.

Saya ingin membuat objek Waktu baru dengan nilai :start_timetetapi zona waktunya ditentukan oleh :timezone. Saya tidak ingin memuat :start_timedan kemudian mengonversinya ke :timezone, karena Rails akan menjadi pintar dan memperbarui waktu dari UTC agar konsisten dengan zona waktu itu.

Saat ini,

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

Sebaliknya, saya ingin melihat

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

yaitu. Aku ingin melakukan:

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
Saya tidak berpikir Anda menggunakan zona waktu dengan benar. Jika Anda menyimpannya ke db Anda sebagai UTC dari lokal, apa yang salah dengan menguraikannya melalui waktu lokalnya dan menyimpannya melalui utc relatifnya?
Trip

1
Ya ... Saya pikir untuk menerima bantuan terbaik Anda mungkin perlu menjelaskan mengapa Anda perlu melakukan ini? Mengapa Anda menyimpan waktu yang salah ke database?
nzifnab

@RYoshiji setuju. terdengar seperti YAGNI atau pengoptimalan prematur bagi saya.
engineerDave

1
jika dokumen tersebut membantu, kami tidak memerlukan StackOverflow :-) Satu contoh di sana, yang tidak menunjukkan bagaimana sesuatu telah disetel - tipikal. Saya juga perlu melakukan ini untuk memaksa perbandingan Apel-Ke-Apel yang tidak rusak saat Daylight Savings masuk atau keluar.
JosephK

Jawaban:


72

Kedengarannya Anda menginginkan sesuatu di sepanjang baris

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

Ini mengatakan konversi waktu lokal ini (menggunakan zona) ke utc. Jika Anda sudah Time.zonemengatur maka Anda tentu saja bisa

Time.zone.local_to_utc(t)

Ini tidak akan menggunakan zona waktu yang dilampirkan ke t - ini mengasumsikan bahwa itu lokal ke zona waktu tempat Anda mengonversi.

Satu kasus tepi yang harus diperhatikan di sini adalah transisi DST: waktu lokal yang Anda tentukan mungkin tidak ada atau mungkin ambigu.


17
Saya membutuhkan kombinasi local_to_utc dan Time.use_zone:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb

Apa yang Anda sarankan terhadap transisi DST? Misalkan waktu target untuk mengkonversi adalah setelah transisi D / ST sedangkan Time.now sebelum perubahan. Apakah itu akan berhasil?
Cyril Duchon-Doris

28

Saya baru saja menghadapi masalah yang sama dan inilah yang akan saya lakukan:

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

Berikut adalah dokumentasi tentang asctime


2
Itu hanya melakukan apa yang saya harapkan
Kaz

Ini bagus, terima kasih! Sederhanakan jawaban saya berdasarkan itu. Satu sisi negatifnya asctimeadalah ia menjatuhkan nilai sub-detik apa pun (yang disimpan oleh jawaban saya).
Henrik N

22

Jika Anda menggunakan Rails, berikut adalah metode lain yang sejalan dengan jawaban Eric Walsh:

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

1
Dan untuk mengubah objek DateTime kembali ke objek TimeWithZone, cukup tempelkan .in_time_zonesampai akhir.
Brian

@ Brian Murphy-Dye Saya mengalami masalah dengan Daylight Saving Time menggunakan fungsi ini. Dapatkah Anda mengedit pertanyaan Anda untuk memberikan solusi yang sesuai dengan DST? Mungkin mengganti Time.zone.nowdengan sesuatu yang paling dekat dengan waktu yang ingin Anda ubah akan berhasil?
Cyril Duchon-Doris

6

Anda perlu menambahkan pengimbangan waktu ke waktu Anda setelah Anda mengubahnya.

Cara termudah untuk melakukannya adalah:

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

Saya tidak yakin mengapa Anda ingin melakukan ini, meskipun mungkin yang terbaik adalah benar-benar bekerja dengan waktu bagaimana mereka dibangun. Saya kira beberapa latar belakang mengapa Anda perlu menggeser waktu dan zona waktu akan membantu.


Ini berhasil untuk saya. Ini harus tetap benar selama pergeseran musim panas, asalkan nilai offset disetel saat digunakan. Jika menggunakan objek DateTime, seseorang dapat menambah atau mengurangi "offset.seconds" darinya.
JosephK

Jika Anda berada di luar Rails, Anda dapat menggunakan Time.in_time_zonedengan meminta bagian yang benar dari active_support:require 'active_support/core_ext/time'
jevon

Ini tampaknya melakukan hal yang salah tergantung ke arah mana Anda masuk, dan tampaknya tidak selalu dapat diandalkan. Misalnya jika saya mengambil waktu Stockholm sekarang dan mengonversi ke waktu London, itu berfungsi jika menambahkan (bukan mengurangi) offset. Tetapi jika saya mengonversi Stockholm ke Helsinki, itu salah apakah saya menambah atau mengurangi.
Henrik N

5

Sebenarnya, saya pikir Anda perlu mengurangi offset setelah Anda mengubahnya, seperti di:

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

Tergantung di mana Anda akan menggunakan Waktu ini.

Ketika waktu Anda adalah atribut

Jika waktu digunakan sebagai atribut, Anda dapat menggunakan permata date_time_attribute yang sama :

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

Saat Anda menetapkan variabel terpisah

Gunakan permata date_time_attribute yang sama :

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

Fungsi kecil cepat yang saya buat untuk menyelesaikan pekerjaan. Jika seseorang memiliki cara yang lebih efisien untuk melakukan ini, harap kirimkan!


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

Premis OP tidak benar: "Saya tidak ingin memuat: start_time lalu mengonversi ke: zona waktu, karena Rails akan pintar dan memperbarui waktu dari UTC agar konsisten dengan zona waktu itu." Ini belum tentu benar, seperti yang ditunjukkan oleh jawaban yang diberikan di sini.


1
Ini tidak memberikan jawaban atas pertanyaan tersebut. Untuk mengkritik atau meminta klarifikasi dari seorang penulis, tinggalkan komentar di bawah postingannya. - Dari Ulasan
Aksen P

Memperbarui jawaban saya untuk mengklarifikasi mengapa ini adalah jawaban yang valid untuk pertanyaan tersebut, dan mengapa pertanyaan tersebut didasarkan pada asumsi yang salah.
kkurian

Ini sepertinya tidak melakukan apa-apa. Tes saya menunjukkan itu menjadi noop.
Itay Grudev

@ItayGrudev Ketika Anda berlari t.zonesebelum t.changeapa yang Anda lihat? Dan bagaimana jika Anda t.zonemengejarnya t.change? Dan parameter apa yang Anda teruskan t.change?
kkurian

0

Saya telah membuat beberapa metode pembantu yang salah satunya hanya melakukan hal yang sama seperti yang diminta oleh penulis asli posting di Ruby / Rails - Ubah zona waktu, tanpa mengubah nilainya .

Saya juga telah mendokumentasikan beberapa keanehan yang saya amati dan juga pembantu ini berisi metode untuk sepenuhnya mengabaikan penghematan siang hari otomatis yang berlaku sementara konversi waktu yang tidak tersedia di luar kotak dalam kerangka kerja 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

Contoh yang dapat dicoba pada rails console atau skrip ruby ​​setelah menggabungkan metode di atas dalam kelas atau modul:

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

Semoga ini membantu.


0

Berikut versi lain yang bekerja lebih baik untuk saya daripada jawaban saat ini:

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

Ini membawa lebih dari nanodetik menggunakan "% N". Jika Anda menginginkan presisi lain, lihat referensi strftime ini .


-1

Saya menghabiskan banyak waktu berjuang dengan TimeZones juga, dan setelah mengutak-atik Ruby 1.9.3 menyadari bahwa Anda tidak perlu mengonversi ke simbol zona waktu bernama sebelum mengonversi:

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

Hal ini menyiratkan bahwa Anda dapat fokus untuk mendapatkan pengaturan waktu yang sesuai terlebih dahulu di wilayah yang Anda inginkan, seperti yang akan Anda pikirkan (setidaknya di kepala saya, saya mempartisi dengan cara ini), dan kemudian mengubahnya di akhir ke zona Anda ingin memverifikasi logika bisnis Anda dengan.

Ini juga berfungsi untuk Ruby 2.3.1.


1
Terkadang offset Timur adalah -4, saya pikir mereka ingin menanganinya secara otomatis dalam kedua kasus.
OpenCoderX
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.