I have a table of Album's that has a date column named release_date.
I want to get a list of all the decades along with the number of albums released in that decade.
So, the output might be something like:
2010 - 11
2000 - 4
1990 - 19
1940 - 2
Ruby 2.3.1 w/ Rails 5 on Postgres 9.6, FWIW.
This is essentially a followup question to a previous one I had: Group by month+year with counts
Which may help with the solution...I'm just not sure how to do the grouping by decade.
Using Ruby for processing db data is inefficient in all senses.
I would suggest doing it on the database level:
Album.group("(DATE_PART('year', release_date)::int / 10) * 10").count
What happens here, is basically you take a year part of the release_date, cast it to integer, take it's decade and count albums for this group.
Say, we have a release_date of "2016-11-13T08:30:03+02:00":
2016 / 10 * 10
#=> 2010
Yes, this is pretty similar to your earlier question. In this case, instead of creating month/year combinations and using the combinations as your grouping criteria, you need a method that returns the decade base year from the album year.
Since you have a pattern developing, think about writing the code so it can be reused.
def album_decades
Album.all.map { |album| album.release_date.year / 10 * 10 }
end
def count_each(array)
array.each_with_object(Hash.new(0)) { |element, counts| counts[element] += 1 }
end
Now you can call count_each(album_decades) for the result you want. See if you can write a method album_months_and_years that will produce the result you want from your earlier question by calling count_each(album_months_and_years).
There are more than one possible solution to your problem, but I would try:
Add a new column to the Album table, called decade. You can use a migration for this porpoise.
Create a callback (its like a trigger, but in the programmer side) that set the decade value before saving the Album in the DB.
Finally you can use this useful query to group the Albums by decade. In your case would be Album.group(:decade).count wich would give you a hash with the numbers of Albums by decade.
...
Profit ?
Jokes aside, the callback should be something like:
class Album < ActiveRecord::Base
# some code ...
before_save :set_decade # this is the 'callback'
# ...
private
def set_decade
self.decade = self.release_date.year / 10
end
Then, if you use the step 3, it would return something like:
# => { '195' => 7, '200' => 12 }
I did not test the answer, so try it out and tell me how it went.
Related
I have Order model in which I have datetime column start and int columns arriving_dur, drop_off_dur, etc.. which are durations in seconds from start
Then in my model I have
class Order < ApplicationRecord
def finish_time
self.start + self.arriving_duration + self.drop_off_duration
end
# other def something_time ... end
end
I want to be able to do this:
Order.where(finish_time: Time.now..(Time.now+2.hours) )
But of course I can't, because there's no such column finish_time. How can I achieve such result?
I've read 4 possible solutions on SA:
eager load all orders and select it with filter - that would not work well if there were more orders
have parametrized scope for each time I need but that means soo much code duplication
have sql function for each time and bind it to model with select() - it's just pain
somehow use http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute ? But I have no idea how to use it for my case or whether it even solves the problem I have.
Do you have any idea or some 'best practice' how to solve this?
Thanks!
You have different options to implement this behaviour.
Add an additional finish_time column and update it whenever you update/create your time values. This could be done in rails (with either before_validation or after_save callbacks) or as psql triggers.
class Order < ApplicationRecord
before_validation :update_finish_time
private
def update_finish_time
self.finish_time = start_time + arriving_duration.seconds + drop_off_duration.seconds
end
end
This is especially useful when you need finish_time in many places throughout your app. It has the downside that you need to manage that column with extra code and it stores data you actually already have. The upside is that you can easily create an index on that column should you ever have many orders and need to search on it.
An option could be to implement the finish-time update as a postgresql trigger instead of in rails. This has the benefit of being independent from your rails application (e.g. when other sources/scripts access your db too) but has the downside of splitting your business logic into many places (ruby code, postgres code).
Your second option is adding a virtual column just for your query.
def orders_within_the_next_2_hours
finishing_orders = Order.select("*, (start_time + (arriving_duration + drop_off_duration) * interval '1 second') AS finish_time")
Order.from("(#{finishing_orders.to_sql}) AS orders").where(finish_time: Time.now..(Time.now+2.hours) )
end
The code above creates the SQL query for finishing_order which is the order table with the additional finish_time column. In the second line we use that finishing_orders SQL as the FROM clause ("cleverly" aliased to orders so rails is happy). This way we can query finish_time as if it was a normal column.
The SQL is written for relatively old postgresql versions (I guess it works for 9.3+). If you use make_interval instead of multiplying with interval '1 second' the SQL might be a little more readable (but needs newer postgresql version, 9.4+ I think).
I've built a Rails app that logs employee time (clockin/clockout) and calculates total hours, allows exports to CSV/PDF, search timecards based on dates, etc.
What I'm really wanting to do is to implement payroll periods via a scope of some sort of a method.
The payroll period begins on a Sunday and ends on a Saturday 14 days later. What would be the best way to write a scope like this? Also is it possible to split the weeks into two for the payroll period?
I wrote these scopes but they are flawed:
scope :payroll_week_1, -> {
start = Time.zone.now.beginning_of_week - 1.day
ending = Time.zone.now.end_of_week - 1.day
where(clock_in: start..ending)
}
scope :payroll_week_2, -> {
start = Time.zone.now.end_of_week.beginning_of_day
ending = Time.zone.now.end_of_week.end_of_day + 6.days
where(clock_in: start..ending)
}
These works if you are currently in a payroll period, but once you pass the end of the week, the scopes no longer work because I'm basing my timing off of Time.zone.now
Is there any way to actually do this? Even if I have to set some sort of static scope or value which says April 10 - 23 is payroll period 1, etc etc. I'm really not sure how to approach this problem and what might work. So far what I've written works in the current pay period but as time advances the scope drifts.
Any help would be greatly appreciated.
I think what you want is to create a scope, which can receive a start_day as a parameter:
scope :payroll_week_starting, -> (start_day) {
where(clock_in: start_day..(start_day + 1.week))
}
Then, in the future you will be able to call your scope with the first day of your pay period:
ModelName.payroll_week_starting(Date.parse('31/12/2015'))
UPDATE:
As per your comment, it seems that you're looking for a bit more information from an architectural perspective. It's pretty tough to help you without understanding your database architecture, so I'm just going to go from a high level.
Let's assume you have an Employee model and a Shift model with the clock_in and clock_out fields. You may also want a model called PayPeriod with the fields start_date and end_date.
Each Employee has_many :shifts and each PayPeriod has_many :shifts
You might add a couple of class methods on PayPeriod, so you can find and/or create the PayPeriod for any given datetime.
def self.for(time)
find_by("start_date < ? AND end_date > ?", time, time)
end
def self.create_for(time)
# yday is the number of days into the current year
period_start_yday = 14 * (time.yday / 14)
start_date = Date.new(time.year) + period_start_yday.days
next_year = Date.new(time.year) + 1.year
create(
start_date: start_date,
end_date: [start_date + 14.days, last_day_of_year].min,
)
end
def self.find_or_create_for(time)
for(time) || create_for(time)
end
The create_for logic is pretty complicated, but an example will help you understand:
Say I clocked in today May 17th, 2016, the yday for today is 138, if you use integer division (default for ruby) to divide by 14 (the length of your pay periods), you'll get 9. By multiplying that by 14 again, you'll get 126, which is the most recent yday divisible by 14. If you add that number of days to the beginning of this year, you'll get the begining of the PayPeriod. The end of the PayPeriod is 14 days after the start_date, but not rolling over to the next year.
What I would then do, is add a before_save callback to Shift to find or create the corresponding PayPeriod
before_save :associate_pay_period
def associate_pay_period
self.pay_period_id = PayPeriod.find_or_create_for(clock_in)
end
Then every PayPeriod will have a bunch of Shifts, and every Shift will belong to a PayPeriod.
If, for example, you wanted to get all of the shifts for a specific employee during a specific PayPeriod (to perhaps sum the hours worked for that PayPeriod) add a scope to Shift:
scope :for, -> (user) { where(user: user) }
And call pay_period.shifts.for(user)
UPDATE #2:
One other (much simpler) thought I had (if you don't want to create an actual PayPeriod model), would be to just add a method to the model that has clock_in (I'm going to refer to it as Shift):
def pay_period
clock_in.to_date.mjd / 14
end
Which will basically just boil down any clock_in time to an integer that represents a 14 day period. Then you can call
Shift.all.group_by { |shift| shift.pay_period }
If you need each pay_period to be contained within a single calendar year, you can do:
def pay_period
[clock_in.year, clock_in.to_date.yday / 14]
end
I have an array of hashes. Something like this...
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230}
{"date"=>"2013-05-21", "amount"=>900},]
I want to be able to save each months total amounts and then show the most recent 3 months to todays date and their total amount. Something like this...
Totals: 06-14 $230
07-14 $200
08-14 $250
I have this method but i am not sure how to get only the last 3 months to put in my database field and how to print it out.
def income_by_month
#payroll_transactions = current_user.transactions
#recent_payroll = #payroll_transactions.find_all {90.days.ago.to_date..Date.today}.map #finds transactions within 90 days
#amount_by_month = #recent_payroll.group_by { |t| t.date.to_date.month }.map do |month, transactions|
[month, transactions.sum(:amount)] #Groups transactions by month and adds month total
end.to_h
-EDIT-
I figured out a method to only get the transactions from the last 30 days I updated my method to show it. Now my question is how do I save the answer (Do i save it in one field as an Array?) and then how to show the answer in my view. Like I show it here. How do I print each key and value line by line in an order?
Totals: 06-14 $230
07-14 $200
08-14 $250
-EDIT-
Sorry my database is a mongoid db. And I want to save the most recent 3 months to todays date regardless of if an amount is available.
Let me start with a quick note on your code snippet:
group_by { |t| t.date.to_date.month }
Note that grouping objects by a single month does not take a year in count, so it would end summing up amounts for transactions of both 2012 and 2014 years in a one container. So what you really want is to group based on both month and year values.
Thinking of reducing the amount of redundant iterations through the input array (and using unnecessary aggregations), I've came to the following suggestion:
last_months = transactions.map{|i| Date.parse(i["date"]).strftime("%m-%Y")}.uniq.sort.last(3)
result = last_months.inject({}){|result, input| result[input] = 0; result}
transactions.inject(result) do |result, object|
# NOTE: we're already doing dates parsing and strftime two times here.
# In case you operate on Date objects themselves in your code, this is not the case.
# But the real perfomance measurement between summing all values up
# and strftiming more than once should be done additionally.
month = Date.parse(object["date"]).strftime("%m-%Y")
result[month] += object["amount"] if result[month]
result
end
# result now equals to {"06-2014"=>230, "07-2014"=>200, "08-2014"=>250}
First, we obtain those last three months (and years).
Next we create a hash to contain aggregated values with only those last months keys. At the end we sum up amount for only those transactions which seem to be one of the latter 3 months.
So, as long as ruby hashes (ruby v.1.9+) preserve the keys order, you can simply iterate over them to print out:
result.each{|k,v| puts "#{k}: #{v}"}
# 06-2014: 230
# 07-2014: 200
# 08-2014: 250
One last thing to note here is that doing this kind of aggregation inside of your server code is not quite efficient. Much more tempting option would be to move this calculations to your database layer.
ActiveSupport has some pretty slick date methods like Date#beginning_of_month:
require "date"
require "active_support/core_ext"
def process_transaction_group(month, transactions)
{
month: month.strftime("%Y/%m"),
total: transactions.map {|t| t["amount"] }.reduce(:+)
}
end
def process_transactions(transactions)
transactions
.group_by {|t| Date.parse(t["date"]).beginning_of_month }
.select {|month, _trxs| month < 3.months.ago }
.map {|month, trxs| process_transaction_group(month, trxs) }
end
###############
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230},
{"date"=>"2013-05-21", "amount"=>900}]
process_transactions(transactions)
#=> [{:month=>"2014/07", :total=>200}, {:month=>"2012/06", :total=>400}, {:month=>"2014/08", :total=>250}, {:month=>"2014/06", :total=>230}, {:month=>"2013/05", :total=>900}]
I have to update an age column based on the value in a date of birth column. There are thousands of records to update.
How do I do this using rails?
Is this the right way to do it?
User.update_all(:age => some_method);
def some_method
age = Date.today.year - dob.year
end
Yes, update_all is the right method but no, you can't do it like this. Your some_method will only get called once to set up a database call (I assume you're persisting to a database). You'll then get an error because dob won't be recognised in the scope of the User class.
You'll need to translate your date logic to SQL functions.
Something like (for mysql):
User.update_all("age = year(now()) -
year(dob) -
(DATE_FORMAT(now(), '%m%d') < DATE_FORMAT(dob, '%m%d'))")
(NB. the date_format stuff is so that you get the right age for people who's birthdays are later in the year than the current date - see this question for more details)
The other option is to use one of the batches functionality in rails.
User.where(some_condition).find_in_batches do |group_of_users|
# some logic
# e.g. group_of_users.update_all(:age => some_logic)
end
This would lock your db for less time. Note that you should pretty much always update with a condition in mind. I can't think of many cases you would want to update an entire table every time something happens.
There are a few options checkout the rails docs or the api.
your query is right.
There are many way to update record in a batch/lot.
But, I think that your query is best. Because it is rails query that will support every condition for all database.
for updating more than one attributes
Model.update_all(:column1 => value1, :column2 => value2, ........)
or
you can use :
Model.update_all("column1 = value1, column2 = value2, ........")
I have a Post model which includes a date_published. I want to display links on the index page to view all the posts of a certain year (a bit like where blogs have months you can view by)
The only way I can think of returning the years is by iterating over all of the posts and returning a unique array of the years that posts fall in. But this seems like a long way round. Does anyone have a better solution to this? Maybe it would be easier just creating new table so a Post belongs_to :year
Any suggestions?
You don't have to iterate over all of them in your Ruby code. You can use the :group option with count:
>> Post.count(:all, :group => "Year(date_published)")
=> [["2005", 5], ["2006", 231], ["2007", 810], ["2009", 3]]
And then build the links from that.
I'd use one of these:
a find_by_sql to get the distinct years, using whatever year-revealing functionality is available in your database. Would probably resolve against an index, which I'd expect you'd have;
or
Post.minimum(:date_published).year..Post.maximum(:date_published).year
I like the second one a lot, even though it doesn't account for the possibility that a year may have no posts!
Use an SQL search of the date time field (in my example it is updated_at)
years = ActiveRecord::Base.connection.select_all("SELECT DISTINCT DATE_FORMAT(updated_at, '%Y') AS updated_at FROM posts ORDER BY updated_at DESC")
This will return all the year values in the form
[{"updated_at"=>"2009"}, {"updated_at"=>"2008"}, {"updated_at"=>"1979"}, {"updated_at"=>"1922"}]
years.each { |y| puts y['updated_at'] }
2009
2008
1979
1924