I have a table, with increasing values each row (year in the code below).
I have a target, that specifies a "threshold". The target is user defined, it can contain a value for one or multiple columns of the table. This means you never know how many columns are specified in the target.
I want to match the first row in the table, where the values in the row are greater than the values in the target. I currently have this:
class Target < ActiveRecord::Base
def loop_sheets(sheets, year_array)
result = nil
elements = self.class.column_names[1..-3].map(&:to_sym)
to_match = elements.select{|e| self.send(e) != nil }
condition = to_match.map do |attr|
"row[:#{attr}] > #{attr}"
end.join " and "
year_array.each do |year|
sheets.each do |sheet|
row = sheet.calculation.datatable.select { |r| r[:year] == year }.first
does_match = eval(condition)
if does_match
result = {
:year => row[:year],
:sheet_name => sheet.name
}
return result
end
end
end
return result
end
end
This works perfectly, but now the algorithm is fixed to use AND matching. I want to support OR matching as well as AND matching. Also I want to avoid using eval, there has to be a more elegant way. Also I want to reduce the complexity of this code as much as possible.
How could I rewrite this code to meet these requirements? Any suggestion is appreciated.
To avoid using eval: Ruby can create code dynamically, so do that instead of adding strings together. All you have to do is take the strings away!
conditions = to_match.map do |attr|
proc {|row| row[attr.to_sym] > attr }
end
Now you have an array of runnable blocks that take the row as their argument and return the result of the condition (return keyword not required). If you're just doing and, it's as simple as:
does_match = conditions.all? {|c| c.call(row) }
which will be true only if all the conditions return a truthy value (i.e. not false or nil).
As for supporting OR logic, if you are happy to just support ORing all of the conditions (e.g. replacing "and" with "or") then this will do it:
does_match = conditions.any? {|c| c.call(row) }
but if you want to support ORing some and ANDing others, you'll need someway to group them together, which is more complex.
Related
Hi there: Ruby Beginner.
So I'm working on a little exercise.
I have this
donation = {
"Randy" => 50,
"Steve" => 50,
"Eddie" => 9,
"Bill" => 12
}
donation.max_by.first { |name, money| money.to_i }
name, money = donation.max_by { |name, money| money.to_i }
puts "#{name} donated the most, at $#{money}!"
But there's a little bug. "Randy" and "Steve" both donated the max, but it outputs "Randy" (because they're the first in key in the for hash, I assume?) and they get all the credit!
I'm thinking the way to go is, I need an IF ELSE statement; IF any? of the "money" values are equal, it moves on. Else, it puts the statement.
SO i guess I am wondering how to compare values?
Select Elements Matching Highest Value
There's more than one way to identify and extract multiple elements that share a maximum value. Based on the semantics you're trying to communicate in your code, one working example is as follows:
max_amt = donation.max_by(&:last)[-1]
donation.select { |name, amt| amt == max_amt }
#=> {"Randy"=>50, "Steve"=>50}
First, we capture the maximum value from the donations Hash. Next, the Hash#select method passes the name and amount of each donation into the block, which returns the Hash elements for which the comparison to the maximum value is truthy. Inside the block, each amount is compared to the maximum value found in the original Hash, allowing all matching key/value pairs to be returned when more than one of them contains a value equal to the max_amt.
As you discovered, max_by returns the first key that has the maximum value. It actually takes a parameter that is the number of elements to return, so doing something like .max_by(donation.size) will return the hash in descending order and you could then go through the elements until the value doesn't match the first (max) value.
But, there's a couple of simpler ways to go about it. If you just want to print the information as you go, one way would be to just iterate through the hash looking for values that match the max value:
mx = donation.values.max
puts "The following people donated the most ($#{mx}):"
donation.each { |k, v| puts k if v == mx }
Output:
The following people donated the most ($50):
Randy
Steve
You could also use .select to match for the max value, which preserves the hash form:
mx = donation.values.max
donation.select { |k, v| v == mx }
=> {"Randy"=>50, "Steve"=>50}
EDIT
Per the follow-up comment, if you use .select to capture the result as a new hash, you can then use conditional logic, loops, etc., to process the data from there. As a very simple example, suppose you want a different message if there's only one top donor vs. multiple:
mx = donation.values.max
max_donors = donation.select { |k, v| v == mx }
if max_donors.size > 1
puts "The following people donated the most ($#{mx}):"
max_donors.keys.each { |name| puts name }
elsif max_donors.size == 1
name, money = max_donors.first
puts "#{name} donated the most, at $#{money}!"
else
puts 'No one donated!'
end
I have a computationally expensive find, and for obvious reasons want the computed value rather than the original:
%x(...).each_line.find { |l| m = l.match(/^(\S+).*System(.*)/) ; break m if m }
It seems the find is necessary because giving the block to each_line returns the original string on failure.
If I want every matched line's group, I can map { ... }.compact, although I gather that's 'not the Ruby way' because I should select before I map. (Is that really considered the only acceptable practice?)
I tried using that approach with each_line turned lazy so I could ask for first at the end, but I couldn't compact it. Needing to write select{|x|x} underwhelms me.
%x(...)
.each_line.lazy
.map { |l| l.match(/^(\S+).*System(.*)/) }
.select { |x| x } # lol, .compact pls.
.first
Is there something more elegant?
Update: I'm going with
"foo\nbar".each_line.lazy.flat_map{ |l| l.match(/([ao])/) || [] }.first
but an honourable mention goes to Simple Lime for the grep based answer.
You could use grep to do what you want, I think:
matches = %x(./script.rb).each_line.lazy.grep(/^(\S+).*System(.*)/) do
Regexp.last_match
end
grep will
Returns an array of every element in enum for which Pattern === element. If the optional block is supplied, each matching element is passed to it, and the block’s result is stored in the output array.
so by passing the block, we're essentially map/collect ing the output of Regexp.last_match only for the lines that matched. And Regexp.last_match
returns the MatchData object generated by the last successful pattern match.
This:
str.each_line.find { |l| m = l.match(/^(\S+).*System(.*)/) ; break m if m }
Is the same as this:
str.each_line.find { |l| l.match(/^(\S+).*System(.*)/) }
If you want all matches back, you're building a new result. That means you probably want a reduce:
str.each_line.reduce([]) do |matches, line|
if match_data = line.match(/^(\S+).*System(.*)/)
matches << match_data
end
matches
end
One loop with an array result that contains only match data.
I was wondering if it is possible to return to an Each iterator in Ruby from within a for-loop placed within the block passed to Each.
def find member = ""
productHash = {}
##entries is a hash, with both the keys and values being strings
#the member parameter is a string
#entries.each do |key, value|
for i in 0...member.size
if(key[i] != member[i])
next #the next keyword doesn't work...all it does is return to the for iterator. I'm looking for a keyword that would return to the each iterator, and allow each to pass back in the next key-value pair.
end
end
productHash[key] = value
end
productHash
end
What I'm trying to accomplish is this: the moment I see that a character in the member parameter doesn't match the corresponding character in a given key, I move on to the next key-value pair.
It looks like you're trying to do some kind of comparison where if the key matches a particular prefix specified by member then you would make an assignment.
This code should be functionally similar:
def find(member = "")
hash = { }
#entries.each do |key, value|
# Unless key begins with prefix, skip it.
next unless (key[0, prefix.length] == prefix)
hash[key] = value
end
hash
end
There's no official goto statement in Ruby, and many would argue this is a good thing, but it means that busting out of nested blocks can be a bit tricky.
Still, if you approach the problem in the right way, there's almost always a solution that's elegant enough.
Update:
To break out of nested loops, an approach might be:
list.each do |i|
broken = false
inner_list.each do |j|
if (j > 10)
broken = true
break
end
end
break if (broken)
end
I have an array of tags per item like so:
item1 = ['new', 'expensive']
item2 = ['expensive', 'lame']
I also have a boolean expression as a string based on possible tags:
buy_it = "(new || expensive) && !lame"
How can I determine if an item matches the buying criteria based on the tags associated with it? My original thought was to do a gsub on all words in buy_it to become 'true' or 'false' based on them existing in the itemx tags array and then exec the resulting string to get a boolean result.
But since the Ruby community is usually more creative than me, is there a better solution?
EDIT:
Just to clarify, buy_it in my example is dynamic, users can change the criteria for buying something at run-time.
Along the lines of your gsub idea, instead of substituting each word for true/false every time, why not substitute each "query" into an expression that can be re-used, e.g. the example buy_it:
buy_it = "(new || expensive) && !lame"
buy_it_expr = buy_it.gsub(/(\w+)/, 'tags.include?("\1")')
puts buy_it_expr
=> (tags.include?("new") || tags.include?("expensive")) && !tags.include?("lame")
It could be evaluated into a Proc and used like this:
buy_it_proc = eval "Proc.new { |tags| #{buy_it_expr} }"
buy_it_proc.call(item1)
=> true
buy_it_proc.call(item2)
=> false
Of course, care must be taken that the expression does not contain malicious code. (One solution might be to strip all but the allowed operator characters from the string and of course be wary of exceptions during eval.)
A hash is a good candidate here.
items = {'ipad' => ['new', 'expensive'], 'kindle' => ['expensive', 'lame']}
items.each do |name,tags|
if tags.include?('new' || 'expensive') && !tags.include?('lame')
puts "buy #{name}."
else
puts "#{name} isn't worth it."
end
end
I have a collection of Post objects and I want to be able to sort them based on these conditions:
First, by category (news, events, labs, portfolio, etc.)
Then by date, if date, or by position, if a specific index was set for it
Some posts will have dates (news and events), others will have explicit positions (labs, and portfolio).
I want to be able to call posts.sort!, so I've overridden <=>, but am looking for the most effective way of sorting by these conditions. Below is a pseudo method:
def <=>(other)
# first, everything is sorted into
# smaller chunks by category
self.category <=> other.category
# then, per category, by date or position
if self.date and other.date
self.date <=> other.date
else
self.position <=> other.position
end
end
It seems like I'd have to actually sort two separate times, rather than cramming everything into that one method. Something like sort_by_category, then sort!. What is the most ruby way to do this?
You should always sort by the same criteria to insure a meaningful order. If comparing two nil dates, it is fine that the position will judge of the order, but if comparing one nil date with a set date, you have to decide which goes first, irrespective of the position (for example by mapping nil to a day way in the past).
Otherwise imagine the following:
a.date = nil ; a.position = 1
b.date = Time.now - 1.day ; b.position = 2
c.date = Time.now ; c.position = 0
By your original criteria, you would have: a < b < c < a. So, which one is the smallest??
You also want to do the sort at once. For your <=> implementation, use #nonzero?:
def <=>(other)
return nil unless other.is_a?(Post)
(self.category <=> other.category).nonzero? ||
((self.date || AGES_AGO) <=> (other.date || AGES_AGO)).nonzero? ||
(self.position <=> other.position).nonzero? ||
0
end
If you use your comparison criteria just once, or if that criteria is not universal and thus don't want to define <=>, you could use sort with a block:
post_ary.sort{|a, b| (a.category <=> ...).non_zero? || ... }
Better still, there is sort_by and sort_by! which you can use to build an array for what to compare in which priority:
post_ary.sort_by{|a| [a.category, a.date || AGES_AGO, a.position] }
Besides being shorter, using sort_by has the advantage that you can only obtain a well ordered criteria.
Notes:
sort_by! was introduced in Ruby 1.9.2. You can require 'backports/1.9.2/array/sort_by' to use it with older Rubies.
I'm assuming that Post is not a subclass of ActiveRecord::Base (in which case you'd want the sort to be done by the db server).
Alternatively you could do the sort in one fell swoop in an array, the only gotcha is handling the case where one of the attributes is nil, although that could still be handled if you knew the data set by selecting the appropriate nil guard. Also it's not clear from your psuedo code if the date and position comparisons are listed in a priority order or an one or the other (i.e. use date if exists for both else use position). First solution assumes use, category, followed by date, followed by position
def <=>(other)
[self.category, self.date, self.position] <=> [other.category, other.date, other.position]
end
Second assumes it's date or position
def <=>(other)
if self.date && other.date
[self.category, self.date] <=> [other.category, other.date]
else
[self.category, self.position] <=> [other.category, other.position]
end
end