views:

229

answers:

4

Provided the following ruby hash:

{
    cat: {
        1: 2,
        2: 10,
        3: 11,
        4: 1
    },
    wings: {
        1: 3,
        2: 5,
        3: 7,
        4: 7
    },
    grimace: {
        1: 4,
        2: 5,
        3: 5,
        4: 1
    },
    stubborn: {
        1: 5,
        2: 3,
        3: 7,
        4: 5
    }
}

How can I sort the hash by the sum of 'leaf' excluding "4", for instance the value to compare for "cat" would be (2 + 10 + 11) = 23, the value for "wings" would be (3 + 5 + 7) = 15 so if I was comparing just those two they would be in the correct order, highest sum on top.

It is safe to assume that it will ALWAYS be {1: value, 2: value, 3: value, 4: value} as those are keys for constants I have defined.

It is also safe to assume that I will only ever want to exclude the key "4", and always use the keys "1", "2", and "3"

Based on Jordan's suggestion I got this to work:

  tag_hash = tag_hash.sort_by do |h| 
    h[1].inject(0) do |sum, n| 
      n[0] == 4 ? sum : sum + (n[1] || 0)
    end
  end

The results seem a bit off but it appears to be my code, once I confirm that I'll accept the answer, thanks Jordan!

I've updated my solution to use Wayne Conrad's idea, see my comment on his answer - Is it possible that its not carrying everything when it sorts, I linked to an image in my comment that shows the result of the actual sort in graph form.. seems odd to me..

+2  A: 
my_hash.sort_by do |_, h|
  h.inject(0) do |sum, n|
    # only add to the sum if the key isn't '4'
    n[0] == 4 ? sum : (sum + n[1])
  end
end

This can, of course, be shortened to a hideously unreadable one-liner:

my_hash.sort_by {|k,h| h.inject(0) {|sum,n| n[0] == 4 ? sum : (sum + n[1]) } }
Jordan
I'm getting "Array can't be coerced into Fixnum"...
Rabbott
Yea this isn't deep enough, looks like it needs to go one level deeper for the addition..
Rabbott
Sorry, Rabbott, you're right. I've updated my code.
Jordan
+7  A: 
tag_hash = tag_hash.sort_by do |_, leaf|
  leaf.reject do |key, _|
    key == 4
  end.collect(&:last).inject(:+)
end
Wayne Conrad
+1, nice solution. I don't know why so many people refuse to use destructuring bind, @Jordan's solution for example is hideously ugly because of not using it. One suggestion: replace the outer `key` parameter with `_` to make it clear you are not using it anywhere.
Jörg W Mittag
This accomplished the same thing (but is more aesthetically pleasing), but i graphed the results from my sort, andthis is what it came out with http://imgur.com/NFxfM it seems odd.. it IS sorted.. but it almost looks like its not sorted quite enough..
Rabbott
Jörg: I agree that my solution is less pretty than Wayne's, but doing one inject struck me as more efficient than chaining reject->collect->inject. I prefer to iterate over a data structure fewer times when possible.
Jordan
@Jordan, I agree -- there is a tension between aesthetics and performance. @Jörg W Mittag, what is "destructuring bind?" It sounds neat. @Rabbott, that's odd. Are you graphing exactly the same data as is being sorted?
Wayne Conrad
@Wayne: destructuring bind (aka destructuring assignment) is a limited form of pattern matching, where you have some complex data structure on the right side of an assignment and a pattern on the left, and the language takes apart the structure of the data structure and binds (assigns) values from *inside* the data structure to variables on the left. Ruby supports destructuring only for arrays, like this: `a, (b, c, *d), e, *f = [1, [2, 3, 4, 5], 6, 7, 8] # => a=1 b=2 c=3 d=[4, 5] e=6 f=[7, 8]` You use it to seperate out key and value in the `[key, value]` array that Ruby passes you.
Jörg W Mittag
@Wayne: it not only works for assignments, but also for binding argumemnts in methods and blocks. In your example, you use `hsh.sort_by do |k, v| ... end` instead of `hsh.sort_by do |el| ... end`, which allows you to refer to the key and value as `k` and `v` instead of `el.first` and `el.last` (or even `el[0]` and `el[1]`) which is much less readable unless you happen to memorize which order Ruby passes them in. In @Jordan's code, using `inject do |sum, (k, v)| k == 4 ? sum : sum+v end` would be easier to read. Actually, I would use a guard clause and do `next sum if k == 4; sum+v` instead.
Jörg W Mittag
@Jörg, a perfect explanation. I never knew there was such a fancy name for it, or that it did as much as in your example. Thanks!
Wayne Conrad
I've learned quite a bit from this question, thanks to all who contributed and commented!
Rabbott
A: 

This

puts a.sort_by { |k, v| -v.values[0..-1].inject(&:+) }

produces the following output, was this what you wanted?

cat
{1=>2, 2=>10, 3=>11, 4=>1}
wings
{1=>3, 2=>5, 3=>7, 4=>7}
stubborn
{1=>5, 2=>3, 3=>7, 4=>5}
grimace
{1=>4, 2=>5, 3=>5, 4=>1}
Michael Kohl
A: 

This seems to work:

x.sort_by{ |_,h| h.values.take(3).inject(:+) }

This assumes that the subhashes are sorted (the first three entries are the entries you want to sum up).

When you use ActiveSupport you could do this:

x.sort_by{ |_,h| h.values.take(3).sum }

or

x.sort_by{ |_,h| h.slice(1,2,3).values.sum }

Hash#slice returns a hash that contains only the keys passed to slice. Array#take returns an array containing the first n entries of the source array.

(Ruby 1.9.1 required i think)

Ragmaanir