58

I understand that in order to sum array elements in Ruby one can use the inject method, i.e.

array = [1,2,3,4,5];
puts array.inject(0, &:+) 

But how do I sum the properties of objects within an object array e.g.?

There's an array of objects and each object has a property "cash" for example. So I want to sum their cash balances into one total. Something like...

array.cash.inject(0, &:+) # (but this doesn't work)

I realise I could probably make a new array composed only of the property cash and sum this, but I'm looking for a cleaner method if possible!

7 Answers 7

74
array.map(&:cash).inject(0, &:+)

or

array.inject(0){|sum,e| sum + e.cash }
Sign up to request clarification or add additional context in comments.

4 Comments

This goes over array twice though, which might not be advisable if there are lots of elements. Why not just use a proper block for inject? Also reduce/inject directly takes a symbol argument, no need for Symbol#to_proc :-)
note that you don't need to send a block, inject knows what to do with a symbol: inject(0, :+)
Yuri, I +1'd your answer, but the second snippet doesn't look good, better a functional: array.inject(0) { |sum, e| sum + e.cash }
i thought it might be a hash my fault)
64

In Ruby On Rails you might also try:

array.sum(&:cash)

Its a shortcut for the inject business and seems more readable to me.
http://api.rubyonrails.org/classes/Enumerable.html

6 Comments

If you're using Rails, this is the way to go.
Note that if your array is the result of some kind of filtering on an ActiveRecord object, e.g. @orders = Order.all; @orders.select { |o| o.status == 'paid' }.sum(&:cost), then you can also get the same result with a query: @orders.where(status: :paid).sum(:cost).
If the records are not stored in the DB, the sum will be 0, where inject would work.
More on @Dennis comment: if you are using Rails 4.1+, you can't array.sum(&:cash) on an activerecord relation, because it want's to make an ActiveRecord sum like so: array.sum(:cash) which is massively different (SQL vs. Ruby). You'll have to convert it into an array to make it work again: array.to_a.sum(&:cash). Quite nasty!
@AugustinRiedinger if possible, it is preferred to do sql sum vs ruby sum, no?
|
11

#reduce takes a block (the &:+ is a shortcut to create a proc/block that does +). This is one way of doing what you want:

array.reduce(0) { |sum, obj| sum + obj.cash }

5 Comments

#reduce is an alias for #inject in 1.9+, btw.
+1 for not iterating over array twice. The alias is also there in 1.8.7 btw.
as Michael says that's more space-efficient that map+reduce, but at the cost of modularity (small in this case, no need to say). In Ruby 2.0 we can have both thanks to laziness: array.lazy.map(&:cash).reduce(0, :+).
I wonder why there is such an alias. They have got the same length.
@Nerian: In Smalltalk this was called inject:into: whereas several other languages call folds reduce (e.g. Clojure, Common Lisp, Perl, Python). The aliases are there to accomodate people with different backgrounds. Same for map/collect.
5

Most concise way:

array.map(&:cash).sum

If the resulting array from the map has nil items:

array.map(&:cash).compact.sum

Comments

3

If start value for the summation is 0, then sum alone is identical to inject:

array.map(&:cash).sum

And I would prefer the block version:

array.sum { |a| a.cash }

Because the Proc from symbol is often too limited (no parameters, etc.).

(Needs Active_Support)

Comments

2

Here some interesting benchmarks

array = Array.new(1000) { OpenStruct.new(property: rand(1000)) }

Benchmark.ips do |x|
  x.report('map.sum') { array.map(&:property).sum }
  x.report('inject(0)') { array.inject(0) { |sum, x| sum + x.property } }
  x.compare!
end

And results

Calculating -------------------------------------
             map.sum   249.000  i/100ms
           inject(0)   268.000  i/100ms
-------------------------------------------------
             map.sum      2.947k (± 5.1%) i/s -     14.691k
           inject(0)      3.089k (± 5.4%) i/s -     15.544k

Comparison:
           inject(0):     3088.9 i/s
             map.sum:     2947.5 i/s - 1.05x slower

As you can see inject a little bit faster

Comments

1

There's no need to use initial in inject and plus operation can be shorter

array.map(&:cash).inject(:+)

1 Comment

You are right about the symbol argument, but if array can be empty, you want the argument: [].inject(:+) #=> nil, [].inject(0, :+) #=> 0 unless you want to deal with the nil separately.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.