56

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!

Spike Fitsch
  • 747
  • 1
  • 7
  • 12

7 Answers7

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

or

array.inject(0){|sum,e| sum + e.cash }
Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
Yuri Barbashov
  • 5,407
  • 1
  • 24
  • 20
  • 4
    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` :-) – Michael Kohl Jun 30 '12 at 11:10
  • note that you don't need to send a block, `inject` knows what to do with a symbol: `inject(0, :+)` – tokland Jun 30 '12 at 15:02
  • 3
    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 }` – tokland Jun 30 '12 at 15:04
  • i thought it might be a hash my fault) – Yuri Barbashov Jun 30 '12 at 20:06
63

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

KrauseFx
  • 11,551
  • 7
  • 46
  • 53
jacklin
  • 2,739
  • 1
  • 24
  • 31
  • 3
    If you're using Rails, this is the way to go. – Dennis Feb 19 '14 at 15:30
  • 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)`. – Dennis Feb 19 '14 at 15:37
  • If the records are not stored in the DB, the sum will be 0, where inject would work. – dgmora Oct 27 '15 at 14:46
  • 6
    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](http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-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! – Augustin Riedinger Nov 10 '15 at 03:33
  • @AugustinRiedinger if possible, it is preferred to do sql sum vs ruby sum, no? – Danny Dec 15 '15 at 18:02
  • It depends on the use case: Say you need to load the array of objects on the page, doing Ruby calculation will avoid querying `select sum(field) from table` but if you only need the sum value, for sure `select sum(field) from table` is faster than `select * from table` parsed in objects and then summed in Ruby. – Augustin Riedinger Dec 15 '15 at 21:52
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 }
Theo
  • 131,503
  • 21
  • 160
  • 205
  • 2
    `#reduce` is an alias for `#inject` in 1.9+, btw. – Theo Jun 30 '12 at 09:16
  • +1 for not iterating over `array` twice. The alias is also there in 1.8.7 btw. – Michael Kohl Jun 30 '12 at 11:07
  • 1
    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, :+)`. – tokland Jun 30 '12 at 12:35
  • I wonder why there is such an alias. They have got the same length. – Nerian Jun 30 '12 at 13:10
  • 3
    @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`. – Michael Kohl Jun 30 '12 at 13:14
5

Most concise way:

array.map(&:cash).sum

If the resulting array from the map has nil items:

array.map(&:cash).compact.sum
vicocas
  • 249
  • 2
  • 7
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)

Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
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

mpospelov
  • 1,510
  • 1
  • 15
  • 24
1

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

array.map(&:cash).inject(:+)
megas
  • 21,401
  • 12
  • 79
  • 130
  • 3
    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. – Michael Kohl Jun 30 '12 at 11:08