Ruby method for values from all associations - ruby

This method works, but I'm sure the performance could be greatly improved. Also, I'm realizing how fun and awesome it is to take smelly code like this, and rubify it. But I need a little more help to get my Ruby skills to the level to refactor something like this.
An objective can have "preassign" objectives. These are pre-requisites that must be completed before the a student can try the objective in question.
ObjectiveStudent is the join model between an objective and a student. It has a method called "points_all_time" that finds the student's best score on that objective.
The check_if_ready method is the one that I'm trying to refactor in this question. It also belong to the ObjectiveStudent model.
It needs to check whether the student has passed ALL of the preassigns for a given objective. If so, return true. Return false if the student has a less-than-passing score on any of the preassigns.
def check_if_ready
self.objective.preassigns.each do |preassign|
obj_stud = self.user.objective_students.find_by(objective_id: preassign.id)
return false if obj_stud.points_all_time < 7
end
return true
end
Right now I suspect this method is making too many calls to the database. What I'm really hoping to find is some way to look at the scores for the pre-reqs with a single db call.
Thank you in advance for any insight.

The following should work for you:
def is_ready?
user.objective_students
.where(objective_id: objective.preassigns.select(:id))
.none? { |obj_stud| obj_stud.points_all_time < 7 }
end
We collect all the objective_students for the user where the objective_id is in the list of objective.preassigns ids. This results in one 1 query being executed.
Then we use Enumerable#none? to make sure that none of the objective_students have points_all_time less than 7.
You could also use the inverse .all? { |obj_stud| obj_stud.points_all_time >= 7 } if you wanted

One way you could "rubify" this method is to rewrite the signature as:
def is_ready?
It is common practice to append ? to functions that return a boolean value in Ruby. (Note: I also don't really see a reason to have the word 'check' in the declaration, but that's just an opinion).
Furthermore, if objective_id is the primary key for the objective_students model, you can simply write objective_students.find(preassign.id) instead of the find_by method.
I would also suggest having a separate method for returning a student's points (especially since I suspect you will need to get a student's points more than just once) :
def getPoints(preAssignId)
return self.user.objective_students.find_by(objective_id: preAssignId).points_all_time
end
Then your main method can be written in a more clear, self-describing manner as:
def is_ready?
self.objective.preassigns.each {|preassign| return false if getPoints(preassign) < 7 }
return true
end

Related

Using Ruby to solve a quiz

So I found this quiz on a website that I was excited to solve with my newly acquired Ruby skills (CodeAcademy, not quite finished yet).
What I want to do is make an array with 100 entries, all set to "open". Then, I planned to create a method containing a for loop that iterates through every nth entry of the array and changes it to either "open" or "closed", based on what it was before. In the for loop, n should be increased from 1 to 100.
What I have so far is this:
change_state = Proc.new { |element| element == "open" ? element = "closed" : element = "open" }
def janitor(array,n)
for i in 1..n
array.each { |element| if array.index(element) % i == 0 then element.change_state end }
end
end
lockers = [*1..100]
lockers = lockers.map{ |element| element = "closed" }
result = janitor(lockers,100)
When trying to execute I receive an error saying:
undefined method `change_state' for "closed":String (NoMethodError)
Anybody an idea what is wrong here? I kinda think I'm calling the "change_state" proc incorrectly on the current array element.
If you know the quiz, no spoilers please!
As you have implemented change_state, it is not a method of any class, and definitely not one attached to any of the individual elements of the array, despite you using the same variable name element. So you cannot call it as element.change_state.
Instead, it is a variable pointing to a Proc object.
To call the code in a Proc object, you would use the call method, and syntax like proc_obj.call( params ) - in your case change_state.call( element )
If you just drop in that change, your error message will change to:
NameError: undefined local variable or method `change_state' for main:Object
That's because the change_state variable is not in scope inside the method, in order to be called. There are lots of ways to make it available. One option would be to pass it in as a parameter, so your definition for janitor becomes
def janitor(array,n,state_proc)
(use the variable name state_proc inside your routine instead of change_state - I am suggesting you change the name to avoid confusing yourself)
You could then call it like this:
result = janitor(lockers,100,change_state)
Although your example does not really need this structure, this is one way in which Ruby code can provide a generic "outer" function - working through the elements of an array, say - and have the user of that code provide a small internal custom part of it. A more common way to achieve the same result as your example is to use a Ruby block and the yield method, but Procs also have their uses, because you can treat them like data as well as code - so you can pass them around, put them into hashes or arrays to decide which one to call etc.
There may be other issues to address in your code, but this is the cause of the error message in the question.

ruby method with for loop, unexpected return

I have a model MyModel with a method to return a specific record (see logic below).
def self.find_future_rec #note2
rec = find(rand(MyModel.count)+1) #note1
while rec.nil? | (rec.expdate<Date.today)
rec = find(rand(MyModel.count)+1)
end
return rec
end
Every record of MyModel class has a variable expdate of Date class. (I know this is a horrible way to find a record, this is more for my own edification and also some test code.)
This method will iterate through several undesirable records before finding an appropriate record, but the record returned is always the one found at note 1, the first record queried. Is there some lazy assignment thing going on here?
I had to add self at note2 to be able to call the method in a static context. Is this the correct interpretation?
The find method never returns nil when you give it a single ID to find: if it can't find the record you're asking for, it raises an ActiveRecord::RecordNotFound exception. So, rec is always non-nil and rec.nil? is always false. That means that your loop is really like this:
while rec.expdate < Date.today
If your loop is always returning the rec from #note1 then you're never entering the while loop at all and you're always getting a desirable MyModel on the first try.
Other points to consider:
Sometimes things get deleted so Model.count + 1 is not necessarily the maximum ID.
find raises an exception to indicate failure so you need to rescue ActiveRecord::RecordNotFound rather than check for nil.
You are using | which is a boolean OR. Try using or or ||.
Adding self to make a method a class method ("static" is a decent approximation) is indeed one of the correct ways to approach this.

Inject with multiple block parameters

The Sunspot gem for Solr has a method that requires a block with 2 elements:
search.each_hit_with_result do |hit,result|
and I'm using it to build a new hash of results like so:
results = Hash.new
search.each_hit_with_result do |hit,result|
results[result.category.title] = hit.score
end
This is cool and everything but I can't help thinking there is a more 'ruby' way of doing it and I've been looking at the awesome inject method. I think something like the following should be possible but I can't get it to syntactically work. Anyone got any ideas?
search.each_hit_with_result.inject({})
{|newhash,|hit,result||newhash[result.category.title]=hit.score}
I believe that method looks like what do you want:
search.each_hit_with_result.inject({}) { |new_hash, current| new_hash[current[0]] = current[1]; new_hash }
Hope its help you.
Object#enum_for is designed exactly for this:
hit_results = search.enum_for(:each_hit_with_result)
results = Hash[hit_results.map { |hit, res| [res.category.title, hit.score] }]
In my opinion, code should never expose each_xyz methods, they promotes smelly imperative code (as you rightly detected). That kind of methods were understandable when there were no enumerators and you needed to return data lazily, but now it should be considered an anti-pattern. They should return an enumerable or enumerator and let the user decide how to use it.

Overriding default range output

Right now the code below produces the output below it, but how would I override the default output to a more logical one for my given situation. I understand that I could just append the string "Hz" after the range but I want to incorporate this into a module which can be included to the Range class when needed or for use with refinements.
Code:
("20Hz"..."40Hz").each { |hz| p hz }
Output:
"20Hz"
"20Ia"
"20Ib"
...etc
Wanted output:
"20Hz"
"21Hz"
"22Hz"
...etc
This is absolutely a bad idea, but just for the sake of experimenting:
class String
alias_method :succ_orig, :succ
def succ
self.gsub(/\d+/, &:succ_orig)
end
end
p ("20Hz".."40Hz").to_a
#=> ["20Hz", "21Hz", "22Hz", "23Hz", "24Hz", "25Hz", "26Hz", "27Hz", "28Hz", "29Hz", "30Hz", "31Hz", "32Hz", "33Hz", "34Hz", "35Hz", "36Hz", "37Hz", "38Hz", "39Hz", "40Hz"]
As you can see, it is not the Range class that should be altered, but String#succ method.
But in real project, you better create a class for your Hertz-strings and define its succ method appropriately.
I think its quite simple.
("20"..."40").each { |hz| p hz + 'Hz'}
I would recommend creating your own function or class for this rather that changing the way in which Ruby ranges behave. There is probably a lot of other code that depends on ranges working in a specific way, and changing the range definition would result in that code breaking. You might want to aim for something like this:
HzRange.new("20Hz", "40Hz").each{ |hz| p hz }
The creation of the HzRange class is up to you, but you should probably delegate to the Array or Range object so that you can inherit some default behavior like Enumerable.

Defining Lua methods as initialization

In the Lua language, I am able to define functions in a table with something such as
table = { myfunction = function(x) return x end }
I wondered if I can created methods this way, instead of having to do it like
function table:mymethod() ... end
I am fairly sure it is possible to add methods this way, but I am unsure of the proper name of this technique, and I cannot find it looking for "lua" and "methods" or such.
My intention is to pass a table to a function such as myfunction({data= stuff, name = returnedName, ?method?init() = stuff}).
Unfortunately I have tried several combinations with the colon method declaration but none of them is valid syntax.
So...anyone here happens to know?
Sure: table:method() is just syntactic sugar for table.method(self), but you have to take care of the self argument. If you do
tab={f=function(x)return x end }
then tab:f(x) won't work, as this actually is tab.f(tab,x) and thus will return tab instead of x.
You might take a look on the lua users wiki on object orientation or PiL chapter 16.

Resources