How do I calculate the offset, in hours, of a given timezone from UTC in ruby? - ruby

I need to calculate the offset, in hours, of a given timezone from UTC in Ruby. This line of code had been working for me, or so I thought:
offset_in_hours = (TZInfo::Timezone.get(self.timezone).current_period.offset.utc_offset).to_f / 3600.0
But, it turns out that was returning to me the Standard Offset, not the DST offset. So for example, assume
self.timezone = "America/New_York"
If I run the above line, offset_in_hours = -5, not -4 as it should, given that the date today is April 1, 2012.
Can anyone advise me how to calculate offset_in_hours from UTC given a valid string TimeZone in Ruby that accounts for both standard time and daylight savings?
Thanks!
Update
Here is some output from IRB. Note that New York is 4 hours behind UTC, not 5, because of daylight savings:
>> require 'tzinfo'
=> false
>> timezone = "America/New_York"
=> "America/New_York"
>> offset_in_hours = TZInfo::Timezone.get(timezone).current_period.utc_offset / (60*60)
=> -5
>>
This suggests that there is a bug in TZInfo or it is not dst-aware
Update 2
Per joelparkerhender's comments, the bug in the above code is that I was using utc_offset, not utc_total_offset.
Thus, per my original question, the correct line of code is:
offset_in_hours = (TZInfo::Timezone.get(self.timezone).current_period.offset.utc_total_offset).to_f / 3600.0

Yes, use TZInfo like this:
require 'tzinfo'
tz = TZInfo::Timezone.get('America/Los_Angeles')
To get the current period:
current = tz.current_period
To find out if daylight savings time is active:
current.dst?
#=> true
To get the base offset of the timezone from UTC in seconds:
current.utc_offset
#=> -28800 which is -8 hours; this does NOT include daylight savings
To get the daylight savings offset from standard time:
current.std_offset
#=> 3600 which is 1 hour; this is because right now we're in daylight savings
To get the total offset from UTC:
current.utc_total_offset
#=> -25200 which is -7 hours
The total offset from UTC is equal to utc_offset + std_offset.
This is the offset from the local time where daylight savings is in effect, in seconds.

Related

Get UTC offset for Timezone at given date via Ruby/tzinfo?

This is probably trivial for anybody who knows the tzinfo API:
Given a Timezone object from tzinfo, how can I get the UTC offset at a given point in time (given either in local time of the Timezone or UTC)?
You can use the period_for_local method. For these examples, I'm using the timezone I live in (America/Sao_Paulo), in where the offset is -03:00 during winter (March to October) and -02:00 during summer (Daylight Saving Time):
# Sao Paulo timezone
zone = TZInfo::Timezone.new('America/Sao_Paulo')
# date in January (Brazilia Summer Time - DST)
d = DateTime.new(2017, 1, 1, 10, 0)
period = zone.period_for_local(d)
puts period.offset.utc_total_offset / 3600.0
# date in July (Brazilia Standard Time - not in DST)
d = DateTime.new(2017, 7, 1, 10, 0)
period = zone.period_for_local(d)
puts period.offset.utc_total_offset / 3600.0
The output is:
-2.0
-3.0
The utc_total_offset method returns the offset in seconds, so I divided by 3600 to get the value in hours.
Note that I also used 3600.0 to force the results to be a float. If I just use 3600, the results will be rounded and timezones like Asia/Kolkata (which has an offset of +05:30) will give incorrect results (5 instead of 5.5).
Note that you must be aware of DST changes, because you can have either a gap or a overlap.
In São Paulo timezone, DST starts at October 15th 2017: at midnight, clocks shift forward to 1 AM (and offset changes from -03:00 to -02:00), so all the local times between 00:00 and 01:00 are not valid. In this case, if you try to get the offset, it will get a PeriodNotFound error:
# DST starts at October 15th, clocks shift from midnight to 1 AM
d = DateTime.new(2017, 10, 15, 0, 30)
period = zone.period_for_local(d) # error: TZInfo::PeriodNotFound
When DST ends, at February 18th 2018, at midnight clocks shift back to 11 PM of 17th (and offset changes from -02:00 to -03:00), so the local times between 11 PM and midnight exist twice (in both offsets).
In this case, you must specify which one you want (by setting the second parameter of period_for_local), indicating if you want the offset for DST or not:
# DST ends at February 18th, clocks shift from midnight to 11 PM of 17th
d = DateTime.new(2018, 2, 17, 23, 30)
period = zone.period_for_local(d, true) # get DST offset
puts period.offset.utc_total_offset / 3600.0 # -2.0
period = zone.period_for_local(d, false) # get non-DST offset
puts period.offset.utc_total_offset / 3600.0 # -3.0
If you don't specify the second parameter, you'll get a TZInfo::AmbiguousTime error:
# error: TZInfo::AmbiguousTime (local time exists twice due do DST overlap)
period = zone.period_for_local(d)
It seems in Ruby 1.9.3 there is some hackery (DateTime to Time) involved, with possible loss of precision, but this is my result based on the answer from #Hugo:
module TZInfo
class Timezone
def utc_to_local_zone(dateTime)
return dateTime.to_time.getlocal(self.period_for_utc(dateTime).utc_total_offset)
end
def offset_to_s(dateTime, format = "%z")
return utc_to_local_zone(dateTime).strftime(format)
end
end
end

ZonedDateTime would use same timezone of winter when summer daylight saving?

ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
ZoneId.of("America/Sao_Paulo"));
System.out.println(zdt); // 2015-10-18T01:30-02:00[America/Sao_Paulo]
You can see the hour is 1 while we set the hour as 0, and timezone is UTC-02:00 while daylight saving timezone should be UTC-03:00.
But here is a different example:
ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
ZoneId.of("America/Los_Angeles"));
System.out.println(zdt); //2015-10-18T00:30-07:00[America/Los_Angeles]
You can see the daylight saving timezone is UTC-07:00 and the hour is 0 as we set.
Why are they different?
This happens because the time you picked falls in the gap between midnight and 01:00 on the night when Brazil switches to summer time. That time is actually impossible and so you get the behavior described in the documentation:
In the case of a gap, when clocks jump forward, there is no valid offset. Instead, the local date-time is adjusted to be later by the length of the gap. For a typical one hour daylight savings change, the local date-time will be moved one hour later into the offset typically corresponding to "summer".
You can observe the same behavior in Los_Angeles zone by picking a time between 02:00 and 03:00 on the corresponding night in March:
zdt = ZonedDateTime.of(2015, 3, 8, 2, 30, 0, 0,
ZoneId.of("America/Los_Angeles"));
System.out.println(zdt);
As already explained in #Misha's answer, it happens due to Daylight Saving Time rules.
In São Paulo, DST starts at the midnight of 2015-10-18: the clocks move forward 1 hour, so it "jumps" from 23:59:59 to 01:00:00. There's a gap between 00:00:00 and 00:59:59, so the time 00:30 is adjusted accordingly.
You can check if the date and time are valid for the timezone using the ZoneRules and ZoneOffsetTransition classes:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();
// check if 2015-10-18 00:30 is valid for this timezone
LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30);
List<ZoneOffset> validOffsets = rules.getValidOffsets(dt);
System.out.println(validOffsets.size()); // size is zero, no valid offsets at 00:30
The getValidOffsets method returns all the valid offsets for the specified date/time. If the list is empty, it means the date/time doesn't "exist" in that timezone (usually because of DST the clocks jump forward).
When the date/time exists in a timezone, an offset is returned:
ZoneId la = ZoneId.of("America/Los_Angeles");
rules = la.getRules();
validOffsets = rules.getValidOffsets(dt);
System.out.println(validOffsets.size()); // 1 - date/time valid for this timezone
System.out.println(validOffsets.get(0)); // -07:00
For Los_Angeles timezone, 1 valid offset is returned: -07:00.
PS: Offset changes usually occur due to DST, but that's not always the case. DST and offsets are defined by governments and laws, and they can change at anytime. So, a gap in the valid offset can also mean that such change occured (some politician decided to change the standard offset of the country, so the gap might not necessarily be related to DST).
You can also check when the change occurs, and what's the offset before and after it:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();
// get the previous transition (the last one that occurred before 2015-10-18 00:30 in Sao_Paulo timezone
ZoneOffsetTransition t = rules.previousTransition(dt.atZone(sp).toInstant());
System.out.println(t);
The output is:
Transition[Gap at 2015-10-18T00:00-03:00 to -02:00]
It means that there's a gap (clocks moving forward) at 2015-10-18T00:00, and the offset will change from -03:00 to -02:00 (so, clock moves 1 hour forward).
You can also get all these info separately:
System.out.println(t.getDateTimeBefore() + " -> " + t.getDateTimeAfter());
System.out.println(t.getOffsetBefore() + " -> " + t.getOffsetAfter());
The output is:
2015-10-18T00:00 -> 2015-10-18T01:00
-03:00 -> -02:00
It shows that, at 00:00 the clock moves directly to 01:00 (so 00:30 can't exist). In the second line, the offsets before and after the change.
If you check the transitions in Los_Angeles timezone, you'll see that its DST starts and ends at different dates:
ZoneId la = ZoneId.of("America/Los_Angeles");
rules = la.getRules();
// 2015-10-18 00:30 in Los Angeles
Instant instant = dt.atZone(la).toInstant();
System.out.println(rules.previousTransition(instant));
System.out.println(rules.nextTransition(instant));
The output is:
Transition[Gap at 2015-03-08T02:00-08:00 to -07:00]
Transition[Overlap at 2015-11-01T02:00-07:00 to -08:00]
So, in Los_Angeles timezone, DST starts at 2015-03-08 and ends at 2015-11-01. That's why at 2015-10-18, all hours are valid (there's no adjustment as it happens in Sao_Paulo timezone).
Some timezones have transition rules (like "DST starts at the third Sunday of October") instead of just transitions (like "DST starts at this specific date and time"), and you can also use them, if available:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();
// hardcoded: Sao_Paulo timezone has 2 transition rules, the second one is relative to October
// but you should always check if the list is not empty
ZoneOffsetTransitionRule tr = rules.getTransitionRules().get(1);
// get the transition for year 2015
ZoneOffsetTransition t = tr.createTransition(2015);
// use t the same way as above (the output will be the same)
Another way to check if a date and time is valid for some timezone is to use the ZonedDateTime.ofStrict method, that throws an exception if the date and time is invalid for a timezone:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneId la = ZoneId.of("America/Los_Angeles");
LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30);
System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-7), la)); // OK
System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-3), sp)); // throws java.time.DateTimeException
The first case is OK, because an offset of -7 is valid for Los Angeles, for the given date/time. The second case throws an exception because an offset of -3 is invalid for São Paulo, at the given date/time.

How to interpret RFC3339 UTC timestamp

How should I interpret all aspects of the following timestamps? Where is the time based and how do timezones apply?
2015-11-15T14:45:28Z
2015-11-15T14:45:28.9694Z
2015-11-15T14:45:28.969412345Z
Below is my thoughts...
Date: 2015-11-15
???: T
Hours: 14
Minutes: 45
Seconds: 28 OR 28.9694 OR 28.969412345
???: Z
Most of your values are attributed correctly. The date portion (2015-11-15) is in the order YYYY-MM-DD, time in HH:MM:SS.ffff.
T indicates the start of the time portion of the date time.
Z indicates the time zone is UTC. Next to Z, you could have a format like Z+02:00, which indicates the time zone is UTC + 2 hours.

compare times with milliseconds

I have two dates a start date and an end date. I want to get a new time object which is the difference between the two. The differences I am concerned with are Hours, Minutes, Seconds and Milliseconds. I need to be able to create a new Time object from the result that includes the milliseconds difference
>> require 'time'
=> true
>> start_time = Time.parse '1970-01-01T00:00:00.200'
=> 1970-01-01 00:00:00 +0000
>> end_time = Time.parse '1970-01-01T01:01:01.400'
=> 1970-01-01 01:01:01 +0000
>> difference = Time.at(end_time - start_time)
=> 1970-01-01 01:01:01 +0000
my problem is that difference does not have the milliseconds
I can see that the Time has milliseconds by running
>> difference.strftime('%H:%M:%S.%L')
=> "01:01:01.199"
but how do I access the milliseconds that are in the Time difference object.
it is critical I have milliseconds as I am working in sub-second calculations?
UPDATE
I don't think my first attempt at this question was as descriptive as it should of been, my apologies for that.
require 'time'
a = Time.now
sleep(0.5)
b = Time.now
b - a
# => 0.505087
Milliseconds!
EDIT: Microseconds!
my problem is that difference does not have the milliseconds
It does have the milliseconds, Time#to_s / Time#inspect just doesn't show it. Its output is equivalent to: strftime "%Y-%m-%d %H:%M:%S %z"
how do I access the milliseconds that are in the Time difference object.
usec returns the microseconds and nsec returns the nanoseconds:
time = Time.at(0.2)
time.usec #=> 200000
time.nsec #=> 200000000
For milliseconds you could use
time.usec / 1000 #=> 200
Ruby's Time class has nanosecond precision: you can use Time#to_f to get a fractional number of seconds since the Unix epoch. If you subtract two Time objects, you'll get a fractional number of seconds between them. Thus, to get the number of milliseconds between two times, try:
((time2 - time1) * 1000).to_i

How do you convert UTC time to EST in ruby (not using Rails)?

I am capturing the current time like so:
Time.now
My server runs on UTC. How can I convert the time to EST without using any Rails libraries? I am guessing some sort of offset but not sure how it works per say.
In plain Ruby you may use Time.zone_offset method:
require 'time'
t = Time.now # 2014-07-30 18:30:00 UTC
t + Time.zone_offset('EST') # 2014-07-30 13:30:00 UTC
The fbonetti's answer leads to the proper UTC to Eastern time conversion while accepted David Unric's answer would give wrong time for 8 months in 2017 (while DST is in effect).
Let's look at the following example:
First we'll need to figure out when DST starts/ends in 2017:
As we can see on March 12th, 2017 deep in the night (2:00am) they change time by adding +1 hour, so they "jump" from 1:59:59am up to 3:00:00am instantaneously! Which means there can not be 2:30am on March 12th, 2017.
Let's choose two UTC timestamps - one before and one after that switch, then we will try to convert those two timestamps from UTC back to Eastern.
First timestamp will be safely far enough from the switch moment:
require 'time'
t1 = Time.parse("2017-03-11 15:00:00 +0000")
=> 2017-03-11 15:00:00 +0000
t1_epoch_s = t1.to_i
=> 1489244400
Second timestamp is just +24 hours from the first one:
t2 = Time.parse("2017-03-12 15:00:00 +0000")
=> 2017-03-12 15:00:00 +0000
t2_epoch_s = t2.to_i
=> 1489330800
Now let us convert t1_epoch_s and t2_epoch_s to Eastern:
method-1: by adding Time.zone_offset('EST')
wrong, gives bad result: 10am for both days :(
and offset portion is shown as "+0000" which is also misleading and would refer to completely wrong point in time for people reading our output : ((
Time.at(t1_epoch_s) + Time.zone_offset('EST')
=> 2017-03-11 10:00:00 +0000
Time.at(t2_epoch_s) + Time.zone_offset('EST')
=> 2017-03-12 10:00:00 +0000
method-2: by changing timezone
Good!! Correctly yields 10am and 11am on next day!-)
ENV['TZ'] = 'America/New_York'
Time.at(t1_epoch_s)
=> 2017-03-11 10:00:00 -0500
Time.at(t2_epoch_s)
=> 2017-03-12 11:00:00 -0400
# resetting timezone back
ENV['TZ'] = nil
Basically manually adding Time.zone_offset('EST') is like adding constant and it will give right result for about 4 months (of 12 total) during the year, but then other time you'd have to manually add Time.zone_offset('EDT'), which is another constant. It pretty much same as "a broken clock is right twice a day": )) nasty!
And just for laughter let's see the "slow mo" how proper method handles the actual +1 hour magic jump in time:
ENV['TZ'] = "America/New_York"
Time.at(1489301999 + 0)
=> 2017-03-12 01:59:59 -0500
Time.at(1489301999 + 1)
=> 2017-03-12 03:00:00 -0400
ENV['TZ'] = nil
magic-magic!
In plain ruby, the timezone is determined by the 'TZ' environment variable. You could do something like this:
ENV['TZ'] = 'America/New_York' # set the TZ to Eastern Daylight Time
time = Time.now
time.zone
# => "EDT"
# do stuff
ENV['TZ'] = nil # reset the TZ back to UTC
If you don't mind using a gem,
require 'tzinfo'
tz = TZInfo::Timezone.get('US/Eastern')
Time.now.getlocal(tz.current_period.offset.utc_total_offset)
Credit: https://stackoverflow.com/a/42702906/2441263

Resources