136

I have a record foo in the database which has :start_time and :timezone attributes.

The :start_time is a Time in UTC - 2001-01-01 14:20:00, for example. The :timezone is a string - America/New_York, for example.

I want to create a new Time object with the value of :start_time but whose timezone is specified by :timezone. I do not want to load the :start_time and then convert to :timezone, because Rails will be clever and update the time from UTC to be consistent with that timezone.

Currently,

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

Instead, I want to see

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

ie. I want to do:

t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST
rwb
  • 4,309
  • 8
  • 36
  • 59
  • 1
    Maybe this will help: http://api.rubyonrails.org/classes/Time.html#method-c-use_zone – MrYoshiji May 29 '13 at 15:43
  • 3
    I don't think you're using timezones correctly. If you save it into your db as UTC from local, whats wrong with parsing it via its local time and saving it via its relative utc? – Trip May 29 '13 at 15:47
  • 1
    Ya... I think to best receive help you might need to explain *why* you would need to do this? Why would you store the wrong time to the database in the first place? – nzifnab May 29 '13 at 16:43
  • @MrYoshiji agreed. sounds like either YAGNI or premature optimization to me. – engineerDave Aug 26 '15 at 20:16
  • 1
    if those docs helped, we would not need StackOverflow :-) One example there, that doesn't show how anything was set - typical. I also need to do this to force an Apples-To-Apples comparison that doesn't break when Daylight Savings kicks in or out. – JosephK Oct 13 '15 at 12:37

14 Answers14

87

Sounds like you want something along the lines of

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

This says convert this local time (using the zone) to utc. If you have Time.zone set then you can of course to

Time.zone.local_to_utc(t)

This won't use the timezone attached to t - it assumes that it's local to the time zone you are converting from.

One edge case to guard against here is DST transitions: the local time you specify may not exist or may be ambiguous.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • 18
    A combination of local_to_utc and Time.use_zone is what i needed: `Time.use_zone(self.timezone) { Time.zone.local_to_utc(t) }.localtime` – rwb May 29 '13 at 17:08
  • What do you suggest against DST transitions ? Suppose the target time to convert is after the D/ST transition while Time.now is before the change. Would that work ? – Cyril Duchon-Doris Oct 26 '16 at 18:42
51

I've just faced the same problem and here is what I'm going to do:

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

Here is the documentation on asctime

jevon
  • 3,197
  • 3
  • 32
  • 40
Zhenya
  • 6,020
  • 6
  • 34
  • 42
  • 3
    It just does what I expected – Kaz Oct 24 '17 at 17:57
  • This is great, thanks! Simplified [my answer](https://stackoverflow.com/a/61226059/6962) based on it. One downside of `asctime` is that it drops any subsecond values (which my answer keeps). – Henrik N Apr 15 '20 at 10:27
24

If you're using Rails, here is another method along the lines of Eric Walsh's answer:

def set_in_timezone(time, zone)
  Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
Brian
  • 967
  • 7
  • 12
  • 1
    And to convert the DateTime object back to a TimeWithZone object, just tack on `.in_time_zone` to the end. – Brian Dec 03 '15 at 17:43
  • @Brian Murphy-Dye I've had problems with Daylight Saving Time using this function. Can you edit your question to provide a solutions that works with DST ? Maybe replacing `Time.zone.now` to something that is closest to the time you want to change would work ? – Cyril Duchon-Doris Oct 26 '16 at 18:37
  • We used `dt.change(zone: 'PST8PDT')`, but this worked for us. Thank you, @Brian ! – 2ps Apr 17 '21 at 14:27
7

You need to add the time offset to your time after you convert it.

The easiest way to do this is:

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

I am not sure why you would want to do this, though it is probably best to actually work with times the way they are built. I guess some background on why you need to shift time and timezones would be helpful.

halfer
  • 19,824
  • 17
  • 99
  • 186
j_mcnally
  • 6,928
  • 2
  • 31
  • 46
  • This worked for me. This should stay correct during daylight-savings shifts, provided the offset value is set when used. If using DateTime objects, one can add or subtract "offset.seconds" from it. – JosephK Oct 13 '15 at 13:03
  • 1
    If you're outside of Rails, you can use `Time.in_time_zone` by requiring the correct parts of active_support: `require 'active_support/core_ext/time'` – jevon Jun 09 '19 at 23:50
  • This seems to do the wrong thing depending on which direction you go in, and doesn't always seem reliable. E.g. if I take a Stockholm time now and convert to London time, it works if if add (not subtract) the offset. But if I convert Stockholm to Helsinki, it's wrong whether I add or subtract. – Henrik N Apr 15 '20 at 09:59
6

Actually, I think you need to subtract the offset after you convert it, as in:

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 
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
3

Depends on where you are going to use this Time.

When your time is an attribute

If time is used as an attribute, you can use the same 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

When you set a separate variable

Use the same 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
Sergei Zinin
  • 161
  • 1
  • 3
3

Here's another version that worked better for me than the current answers:

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

It carries over nanoseconds using "%N". If you desire another precision, see this strftime reference.

Henrik N
  • 15,786
  • 5
  • 82
  • 131
  • Even if I realize that I need the crucial insight from @jevon that's missing from so many of the answers on this page about the ActiveSupport library, I'm still boggling at the idea that String might gain an `in_time_zone` method and indeed it doesn't, at least for me: ```$ ruby -we 'require "active_support/core_ext/time"; puts(Time.now().strftime("%F %T.%N").in_time_zone("Europe/London"))' -e:1:in `
    ': undefined method `in_time_zone' for "2021-04-22 13:59:37.783521783":String (NoMethodError)```
    – Martin Dorey Apr 22 '21 at 21:02
  • 1
    @MartinDorey Hehe, it wouldn't be the boggliest thing Rails has done. My mind boggled when they monkeypatched ERB to handle Rails HTML safety. This code certainly works for me on Rails 5.2 and is courtesy of `core_ext/string/zones`: `Time.now.strftime("%F %T.%N").method(:in_time_zone) => #` – Henrik N Apr 23 '21 at 15:42
2

The question's about Rails but it seems, like me, not everyone here is on the ActiveSupport train, so yet another option:

irb(main):001:0> require "time"
=> true
irb(main):003:0> require "tzinfo"
=> true
irb(main):004:0> t = Time.parse("2000-01-01 14:20:00 UTC")
=> 2000-01-01 14:20:00 UTC
irb(main):005:0> tz = TZInfo::Timezone.get("America/New_York")
=> #<TZInfo::DataTimezone: America/New_York>
irb(main):008:0> utc = tz.local_to_utc(t)
=> 2000-01-01 19:20:00 UTC
irb(main):009:0> tz.utc_to_local(utc)
=> 2000-01-01 14:20:00 -0500
irb(main):010:0> 

local_to_utc not doing the opposite of utc_to_local might look like a bug but it is at least documented: https://github.com/tzinfo/tzinfo says:

The offset of the time is ignored - it is treated as if it were a local time for the time zone

Martin Dorey
  • 2,944
  • 2
  • 24
  • 16
2

I managed to do this by calling change with the desired time zone:

>> t = Time.current.in_time_zone('America/New_York')
=> Mon, 08 Aug 2022 12:04:36.934007000 EDT -04:00
>> t.change(zone: 'Etc/UTC')
=> Mon, 08 Aug 2022 12:04:36.934007000 UTC +00:00

https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-change

Roy
  • 648
  • 9
  • 17
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

Quick little function I came up with to solve the job. If someone has a more efficient way of doing this please post it!

Eric Walsh
  • 3,035
  • 1
  • 16
  • 21
1

I spent significant time struggling with TimeZones as well, and after tinkering with Ruby 1.9.3 realized that you don't need to convert to a named timezone symbol before converting:

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

What this implies is that you can focus on getting the appropriate time setup first in the region you want, the way you would think about it (at least in my head I partition it this way), and then convert at the end to the zone you want to verify your business logic with.

This also works for Ruby 2.3.1.

Torrey Payne
  • 107
  • 5
0

I have created few helper methods one of which just does the same thing as is asked by the original author of the post at Ruby / Rails - Change the timezone of a Time, without changing the value.

Also I have documented few peculiarities I observed and also these helpers contains methods to completely ignore automatic day-light savings applicable while time-conversions which is not available out-of-the-box in Rails framework:

  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

Examples which can be tried on rails console or a ruby script after wrapping the above methods in a class or 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

Hope this helps.

Community
  • 1
  • 1
Jignesh Gohel
  • 6,236
  • 6
  • 53
  • 89
0

This worked well for me

date = '23/11/2020'
time = '08:00'
h, m = time.split(':')
timezone = 'Europe/London'

date.to_datetime.in_time_zone(timezone).change(hour: h, min: m)
Aryeh Beitz
  • 1,974
  • 1
  • 22
  • 23
0

This changes the timezone to 'EST' without changing the time:

time = DateTime.current

Time.find_zone("EST").local(
  time.year,
  time.month,
  time.day,
  time.hour,
  time.min,
  time.sec,
)