Ruby / Rails-更改时间的时区,而不更改值


108

foo在数据库中有一个具有:start_time:timezone属性的记录。

例如,这:start_time是UTC-中的时间2001-01-01 14:20:00。例如,:timezone是字符串- America/New_York

我想创建一个新的Time对象,其值为,: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


3
我认为您使用的时区不正确。如果您将其从本地以UTC格式保存到数据库中,那么通过其本地时间解析并通过其相对utc进行保存又有什么问题呢?
旅行

1
是的...我想最好地获得帮助,您可能需要解释为什么需要这样做?为什么首先将错误的时间存储到数据库中?
nzifnab

@MrYoshiji同意。对我来说,听起来像是YAGNI或过早的优化。
EngineerDave

1
如果这些文档有所帮助,我们将不需要StackOverflow :-)那里的一个示例不显示任何设置方式-典型。我还需要执行此操作,以强制进行夏令时开始或结束时的Apple与Apple比较。
JosephK

Answers:


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在更改之前。那行得通吗?
西里尔·杜尚·多丽丝

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 Murphy-Dye我在使用此功能的夏令时遇到了问题。您可以编辑问题以提供适用于DST的解决方案吗?也许替换Time.zone.now为最接近您要更改时间的东西行得通吗?
西里尔·杜尚·多丽丝

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
这不能为问题提供答案。要批评或要求作者澄清,请在其帖子下方发表评论。- 评分
阿克申P

更新了我的答案,以阐明为什么这是对该问题的有效答案,以及为什么该问题基于错误的假设。
kkurian

这似乎无能为力。我的测试表明它没有问题。
伊泰格鲁杰夫

@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

将上述方法包装在类或模块中后,可以在rails控制台或ruby脚本上尝试这些示例:

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”来进行纳秒级的传输。如果需要其他精度,请参阅此strftime参考


-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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.