rails sort collection by custom method - ruby

I have a method which takes ID of project and find all payments, do the math and the output is percentage of success (target amount vs collected)
def projectCollectedMoneyPerc(id)
#project= Project.find(id)
#collected = Payment.where('project_id = ? and confirmed = true', #project.id)
#col = #collected.sum(:amount)
#perc = ((#col.to_f / #project.amount) * 100).round(0)
end
now I need to find projects which have most % success. My idea was to call this method by sort_by but I have no idea how to put ID from collection to this sort
my collection is simple
#projects=Project.where('enabled = true and enddate > ?', Time.now)
thanks

I would define a method like to following in your model:
# in app/models/project.rb
has_many :payments
def collected_money_percentage
sum = payments.where(confirmed: true).sum(:amount)
(100.0 * sum / amount ).round
end
Then you cound use that method like this:
Project.where('enabled = true and enddate > ?', Time.now)
.sort_by(&:collected_money_percentage)
Please note that this first loads all matching record and then calculations the percentage in memory. It would probably be faster to calculate this values in your database:
Project.joins(:payments)
.where('enabled = true and enddate > ?', Time.now)
.group('projects.id')
.order('SUM(payments.amount) / projects.amount')

Related

Split a string into two separate attributes ruby

I am performing a screen grab to get football results and the score comes as a string, 2-2 for example. What I would ideally like to have is have that score split into home_score and away_score which is then saved into my model for each result
At the moment i do this
def get_results # Get me all results
doc = Nokogiri::HTML(open(RESULTS_URL))
days = doc.css('.table-header').each do |h2_tag|
date = Date.parse(h2_tag.text.strip).to_date
matches = h2_tag.xpath('following-sibling::*[1]').css('tr.report')
matches.each do |match|
home_team = match.css('.team-home').text.strip
away_team = match.css('.team-away').text.strip
score = match.css('.score').text.strip
Result.create!(home_team: home_team, away_team: away_team, score: score, fixture_date: date)
end
end
From some further reading i can see that you can use the .split method
.split("x").map(&:to_i)
so would i be able to do this
score.each do |s|
home_score, away_score = s.split("-").map(&:to_i)
Result.create!(home_score: home_score, away_score: away_score)
end
but how to integrate into my current setup is whats throwing me and thats even if my logic is correct, I still want the home_score and away_score to be assigned to the correct result
Thanks in advance for any help
EDIT
Ok so far the answer is no i cannot do it this way, after running the rake task I get an error
undefined method `each' for "1-2":String
The reason .each doesnt work is because each was a method of String in ruby 1.8 and it was removed in Ruby 1.9. i have tried each_char, which now saves some results and not others and when it does save home_score and away_score are not assigned correctly
Answer
As #seph pointed out the each was not needed, if it helps anyone else my final task looks like this
def get_results # Get me all results
doc = Nokogiri::HTML(open(RESULTS_URL))
days = doc.css('.table-header').each do |h2_tag|
date = Date.parse(h2_tag.text.strip).to_date
matches = h2_tag.xpath('following-sibling::*[1]').css('tr.report')
matches.each do |match|
home_team = match.css('.team-home').text.strip
away_team = match.css('.team-away').text.strip
score = match.css('.score').text.strip
home_score, away_score = score.split("-").map(&:to_i)
Result.create!(home_team: home_team, away_team: away_team, fixture_date: date, home_score: home_score, away_score: away_score)
end
end
end
No need for the each. Do this:
home_score, away_score = score.split("-").map(&:to_i)

Rails 3 way working with multiple if statements and form create/new

I have a problem to approach and not sure what the most appropriate method will be to make this work. Here the background to begin:
There are two models I am working with Procedures and Appointments. The Appointments model belongs_to the Procedures model and Procedures model has_many Appointments.
Now on the procedures model there are two key points to focus on, rather, two key columns.
attr_accessible :visits, :occurence
visits is the specific number of times to schedule the Appointment(s).
occurence is the frequency of the visits. An example would be visits: "5", occurence: "weekly"
So when I am submitting my form I would like to write a method that looks at both visits: "x" and occurence: ["weekly", "biweekly", "monthly"] to then create a if or a switch -- php does switch still looking into ruby version -- but I suspect there is an elegant way to write this up.
My current create method looks like this:
def create
#appointment = Appointment.new(params[:appointment])
set_variables
if #appointment.save
flash[:success] = "Appointment scheduled!"
redirect_to patient_path(#current_patient)
else
redirect_to patient_path(#current_patient)
flash[:error] = "Appointment Date and Time cannot be blank, please try again."
end
end
What would be the best way to tackle a) identifying occurence: ["weekly", "biweekly", "monthly"] and then processing visits: "x" based on something similar to:
if #appointment.occurence == "weekly"
(x-1).times do |n|
submit same params but change within params appointment_date: = ((#appointment.appointment_date) + (n+1).week.to_formatted_s(:db)
#appointment.save
end
end
...and so on and so forth using (n+1).month for monthly occurrence (n+2).day and for bi-weekly occurrence(s).
Thank you in advance, hope this clarifies things. Just one item to note, do I need to store in database visits: and occurence:, I suspect not but would like to be certain they are used when hitting the models_controller create function.
Here's a slightly less cluttered solution that should do what you need, though it also assumes that you get rid of the :appointment_date field and change :appointment_time to a DateTime field. For more info on DateTimes check out:
(Stackoverflow will only allow me to post 2 links because I'm a n00b so search "DateTime Ruby" on your favorite search engine for documentation for the Ruby and rails methods for DateTime)
Formatting DateTime to string for views: http://apidock.com/ruby/DateTime/strftime
Intro on using DateTimes in forms: http://guides.rubyonrails.org/form_helpers.html#using-date-and-time-form-helpers
#appointment = Appointment.new(params[:appointment])
set_variables
if #appointment.save
if #procedure.occurence == "WEEKLY"
multiplier = 7
elsif #procedure.occurence == "BIWEEKLY"
multplier = 14
else
multiplier = 30
end
#visits = #procedure.visits - 1
#visits.times do |n|
Appointment.create!(
:procedure_id => #appointment.procedure_id,
:patient_id => #appointment.patient_id,
:appointment_time => (#appointment.appointment_time + (multiplier * n).days),
:attendance => "SCHEDULED"
)
end
else
flash.now[:error] = "There appears to be an error, please try again."
render 'new'
end
Solved for the moment, rather crude--as is my current ruby skill set--but it seems to have done the job.
#appointment = Appointment.new(params[:appointment])
set_variables
#appointment.save
if #procedure.occurence == "BIWEEKLY"
#visits = #procedure.visits - 1
#visits.times do |n|
procedure_id = #appointment.procedure_id
patient_id = #appointment.patient_id
appointment_date = (#appointment.appointment_date +
((n+2)*2).days).to_formatted_s(:db)
appointment_time = #appointment.appointment_time
appointment_notes = #appointment.appointment_notes
attendance = "SCHEDULED"
#scheduled = Appointment.create(procedure_id: procedure_id,
patient_id: patient_id, appointment_date: appointment_date,
appointment_time: appointment_time,
appointment_notes: appointment_notes, attendance: attendance)
end
end
if #procedure.occurence == "WEEKLY"
#visits = #procedure.visits - 1
#visits.times do |n|
procedure_id = #appointment.procedure_id
patient_id = #appointment.patient_id
appointment_date = (#appointment.appointment_date +
(n+1).week).to_formatted_s(:db)
appointment_time = #appointment.appointment_time
appointment_notes = #appointment.appointment_notes
attendance = "SCHEDULED"
#scheduled = Appointment.create(procedure_id: procedure_id,
patient_id: patient_id, appointment_date: appointment_date,
appointment_time: appointment_time,
appointment_notes: appointment_notes, attendance: attendance)
end
end
if #procedure.occurence == "MONTHLY"
#visits = #procedure.visits - 1
#visits.times do |n|
procedure_id = #appointment.procedure_id
patient_id = #appointment.patient_id
appointment_date = (#appointment.appointment_date + (n+1).month).to_formatted_s(:db)
appointment_time = #appointment.appointment_time
appointment_notes = #appointment.appointment_notes
attendance = "SCHEDULED"
#scheduled = Appointment.create(procedure_id: procedure_id,
patient_id: patient_id, appointment_date: appointment_date,
appointment_time: appointment_time,
appointment_notes: appointment_notes, attendance: attendance)
end
end

Convert array of objects into number for inject sum

I am using Ruby 1.9.2, Rails 3.1. I have the following:
# review.rb
def calculate_rating
all_rating = Review.select("rating").where("reviewable_id = ?", self.reviewable_id)
all_rating.inject(:+)
end
# reviews_controller.rb
def create
#reviewable = find_reviewable
#review = #reviewable.reviews.where("user_id = ?", current_user).first
if #review.save
#review.calculate_rating
redirect_to :id => nil
else
flash[:error] = 'Error saving review. Please try again.'
redirect_to :id => nil
end
end
The idea behind this is that when a new review with rating is submitted and saved, it will find all ratings for all #reviewable, sum all the ratings and divide by the total number of ratings.
Problem that I am facing currently is this line: all_rating = Review.select("rating").where("reviewable_id = ?", self.reviewable_id) where all_rating returns an array of objects, like below:
[#<Review rating: #<BigDecimal:1050f0a40,'0.3E1',9(18)>>, #<Review rating: #<BigDecimal:1050f0928,'0.1E1',9(18)>>]
which I can't do any arithmetic calculation to it. I need it to be an array of numbers before I could use the inject to sum it and divide by the number of ratings.
Please advise me how I can get the inject to work. Many thanks!
AR/SQL (faster):
Review.select("rating").where(:reviewable_id => self.reviewable_id).sum(:rating)
Ruby (slower):
Review.select("rating").where(:reviewable_id => self.reviewable_id).map(&:rating).sum
How about just doing this:
def calculate_rating
all_rating = Review.select(:rating).where(:reviewable_id => reviewable_id).map(&:rating)
all_rating.inject(:+) # or you could just do all_rating.sum
end

How can I avoid running singular expressions against arrays in activerecord and rails 3?

I am sorry if I am asking the question poorly. I have a Rails 3.1 app with models (simplified) like so:
class Employee < ActiveRecord::Base
has_many :merged_children, :class_name => 'Employee', :foreign_key => "merge_parent_id"
has_many :timesheets
def total_time
merged_children.timesheets.in_range(range).hours_minutes.sum
end
end
class Timesheet < ActiveRecord::Base
belongs_to :employee
def in_range(range)
# filter records based on transaction_date in range
end
def hours_minutes
(hours + minutes/60.0).to_f
end
end
Note: The in_range method acts as a scope, essentially, and hours_minutes is a calculation. hours_minutes is valid for each timesheet record in the resulting dataset, and then total_time should sum those values and return the amount.
The "total_time" method is not working because employee.merged_children returns an array and timesheets is meant to run against a single Employee object.
Is there any way to structure the "total_time" so that it still sends one query to the db? It seems inelegant to iterate over the merged_children array, issuing a query for each. Not sure if a direct call to an Arel table would help or hurt, but I am open to ideas.
If we get it right, the resulting SQL should effectively look something like:
SELECT sum(hours + minutes/60.0)
FROM employees e1 join employees e2 on e1.id = e2.merge_parent_id join timesheets t on t.employee_id = e2.id
WHERE e1.id = [#employee.id] and t.transaction_date BETWEEN [#range.begin] and [#range.end]
Thanks so much!
The easiest thing here might be to add
has_many :children_timesheets, :through => :merged_children, :source => :timesheets
To your employee model,
Then (assuming in_range is actually a scope, or a class method that does a find)
children_timesheets.in_range(...)
Should be the collection of timesheets you're interested in and you can do something like
children_timesheets.in_range(...).collect(&:hours_minutes).sum
Untested with actual data.
range = ((1.day.ago)...(2.days.ago))
merge_parent = Employee.find(some_id)
Timesheet.where(:transaction_date => range)
.joins(:employee).where(:employees => {:merge_parent_id => merge_parent.id})
.sum('hours*60 + minutes')
(0.3ms) SELECT SUM(hours*60 + minutes) AS sum_id FROM "timesheets" INNER JOIN "employees" ON "employees"."id" = "timesheets"."employee_id" WHERE "employees"."merge_parent_id" = 1 AND ("timesheets"."created_at" >= '2011-12-13 03:04:35.085416' AND "timesheets"."created_at" < '2011-12-12 03:04:
Returns "0" for me. So hopefully it will return something nicer for you

I need to generate a random date, MM/DD/YYYY

I need to generate a random Date.
I do not need time in my calculation.
What I am trying to use is:
def date_rand from = 0.0, to = Time.now
Time.at(from + rand * (to.to_f - from.to_f))
end
This gets me close, but has a bunch other information I do not need. (time, zone, etc.)
If there is a way to get the date without all the other data I would appreciate some help on knowing it.
In Ruby 1.9, including the date library adds a #to_date method to the Time class (as well as a #to_datetime method). Ruby 1.8 has it too, but it's a private method.
require 'date'
def date_rand(from = 0.0, to = Time.now)
Time.at(from + rand * (to.to_f - from.to_f)).to_date
end
In Ruby 1.8, you could do something like this:
def date_rand(from = 0.0, to = Time.now)
time = Time.at(from + rand * (to.to_f - from.to_f))
Date.civil(time.year, time.month, time.day)
end
dmarkov's answer is fine. You can do the same with dates:
require 'date'
def date_rand(from = Date.new(1970,1,1), to = Date.today)
low, high = from.ajd.to_i, to.ajd.to_i
r = rand(high-low+1) + low
Date.jd(r)
end
From a blog post by Obie Fernandez.
class Time
def self.random(years_back=5)
year = Time.now.year - rand(years_back) - 1
month = rand(12) + 1
day = rand(31) + 1
Time.local(year, month, day)
end
end
This allows you to call Time.random. I'm presenting this as an alternate answer to your question and depending on how you're planning on using this, please be careful as monkey patching the standard lib classes isn't usually the best way to go about things if someone else is going to have to debug/support your code one of these days.

Resources