Save the most recent months of a hash - ruby-on-rails

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}]

Related

How to get records created in each month in rails

Looking for a way to get all the records that are created in each month for a table
For example i need to know how to get a result like:
January: 6,
Feb: 9,
March: 10
Ideally i'm looking at using the created_at field in the database to compare against.
You can use GROUP BY and COUNT from within SQL to efficiently retrieve the data. Rails offers various options here to build an SQL query which performs aggregations and calculations with ActiveRecord::Calculations.
Assuming you have a model named Record for your records and you use MySQL / MariaDB for your database, this can be used to get the number of records per month:
records_per_month = Record.group('EXTRACT(YEAR_MONTH FROM created_at)').count
This will return a hash of Integers (corresponding to the year and month of the group so that e.g. records in May 2022 will groups under the key 202205) and the number of records within this month as values.
From your example, this would be
{
202201 => 6,
202202 => 9,
202203 => 10
}
If desired, you can then further "format" the keys, e.g.
records_per_month.transform_keys! do |year_month|
Date.strptime(year_month.to_s, '%Y%m').strftime('%B %Y')
end
Here, we parse year-month integer as a date with Date.strptime and format the date with Date#strftime to show the month name and year, e.g. "February 2022".
Imagine you have a Users table (my Rails application has one), like this:
id
name
.
.
.
created_at
updated_at
You could use this code, which would return a hash of months with the count:
users = User.all
users.group_by {|u| u.created_at.strftime("%B")}.transform_values {|v| v.count}
Returns something like:
{"September"=>33,
"August"=>1,
"October"=>1,
"February"=>55,
"January"=>185,
"May"=>4,
"December"=>145,
"June"=>8,
"November"=>19,
"March"=>51,
"April"=>27,
"July"=>5}
Explanation
created_at.strftime("%B")
This converts the date to a Month, using strftime
users.group_by {|u| u.created_at.strftime("%B")}
Creates a hash that groups the user records by the Month name, using group_by
.transform_values {|v| v.count}
Instead of a collection of records, we just want the count. We leave the key alone in the hash, and use transform_values to count the values.

Group by decade with counts

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.

filtering and segmentation of a child object

My user can have many questions, however the questions are asked in different frequencies. Like weekly, biweekly, monthly, quarterly. Now I store the frequency of a Question in a QuestionFrequency model. That accepts frequency:string and begins:string.
The values accepted for frequency are:
weekly
biweekly
monthly
quarterly
now I use this together with the begins to understand the setting. So begins accepts:
if its biweekly I note down the week number if wants it to start
(thus I can check if that week number is odd or even)
if it's monthly it saves "end" or "beginning" thus I can check if its beginning of month with rails.
quarterly it saves "end" or "beginning"
Thus I can call
question.question_frequency.frequency
f.ex to get one of the 4 accepted values. Now what I'm trying to do is create a grouped list of all questions that might be available to the User in this week.
I have a method in my user model called all_questions, which job it is to get all questions that is relevant to a user "this" week.
# Collection of Users weekly questions
def all_questions
questions
end
now how can I filter "questions" to get things like
if biweekly.odd? and Time.zone.now.strftime("%V").odd?
then add that question whilst if one is odd || even then we don't want that question this week.
I would handle it differently.
Remove the QuestionFrequency model.
Add a frequency column to Question as an integer and use Rails' Enum method to define the frequency names.
Add a valid_at date/datetime column to the Question model and have it set to the next valid date (either 1 week from now, 2 weeks, 1 month, etc.) depending on the frequency.
Now, once a question is shown to the user (or when it's answered), have the valid_at column update for the question according to its frequency:
##question.rb example
enum frequency: [:weekly, :biweekly, :monthly]
before_save :update_valid_at
def update_valid_at
if weekly?
self[:valid_at] = 1.week.from_now
elsif biweekly?
self[:valid_at] = 2.weeks.from_now
elsif monthly?
self[:valid_at] = 1.month.from_now
end
end
This way, you can change your all_questions to:
def all_questions
questions.where('valid_at < ?', Date.today)
end

Rails 4 where,order,group,count include zero's - postgresql

Here is my query:
User.where("created_at >= ? AND created_at <=?", date1,date2).order('DATE(created_at) DESC').group("DATE(created_at)").count
and I get output as:
{Thu, 15 May 2014=>1}
But I want to get output as 0 for the rest of the days. For ex
{Thu, 15 May 2014=>1,Fri, 15 May 2014=>0}
What I want to get is Users created in a date range, ordered and grouped by created_at and number of such Users for each day. When no users are there for a particular day it should return 0, which the current query doesn't return.
I agree with Himesh
Try this:
User.where("created_at >= ? AND created_at <=?", date1,date2).order('DATE(created_at) DESC').group("DATE(created_at)").count
Which will give:
{Thu, 15 May 2014=>1}
Get the date range as hash initialized to 0:
hash1 = Hash[(date1..date2).collect { |v| [v, 0] }]
Now merge the two hashes:
hash = hash1.merge(hash)
Do remember to merge hash with hash1 and not the vice-versa, because we want to overwrite the value of keys from hash to hash1
Although it doesn’t seem to be well known, Ruby hashes have a “default value” feature that will help you do what you want in a better way. Another way of expressing what you’re trying to do is this:
Create a hash that gives 0 for any key within my data range unless I have stored some other value under that key.
You can accomplish exactly this using a block to establish the default value of the hash:
#data = Hash.new do |h, key|
if (date1..date2).include?(key)
0
else
nil
end
end
Or, more succinctly:
#data = Hash.new {|h, key| (date1..date2).include?(key) ? 0 : nil }
Then load your data from the database and merge it into this hash:
#data.merge! User.where(…).group(…).count
Note: Your order() is irrelevant because you are grouping. It will probably get optimized away, but you can simply leave it off and get the same result.
Now if you do #data[foo]:
if foo is not comparable to a date you will get nil
if foo is outside the date range you will get nil
if foo is within the date range but no data was returned by your query you will get 0
if foo is a date with data in the database you will get the correct count
This method is preferable to pre-loading the hash because it will work efficiently (in time and space) even for very large date ranges. And arguably it is more straightforward too.
I created a gem exactly for this - check out Groupdate.
Perhaps, this is a bit difficult as if any user is not created on 16th May then there would be no record of 16th May as created_at in the DB and group by query result wont contain any value for 16th May.
Probably you will have to handle this in ruby. Or what you can do is check if a particular date is present as key in the hash_result, and if it is not present then user count is 0 by default.
Hope this helps :)

How to use where clause on methods rather than database columns?

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

Resources