Find records within date range - ruby-on-rails

I set a search_start_date and a search_end_date and then I want to find records which fall into this time range. But I get nothing back using the code below. What should I change to make this work?
search_start_date = "01-01-2013"
search_end_date = "01-12-2013"
#activities = #employee.activities
projects = #activities.where("start_date >= ? and end_date =< ?", search_start_date, search_end_date)
Edit: I now use search_start_date = DateTime.new(2013, 1, 1), which seems to work.

You will need to feed "Date" or "Time" objects into the ActiveRecord query, try something like this:
search_start_date = Date.parse("01-01-2013")
search_end_date = Date.parse("01-12-2013")
projects = #activities.where("start_date >= ? and end_date =< ?", search_start_date, search_end_date)

The Dates you are using are ambiguous. It is not clear if it's MM-DD-YYYY or DD-MM-YYYY. Depending on the region where you live, both could be correct. Use the default ruby unambiguous formatting YYYY-MM-DD or even better, use actual date objects.
You can also parse your strings to dates like in the example below. I also corrected the second compare operator to make it more concise. I'm unsure if both would have worked anyway. And don't forget the .all in case you didn't just leave it out of your querstion on purpose.
search_start_date = Date.strptime("01-01-2013", "%d-%m-%Y")
search_end_date = Date.strptime("01-12-2013", "%d-%m-%Y")
projects = #activities.where("start_date >= ? and end_date <= ?", search_start_date, search_end_date).all

Just modifying the #derekyau solution:-
search_start_date = "01-01-2013".to_date
search_end_date = "01-12-2013".to_date
projects = #activities.where("start_date >= ? and end_date =< ?", search_start_date, search_end_date)
Edit: I now use search_start_date = DateTime.new(2013, 1, 1), which seems to work.

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

How to combine ActiveRecord Relations? Merge not working?

Initially when I was trying to build a histogram of all Items that have an Order start between a given set of dates based on exactly what the item was (:name_id) and the frequency of that :name_id, I was using the following code:
dates = ["May 27, 2016", "May 30, 2016"]
items = Item.joins(:order).where("orders.start >= ?", dates.first).where("orders.start <= ?", dates.last)
histogram = {}
items.pluck(:name_id).uniq.each do |name_id|
histogram[name_id] = items.where(name_id:name_id).count
end
This code worked FINE.
Now, however, I'm trying to build a histogram that's more expansive. I still want to capture frequency of :name_id over a period of time, but now I want to bound that time by Order start and end. I'm having trouble however, combining the ActiveRecord Relations that follow the queries. Specifically, if my queries are as follows:
items_a = Item.joins(:order).where("orders.start >= ?", dates.first).where("orders.start <= ?", dates.last)
items_b = Item.joins(:order).where("orders.end >= ?", dates.first).where("orders.end <= ?", dates.last)
How do I join the 2 queries so that my code below that acts on query objects still works?
items.pluck(:name_id).each do |name_id|
histogram[name_id] = items.where(name_id:name_id).count
end
What I've tried:
+, but of course that doesn't work because it turns the result into an Array where methods like pluck don't work:
(items_a + items_b).pluck(:name_id)
=> error
merge, this is what all the SO answers seem to say... but it doesn't work for me because, as the docs say, merge figures out the intersection, so my result is like this:
items_a.count
=> 100
items_b.count
=> 30
items_a.merge(items_b)
=> 15
FYI currently, I've monkey-patched this with the below, but it's not very ideal. Thanks for the help!
name_ids = (items_a.pluck(:name_id) + items_b.pluck(:name_id)).uniq
name_ids.each do |name_id|
# from each query object, return the ids of the item objects that meet the name_id criterion
item_object_ids = items_a.where(name_id:name_id).pluck(:id) + items_b.where(name_id:name_id).pluck(:id) + items_c.where(name_id:name_id).pluck(:id)
# then check the item objects for duplicates and then count up. btw I realize that with the uniq here I'm SOMEWHAT doing an intersection of the objects, but it's nowhere near as severe... the above example where merge yielded a count of 15 is not that off from the truth, when the count should be maybe -5 from the addition of the 2 queries
histogram[name_id] = item_object_ids.uniq.count
end
You can combine your two queries into one:
items = Item.joins(:order).where(
"(orders.start >= ? AND orders.start <= ?) OR (orders.end >= ? AND orders.end <= ?)",
dates.first, dates.last, dates.first, dates.last
)
This might be a little more readable:
items = Item.joins(:order).where(
"(orders.start >= :first AND orders.start <= :last) OR (orders.end >= :first AND orders.end <= :last)",
{ first: dates.first, last: dates.last }
)
Rails 5 will support an or method that might make this a little nicer:
items_a = Item.joins(:order).where(
"orders.start >= :first AND orders.start <= :last",
{ first: dates.first, last: dates.last }
).or(
"orders.end >= :first AND orders.end <= :last",
{ first: dates.first, last: dates.last }
)
Or maybe not any nicer in this case
Maybe this will be a bit cleaner:
date_range = "May 27, 2016".to_date.."May 30, 2016".to_date
items = Item.joins(:order).where('orders.start' => date_range).or('orders.end' => date_range)

Activerecord where array with less than condition

I have an array of conditions i'm passing to where(), with the conditions being added one at a time such as
conditions[:key] = values[:key]
...
search = ModelName.where(conditions)
which works fine for all those that i want to compare with '=', however I want to add a '<=' condition to the array instead of '=' such as
conditions[:key <=] = values[:key]
which of course won't work. Is there a way to make this work so it i can combine '=' clauses with '<=' clauses in the same condition array?
One way of doing it:
You could use <= in a where clause like this:
User.where('`users`.`age` <= ?', 20)
This will generate the following SQL:
SELECT `users`.* FROM `users` WHERE (`users`.`age` <= 20)
Update_1:
For multiple conditions, you could do this:
User.where('`users`.`age` <= ?', 20).where('`users`.`name` = ?', 'Rakib')
Update_2:
Here is another way for multiple conditions in where clause:
User.where('(id >= ?) AND (name= ?)', 1, 'Rakib')
You can add any amount of AND OR conditions like this in your ActiveRecord where clause. I just showed with 2 to keep it simple.
See Ruby on Rails Official Documentation for Array Conditions for more information.
Update_3:
Another slight variation of how to use Array Conditions in where clause:
conditions_array = ["(id >= ?) AND (name = ?)", 1, "Rakib"]
User.where(conditions_array)
I think, this one will fit your exact requirement.
You could use arel.
conditions = {x: [:eq, 1], y: [:gt, 2]}
model_names = ModelName.where(nil)
conditions.each do |field, options|
condition = ModelName.arel_table[field].send(*options)
model_names = model_names.where(condition)
end
model_names.to_sql --> 'SELECT * FROM model_names WHERE x = 1 and y > 2'

How to check if time is in range using 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

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.

Resources