The goal is to compare timestamp to a range of times.
The range of times were defined in the rails schema as t.time for postgresql database. However the data returned upon querying the console attributes a date to the record's field...
start_time: "2000-01-01 08:00:00"
end_time: "2000-01-01 17:59:59"
Now if I want to validate whether a record created_at: "2017-03-18 03:44:04" is in the time range, I am also comparing the date, which is throwing the query into empty-array-land.
What rails or ruby tools can be used in this case in a database-agnostic manner?
def within_time_range?(start_time, end_time, time_check)
t = time_check[11..-1]
t >= start_time[11..-1] && t <= end_time[11..-1]
end
start_time = "2000-01-01 08:00:00"
end_time = "2000-01-01 17:59:59"
within_time_range?(start_time, end_time, "2017-03-18 03:44:04")
#=> false
within_time_range?(start_time, end_time, "2017-03-18 09:05:01")
#=> true
within_time_range?(start_time, end_time, "2017-03-18 19:05:01")
#=> false
Note
start_time[11..-1]
#=> "08:00:00"
I've used String#<= and String#>=, which are obtained from String#<=> and the inclusion of the module Comparable into the String class.
You could force every time object to be parsed at the same date (e.g. 1st of january 2000) :
require 'time'
def parse_time_not_date(string)
time = Time.parse(string)
Time.local(2000, 1, 1, time.hour, time.min, time.sec, time.usec)
end
start_time = parse_time_not_date("2000-01-01 08:00:00")
end_time = parse_time_not_date("2000-01-01 17:59:59")
my_time = parse_time_not_date("2017-03-18 03:44:04")
puts (start_time..end_time).cover?(my_time)
# false
puts (start_time..end_time).cover?(parse_time_not_date("2017-03-18 14:59"))
# true
I am going to chime in, as the discussion from the first answer was pertinent. Cary's answer works for that case. The question was ambivalent in terms of possible solutions: ruby or rails. And hence some potential gum ups. What follows is another way.
With rails, there is an issue in considering time zones. Calling an object of data type time actually stays in UTC. With date_time you are getting a string with + or - hours(or fraction thereof - yay Newfoundland!)
So, processing with rails, the proper way to handle UTC data is to assign it the rails application time zone with in_time_zone, chain to string and then extracting. Thus, comparisons ended up as:
p = Time.parse(#interruptions[0].pause.to_s[11,8])
p >= Time.parse(d_s.start_time.in_time_zone.to_s[11,8]) && p <= Time.parse(d_s.end_time.in_time_zone.to_s[11,8])
Note: could not get [11..-1] working in this context
Related
When Rails creates an active record and inserts it, is the created_at value practically the same as Time.now.utc.to_date?
In most cases yes, but it depends on the default timezone configuration option.
ActiveRecord::Timestamp code:
def current_time_from_proper_timezone
default_timezone == :utc ? Time.now.utc : Time.now
end
You can change timezone setting in:
config.active_record.time_zone_aware_attributes = false
If you meant to_date, then no, in the worst case it could be nearly 24 hours off.
If you meant to_datetime, then I believe it will be the same to the second. But note that if you call Time.now immediately before or after creating a record it may not match to the second. I'm curious to know why you need to convert to a DateTime though.
Just test it yourself (let's say your AR class is Post):
dtm_before = Time.now.to_datetime
post = Post.create!(attributes)
dtm_after = Time.now.to_datetime # zone does not matter!
# these differences should be tiny
dtm_before.to_time - post.created_at
dtm_after.to_time - post.created_at
I said the zone doesn't matter because when you're doing time arithmetic, zones are automatically taken into account. Example:
# let's assume your local TZ-offset isn't zero
t = Time.now
t == t.getutc # true
t - t.getutc # 0.0 (these are the exact same instant)
Why do I get different outcomes in these two queries?
Query 1 = false (creating array of shift IDs then checking if shift ID is in array):
shifts = Shift.where('date_time BETWEEN ? AND ?', Time.zone.now - 3.hours, Time.zone.now + 24.hours).pluck(:id)
shifts.include?(16923) # false
Query 2 = true (checking if shift date_time is between a time range):
Shift.find(16923).date_time.between?(Time.zone.now - 3.hours, Time.zone.now + 24.hours) # true
The attribute date_time is a string, can that have anything to do with it?
At the time of writing, here are the current results:
Shift.find(16923).date_time = "2018-09-01T07:45:00+00:00"
Time.zone.now - 3.hours = Fri, 31 Aug 2018 13:34:07 BST +01:00
Time.zone.now + 24.hours = Sat, 01 Sep 2018 16:35:22 BST +01:00
Looks to me that it's between those two ranges, so why is it not included in the first array?
date_time is a String or a DateTime object? that may be the problem.
Because both String and Datetime objects accept a between? method, both inherited at the end from Comparable https://apidock.com/ruby/Comparable/between%3F
A String will be compared alfabethically
'11'.between?('0','2') # => true
A DateTime object will call between? on a Time objects which is represented as an integer and it's compared ordinally
11.between?(0,2) # => false
If date_time is a String, I'd suggest to change it to a DATETIME column type, it makes more sense semantically. If you can't change it, you can add a custom getter to return a DateTime object parsing the string.
def date_time
DateTime.parse(self[:date_time])
end
I'm not that familliar with SQL operators and it's optimizations, but I'm guessing it is comparring both ordinally, that's would explain why the query works and not the between? method. Still I can't confirm this, I'm just guessing (maybe it even depends on the implementation and the table engine, I don't know).
I know there is a method to determine if a certain time is on Daylight Savings Time (Time.now.dst?) but is there a method to give us the next date when Daylight Savings will change?
For example, Google returns Sunday, November 1 as the next Daylight Savings Time change in 2015.
Since these are dates that are based on other values, like the timezone you are working with, it requires a module like ActiveSupport/TZInfo.
require 'active_support/core_ext/time/zones'
tz = TZInfo::Timezone.get('US/Pacific')
# pick a timezone to work with
tz.current_period #returns an object of the current period
=> #<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition:
#<TZInfo::TimeOrDateTime: 1425808800>,#<TZInfo::TimezoneOffset: -28800,3600,PDT>>,
#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1446368400>,
#<TZInfo::TimezoneOffset: -28800,0,PST>>>
tz.current_period.local_start.to_s
# => "2015-03-08T03:00:00+00:00"
tz.current_period.local_end.to_s
# => "2015-11-01T02:00:00+00:00"
One thing I haven't figured out is that since regular Ruby Core does this:
Time.now.dst?
# => true
Where is it getting this info? I found the TZInfo classes through ActiveSupport. Is Ruby just getting a boolean value from the OS?
How about this extension of the Time class:
class Time
class << self
def next_dst_change
startdate = Date.today
enddate = Date.today.end_of_year
match = Date.today.to_time.dst? ? false : true
startdate.upto(enddate).find { |date| date.to_time if date.to_time.dst? == match }
end
end
end
Now you can do Time.next_dst_change. You can apply this on your own timezone only but it solves your problem.
I'm trying to extract the time component from a DateTime object (which is represented as "at" in my example). How do I do this, I am absolutely stumped? (I don't want to parse it to a string with strftime as i did here):
#session_date.at.strftime("%H:%M")
I would really like to return the hours and minutes as a Time object.
Is there a specific reason you want a Time object?
Just so we're clear, the Time class in Ruby isn't just "DateTime without the date." As "What's the difference between DateTime and Time in Ruby?" explains, "Time is a wrapper around POSIX-standard time_t, or seconds since January 1, 1970." Like DateTime, a Time object still has year, month, and day, so you don't really gain anything by using Time instead. There's not really a way to represent just hour and minute using either Time or DateTime.
The best you could do with Time, I think, would be this:
date_time = DateTime.now
seconds = date_time.hour * 60 * 60 + date_time.minute * 60
time = Time.at(seconds)
# => 1970-01-01 09:58
...but then you still have to call time.hour and time.min to get at the hour and minute.
If you're just looking for a lightweight data structure to represent an hour and minute pair, though, you might as well just roll your own:
HourAndMinute = Struct.new(:hour, :minute) do
def self.from_datetime(date_time)
new(date_time.hour, date_time.minute)
end
end
hm = HourAndMinute.from_datetime(DateTime.now)
# => #<struct HourAndMinute hour=15, minute=58>
hm.to_h
# => { :hour => 15, :minute => 58 }
hm.to_a
# => [ 15, 58 ]
Edit re:
I have a variable that stores an appointment -- this variable is a DateTime object. I have two table fields that store the start and end times of a location. I need to check if the time scheduled for that appointment lies between the start and end times.
Ah, it seems you had a bit of a XY problem. This makes a lot more sense now.
Absent any more information, I'm going to assume your "fields that store the start and end times of a location" are MySQL TIME columns called start_time and end_time. Given MySQL TIME columns, Rails casts the values to Time objects with the date component set to 1/1/2000. So if your database has the values start_time = '09:00' and end_time = '17:00', Rails will give you Time objects like this:
start_time = Time.new(2000, 1, 1, 9, 0) # => 2000-01-01 09:00:00 ...
end_time = Time.new(2000, 1, 1, 17, 0) # => 2000-01-01 17:00:00 ...
Now you say your appointment time is a DateTime, so let's call it appointment_datetime and suppose it's at 10:30am tomorrow:
appointment_datetime = DateTime.new(2014, 11, 18, 10, 30) # => 2014-11-18 10:30:00 ...
So now to rephrase your question: How do we tell if the time part of appointment_datetime is between the time part of start_time and end_time. The answer is, we need to either change the date part of start_time and end_time to match the date part of appointment_datetime, or the other way around. Since it's easier to change one thing than two, let's do it the other way around and change appointment_datetime to match start_time and end_time (and, since those two are Time objects, we'll create a Time object):
appointment_time = DateTime.new(2000, 1, 1, appointment_datetime.hour, appointment_datetime.minute)
# => 2000-01-01 10:30:00 ...
Now we can compare them directly:
if appointment_time >= start_time && appointment_time <= end_time
puts "Appointment time is good!"
end
# Or, more succinctly:
if (start_time..end_time).cover?(appointment_time)
puts "Appointment time is good!"
end
You would, of course, want to wrap all of this up in a method, perhaps in your Location model (which, again, I'm assuming has start_time and end_time attributes):
class Location < ActiveRecord::Base
# ...
def appointment_time_good?(appointment_datetime)
appointment_time = DateTime.new(2000, 1, 1,
appointment_datetime.hour, appointment_datetime.minute)
(start_time..end_time).cover?(appointment_time)
end
end
location = Location.find(12) # => #<Location id: 12, ...>
location.appointment_time_good?(appointment_time) # => true
I hope that's helpful!
P.S. Another way to implement this would be to ditch the date/time objects entirely and do a straight numeric comparison:
def appointment_time_good?(appointment_datetime)
appointment_hour_min = [ appointment_datetime.hour, appointment_datetime.minute ]
appointment_hour_min >= [ start_time.hour, start_time.min ]
&& appointment_hour_min <= [ end_time.hour, end_time.min ]
end
If you have a DateTime object:
date_time = DateTime.now
date_time.hour
# => 16
date_time.minute
# => 1
If you are looking for the Time since now in words (which is common in Rails apps), then this may be a good read: http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-time_ago_in_words
Example I have:
range = start.to_date..(end.to_date + 1.day)
end and start are dates.
How do I create a month array based on this range?
Example:
I have the dates 23/1/2012 and 15/3/2012
The months are Januar, Februar and Marts.
I want to get a array like ["1/1/2012", "1/2/2012", "1/3/2012"]
and if the range was betweeen 25/6/2012 to the 10/10/2012
the array would be: ["1/6/2012", "1/7/2012", "1/8/2012", "1/9/2012", "1/10/2012"]
require 'date'
date_from = Date.parse('2011-10-14')
date_to = Date.parse('2012-04-30')
date_range = date_from..date_to
date_months = date_range.map {|d| Date.new(d.year, d.month, 1) }.uniq
date_months.map {|d| d.strftime "%d/%m/%Y" }
# => ["01/10/2011", "01/11/2011", "01/12/2011", "01/01/2012",
# "01/02/2012", "01/03/2012", "01/04/2012"]
Rails ActiveSupport core extensions includes a method for Date: beginning_of_month. Your function could be written as follows:
def beginning_of_month_date_list(start, finish)
(start.to_date..finish.to_date).map(&:beginning_of_month).uniq.map(&:to_s)
end
Caveats: this could be written more efficiently, assumes start and finish are in the expected order, but otherwise should give you the months you're looking for. You could also rewrite to pass a format symbol to the #to_s method to get the expected month format.
I was curious about performance here so I tested some variations. Here's a solution better optimized for performance (about 8x faster in my benchmark than the accepted solution). By incrementing by a month at a time we can remove the call to uniq which cuts quite a bit of time.
start_date = 1.year.ago.to_date
end_date = Date.today
dates = []
date = start_date.beginning_of_month
while date <= end_date.beginning_of_month
dates << date.to_date.to_s
date += 1.month
end
dates
#=> ["2019-02-01", "2019-03-01", "2019-04-01", "2019-05-01", "2019-06-01", "2019-07-01", "2019-08-01", "2019-09-01", "2019-10-01", "2019-11-01", "2019-12-01", "2020-01-01", "2020-02-01"]
Benchmark Results:
Comparison:
month increment loop: 17788.3 i/s
accepted solution: 2140.1 i/s - 8.31x slower
gist of the benchmark code
Similar to one of the solutions above using beginning_of_month .. but taking less space (by using Set) and is neater for using inject.
(start_month..end_month).inject(Set.new) { |s, i| s << i.beginning_of_month; s }.to_a