How to check if time is in range using Rails - ruby-on-rails

There is the following times:
now = "2014-01-24T15:58:07.169+04:00",
start = "2000-01-01T10:00:00Z",
end = "2000-01-01T16:00:00Z"
I need to check if now is between start and end. I use the following code:
Range.new(start, end).cover?(now)
Unfortunately, this code returns false for my data. What am I doing wrong? How can I fix it? Thanks.

Well, I would use between? method. Because it's faster than cover? and include? variants. Here's an example:
yesterday = Date.yesterday
today = Date.today
tomorrow = Date.tomorrow
today.between?(yesterday, tomorrow) #=> true
Here's a gist with performance tests Include?, Cover? or Between?
Update
According to your recent comment, you want to compare 'only time' without date. If I get you correctly, there's a way to do it - strftime. But before that, to make comparison correctly, you need to convert all your datetimes to a single timezone (for example, using utc). Here's an example:
start_time_with_date = Time.parse('2000-01-01T16:00:00Z').utc
end_time_with_date = Time.parse('2014-01-24T15:58:07.169+04:00').utc
start_time = start_time_with_date.strftime('%I:%M:%S') #=> '04:00:00'
end_time = end_time_with_date.strftime('%I:%M:%S') #=> '11:58:07'
current_time = Time.now.utc.strftime('%I:%M:%S') #=> '01:45:27' (my current time)
current_time.between?(start_time, end_time) #=> false
And yes. Sadly, it's a string comparison.

You can use Range#cover? with time objects.
start = Time.parse('2000-01-01T10:00:00Z')
end_time = Time.parse('2000-01-01T16:00:00Z')
now = Time.parse('2014-01-24T15:58:07.169+04:00')
(start..end_time).cover?(now)
You're currently using strings, Ruby cannot know you're speaking about time.

I see the only variant, to define additional method to Range:
class Range
def time_cover? now
(b,e,n) = [ self.begin.utc.strftime( "%H%M%S%N" ),
self.end.utc.strftime( "%H%M%S%N" ),
now.utc.strftime( "%H%M%S%N" ) ]
if b < e
b <= n && e >= n
else
e <= n && b >= n
end
end
end
now = Time.parse "2014-01-24T15:58:07.169+04:00"
s = Time.parse "2000-01-01T10:00:00Z"
e = Time.parse "2000-01-01T16:00:00Z"
Range.new(s, e).time_cover?(now)
# => true

your date time(now) is not in between start and end time

Related

Finding the months between two dates in rails

I can currently set a time range like so:
start_date: "2018-09-11"
end_date: "2018-11-19"
How can I do this for start to end of months? Examples:
time_range = ["2018-09-11".."2018-09-30"]
time_range = ["2018-10-01".."2018-10-31"]
time_range = ["2018-11-01".."2018-11-19"]
I'm not sure what's exactly your desired outcome but, given start date and end date as Date objects, you can perform
(start_date..end_date).to_a.group_by(&:month).values
and at the end what you get is a three element array, and each element contains an array with all the dates in that range for a month
I do not know if I understand very well what you asked, but I'll try to help you.
The Date class has several methods that will help you to work with dates.
Date < Object
Examples
my_date_range_array = [Date.today.beginning_of_year..Date.today.end_of_year]
my_date_time_range_array = [Time.now.beginning_of_year..Time.now.end_of_year]
my_date_range_array = [6.months.ago..Date.today]
YourModel.where date: Date.today.beginning_of_month..Date.today
YourModel.where date: 6.months.ago..Date.today
If you need every single date in the range, you can use something like this:
(Date.today.beginning_of_year..Date.today.end_of_year).map{ |date| date }
I hope that my answer helps you
This is a pure Ruby solution, but I believe (though I don't know Rails) it can be simplified slightly by replacing my methods first_day_of_month and first_day_of_month with Rails methods beginning_of_month and end_of_month, respectively. I designed the method for efficiency over simplicity.
require 'date'
DATE_FMT = "%Y-%m-%d"
def date_ranges(start_date_str, end_date_str)
start_date = Date.strptime(start_date_str, DATE_FMT)
end_date = Date.strptime(end_date_str, DATE_FMT)
return [start_date_str..end_date_str] if
[start_date.year, start_date.month] == [end_date.year, end_date.month]
d = start_date
ranges = [start_date_str..last_day_of_month(d)]
loop do
d = d >> 1
break if [d.year, d.month] == [end_date.year, end_date.month]
ranges << (first_day_of_month(d)..last_day_of_month(d))
end
ranges << (first_day_of_month(d)..end_date_str)
end
def first_day_of_month(d)
(d - d.day + 1).strftime(DATE_FMT)
end
def last_day_of_month(d)
((d >> 1)-d.day).strftime(DATE_FMT)
end
date_ranges("2018-09-11", "2019-02-11")
#=> ["2018-09-11".."2018-09-30", "2018-10-01".."2018-10-31",
# "2018-11-01".."2018-11-30", "2018-12-01".."2018-12-31",
# "2019-01-01".."2019-01-31", "2019-02-01".."2019-02-11"]
date_ranges("2018-09-08", "2018-09-23")
#=> ["2018-09-08".."2018-09-23"]
With the information provided by the OP, this is what I understand he is looking for.
Given a set range for example:
time_range = "2018-09-11".."2018-09-19"
new_range_min = time_range.min.to_date.beginning_of_month
new_range_max = time_range.max.to_date.end_of_month
new_range = new_range_min..new_range_max

Find out if current time is between two times

I have two time columns stored in a Postgresql database: open_time and close_time. I'm trying to find out if the current time, ignoring the date, is between the two times, ignoring the dates.
This code compares the dates as well as the time:
current_time = Time.now
if current_time.between?(store.open_time, store.close_time)
puts "IN BETWEEN"
end
It doesn't work, for example, when current_time # => 2018-06-06 23:59:49 -0600 and open_time # => 2000-01-01 22:59:00 UTC.
How do I get it to not include the dates, and just compare the times?
require 'time'
TIME_FMT = "%H%M%S"
def store_open_now?(open_time, close_time)
nt = Time.now.strftime(TIME_FMT)
ot = open_time.strftime(TIME_FMT)
ct = close_time.strftime(TIME_FMT)
ot <= ct ? (nt >= ot && nt <= ct) : (nt >= ot || nt <= ct)
end
As I write, the time is now about 32 minutes past midnight.
Time.now.strftime(TIME_FMT)
#=> "003252"
Suppose
open_time = DateTime.parse("09:00")
#=> #<DateTime: 2018-06-07T09:00:00+00:00 ((2458277j,32400s,0n),
# +0s,2299161j)>
close_time = DateTime.parse("17:00")
#=> #<DateTime: 2018-06-07T17:00:00+00:00 ((2458277j,61200s,0n),
# +0s,2299161j)>
Then
open_time.strftime(TIME_FMT)
#=> "090000"
close_time.strftime(TIME_FMT)
#=> "170000"
store_open_now?(open_time, close_time)
#=> false
Now suppose the open time is the same, but the close time is later.
close_time = DateTime.parse("01:00")
#=> #<DateTime: 2018-06-07T01:00:00+00:00 ((2458277j,3600s,0n),
# +0s,2299161j)>
Then
close_time.strftime(TIME_FMT)
#=> "010000"
store_open_now?(open_time, close_time)
#=> true
Perhaps you want something like this:
current_time = Time.now
open_time = store.open_time
close_time = store.close_time
current_time -= current_time.beginning_of_day
open_time -= open_time.beginning_of_day
close_time -= close_time.beginning_of_day
if current_time.between?(open_time, close_time)
puts "IN BETWEEN"
end
or
current_time = Time.now
open_time = store.open_time
close_time = store.close_time
current_time = [current_time.hour, current_time.min, current_time.sec]
open_time = [open_time.hour, open_time.min, open_time.sec]
close_time = [close_time.hour, close_time.min, close_time.sec]
if open_time <=> current_time == -1 and current_time <=> close_time == -1
puts "IN BETWEEN"
end
You could CAST() your datetime to time by using,
cast(tbl_store.open_time as time) as SomeVariable
cast(tbl_store.close_time as time) as SomeOtherVariable
That would give you the time only instead of the full datetime value that you had to begin with, which is what you wanted.
You can then use the same logic with your curtime() between to the get value that you were looking for.
Example:
SELECT
CAST(tbl_store.open_time as TIME) as open_time,
CAST(tbl_store.close_time as TIME) as close_time,
CURTIME() BETWEEN (cast(tbl_store.open_time as TIME)) AND (cast(tbl_store.close_time as TIME)) as time_between
FROM
tbl_store
Working SQL Fiddle
You can change the schema build in the fiddle to test the datetime values you desire.
Note that if you ever have a logic that will include midnight time, you will have to make a CASE WHEN logic against that, else it will fail and return 0, whereas it should return 1.
You can take advantage of ranges and how numeric strings are compared
r = Range.new('09:00', '18:00')
r.include?('08:59') # => false
r.include?('09:01') # => true
r.include?('18:01') # => false
Then we could use
open_hours_range = Range.new(open_time.strftime('%R'), close_time.strftime('%R'))
shop_open? = open_hours_range.include?(Time.now.strftime('%R'))

RoR, Can't iterate from DateTime/TimeWithZone

I have a simple task where I want to take a starting date and an ending date and loop over the days/dates. This code is being used in my db:seed rake task. Currently, my code has gone through the following attempts.
(someModel.start_date.to_datetime..someModel.end_date.to_datetime).each {
|x| puts x
}
......
(someModel.start_date...someModel.end_date).each { |x| puts x }
In each case, I get an error like this.
can't iterate from ActiveSupport::TimeWithZone
or
can't iterate from DateTime
If anyone has any clue on how to iterate over a range of DateTimes I'd be greatly appreciative.
start = someModel.start_date.to_datetime
finish = someModel.end_date.to_datetime
while(start < finish) do
#bunch of awesome stuff
start += 1.day
end
You must make sure that you are dealing with a Date object (by calling to_date), then everything works as expected:
start_date.to_date.upto(end_date.to_date) {|date| puts date }
Or with a range:
(start_date.to_date..end_date.to_date).to_a
You can't iterate from DateTime. But you can iterate when start and end of
interval are instances of Date. Convert them if possible.
And then look at these Date methods:
step()
upto()
downto()
to use instead of each
If you want to iterate on the range you might consider a loop and step each day using something like someModel.start_date + 1.day.to_i
Here's what we're doing to iterate by minutes over two TimeWithZones:
# allow iteration over a set of TimeWithZones
def self.time_map(range, step: 30.minutes, &block)
result = []
time = range.first
max = range.last
test = range.exclude_end? ? ->(t) { t < max } : ->(t) { t <= max }
value = block_given? ? ->(t) { yield(t) } : ->(t) { t }
while test.call(time) do
result << value.call(time)
time += step
end
result
end

Using scope to return results within multiple DateTime ranges in ActiveRecord

I've got a Session model that has a :created_at date and a :start_time date, both stored in the database as :time. I'm currently spitting out a bunch of results on an enormous table and allowing users to filter results by a single date and an optional range of time using scopes, like so:
class Session < ActiveRecord::Base
...
scope :filter_by_date, lambda { |date|
date = date.split(",")[0]
where(:created_at =>
DateTime.strptime(date, '%m/%d/%Y')..DateTime.strptime(date, '%m/%d/%Y').end_of_day
)
}
scope :filter_by_time, lambda { |date, time|
to = time[:to]
from = time[:from]
where(:start_time =>
DateTime.strptime("#{date} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{date} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
}
end
The controller looks more or less like this:
class SessionController < ApplicationController
def index
if params.include?(:date) ||
params.include?(:time) &&
( params[:time][:from][:digits].present? && params[:time][:to][:digits].present? )
i = Session.scoped
i = i.filter_by_date(params[:date]) unless params[:date].blank?
i = i.filter_by_time(params[:date], params[:time]) unless params[:time].blank? || params[:time][:from][:digits].blank? || params[:time][:to][:digits].blank?
#items = i
#items.sort_by! &params[:sort].to_sym if params[:sort].present?
else
#items = Session.find(:all, :order => :created_at)
end
end
end
I need to allow users to filter results using multiple dates. I'm receiving the params as a comma-separated list in string format, e.g. "07/12/2012,07/13/2012,07/17/2012", and need to be able to query the database for several different date ranges, and time ranges within those date ranges, and merge those results, so for example all of the sessions on 7/12, 7/13 and 7/17 between 6:30 pm and 7:30 pm.
I have been looking everywhere and have tried several different things but I can't figure out how to actually do this. Is this possible using scopes? If not what's the best way to do this?
My closest guess looks like this but it's not returning anything so I know it's wrong.
scope :filter_by_date, lambda { |date|
date = date.split(",")
date.each do |i|
where(:created_at =>
DateTime.strptime(i, '%m/%d/%Y')..DateTime.strptime(i, '%m/%d/%Y').end_of_day
)
end
}
scope :filter_by_time, lambda { |date, time|
date = date.split(",")
to = time[:to]
from = time[:from]
date.each do |i|
where(:start_time =>
DateTime.strptime("#{i} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{i} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
end
}
Another complication is that the start times are all stored as DateTime objects so they already include a fixed date, so if I want to return all sessions started between 6:30 pm and 7:30 pm on any date I need to figure something else out too. A third party is responsible for the data so I can't change how it's structured or stored, I just need to figure out how to do all these complex queries. Please help!
EDIT:
Here's the solution I've come up with by combining the advice of Kenichi and Chuck Vose below:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(created_at >= ? AND created_at <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
scope :filter_by_time, lambda { |times|
args = []
[times[:from], times[:to]].each do |time|
h, m, s = time[:digits].split(':')
h = (h.to_i + 12).to_s if time[:meridian] == 'pm'
h = '0' + h if h.length == 1
s = '00' if s.nil?
args.push "#{h}:#{m}:#{s}"
end
where("CAST(start_time AS TIME) >= ? AND
CAST(start_time AS TIME) <= ?", *args)
}
This solution allows me to return sessions from multiple non-consecutive dates OR return any sessions within a range of time without relying on dates at all, OR combine the two scopes to filter by non-consecutive dates and times within those dates. Yay!
An important point I overlooked is that the where statement must come last -- keeping it inside of an each loop returns nothing. Thanks to both of you for all your help! I feel smarter now.
something like:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
and
scope :filter_by_time, lambda { |dates, time|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
f = time[:from] # convert to '%H:%M:%S'
t = time[:to] # again, same
b = "#{y}-#{m}-#{d} #{f}"
e = "#{y}-#{m}-#{d} #{t}"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
So, the easy part of the question is what to do about datetimes. The nice thing about DateTimes is that they can be cast to times really easily with this:
CAST(datetime_col AS TIME)
So you can do things like:
i.where("CAST(start_time AS TIME) IN(?)", times.join(", "))
Now, the harder part, why aren't you getting any results. The first thing to try is to use i.to_sql to decide whether the scoped query looks reasonable. My guess is that when you print it out you'll find that all those where are chaining together with AND. So you're asking for objects with a date that is on 7/12, 7/13, and 7/21.
The last part here is that you've got a couple things that are concerning: sql injections and some overeager strptimes.
When you do a where you should never use #{} in the query. Even if you know where that input is coming from your coworkers may not. So make sure you're using ? like in the where I did above.
Secondly, strptime is extremely expensive in every language. You shouldn't know this, but it is. If at all possible avoid parsing dates, in this case you can probably just gsub / into - in that date and everything will be happy. MySQL expects dates in m/d/y form anyways. If you're still having trouble with it though and you really need a DateTime object you can just as easily do: Date.new(2001,2,3) without eating your cpu.

Can using Chronic impair your sense of time?

Haha..
I'm using Chronic to parse the time users add in the Calendar. Where the code works and implements the right time, the end result is that, IF a user adds a time, then it has no date, and because it has no date, it will not show in results. Any ideas?
def set_dates
unless self.natural_date.blank? || Chronic.parse(self.natural_date).blank?
# check if we are dealing with a date or a date + time
if time_provided?(self.natural_date)
self.date = nil
self.time = Chronic.parse(self.natural_date)
else
self.date = Chronic.parse(self.natural_date).to_date
self.time = nil
end
end
unless self.natural_end_date.blank? || Chronic.parse(self.natural_end_date).blank?
# check if we are dealing with a date or a date + time
if time_provided?(self.natural_end_date)
self.end_date = nil
self.end_time = Chronic.parse(self.natural_end_date)
else
self.end_date = Chronic.parse(self.natural_end_date).to_date
self.end_time = nil
end
end
end
Edit:
Here is the time_provided? method:
def time_provided?(natural_date_string)
date_span = Chronic.parse(natural_date_string, :guess => false)
(date_span.last - date_span.first).to_i == 1
end
First, I'm not really sure what are you asking about, because it looks like the code intentionally does what you describe... When there's time provided, the date fields are assigned nil. And I don't think that is Chronic is to blame because that's how your code works.
Not knowing your design (why there are separate date & time fields), the types of fields etc., I would suggest starting with a little kludge like this:
if time_provided?(self.natural_date)
self.time = Chronic.parse(self.natural_date)
self.date = self.time.to_date
or:
self.end_date = Chronic.parse(self.natural_date).to_date
if time_provided?(self.natural_date)
self.time = Chronic.parse(self.natural_date)
end
Or maybe the problem is outside the code you provided: in the part that is responsible for the "because it has no date, it will not show in results" behavior? Maybe you should make the conditions more flexible?

Resources