I want to calculate the average number of days since 'date_from' (it varies)
User.all.average('? - date_from', Time.now.to_date)
Gives the error
undefined method `except' for Thu, 27 Nov 2014:Date
See Ruby_on_Rails/ActiveRecord/Calculations
The second parameter of average is options(an hash)
The options can be used to customize the query with :conditions, :order, :group, :having and :joins.
So you should generate average expression like this:
User.average("'#{Time.now.to_date}' - date_from" )
Then Rails will generate SQL like this:
SELECT AVG('2014-11-27' - date_from) AS avg_id FROM ...
You can try Date's ::today method, instead of Time's ::now:
User.all.average('? - date_from', Date.today)
How about:
#get difference fro all users from Time.now and date_from
dates = User.all.pluck(:date_from).map{|date| Time.now - date}
#sum all elements from array and divide with number of elements
#to get average
days = dates.reduce(0,:+)/dates.length
#round to 2 decimal places for better display
days = days.round(2)
Related
I have a series of Appointments where the date and time is stored under start_date as DateTime. I'd like to categorize them as starting in the Morning, Daytime, or Evening. I created an array of hashes with labels and ranges I used for a SQL statement where I convert the start_date records into seconds using CAST(EXTRACT (EPOCH FROM start_time) AS INT)
TIME_RANGES = [
{label: "Morning", min: 0, max: 32399},
{label: "Daytime", min: 32400, max: 61199},
{label: "Evening", min: 61200, max: 86399}
]
cases = TIME_RANGES.map do |r|
"when CAST (EXTRACT (EPOCH FROM start_date) AS INT) % 86400 between '#{r[:min]}' and '#{r[:max]}' then '#{r[:label]}'"
end
time_ranges = Appointment.select("count(*) as n, case #{time_cases.join(' ')} end as time_range").group('time_range')
This takes the number of seconds in a day (86400) and labels the appointments based on the modulo of the start_date with 86400. However, a number of appointments take place in different timezones, but they're all stored as UTC. So an appointment at 08:00 AM EST is equivalent to one at 07:00 AM CST, but both are stored internally as 12:00 PM UTC. This would cause the appointments to be incorrectly labelled as "Daytime" when they're intended to be "Morning" (from the perspective of the User booking it).
Ideally, I would like some way to convert the start_date based on the User's timezone to make it look like it occurred in UTC. So I would want a 12:00 PM EST appointment to be labelled as if it were a 12:00 PM UTC appointment instead of 04:00 PM UTC. More specifically, I would like to subtract 14400 seconds from the converted start_date before performing the modulo.
I can join Appoinments to Users, which contains the User's timezone. How can I incorporate this information into my query above, so that a modified start_date is used for each record, depending on the User's timezone in that same record?
I know I could accomplish this with a loop of each timezone and adding/substracting a specific amount of seconds in each loop, then combining the results of all the loops, but I was wondering if there was a way to do it in one query.
Per my comment, I am assuming we have three tables: appointments, users, and preferences. In appointments we have start_date and user_id. In users we have preference_id. In preferences we have some column that names the time zone, so I'll call that tz_name.
Note: Postgres timezone functions are messy. I would highly recommend you read up on them. This excellent article is a good place to start.
It is possible to use pure SQL to generate the time ranges and return a grouped result. A pure SQL solution would be best if you need to label and group many records (thousands or more) at a time.
Assuming you are working with 1000 records or fewer at a time, you'll probably want to use Rails scopes, as this will give you an ActiveRecord result. Then you'll do your grouping using Ruby's Array methods.
That solution would look something like this:
# app/user.rb
class User < ApplicationRecord
belongs_to :preference
has_many :appointments
end
# app/appointment.rb
class Appointment < ApplicationRecord
belongs_to :user
scope :with_seconds, lambda {
joins(user: :preference)
.select('appointments.*, extract(epoch from timezone(tz_name, start_date::timestamptz)::time)::int as seconds')
}
# This method is optional. If it is excluded, calling #seconds
# on an appointment instance will raise an error if the
# .with_seconds scope has not been applied.
def seconds
attributes['seconds']
end
def time_range
return nil unless seconds.present?
if seconds.between?(0, 32_399)
'Morning'
elsif seconds.between?(32_400, 61_199)
'Daytime'
else
'Evening'
end
end
end
The select portion of the scope probably deserves some explanation.
start_date::timestamptz: Take the start_date, which is stored as a Postgres timestamp, and convert it into a Postgres timestamp with time zone in the time zone of the Postgres server (presumably UTC).
timezone(tz_name, start_date::timestamptz): Convert the timestamp with time zone back into a timestamp type in the local time of the tz_name time zone.
timezone(tz_name, start_date::timestamptz)::time: Drop the date and keep the time component.
Then we extract epoch from that time component, which converts it into seconds.
Finally we convert the result to an integer to avoid anything falling through the cracks when we determine the time range.
Now you can do:
Appointment.all.with_seconds.group_by(&:time_range)
or
user = User.first
user.appointments.with_seconds.group_by(&:time_range)
For a pure SQL solution that will return ids grouped under the three time ranges, add this method to your Appointment model:
def self.grouped_by_time_range
current_scope = with_seconds.to_sql
query = <<~SQL
with converted_seconds as (#{current_scope})
select array_agg(id) as ids, case when seconds < 32400 then 'Morning'
when seconds < 61200 then 'Daytime'
else 'Evening' end as time_range
from converted_seconds
group by time_range
SQL
result = ActiveRecord::Base.connection.execute(query.squish)
result.to_a
end
If you don't need strictly SQL based solution you might use Ruby's select method to extract this appointments as in this example:
(I am assuming there is some kind of tz_name field in appointment model which holds timezone name)
morning_appointments = Appointment.all.select do |a|
a.start_date.in_time_zone(a.tz_name).hour > 6 && a.start_date.in_time_zone(a.tz_name).hour < 20
end
Edit:
Thanks #moveson for pointing out my mistake, I changed solution a bit.
I have problem with connecting conditional OR with filterint by date range.
Something like that
#pseudo SQL syntax
(#date_start < params[:date_to_search] < #date_end) && (#every_day==1 || #day_of_week== params[:day])
--
class OfferTime < ActiveRecord::Base
define_index do
indexes every_day #boolean
indexes day_of_week #string eg. Tue, Mon etc
has date_start #Date NOT datetime
has date_end #Date
end
end
To make conditional OR I use this solution:
Conditional "or" in Thinking Sphinx search
This works quite well.
(OfferTime.search "#every_day 1 | #day_of_week Tue", :match_mode => :extended).size
# => 2
# Correct answer
But I don't know how to connect that with that date range
(#date_start < params[:date_to_search] < #date_end)
Example:
(OfferTime.search "#every_day 1 | #day_of_week Tue & #date_start < #{1.year.ago}", :match_mode => :extended).size
# => 1
# wrong answer! should be 0!
Is it possible to make that with ThinkingSphinx?
Not sure exactly the logic you're after - is it every day OR (day of week and date start), or (every day OR day of week) AND date start?
The latter is possible - but you'll want to filter on the attribute using the :with arguments - it should be well-covered in the docs. To get a 'less than 1 year ago' filter, you'll need to use a range - from one year ago to today (or well into the future, if that's more appropriate).
If it's the former, I'm not sure if that's possible - but if it is, you'll likely need to investigate the expression syntax and Sphinx's select clause. Again, covered a bit in the TS docs, with links to the relevant Sphinx sections.
I want to sum up the differences between two time columns in a table. I'm using the following code but it returns zero because end_time is not getting parsed correctly. Both columns are timestamps.
table.sum("end_time - start_time")
I found that when I typed in table.sum("end_time"), the sum was 2010. This is odd because there is one row in table with an end_time of "2010-12-18 23:42:30".
You can't "subtract" times unless they are UNIX timestamps...
Just let rails do the work. Write a query like:
t = Table.find(:all, :conditions => {...})
t.each do |a|
puts a.end_time - a.start_time
end
I need to retrieve all rows from a table where the created_at timestamp is during a certain hour ... say 04:00 and 05:00. Anyone know how to do this?
RecordNameHere.find_by_sql("SELECT * FROM `table_name_here` WHERE HOUR(created_at) = HOUR('4:01:00')")
The MySQL documentation is awesome: http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_hour
For multiple hour range (eg: records between in 4:00 to 6:00)
User.all(:conditions => "HOUR(created_at) BETWEEN ? AND ?", 4, 5)
For single hour use the following syntax:
User.all(:conditions => "HOUR(created_at) = ?", 4)
Note 1
The HOUR method returns the hour in 24 hour format. Provide the hour value accordingly.
I have a table with a float called 'cost' and timestamp called'created_at'.
I would like it to output an array with the summing the costs for each particular day in the last month.
Something like:
#newarray = [] #creating the new array
month = Date.today.month # Current Month
year = Date.today.year # Current Year
counter = 1 # First Day of month
31.times do #for each day of the month (max 31)
#adding sales figures for that day
#newarray.push(Order.sum(:cost, :conditions => {:created_at => "#{year}-#{month}-#{counter}"}))
counter = counter + 1 #go onto next day
end
However this doesn't work as all the timestamps have a time as well.
Apologies in advance for the poor title, I can't seem to think of a sensible one.
You should be able to use code like the following:
sales_by_day = Order.sum(:cost,
:group => 'DATE(created_at)',
:conditions => ['DATE(created_at) > ?', 31.days.ago])
(0..30).collect { |d| sales_by_day[d.days.ago.to_date.to_s] || 0 }.reverse
This will sum the order cost by day, then create an array with index 0 being the last 24 hours. It will also take only one query, whereas your example would take 31.
To change the cut off date, replace 31.days.ago with an instance of the Time class. Documentation here: http://ruby-doc.org/core/classes/Time.html
Good luck!
This should work:
(Date.today.beginning_of_month..Date.today.end_of_month).map { |d|
Order.sum(:cost, :conditions => ['created_at >= ? AND created_at < ?', d, d+1])
}
Although I think you should try getting it using a single query, instead of making one for each day.