Ruby / Rails - เปลี่ยนเขตเวลาของเวลาโดยไม่ต้องเปลี่ยนค่า


109

ฉันมีบันทึกfooในฐานข้อมูลที่มี:start_timeและ:timezoneคุณลักษณะ

:start_timeเป็นเวลาใน UTC - การ2001-01-01 14:20:00ยกตัวอย่างเช่น :timezoneเป็นสตริง - America/New_Yorkยกตัวอย่างเช่น

ฉันต้องการที่จะสร้างวัตถุเวลาใหม่ที่มีค่าของแต่มีเขตเวลาที่ระบุไว้โดย:start_time :timezoneฉันไม่ต้องการโหลด:start_timeแล้วแปลงเป็น:timezoneเพราะ Rails จะฉลาดและอัปเดตเวลาจาก UTC ให้สอดคล้องกับเขตเวลานั้น

ปัจจุบัน

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

แต่ฉันอยากเห็น

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

กล่าวคือ. ฉันต้องการทำ:

t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST

1
บางทีนี่อาจช่วยได้: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji

3
ฉันคิดว่าคุณใช้เขตเวลาไม่ถูกต้อง หากคุณบันทึกลงในฐานข้อมูลของคุณเป็น UTC จากท้องถิ่นเกิดอะไรขึ้นกับการแยกวิเคราะห์ผ่านเวลาท้องถิ่นและบันทึกผ่าน utc สัมพัทธ์
ทริป

1
ยะ ... ฉันคิดว่าจะได้รับความช่วยเหลืออย่างดีที่สุดคุณอาจต้องอธิบายว่าทำไมคุณถึงต้องทำสิ่งนี้? ทำไมคุณถึงเก็บเวลาผิดไปยังฐานข้อมูลตั้งแต่แรก?
nzifnab

@MrYoshiji เห็นด้วย ดูเหมือน YAGNI หรือการเพิ่มประสิทธิภาพก่อนกำหนดสำหรับฉัน
engineerDave

1
หากเอกสารเหล่านั้นช่วยได้เราก็ไม่จำเป็นต้องใช้ StackOverflow :-) ตัวอย่างหนึ่งที่นั่นไม่ได้แสดงให้เห็นว่ามีการตั้งค่าอะไรไว้บ้าง - โดยทั่วไป ฉันต้องทำสิ่งนี้ด้วยเพื่อบังคับให้มีการเปรียบเทียบแอปเปิ้ลกับแอปเปิ้ลที่ไม่แตกเมื่อ Daylight Savings เริ่มเข้าหรือออก
JosephK

คำตอบ:


72

ดูเหมือนว่าคุณต้องการบางสิ่งบางอย่างตามแนวของ

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

สิ่งนี้ระบุว่าแปลงเวลาท้องถิ่นนี้ (โดยใช้โซน) เป็น utc หากคุณTime.zoneตั้งค่าแล้วคุณสามารถทำได้แน่นอน

Time.zone.local_to_utc(t)

การดำเนินการนี้จะไม่ใช้เขตเวลาที่แนบมากับ t โดยจะถือว่าเป็นเขตเวลาท้องถิ่นที่คุณกำลังแปลง

ขอบกรณีหนึ่งที่ต้องระวังคือการเปลี่ยน DST: เวลาท้องถิ่นที่คุณระบุอาจไม่มีอยู่หรืออาจไม่ชัดเจน


17
การรวมกันของ local_to_utc และ Time.use_zone คือสิ่งที่ฉันต้องการ:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb

คุณแนะนำอะไรเกี่ยวกับการเปลี่ยน DST สมมติว่าเวลาเป้าหมายในการแปลงอยู่หลังจากการเปลี่ยน D / ST ในขณะที่ Time.now อยู่ก่อนการเปลี่ยนแปลง จะได้ผลหรือไม่?
Cyril Duchon-Doris

28

ฉันเพิ่งประสบปัญหาเดียวกันและนี่คือสิ่งที่ฉันจะทำ:

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

นี่คือเอกสารเกี่ยวกับ asctime


2
มันทำในสิ่งที่ฉันคาดหวัง
Kaz

ยอดเยี่ยมมากขอบคุณ! ทำให้คำตอบของฉันง่ายขึ้นตามมัน ข้อเสียอย่างหนึ่งasctimeคือมันลดค่ารองลงมา (ซึ่งคำตอบของฉันยังคงอยู่)
Henrik N

22

หากคุณใช้ Rails นี่เป็นอีกวิธีหนึ่งตามคำตอบของ Eric Walsh:

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

1
และในการแปลงวัตถุ DateTime กลับเป็นวัตถุ TimeWithZone เพียงแค่จับ.in_time_zoneไปที่จุดสิ้นสุด
Brian

@Brian Murphy-Dye ฉันมีปัญหากับเวลาออมแสงโดยใช้ฟังก์ชันนี้ คุณสามารถแก้ไขคำถามเพื่อให้คำตอบที่ใช้ได้กับ DST หรือไม่ บางทีการแทนที่ด้วยTime.zone.nowสิ่งที่ใกล้เคียงที่สุดกับเวลาที่คุณต้องการเปลี่ยนแปลงจะได้ผลหรือไม่?
Cyril Duchon-Doris

6

คุณต้องเพิ่มค่าชดเชยเวลาให้กับเวลาของคุณหลังจากที่คุณแปลงแล้ว

วิธีที่ง่ายที่สุดคือ:

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

ฉันไม่แน่ใจว่าทำไมคุณถึงต้องการทำสิ่งนี้แม้ว่าจะเป็นการดีที่สุดที่จะใช้งานได้จริงตามเวลาที่สร้างขึ้น ฉันเดาว่าทำไมคุณต้องเปลี่ยนเวลาและเขตเวลาจะเป็นประโยชน์


สิ่งนี้ได้ผลสำหรับฉัน สิ่งนี้ควรจะคงอยู่อย่างถูกต้องในระหว่างการเปลี่ยนแปลงการออมแสงโดยมีการตั้งค่าออฟเซ็ตเมื่อใช้ หากใช้วัตถุ DateTime เราสามารถเพิ่มหรือลบ "offset.seconds" ออกจากวัตถุได้
JosephK

หากคุณอยู่นอก Rails คุณสามารถใช้ได้Time.in_time_zoneโดยต้องการส่วนที่ถูกต้องของ active_support:require 'active_support/core_ext/time'
jevon

สิ่งนี้ดูเหมือนจะผิดพลาดขึ้นอยู่กับทิศทางที่คุณไปและดูไม่น่าเชื่อถือเสมอไป เช่นถ้าฉันใช้เวลาสตอกโฮล์มตอนนี้และแปลงเป็นเวลาลอนดอนมันจะได้ผลถ้าบวก (ไม่ลบ) ชดเชย แต่ถ้าฉันเปลี่ยนสตอกโฮล์มเป็นเฮลซิงกิไม่ว่าฉันจะบวกหรือลบ
Henrik N

5

อันที่จริงฉันคิดว่าคุณต้องลบออฟเซ็ตหลังจากที่คุณแปลงมันดัง:

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

ขึ้นอยู่กับว่าคุณจะใช้เวลานี้ที่ไหน

เมื่อเวลาของคุณเป็นคุณลักษณะ

หากใช้เวลาเป็นแอตทริบิวต์คุณสามารถใช้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

เมื่อคุณตั้งค่าตัวแปรแยกต่างหาก

ใช้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

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

ฟังก์ชั่นเล็ก ๆ น้อย ๆ ที่ฉันคิดขึ้นมาเพื่อแก้ปัญหางาน หากใครมีวิธีที่มีประสิทธิภาพมากกว่านี้โปรดโพสต์!


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

สมมติฐานของ OP ไม่ถูกต้อง:“ ฉันไม่ต้องการโหลด: start_time แล้วแปลงเป็น: timezone เพราะ Rails จะฉลาดและอัปเดตเวลาจาก UTC เพื่อให้สอดคล้องกับเขตเวลานั้น” สิ่งนี้ไม่จำเป็นต้องเป็นความจริงดังที่แสดงให้เห็นในคำตอบที่ให้ไว้ที่นี่


1
สิ่งนี้ไม่ได้ให้คำตอบสำหรับคำถาม หากต้องการวิจารณ์หรือขอคำชี้แจงจากผู้เขียนโปรดแสดงความคิดเห็นใต้โพสต์ของพวกเขา - จากรีวิว
Aksen P

อัปเดตคำตอบของฉันเพื่อชี้แจงว่าเหตุใดจึงเป็นคำตอบที่ถูกต้องสำหรับคำถามและเหตุใดคำถามจึงตั้งอยู่บนสมมติฐานที่มีข้อบกพร่อง
kkurian

นี่ดูเหมือนไม่ได้ทำอะไร การทดสอบของฉันแสดงให้เห็นว่ามันเป็น noop
Itay Grudev

@ItayGrudev เมื่อt.zoneก่อนวิ่งt.changeคุณเห็นอะไร? แล้วเมื่อคุณวิ่งt.zoneตามt.change? และพารามิเตอร์ที่คุณส่งผ่านไปt.changeคืออะไร?
kkurian

0

ฉันได้สร้างวิธีการช่วยเหลือสองสามวิธีซึ่งหนึ่งในนั้นก็ทำสิ่งเดียวกับที่ถามโดยผู้เขียนต้นฉบับของโพสต์ที่Ruby / Rails - เปลี่ยนเขตเวลาของเวลาโดยไม่ต้องเปลี่ยนค่าเปลี่ยนเขตเวลาของเวลาโดยไม่มีการเปลี่ยนแปลงค่า

นอกจากนี้ฉันยังได้บันทึกลักษณะเฉพาะบางประการที่ฉันสังเกตเห็นและตัวช่วยเหล่านี้ยังมีวิธีการที่จะละเว้นการประหยัดแสงโดยอัตโนมัติในขณะที่การแปลงเวลาซึ่งไม่สามารถใช้งานได้ทันทีในกรอบ 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

ตัวอย่างที่สามารถทดลองใช้บนคอนโซลรางหรือสคริปต์ทับทิมหลังจากรวมวิธีการข้างต้นในคลาสหรือโมดูล:

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

หวังว่านี่จะช่วยได้


0

นี่เป็นอีกเวอร์ชันที่ทำงานได้ดีกว่าคำตอบปัจจุบันสำหรับฉัน:

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

โดยใช้หน่วยนาโนวินาทีโดยใช้ "% N" หากคุณต้องการความแม่นยำอื่น ๆโปรดดูข้อมูลอ้างอิงช่วงเวลานี้


-1

ฉันใช้เวลาอย่างมากในการต่อสู้กับ TimeZones เช่นกันและหลังจากซ่อมแซม Ruby 1.9.3 ก็รู้ว่าคุณไม่จำเป็นต้องแปลงเป็นสัญลักษณ์เขตเวลาที่ตั้งชื่อก่อนที่จะแปลง:

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

สิ่งนี้หมายความว่าคุณสามารถมุ่งเน้นไปที่การตั้งค่าเวลาที่เหมาะสมก่อนในภูมิภาคที่คุณต้องการวิธีที่คุณคิดเกี่ยวกับเรื่องนี้ (อย่างน้อยก็ในหัวของฉันฉันแบ่งพาร์ติชั่นด้วยวิธีนี้) จากนั้นแปลงในตอนท้ายเป็นโซน คุณต้องการตรวจสอบตรรกะทางธุรกิจของคุณด้วย

นอกจากนี้ยังใช้ได้กับ Ruby 2.3.1


1
บางครั้งค่าชดเชยตะวันออกคือ -4 ฉันคิดว่าพวกเขาต้องการจัดการโดยอัตโนมัติในทั้งสองกรณี
OpenCoderX
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.