Calculate Month Statistics - ruby-on-rails

I have a donations table where I'm trying to calculate the total amount for each month. For months without without any donations, I'd like the result to return 0.
Here's my current query:
Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
which returns:
{7=>220392, 8=>334210, 9=>475188, 10=>323661, 11=>307689, 12=>439889}
Any ideas how to grab the empty months?

Normally you'd left join to a calendar table (or generate_series in PostgreSQL) to get the missing months but the easiest thing with Rails would be to merge your results into a Hash of zeroes; something like this:
class Donation
def self.by_month
h = Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
Hash[(1..12).map { |month| [ month, 0 ] }].merge(h)
end
end
then just call the class method, h = Donation.by_month, to get your results.

In addition to mu is too short answer, in Rails 3.2.12 did not work for me, ActiveRecord returns the keys as strings:
h = Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
Which returns:
{"7"=>220392, "8"=>334210, "9"=>475188, "10"=>323661, "11"=>307689, "12"=>439889}
So when I merge the hash with zeros:
{1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0, 7=>0, 8=>0, 9=>0, 10=>0, 11=>0, 12=>0, "7"=>220392, "8"=>334210, "9"=>475188, "10"=>323661, "11"=>307689, "12"=>439889}
The little fix (to_s):
Hash[(1..12).map { |month| [ month.to_s, 0 ] }].merge(h)

Related

How to merge hash with activerecord relations in controller / scope

I have Item model(table) with column [id,name,notes]. then I have hash lets call it stock with column [id_of_item,total_stock],
when I do query in controller I would like to join the hash into the table as additional column so I can show the total_stock of the item.
I prefer not to use map/each (looping through all the items since the items table has thousand records. I still don't know whether this possibly or not, thank you.
if your stock is
[[1, "total_stock_1"], [2, "total_stock_2"]]
you should use
stock = Hash[[[1, "total_stock_1"], [2, "total_stock_2"]]]
to translate your hash to this style
stock = {1 => "total_stock_1", 2 => "total_stock_2"}
stock = {1 => "total_stock_1", 2 => "total_stock_2"}
#items = Item.all.map{|item| item.attributes.merge({total_stock: stock[item.id]})}
# the output will be a json not a ActiveRecordRelation
[
{:id => 1, :name => 'item1', :notes => xxx, :total_stock => "total_stock_1"},
{:id => 2, :name => 'item2', :notes => yyy, :total_stock => "total_stock_2"}
]
You can do this in controller:
#items = Item.all
render json: #items.map{|item| {'item': item.as_json.merge stock.select{|item| item['id_of_item'] == item.id}['total_stock']} }}

How to collect in Rails?

baza_managers = BazaManager.find(:all,
:conditions => ["or_unit_id != ?", 1]).collect {
|mou| [mou.email, mou.or_unit_id]}
respondent_emails = Respondent.find(:all).collect {|r| r.email }
ERROR:
from lib/scripts/baza_sync.rb:26:in `each'
from lib/scripts/baza_sync.rb:26
26 line ↓
baza_managers.each do |moi|
if !respondent_emails.include?(moi)
Respondent.create(:email => moi, :user_id => 1, :respondent_group_id => moi)
end
end
ERROR I GET:
undefined method `email' for ["vadasd#test.test.com", 8]:Array (NoMethodError)
I don't know why I'm getting this error.
try with:
baza_managers = BazaManager.find(:all,
:conditions => ["or_unit_id != ?", 1]).collect {
|mou| [mou.email, mou.or_unit_id]}
respondent_emails = Respondent.find(:all).collect {|r| r.email }
baza_managers.each do |moi|
if !respondent_emails.include?(moi[0])
Respondent.create(:email => moi[0], :user_id => 1, :respondent_group_id => moi[1])
end
end
Fix your code with following:
if !respondent_emails.include?(moi[0])
Respondent.create(:email => moi[0], :user_id => 1, :respondent_group_id => moi[1])
end
I would think there is at least one error not in the way you are using collect but in the logic you write on the last lines when you go through the baza_managers array.
With this code the condition respondent_emails.include?(moi) will be always false because respondent_emails is an array of email addresses but moi is an array like ["vadasd#test.test.com", 8] so they will never match.
I think this mistake made you make an error in the line :
Respondent.create(:email => moi, :user_id => 1, :respondent_group_id => moi)
Because this line will be evaluate as (for example) :
Respondent.create(:email => ["vadasd#test.test.com", 8], :user_id => 1, :respondent_group_id => ["vadasd#test.test.com", 8])
Which is probably not what you want.
Last, I would suggest you to read the debugger rails guide, I often use debugger to figure out where and what is the problem in this kind of code and error.
I would rewrite your code as follows:
baza_managers = BazaManager.all(:conditions => ["or_unit_id != ?", 1]).
collect { |mou| [mou.email, mou.or_unit_id]}
respondent_emails = Respondent.find(:all).collect {|r| r.email }
baza_managers.each do |email, unit_id|
unless respondent_emails.include?(email)
Respondent.create(:email => email, :user_id => 1,
:respondent_group_id => unit_id)
end
end
This solution can be further optimized by using OUTER JOIN to detect missing Respondents
BazaManager.all(
:include => "OUTER JOIN respondents A ON baza_managers.email = A.email",
:conditions => ["baza_managers.or_unit_id != ? AND A.id IS NULL", 1]
).each do |bm|
Respondent.create(:email => bm.email, :respondent_group_id => bm.or_unit_id,
:user_id => 1)
end
The solution can be made elegant and optimal by adding associations and named_scope.
class BazaManager
has_many :respondents, :foreign_key => :email, :primary_key => :email
named_scope :without_respondents, :include => :respondents,
:conditions =>["baza_managers.or_unit_id != ? AND respondents.id IS NULL", 1]
end
Now the named_scope can be used as follows:
BazaManager.without_respondents.each do |bm|
Respondent.create(:email => bm.email, :respondent_group_id => bm.or_unit_id,
:user_id => 1)
end

rails data aggregation

I have to create a hash of the form h[:bill] => ["Billy", "NA", 20, "PROJ_A"] by login where 20 is the cumulative number of hours reported by the login for all task transactions returned by the query where each login has multiple reported transactions. Did I do this in a bad way or this seems alright.
h = Hash.new
Task.find_each(:include => [:user], :joins => :user, :conditions => ["from_date >= ? AND from_date <= ? AND category = ?", Date.today - 30, Date.today + 30, 'PROJ1']) do |t|
h[t.login.intern] = [t.user.name, 'NA', h[t.login.intern].nil? ? (t.hrs_per_day * t.num_days) : h[t.login.intern][2] + (t.hrs_day * t.workdays), t.category]
end
Also if I have to aggregate this data not just by login but login and category how do I accomplish this?
thanks,
ash
I would make this
h[:bill] => ["Billy", "NA", 20, "PROJ_A"]
a hash like so
{ :user => t.user.name, :your_key_name => 'NA', :cumulative_hours => 20, :category => 'PROJ_A' }
so the values are accessible with keys instead of element indexes which becomes a bit hard to see when you are not iterating through a array
To access the data by user and category you can do some thing like this
user_hash = {}
Task.find_each(:include => [:user], :joins => :user, :conditions => ["from_date >= ? AND from_date <= ? AND category = ?", Date.today - 30, Date.today + 30, 'PROJ1']) do |task|
user_hash[task.login.intern] ||= {}
user_hash[task.login.intern][task.category] = { :user => task.user.name, :your_key_name => 'NA', :cumulative_hours => cumulative_hours(user_hash, task), :category => task.category }
end
def cumulative_hours(user_hash, task)
if user_hash[task.login.intern] && user_hash[task.login.intern][task.category]
return user_hash[task.login.intern][task.category][:cumulative_hours] + (task.hrs_day * task.workdays)
else
return task.hrs_per_day * task.num_days
end
end
For readability reasons I have added meaningful variable names and also created a method to calculate cumulative_hours to keep the code clear, separate code concern and to follow Single Responsibility Principle.

String to Array and Hash with Regexp

I would like to turn a string with opening hours like this:
"Monday-Friday>10:00-18:00;Saturday>12:00-17:00;Sunday>12:00-15:00"
Into this:
[ {:period => "Monday-Friday", :hours => "10:00-18:00"}, {:period => "Saturday", :hours => "12:00-17:00"}, {:period => "Sunday", :hours => "12:00-15:00"} ]
I'm trying it with the String.scan() method but can't figure out the Regexp.
Also if you have any suggestions of how to do it in reverse the best way (i.e. when getting the opening hours from a form.)
Update - Thank you all found perfect solutions! Right now I'm using (thanks kejadlen):
str.scan(/([\w-]+)>([\d:-]+)-([\d:]+)/).map { |(p,o,c)| {:period => p, :opens => o, :closes => c} }
But now how about reversing it =) So given:
[ {:opens=>"10:00", :closes=>"18:00", :period=>"Monday-Friday"},
{:opens=>"12:00", :closes=>"17:00", :period=>"Saturday"},
{:opens=>"12:00", :closes=>"15:00", :period=>"Sunday"} ]
I want to merge it to:
"Monday-Friday>10:00-18:00;Saturday>12:00-17:00;Sunday>12:00-15:00"
If you prefer one-liners:
s = "Monday-Friday>10:00-18:00;Saturday>12:00-17:00;Sunday>12:00-15:00"
s.split(/;/).map{|i| Hash[[[:period, :hours], i.split(/>/)].transpose]}
# or
s.split(/;/).map{|i| p, h = i.split(/>/); {:period => p, :hours => h}}
#=> [{:period=>"Monday-Friday", :hours=>"10:00-18:00"}, {:period=>"Saturday", :hours=>"12:00-17:00"}, {:period=>"Sunday", :hours=>"12:00-15:00"}]
Edit:
Regarding the reverse, this should do the job:
a.map{|i| "#{i[:period]}>#{i[:opens]}-#{i[:closes]}"}.join(';')
=> "Monday-Friday>10:00-18:00;Saturday>12:00-17:00;Sunday>12:00-15:00"
this is how I would do it
str="Monday-Friday>10:00-18:00;Saturday>12:00-17:00;Sunday>12:00-15:00"
periods = str.split(';')
#=> ["Monday-Friday>10:00-18:00", "Saturday>12:00-17:00", "Sunday>12:00-15:00"]
period_array=[]
periods.each do |period|
period_with_hours = period.split('>')
period_array << {:period => period_with_hours.first, :hours => period_with_hours.last}
end
period_array
#=> [{:period=>"Monday-Friday", :hours=>"10:00-18:00"}, {:period=>"Saturday", :hours=>"12:00-17:00"}, {:period=>"Sunday", :hours=>"12:00-15:00"}]
Try this:
String S = ([^\>]*)\>([^\;]*)\;
String T = " {:period => $1, :hours => $2}, "
originalString.replaceAll(S,T);
Might have to play with the regexp a little more but that should about do it.
Edit - Well, you asked for the answer in the context of ruby and I gave you the Java answer but the regular expression should work anyway...
This looks like it works
the_input.split(';').collect{|pair|
period, hours = pair.split('>')
{:period => period, :hours => hours}
}
=> [{:hours=>"10:00-18:00", :period=>"Monday-Friday"}, {:hours=>"12:00-17:00", :
period=>"Saturday"}, {:hours=>"12:00-15:00", :period=>"Sunday"}]
str.scan(/([\w-]+)>([\d:-]+)/).map {|(p,h)| {:period => p, :hours => h }}

rails active record - complicated conditions clause

this works:
ids = [1,2]
varietals = Varietal.find(:all, :conditions => [ "id IN (?)",ids])
But what I want to do is that plus have a condition of: deleted => false
varietals = Varietal.find(:all, :conditions =>{ :deleted => false})
any ideas?
am i going to have to use find_by_sql?
I would handle this with a named_scope to communicate intent and foster re-use:
named_scope :undeleted,
:conditions => { :deleted => false }
Then you can simply use:
varietals = Varietal.undeleted.find([1,2])
You can do it a few ways, but this is the most straight forward:
varietals = Varietal.find( [1,2], :conditions => { :deleted => false })
You can see in the docs that the first parameter of find can take an integer or an array.
ids = [1,2]
varietals = Varietal.find(:all, :conditions => {:id => ids, :deleted => false})
This should work, haven't tested it though.
From the docs:
An array may be used in the hash to
use the SQL IN operator:
Student.find(:all, :conditions => { :grade => [9,11,12] })

Resources