Getting number of events within a season based on date - ruby-on-rails

I have a model called Event with a datetime column set by the user.
I'm trying to get a total number of events in each season (spring, summer, fall, winter).
I'm trying with something like:
Event.where('extract(month from event_date) >= ? AND extract(day from event_date) >= ? AND extract(month from event_date) < ? AND extract(day from event_date) < ?', 6, 21, 9, 21).count
The example above would return the number of events in the Summer, for example (at least in the northern hemisphere).
It doesn't seem like my example above is working, i'm getting no events returned even though there are events in that range. I think my order of operations (ands) may be messing with it. Any idea of the best method to get what I need?
Edit: actually looking at this more this will not work at all. Is there anyway select dates within a range without the year?
Edit 2: I'm trying to somehow use the answer here to help me out, but this is Ruby and not SQL.
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end

You can concat the month and day and query everything in between.
e.g 621..921
In SQL it would be something like
SUMMER_START = 621
SUMMER_END = 921
Event.where("concat(extract(month from event_date), extract(day from event_date)) > ? AND concat(extract(month from event_date), extract(day from event_date)") < ?,
SUMMER_START, SUMMER_END)
This can be easily made into a scope method that accepts a season (e.g 'winter'), takes the appropriate season start and end and returns the result.

This is what I ended up doing:
in lib created a file called season.rb
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end
in lib created a file called count_by.rb:
module Enumerable
def count_by(&block)
list = group_by(&block)
.map { |key, items| [key, items.count] }
.sort_by(&:last)
Hash[list]
end
end
Now I can get the season for any date, as well as use count_by on the model.
So then ultimately I can run:
Event.all.count_by { |r| r.event_date.season }[:spring]

Related

How does one get the "next" record from database sorted by a specific attribute without loading all the records?

Here's the situation:
I have an Event model and I want to add prev / next buttons to a view to get the next event, but sorted by the event start datetime, not the ID/created_at.
So the events are created in the order that start, so I can compare IDs or get the next highest ID or anything like that. E.g. Event ID 2 starts before Event ID 3. So Event.next(3) should return Event ID 2.
At first I was passing the start datetime as a param and getting the next one, but this failed when there were 2 events with the same start. The param start datetime doesn't include microseconds, so what would happen is something like this:
order("start > ?",current_start).first
would keep returning the same event over and over because current_start wouldn't include microseconds, so the current event would technically be > than current_start by 0.000000124 seconds or something like that.
The way I got to work for everything was with a concern like this:
module PrevNext
extend ActiveSupport::Concern
module ClassMethods
def next(id)
find_by(id: chron_ids[current_index(id)+1])
end
def prev(id)
find_by(id: chron_ids[current_index(id)-1])
end
def chron_ids
#chron_ids ||= order("#{order_by_attr} ASC").ids
end
def current_index(id)
chron_ids.find_index(id)
end
def order_by_attr
#order_by_attr ||= 'created_at'
end
end
end
Model:
class Event < ActiveRecord::Base
...
include PrevNext
def self.order_by_attr
#order_by_attr ||= "start_datetime"
end
...
end
I know pulling all the IDs into an array is bad and dumb* but i don't know how to
Get a list of the records in the order I want
Jump to a specific record in that list (current event)
and then get the next record
...all in one ActiveRecord query. (Using Rails 4 w/ PostgreSQL)
*This table will likely never have more than 10k records, so it's not catastrophically bad and dumb.
The best I could manage was to pull out only the IDs in order and then memoize them.
Ideally, i'd like to do this by just passing the Event ID, rather than a start date params, since it's passed via GET param, so the less URL encoding and decoding the better.
There has to be a better way to do this. I posted it on Reddit as well, but the only suggested response didn't actually work.
Reddit Link
Any help or insight is appreciated. Thanks!
You can get the next n records by using the SQL OFFSET keyword:
china = Country.order(:population).first
india = City.order(:population).offset(1).take
# SELECT * FROM countries ORDER BY population LIMIT 1 OFFSET 1
Which is how pagination for example often is done:
#countries = Country.order(:population).limit(50)
#countries = scope.offset( params[:page].to_i * 50 ) if params[:page]
Another way to do this is by using would be query cursors. However ActiveRecord does not support this and it building a generally reusable solution would be quite a task and may not be very useful in the end.

Summing an array of numbers in ruby and extracting time over 40 hours

I wrote a payroll type app which takes a clock_event and has a punch_in and punch_out field. In a the clock_event class I have a method that takes the employee, sums their total hours and exports to CSV. This gives total hours for the employee, and then a total sum of their total_hours which is a method in the class calculated on the fly.
Here is my code:
clock_event.rb
def total_hours
self.clock_out.to_i - self.clock_in.to_i
end
def self.to_csv(records = [], options = {})
CSV.generate(options) do |csv|
csv << ["Employee", "Clock-In", "Clock-Out", "Station", "Comment", "Total Shift Hours"]
records.each do |ce|
csv << [ce.user.try(:full_name), ce.formatted_clock_in, ce.formatted_clock_out, ce.station.try(:station_name), ce.comment, TimeFormatter.format_time(ce.total_hours)]
end
records.map(&:user).uniq.each do |user|
csv << ["Total Hours for: #{user.full_name}"]
csv << [TimeFormatter.format_time(records.select{ |r| r.user == user}.sum(&:total_hours))]
end
csv << ["Total Payroll Hours"]
csv << [TimeFormatter.format_time(records.sum(&:total_hours))]
end
end
end
This method works and exports a CSV with all total time entries for each day then a sum of the hours at the bottom of the CSV file.
Here's my problem...
I can sum no problem, but I need to show the following:
Total Sum of hours (done)
Any hours over 40 hours I need to pull that amount into another field in the CSV file. So if an employee hits 40 hours it will show the 40 hours in one field then show the remaining ours as overtime right next to it.
I know how to sum the hours but am unsure as to how to extra the hours over 40 into another field into the CSV.
I'm sure there's a Ruby way to do it but I'm not certain on how this would work.
Any help would be greatly appreciated. If you need more code or context, please let me know.
Firstly, you should let total_hours return exactly what it says it returns in its method definition, the "total hours". Include your time formatting within this method to avoid scattering TimeFormatter logic through out your codebase:
def total_hours
TimeFormatter.format_time(self.clock_out.to_i - self.clock_in.to_i)
end
Second, you need to calculate overtime hours situationally, that will look like this:
def overtime_hours(num_weeks = 1, hours_per_week = 40)
ot = total_hours - (num_weeks * hours_per_week)
ot > 0 ? ot : 0
end
This gives you some customization so that if your business rules ever change you can actually maintain your code a little easier. Your defaults will always be 1 week and 40 hours, however you may do this:
ce.overtime_hours(2, 40)
or:
ce.overtime_hours(2, 39)
Some people only work 39 hours so that businesses can avoid paying for healthcare and other mandatory benefits that kick in at 40 hours. This will give you the ability to control what overtime_hours outputs.
And in your CSV for Week 1:
records.where('user_id is ? and date > ? and date < ?', user.id, begin_week_1, end_week_1).sum(&:overtime_hours)
and week 2:
records.where('user_id is ? and date > ? and date < ?', user.id, begin_week_2, end_week_2).sum(&:overtime_hours)

Generate array of daily avg values from db table (Rails)

Context:
Trying to generating an array with 1 element for each created_at day in db table. Each element is the average of the points (integer) column from records with that created_at day.
This will later be graphed to display the avg number of points on each day.
Result:
I've been successful in doing this, but it feels like an unnecessary amount of code to generate the desired result.
Code:
def daily_avg
# get all data for current user
records = current_user.rounds
# make array of long dates
long_date_array = records.pluck(:created_at)
# create array to store short dates
short_date_array = []
# remove time of day
long_date_array.each do |date|
short_date_array << date.strftime('%Y%m%d')
end
# remove duplicate dates
short_date_array.uniq!
# array of avg by date
array_of_avg_values = []
# iterate through each day
short_date_array.each do |date|
temp_array = []
# make array of records with this day
records.each do |record|
if date === record.created_at.strftime('%Y%m%d')
temp_array << record.audio_points
end
end
# calc avg by day and append to array_of_avg_values
array_of_avg_values << temp_array.inject(0.0) { |sum, el| sum + el } / temp_array.size
end
render json: array_of_avg_values
end
Question:
I think this is a common extraction problem needing to be solved by lots of applications, so I'm wondering if there's a known repeatable pattern for solving something like this?
Or a more optimal way to solve this?
(I'm barely a junior developer so any advice you can share would be appreciated!)
Yes, that's a lot of unnecessary stuff when you can just go down to SQL to do it (I'm assuming you have a class called Round in your app):
class Round
DAILY_AVERAGE_SELECT = "SELECT
DATE(rounds.created_at) AS day_date,
AVG(rounds.audio_points) AS audio_points
FROM rounds
WHERE rounds.user_id = ?
GROUP BY DATE(rounds.created_at)
"
def self.daily_average(user_id)
connection.select_all(sanitize_sql_array([DAILY_AVERAGE_SELECT, user_id]), "daily-average")
end
end
Doing this straight into the database will be faster (and also include less code) than doing it in ruby as you're doing now.
I advice you to do something like this:
grouped =
records.order(:created_at).group_by do |r|
r.created_at.strftime('%Y%m%d')
end
At first here you generate proper SQL near to that you wish to get in first approximation, then group result records by created_at field converted to just a date.
points =
grouped.map do |(date, values)|
[ date, values.reduce(0.0, :audio_points) / values.size ]
end.to_h
# => { "1-1-1970" => 155.0, ... }
Then you remap your grouped hash via array, to calculate average values with audio_points.
You can use group and calculations methods built in AR: http://guides.rubyonrails.org/active_record_querying.html#group
http://guides.rubyonrails.org/active_record_querying.html#calculations

How can I limit an existing Rails AR query by a certain time frame?

I have a dashboard(esque) view in a Rails app which is showing some data in similar ways but broken out into many time periods.
I have some code in my controller like so:
#issues_this_month = Issue.where('issues.created_at BETWEEN ? AND ?', DateTime.now.in_time_zone.beginning_of_month, DateTime.now.in_time_zone.end_of_month)
and I also want to create a variables which shows issues this year and issues all time so I have this code:
#issues_this_year = Issue.where('issues.created_at BETWEEN ? AND ?', DateTime.now.in_time_zone.beginning_of_year, DateTime.now.in_time_zone.end_of_year)
I am curious if someone can think of a good way of doing one query, and from that inferring the date ranges all while avoiding the extra queries. Should I pass the results to a helper method and do the logic there?
in the model... you can define
def date
self.created_at.to_date
end
then in the controller
start = Date.today.beginning_of_year
end = Date.today.end_of_year
#issues_this_year = Issue.where(create_at: start..end).group_by(&:date)
now you have a hash of [month_1, {issues that exist in month_1}, month_2, {issues that exist in month_2}, etc]. play with it in the console to find the proper keys... #issues_this_year.keys
How about defining a method like
def self.in_timeframe(start_time=DateTime.now.in_time_zone.beginning_of_month,
end_time=DateTime.now.in_time_zone.end_of_month)
Issue.where('issues.created_at BETWEEN ? AND ?', start_time, end_time)
end
You can now invoke this as follows:
Issue.in_timeframe # For issues in the month
Issue.in_timeframe(x,y) # For issues within x-y timeframe
If you want the data in a single query, you could do stuff like:
def self.in_timeframes(time_frames)
data = {}
times_frames.each do |time_frame|
data[time_frame[:name]] = Issue.in_timeframe(time_frame[:srtart]. time_frame[:end])
end
data
end
You can invoke the above method using:
time_frames = [{:name=>"month"},
{:name=>"x-y", :start=>x, :end=>y}]
Issue.in_timeframes(time_frames)

ActiveRecord where method call optimisation

I have a piece of code witch looks like this:
Post.all.reject {|p| p.created_at.beginning_of_month != params[:date].to_date}
Is there a method to write the same code using where method and to not get all elements?
If you want to use where, I'd go by:
# x-month being a date from your desired month.
# .. defines the range between the beginning and the end
Post.where(:created_at => x-month.beginning_of_month..x-month.end_of_month)
AFAIK, there is no database-agnostic solution to this, because you need to extract the month from the date. So, in raw SQL you would have :
date = params[:date].to_date
Post.where("MONTH(created_at) != ? AND YEAR(created_at) = ?", [date.month, date.year])
Now it is possible to cheat a bit with normalization in order to use a db-agnostic solution.
Just add some created_at_month and created_at_year columns to your model, along with this callback :
after_create :denormalize_created_at
def denormalize_created_at
assign_attributes created_at_month: created_at.month,
created_at_year: created_at.year
save validate: false
end
Now you can do:
Rails < 4 :
date = params[:date].to_date
Post
.where(Post.arel_table[:created_at_month].not_eq date.month)
.where(created_at_year: date.year)
Rails 4+ :
date = params[:date].to_date
Post.not(created_at_month: date.month).where(created_at_year: date.year)
mysql has a MONTH function to get the month of a datetime column.
Post.where("MONTH(created_at) != ?", params[:date].to_date.month)

Resources