I have a problem in Ruby on Rails, where I need to allow a user to set a two time columns, and then query all instances of that model that have the current time within the hours of the two timestamps. Days do not matter, just within the times specified be the start and end timestamps.
Thanks,
Brian
Something like this should work (assuming MySQL):
Model.where("TIME(created_at) BETWEEN '07:00:00' AND '09:00:00'")
You can convert a Time instance to this format with:
Time.now.strftime("%T") #=> "23:02:25"
If the start time is larger than the end time you could reverse the logic, so instead of:
Model.where("TIME(created_at) BETWEEN '23:00:00' AND '01:59:59'")
You could use:
Model.where("TIME(created_at) NOT BETWEEN '02:00:00' AND '22:59:59'")
You might be looking for something like
Model.where("starts_at >= ? AND ends_at =< ?", Time.current, Time.current)
Alternatively, you can use placeholder conditions to make the query more concise.
Client.where("created_at >= :start_date AND created_at <= :end_date",
{start_date: params[:start_date], end_date: params[:end_date]})
How you solve this problem will depend on what database you're using b/c as far as I'm aware there aren't any ActiveRecord helpers for this type of operation. But there are date functions that should help you figure this out
Postgres Date functions
Mysql Date functions
So for example, in Postgres you might try
extract(hour from timestamp 'a') BETWEEN (extract(hour from timestamp 'b') AND extract(hour from timestamp 'c'))
Related
So I have a start_time and end_time. Which is maximum 24 hours.
I guess I firstly need to check if this timerange is within another timerange say 21:00-01:00. If it is I need to know how many of the hours is within the timerange.
Any ideas?
Use ((two- one) / 3600).round
for example
one=Time.now #=>2019-01-23 21:19:19 +0530
two=Time.now + 1*60*60 #=>2019-01-23 22:19:27 +0530
((two- one) / 3600).round #=>1
difference is 1 hour for two different Time instance.
What do you mean by "within"? Do you mean that you want to test if there's any overlap between the ranges, or if the first range is fully contained within another?
You can use Time objects in ranges and exploit #cover? to check for overlap
start_time = 5.hours.ago
end_time = 4.hours.ago
check_range_start = (4.5).hours.ago
check_range_end = (3.5).hours.ago
check_range = (check_range_start..check_range_end)
check_range.cover?(end_time) # => true
check_range.cover?(start_time) # =>false
If both ends of the range is covered by your check range, then your range is fully covered. If only one end is covered, then you have overlap. If neither end is covered, then the ranges do not intersect.
Once you know if there's intersection, then you can trivially compute the distance from one end of the time range to one end of your check range; if you want to know how much time between your check range start and your input start time, simply subtract start_time - check_range_start.
I assume the four arguments of the method below are strings of the form "HH:MM" or "HH:MM:SS". I have provided a pure-Ruby solution that makes string comparisons only (for example: "17:26" < "18:00" #=> true). That is, I do not convert the strings to Time objects.
def time_within_ref?(time_start, time_end, ref_start, ref_end)
(time_start >= ref_start) &&
(adj_hour(time_start, time_end) <= adj_hour(ref_start, ref_end))
end
def adj_hour(start_str, end_str)
end_str[0,2] = (end_str[0,2].to_i + 24).to_s if end_str < start_str
end_str
end
Here are a few examples.
time_within_ref?("19:00", "02:00", "18:00", "03:00")
#=> true
time_within_ref?("19:00", "04:00", "18:00", "03:00")
#=> false
time_within_ref?("17:00", "02:00", "18:00", "03:00")
#=> false
time_within_ref?("18:00", "02:00", "18:00", "03:00")
#=> true
There are several ways to perform this in the database which are far more performant than loading a bunch of records into Rails.
While "pure ruby" solutions are a quick fix they will exhaust the available memory and crash your server given a non-trivial amount of records.
You can use a range together with .where to create a WHERE start_time BETWEEN a AND b clause that ensures that a time is within A and B.
starts_today = Model.where(
start_time: Time.current.beginning_of_day..Time.current.end_of_day
)
If you want to ensure that the entire span between start_time and end_time is within a set of bounds you can write you own where clause:
Model.where(
"start_time > :a AND end_time < :b",
a: Time.current.beginning_of_day,
b: Time.current.end_of_day
)
I need to know how many of the hours is within the timerange.
You can do this with the time/date functions in your database.
On postgres for example you can do:
bounds = Time.current.beginning_of_day..Time.current.end_of_day
Model.select(
"models.*, (EXTRACT(HOUR FROM end_time - start_time ) - EXTRACT(HOUR FROM end_time - '#{ bounds.end.iso8601 }')) AS number_of_hours"
)
Again calculating this in the DB is vital if you for example want to use this in a WHERE clause.
I think I figured it out.
Shift.start_time.strftime("%H:%M") => '20:00' && Shift.end_time.strftime("%H:%M") <= '01:00'
The .to_time was messing with me and also the ordering.
One problem I often run into in Rails is this:
Let's say I have an invoices table with a date and a days column.
How can I retrieve all invoices which are due?
class Invoice < ActiveRecord::Base
def self.due
where("due_date > ?", Date.today) # this doesn't work because there is no database column "due_date"
end
private
def due_date
date + days
end
end
Can anybody tell me how to do this without having to add a database column due_date to my invoices table?
Thanks for any help.
In PostgreSQL, adding an integer to a date adds that many days:
date '2001-09-28' + integer '7' = date '2001-10-05'
so you can simply say:
where('due_date + days > :today', :today => Date.today)
However, SQLite doesn't really have a date type at all, it stores dates as ISO 8601 strings. That means that adding a number to a date will end up concatenating the strings and that's sort of useless. SQLite does have a date function though:
date(timestring, modifier, modifier, ...)
[...]
All five date and time functions take a time string as an argument. The time string is followed by zero or more modifiers.
so you can say things like date('2014-01-22', '+ 11 days') to do your date arithmetic. That leaves you with this:
where("date(due_date, '+' || days || ' days') > :today", :today => Date.today)
Thankfully, ISO 8601 date strings compare properly as strings so > still works.
Now you're stuck with two versions of the same simple query. You could check what sort of thing self.connection is to differentiate between dev/SQLite and production/PostgreSQL or you could look at Rails.env.production?. This of course leaves a hole in your test suite.
I think you should stop developing on top of SQLite if you intend on deploying on top of PostgreSQL and you should do that right now to minimize the pain and suffering. The truth is that any non-trivial application will be wedded to the database you use in production or you will have to expend significant effort (including running your test suite against all the different databases you use) to maintain database portability. Database independence is a nice idea in theory but wholly impractical unless someone is prepared to cover the non-trivial costs (in time and treasure) that such independence requires. ORMs won't protect you from the differences between databases unless your application is yet another "15 minute blog" toy.
You could do something like:
class Invoice < ActiveRecord::Base
def self.due
Invoice.all.select { |invoice| invoice.due_date > Date.today }
end
private
def due_date
date + days
end
end
I have two datetime columns in a User/users table: created_at and birthdate. I'd like to find users whose birthdate is less than 13 years before their creation date.
The equivalent Rails if statement would be ((created_at - birthdate) < 13.years)
How do I translate this to an ActiveRecord query (one that'll work on PostgreSQL)? Is this even possible, or will I have to iterate over all records manually?
The easiest way to do this is to use an interval, then it is pretty much a straight transliteration of the Rails version:
User.where(%q{created_at - birthdate < interval '13 years'})
The difference between two timestamps (or a timestamp and a date) is an interval so you just need the appropriate value on the right side of your comparison.
You simply have to formulate that in PostgreSQL syntax inside your where clause.
For MySQL this would look similar to this using the datediff function:
User.where("DATEDIFF(created_at, birthdate) > (13 * 365)")
13*356 is there to represent 3 years in days since datediff returns difference in days.
I would then encapsulate that in a scope-like function like the following:
class User < ActiveRecord::Model
def self.age_difference(years)
where("DATEDIFF(created_at, birthdate) > (? * 365)", years)
end
end
So you can call it:
User.age_difference(13).each do |user|
puts user.inspect
end
I guess it's similar in Postgres.
I'm trying to do this: map the total sales on a day to an array of dates for highcharts (yes my project is effectively exactly the same as the railscast example).
I'm unfortunately just ending up with a lot of 0s; I believe the piece in my model:
def self.total_revenue_on(date)
where("date(created_at) = ?", date).sum(:amt)
end
is failing to match the date to the datetime written in my database, e.g. "2011-07-21 09:22:28.388944+0000". Pretty sure that's where it's failing because if I remove the timezone piece manually from my database (get rid of "+0000" and leave just "2011-07-21 09:22:28.388944") it works just fine.
I think this is really a rails/sqlite question: am I storing the timestamp improperly, or comparing improperly? Any help is greatly appreciated!
The best practice is to use to_s(:db) for referencing datetimes in a database in Rails. Try:
def self.total_revenue_on(date)
where("date(created_at) = ?", date.to_s(:db)).sum(:amt)
end
OK, I managed to solve this by using a different lookup method:
def self.total_revenue_on(date)
where("datetime >= ? and datetime < ?", date, date + 1.day).sum(:amt)
end
Still completely perplexed by the problem with the original, but this seems to be working.
Is it possible to do an activerecord query that only looks at the time component of a datetime field?
e.g. Battle.where('start_time < ? and start_time > ?','12:00','06:00')
to find all battles that were started between 6am and 12pm regardless of the day they occurred? In this example, start_time is defined as a datetime.
The only way to do this is using a SQL function, if you're on MySQL you could do it like this:
Battle.where( 'HOUR( start_time ) >= ? AND HOUR( start_time ) <= ?', 12, 6 )
But this is hugely inefficient and is always going to generate a full table scan and you surely don't want that.
The best solution is to add columns with the hour values alone at your battle model, index them and query directly on them like this:
Battle.where( 'start_time_hour >= ? start_time_hour <= ?', 12, 6 )
Just add an before_save callback that sets this values before saving your Battle model based on the start_time property.
EDIT
BTW, if you're using PostgreSQL, it's capable of creating an index on the result of a function, so, if you don't mind having to stick with PostgreSQL you could just use the SQL function. And before you ask, no, i don't know how it works, I only know the database allows you to do so and that this is not available on MySQL.