Ruby / Rails - Thay đổi múi giờ của Thời gian mà không thay đổi giá trị


108

Tôi có một bản ghi footrong cơ sở dữ liệu có:start_time:timezonecác thuộc tính.

Các :start_timelà một lần trong UTC - 2001-01-01 14:20:00, ví dụ. Các:timezone là một chuỗi - America/New_York, ví dụ.

Tôi muốn tạo một đối tượng Thời gian mới với giá trị :start_timenhưng có múi giờ được chỉ định bởi :timezone. Tôi không muốn tải:start_time và sau đó chuyển đổi sang :timezone, vì Rails sẽ thông minh và cập nhật thời gian từ UTC để phù hợp với múi giờ đó.

Hiện tại,

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

Thay vào đó, tôi muốn thấy

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

I E. Tôi muốn làm:

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
Có thể điều này sẽ hữu ích: api.rubyonrails.org/classes/Time.html#method-c-use_zone
MrYoshiji,

3
Tôi không nghĩ rằng bạn đang sử dụng múi giờ một cách chính xác. Nếu bạn lưu nó vào db của mình dưới dạng UTC từ cục bộ, thì có gì sai khi phân tích nó qua giờ địa phương của nó và lưu nó qua utc tương đối của nó?
Chuyến đi

1
Ya ... Tôi nghĩ để nhận được sự giúp đỡ tốt nhất bạn có thể cần giải thích lý do tại sao bạn cần làm điều này? Tại sao bạn lại lưu trữ sai thời gian vào cơ sở dữ liệu ngay từ đầu?
nzifnab

@MrYoshiji đồng ý. đối với tôi nghe giống như YAGNI hoặc tối ưu hóa quá sớm.
kỹ

1
nếu những tài liệu đó hữu ích, chúng tôi sẽ không cần StackOverflow :-) Một ví dụ ở đó, điều đó không cho thấy cách mọi thứ được thiết lập - điển hình. Tôi cũng cần làm điều này để bắt buộc so sánh Táo-Tới-Táo không bị phá vỡ khi Chế độ tiết kiệm ánh sáng ban ngày bắt đầu hoặc kết thúc.
JosephK

Câu trả lời:


72

Có vẻ như bạn muốn một cái gì đó dọc theo dòng

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

Điều này cho biết chuyển đổi giờ địa phương này (sử dụng múi giờ) thành utc. Nếu bạn đã Time.zoneđặt thì tất nhiên bạn có thể

Time.zone.local_to_utc(t)

Điều này sẽ không sử dụng múi giờ gắn liền với t - nó giả định rằng nó cục bộ với múi giờ bạn đang chuyển đổi.

Một trường hợp quan trọng cần đề phòng ở đây là chuyển đổi DST: giờ địa phương bạn chỉ định có thể không tồn tại hoặc có thể không rõ ràng.


17
Sự kết hợp giữa local_to_utc và Time.use_zone là những gì tôi cần:Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime
rwb

Bạn đề xuất điều gì chống lại quá trình chuyển đổi DST? Giả sử thời gian đích để chuyển đổi là sau quá trình chuyển đổi D / ST trong khi Time.now là trước khi thay đổi. Liệu điều đó có hiệu quả ?
Cyril Duchon-Doris,

28

Tôi vừa phải đối mặt với vấn đề tương tự và đây là những gì tôi sẽ làm:

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

Đây là tài liệu về asctime


2
Nó chỉ làm những gì tôi mong đợi
Kaz

Điều này thật tuyệt, cảm ơn! Đơn giản hóa câu trả lời của tôi dựa trên nó. Một nhược điểm của asctimenó là nó giảm bất kỳ giá trị giây nào (mà câu trả lời của tôi giữ lại).
Henrik N

22

Nếu bạn đang sử dụng Rails, đây là một phương pháp khác dọc theo câu trả lời của Eric Walsh:

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

1
Và để chuyển đổi đối tượng DateTime trở lại đối tượng TimeWithZone, chỉ cần nhấn .in_time_zonevào phần cuối.
Brian

@Brian Murphy-Dye Tôi đã gặp sự cố với Thời gian tiết kiệm ánh sáng ban ngày khi sử dụng chức năng này. Bạn có thể chỉnh sửa câu hỏi của mình để đưa ra giải pháp hoạt động với DST không? Có thể thay thế Time.zone.nowmột cái gì đó gần nhất với thời điểm bạn muốn thay đổi sẽ hiệu quả?
Cyril Duchon-Doris,

6

Bạn cần thêm thời gian bù vào thời gian của mình sau khi chuyển đổi nó.

Cách dễ nhất để làm điều này là:

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

Tôi không chắc tại sao bạn lại muốn làm điều này, mặc dù có lẽ tốt nhất là bạn nên thực sự làm việc với thời gian như cách chúng được xây dựng. Tôi đoán một số cơ sở về lý do tại sao bạn cần thay đổi thời gian và múi giờ sẽ hữu ích.


Điều này đã làm việc cho tôi. Điều này sẽ luôn đúng trong các ca thay đổi tiết kiệm ánh sáng ban ngày, miễn là giá trị bù được đặt khi sử dụng. Nếu sử dụng các đối tượng DateTime, người ta có thể thêm hoặc trừ "offset.seconds" khỏi nó.
JosephK

Nếu bạn đang ở bên ngoài Rails, bạn có thể sử dụng Time.in_time_zonebằng cách yêu cầu các phần đúng active_support:require 'active_support/core_ext/time'
jevon

Điều này dường như làm sai tùy thuộc vào hướng bạn đi và không phải lúc nào cũng đáng tin cậy. Ví dụ: nếu tôi lấy giờ Stockholm bây giờ và chuyển đổi sang giờ Luân Đôn, nó hoạt động nếu cộng (không trừ) phần bù. Nhưng nếu tôi chuyển đổi Stockholm thành Helsinki, thì việc tôi cộng hay trừ sẽ sai.
Henrik N

5

Trên thực tế, tôi nghĩ bạn cần phải trừ đi phần bù sau khi chuyển đổi nó, như trong:

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

Phụ thuộc vào nơi bạn sẽ sử dụng Thời gian này.

Khi thời gian của bạn là một thuộc tính

Nếu thời gian được sử dụng làm thuộc tính, bạn có thể sử dụng cùng một viên đá quý 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

Khi bạn đặt một biến riêng biệt

Sử dụng cùng một viên ngọc 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

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

Chức năng nhỏ nhanh chóng mà tôi nghĩ ra để giải quyết công việc. Nếu ai đó có một cách hiệu quả hơn để làm điều này, xin vui lòng đăng nó!


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

Tiền đề của OP không chính xác: "Tôi không muốn tải: start_time và sau đó chuyển đổi thành: múi giờ, vì Rails sẽ thông minh và cập nhật thời gian từ UTC để phù hợp với múi giờ đó." Điều này không nhất thiết phải đúng, như được chứng minh bằng câu trả lời được cung cấp ở đây.


1
Điều này không cung cấp câu trả lời cho câu hỏi. Để phê bình hoặc yêu cầu làm rõ từ tác giả, hãy để lại bình luận bên dưới bài đăng của họ. - Từ xét
Aksen P

Đã cập nhật câu trả lời của tôi để làm rõ lý do tại sao đây là câu trả lời hợp lệ cho câu hỏi và tại sao câu hỏi lại dựa trên một giả định thiếu sót.
kkurian

Điều này dường như không làm bất cứ điều gì. Thử nghiệm của tôi cho thấy nó là một noop.
Itay Grudev

@ItayGrudev Khi chạy t.zonetrước t.changebạn thấy gì? Và khi bạn chạy t.zonetheo t.changethì sao? Và bạn đang chuyển t.changechính xác những tham số nào?
kkurian

0

Tôi đã tạo một số phương thức trợ giúp, một trong số đó chỉ thực hiện điều tương tự như được yêu cầu bởi tác giả gốc của bài đăng tại Ruby / Rails - Thay đổi múi giờ của Thời gian, mà không thay đổi giá trị .

Ngoài ra, tôi đã ghi lại một số điểm đặc biệt mà tôi quan sát được và những trình trợ giúp này cũng chứa các phương pháp để hoàn toàn bỏ qua việc tiết kiệm ánh sáng ban ngày tự động áp dụng trong khi chuyển đổi thời gian không có sẵn trong khung 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

Các ví dụ có thể được thử trên bảng điều khiển rails hoặc tập lệnh ruby ​​sau khi gói các phương thức trên trong một lớp hoặc mô-đun:

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

Hi vọng điêu nay co ich.


0

Đây là một phiên bản khác hoạt động tốt hơn cho tôi so với các câu trả lời hiện tại:

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ó truyền hơn nano giây bằng cách sử dụng "% N". Nếu bạn muốn độ chính xác khác, hãy xem tham chiếu strftime này .


-1

Tôi cũng đã dành thời gian đáng kể để đấu tranh với TimeZones và sau khi mày mò với Ruby 1.9.3 nhận ra rằng bạn không cần phải chuyển đổi sang biểu tượng múi giờ đã đặt tên trước khi chuyển đổi:

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

Điều này ngụ ý rằng bạn có thể tập trung vào việc thiết lập thời gian thích hợp trước tiên ở khu vực bạn muốn, theo cách bạn sẽ nghĩ về nó (ít nhất là trong đầu tôi, tôi phân vùng nó theo cách này), và sau đó chuyển đổi ở cuối khu vực bạn muốn xác minh logic kinh doanh của mình với.

Điều này cũng hoạt động cho Ruby 2.3.1.


1
Đôi khi độ lệch Đông là -4, tôi nghĩ họ muốn xử lý tự động trong cả hai trường hợp.
OpenCoderX
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.