Ruby Comparing Time objects considers date. How to avoid that? - ruby

I have a model with a Time attribute
create_table "opened_intervals", force: :cascade do |t|
t.time "start"
t.time "end"
An example of a value would be:
>> oi.start
=> Sat, 01 Jan 2000 06:26:00 UTC +00:00
(I am living in Germany)
If I use the current time, I get following value:
current_time = Time.now
>> current_time
=> 2019-02-14 18:36:12 +0100
If I compare the class of both objects, I have
>> current_time.class
=> Time
>> oi.start.class
=> ActiveSupport::TimeWithZone
In order to make both instances same class, I change the Time class with the .zone method
>> Time.zone.now
=> Thu, 14 Feb 2019 17:38:48 UTC +00:00
>> Time.zone.now.class
=> ActiveSupport::TimeWithZone
Now both instances have same Class.
If I compare them, I get wrong results because of the date:
>> oi.start
=> Sat, 01 Jan 2000 06:26:00 UTC +00:00
>> current_time
=> Thu, 14 Feb 2019 17:40:13 UTC +00:00
>> current_time < oi.end
=> false
I thought about creating a new Time with the hours, minutes and seconds, but then I have the same problem, Ruby always appends a Date.
Of course I could extract the hour, the minutes, the time, create an integer and compare them, but it feels too much.
How can I deal with this issue the Ruby way?
The Time object is always a time with a date? Is there no other way to achieve a Time just with the time?
What would be the best approach for this problem?
I solved it this way:
def to_integer(time)
hours = time.hour < 10 ? "0#{time.hour}" : time.hour
minutes = time.min < 10 ? "0#{time.min}" : time.min
seconds = time.sec < 10 ? "0#{time.sec}" : time.sec
"#{hours}#{minutes}#{seconds}"
end
It feels too much (I could refactor it, but still too much)
to_integer(current_time) < to_integer(oi.end)

You can just modulus the days off to get the remaining time value.
t1 = Time.now
t2 = t2 = Time.at(Time.now.to_i - (5*24*3600)) # later time in day but previous date
tod1 = t.to_i % (24*3600)
=> 69734
tod2 = t2.to_i % (24*3600)
=> 69912
we can clearly see that t2 is a later time of day or clock time if you will and the modulus operation is very clear if you know anything about the unix epoch.

Related

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

inscrutable behaviour of date in ruby

I have a variable which has some future date(for example I have taken 30 days after date here), I want to have a conditional statement based on if this future date is 10 days advanced, I was trying following code, but it doesn't work, what am I doing wrong here?
> future_date = Date.today + 30.day
=> Wed, 16 Jul 2014
> future_date - Date.today > 10.day
=> false # Shouldn't this has been true
future_date - Date.today will always give you an answer in days (30)
You can see this by
future_date - Date.today
=> 30
So you can do...
future_date - Date.today > 10
And that will work fine.
10.days converts the interval into seconds: 864_000 (number of seconds in ten days) and 30 is not greater than 864_000!

What's the method for converting time from ms to days?

today = Time.now
=> 2014-01-12 22:24:39 -0500
future = Time.parse("01-03-2014")
=> 2014-03-01 00:00:00 -0500
"%2.f" % ((future - today)/60/60/24)
=> 47
How else can I get 47 days without having to do /60/60/24 ?
You could compare two dates.
today = Date.today
future = Date.parse("01-03-2014")
days_diff = (future - today).to_i

Rails Time Zone and cron scheduling

I have an AWS server that runs daily cron jobs reporting on our user base. I want to ensure my report is run for the full day the previous day in MST. Currently I use this as the code for the data quering
Time.new(Time.now.year, Time.now.month, Time.now.day).yesterday.beginning_of_day.in_time_zone('MST)..Time.new(Time.now.year, Time.now.month, Time.now.day).yesterday.end_of_day.in_time_zone('MST)
I read it is bad practice to use Time.now as that is the system (UTC) time? I am wondering if what I am doing is a big no no or if there is a more efficient way?
thank you!
Mountain Standard Time is 7 hours behind UTC, so when you capture all the data points from the day of July 22rd in MST, you want the UTC times to be from 7/22 at 7:00AM UTC to 7/23 at 7:00AM UTC.
I don't think your code is correct because you are calling in_time_zone("MST") after beginning_of_day.
When you run this code on a server that is on UTC, the evaluated times are different:
>> Time.new.yesterday.beginning_of_day.in_time_zone('MST').utc
=> 2013-07-22 00:00:00 UTC
>> Time.new.in_time_zone("MST").yesterday.beginning_of_day.utc
=> 2013-07-22 07:00:00 UTC
Here is how you can determine the start and end times properly:
>> t = Time.new
=> 2013-07-23 19:45:10 +0000
>> start_time = t.in_time_zone("MST").yesterday.beginning_of_day
=> Mon, 22 Jul 2013 00:00:00 MST -07:00
>> end_time = t.in_time_zone("MST").yesterday.end_of_day
=> Mon, 22 Jul 2013 23:59:59 MST -07:00
When we convert the start and end times to UTC, we get the desired result.
>> start_time = t.in_time_zone("MST").yesterday.beginning_of_day.utc
=> 2013-07-22 07:00:00 UTC
>> end_time = t.in_time_zone("MST").yesterday.end_of_day.utc
=> 2013-07-23 06:59:59 UTC
I don't know what you are trying to do, but
Time.new(Time.now.year, Time.now.month, Time.now.day)
is definitely a terrible code fragment. For example, if the time lag between the execution time of Time.now.year and that of Time.now.month overlaps the moment of the change of the year, then the time object created with the main Time.new will be neither of the two moments. If you want to get the current time, just do
Time.new
or
Time.now
If you are trying to create a time range calculated out of a single time, then whatever your code should be, create time only once:
t = Time.now
and use that in the rest of your code:
t.some_method..t.some_other_method

Ruby: combine Date and Time objects into a DateTime

Simple question, but I can't find a good or definitive answer. What is the best and most efficient way to combine Ruby Date and Time objects (objects, not strings) into a single DateTime object?
I found this, but it's not as elegant you would hope:
d = Date.new(2012, 8, 29)
t = Time.now
dt = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
By the way, the ruby Time object also stores a year, month, and day, so you would be throwing that away when you create the DateTime.
When using seconds_since_midnight, changes in daylight savings time can lead to unexpected results.
Time.zone = 'America/Chicago'
t = Time.zone.parse('07:00').seconds_since_midnight.seconds
d1 = Time.zone.parse('2016-11-06').to_date # Fall back
d2 = Time.zone.parse('2016-11-07').to_date # Normal day
d3 = Time.zone.parse('2017-03-12').to_date # Spring forward
d1 + t
#=> Sun, 06 Nov 2016 06:00:00 CST -06:00
d2 + t
#=> Mon, 07 Nov 2016 07:00:00 CST -06:00
d3 + t
#=> Sun, 12 Mar 2017 08:00:00 CDT -05:00
Here's an alternative, similar to #selva-raj's answer above, using string interpolation, strftime, and parse. %F is equal to %Y-%m-%d and %T is equal to %H:%M:%S.
Time.zone = 'America/Chicago'
t = Time.zone.parse('07:00')
d1 = Time.zone.parse('2016-11-06').to_date # Fall back
d2 = Time.zone.parse('2016-11-07').to_date # Normal day
d3 = Time.zone.parse('2017-03-12').to_date # Spring forward
Time.zone.parse("#{d1.strftime('%F')} #{t.strftime('%T')}")
#=> Sun, 06 Nov 2016 07:00:00 CST -06:00
Time.zone.parse("#{d2.strftime('%F')} #{t.strftime('%T')}")
#=> Sun, 07 Nov 2016 07:00:00 CST -06:00
Time.zone.parse("#{d3.strftime('%F')} #{t.strftime('%T')}")
#=> Sun, 12 Mar 2017 07:00:00 CDT -05:00
Simple:
Date.new(2015, 2, 10).to_datetime + Time.parse("16:30").seconds_since_midnight.seconds
# => Object: Tue, 10 Feb 2015 16:30:00 +0000
You gotta love Ruby!
If using Rails, try any of these:
d = Date.new(2014, 3, 1)
t = Time.parse("16:30")
dt = d + t.seconds_since_midnight.seconds
# => ActiveSupport::TimeWithZone
dt = (d + t.seconds_since_midnight.seconds).to_datetime
# => DateTime
dt = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec)
# => DateTime
If you are using Ruby on Rails, this works great.
I built a method to extend the DateTime class to combine a date and a time. It takes the zone from the date so that it does not end up an hour off with daylight savings time.
Also, for convenience, I like being able to pass in strings as well.
class DateTime
def self.combine(d, t)
# pass in a date and time or strings
d = Date.parse(d) if d.is_a? String
t = Time.zone.parse(t) if t.is_a? String
# + 12 hours to make sure we are in the right zone
# (eg. PST and PDT switch at 2am)
zone = (Time.zone.parse(d.strftime("%Y-%m-%d")) + 12.hours ).zone
new(d.year, d.month, d.day, t.hour, t.min, t.sec, zone)
end
end
So you can do:
DateTime.combine(3.weeks.ago, "9am")
or
DateTime.combine("2015-3-26", Time.current)
etc...
I found another way, I hope this is correct.
datetojoin=Time.parse(datetime).strftime("%Y-%m-%d")
timetojoin=Time.parse(time).strftime("%T")
joined_datetime = Time.parse(datetojoin +" "+ timetojoin).strftime("%F %T")
Any thoughts? Please share.

Resources