1
\$\begingroup\$

I have an array of hashes, each of which has two attributes: "title" and "Zscore". I want to return either the result of find or the last element. This can be done with:

a = array.select{|i| i["title"] == "some title"}
b = a.find{|i| i["Zscore"].to_f >= 5} || a.last

Is there an elegant way to put the conditional inside the block, something like the following?

array.select{|i| i["title"] == "some title"}.find{|i| i["Zscore"].to_f >= 5 || last}

Any built-in method? Any ideas of one-liner that is nicer than the following?

array.select{|i| i["title"] == "some title"}.find{|i| i["Zscore"].to_f >= 5} ||
array.select{|i| i["title"] == "some title"}.last
\$\endgroup\$
5
  • \$\begingroup\$ Why? Why would you want a built-in method, and why would you think it would be better than the two characters ||? What you have right now is already elegant. I can't imagine what you think you'll accomplish by moving the conditional inside the block, except to make the code less readable and less elegant. \$\endgroup\$
    – meagar
    Jun 26 '16 at 14:35
  • \$\begingroup\$ @meagar The OP's intention is not clearly expressed, but I suspect the real intention is to avoid intermediate variable assignment a = and doing it in a chain style. \$\endgroup\$
    – sawa
    Jun 26 '16 at 14:39
  • \$\begingroup\$ @sawa That moves the question "Why?", but it doesn't answer it. \$\endgroup\$
    – meagar
    Jun 26 '16 at 14:40
  • \$\begingroup\$ @meagar Yeah. I don't think it is worth doing it. \$\endgroup\$
    – sawa
    Jun 26 '16 at 14:43
  • \$\begingroup\$ @meagar Why? because ruby provides many elegant one-liners and having all of array.select{|i| i["title"] == "some title"}.find{|i| i["Zscore"].to_f >= 5} || array.select{|i| i["title"] == "some title"}.last is long and unreadable. As for the original tag of ruby-on-rails, it is within a Rails app. Rails provides methods such as first_or_create and I have wondered about the existence of a method of such kind that might come in handy. \$\endgroup\$
    – Spätzle
    Jun 26 '16 at 17:06
1
\$\begingroup\$

You can always define your own custom method to refactor your code in the Array class:

class Array
  def find_or_last(&block)
    find(&block) || last 
  end
end

And use it like this:

array.select { |i| i['title'] == 'some title' }.find_or_last { |i| i['Zscore'].to_f >= 5 }

You can also use procs or lambdas to achieve this.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ Any developer on my team who tried to introduce this code to a project would immediately have a his pull request denied and be given a stern talking to about monkey-patching core classes. \$\endgroup\$
    – meagar
    Jun 26 '16 at 14:42
  • 2
    \$\begingroup\$ @meagar Unless Spätzle is in your team, this comment has nothing to do with a correct robust answer, provided by Wikiti. \$\endgroup\$ Jun 26 '16 at 14:49
  • 4
    \$\begingroup\$ @meagar Then I believe it worth it to mention best practices rather than your personal leadership preferences in first place. \$\endgroup\$ Jun 26 '16 at 14:53
  • 2
    \$\begingroup\$ @meagar I don't like extending core classes either, but the OP asked for an elegant or one-liner solution. If he's working on a personal or basic project, this solution should work for him. \$\endgroup\$
    – Wikiti
    Jun 26 '16 at 15:05
  • 2
    \$\begingroup\$ I am NOT working on a personal / basic project and monkey-patching core classes would make my boss, just as @meagar said, reject my work. The whole purpose was having a nice one-liner (which eventually reduces db queries) W/O patching. If patching was legitimate for this, I wouldn't even have bothered asking this question. \$\endgroup\$
    – Spätzle
    Jun 30 '16 at 4:45
1
\$\begingroup\$

I have assumed a is not needed for some unspecified purpose.

This is a way of achieving the desired result with a single pass through the array.

last = nil
array.find do |h|
  next unless h["title"] == "some title"
  last = h
  h["Zscore"].to_f >= 5
end || last
\$\endgroup\$
0
0
\$\begingroup\$

Here is one way to do this using Enumerable#detect:

array = [{"title"=>"Foo", "Zscore" => "2.0"},
         {"title"=>"Bar", "Zscore" => "5.0"},
         {"title"=>"some title", "Zscore" => "2.0"}]

result = (t = array.select {|i| i["title"] == "some title"})
             .detect (-> {t.last} ) {|i| i["Zscore"].to_f >= 5}

#=> {"title"=>"some title", "Zscore"=>"2.0"}

The parameter passed to detect is the ifnone method that will be evaluated and returned if detect could not find any matching element.

\$\endgroup\$
5
  • \$\begingroup\$ You are close. But this is not correct. It will give you a wrong answer when the last element of array does not satisfy the first condition, and the default has to be used. \$\endgroup\$
    – sawa
    Jun 26 '16 at 14:28
  • \$\begingroup\$ The goal is not to return array.last, it is to return array.selet { |a| a['title'] == 'some_title' }.last \$\endgroup\$
    – meagar
    Jun 26 '16 at 14:49
  • \$\begingroup\$ @meagar Okay, I was working on the solution as sawa already pointed out the problem. Have fixed it now. \$\endgroup\$
    – Wand Maker
    Jun 26 '16 at 14:50
  • \$\begingroup\$ You can simply give up doing it with method chain, but use the default. That would still be an improvement over the OP's code. \$\endgroup\$
    – sawa
    Jun 26 '16 at 14:58
  • 1
    \$\begingroup\$ You haven't eliminated the intermediate variable, you've just wrapped it in parenthesis. \$\endgroup\$
    – meagar
    Jun 26 '16 at 15:22
0
\$\begingroup\$

Variation with Enumerator#each_with_object (O(n)):

array.each_with_object({}) { |el, hsh| el['title'] == 'some title' && hsh.replace(el) && el['Zscore'].to_f >= 5.0 && (break el) }

Some kind of "functional" style. Maybe, somewhat too verbose but readable. Can't assess its elegance yet.

\$\endgroup\$
0
\$\begingroup\$

There is nothing wrong with using a local variable, but you can always transform variables to chains with a let abstraction. Ruby has no such abstraction, but you can create it it yourself (it has a myriad of names, I like as):

class Object
  def as
    yield self
  end
end

result = xs.select { |x| x["title"] == "some title" }.
  as { |ys| ys.find { |y| y["Zscore"].to_f >= 5 } || ys.last }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.