0

Imagine I have a portfolio p that has 2 stocks port_stocks. What I want to do is run a calculation on each port_stock, and then sum up all the results.

[60] pry(main)> p.port_stocks
=> [#<PortStock:0x00007fd520e064e0
  id: 17,
  portfolio_id: 1,
  stock_id: 385,
  volume: 2000,
  purchase_price: 5.9,
  total_spend: 11800.0>,
 #<PortStock:0x00007fd52045be68
  id: 18,
  portfolio_id: 1,
  stock_id: 348,
  volume: 1000,
  purchase_price: 9.0,
  total_spend: 9000.0>]
[61] pry(main)> 

So, in essence, using the code above I would like to do this:

ps = p.port_stocks.first #(`id=17`)
first = ps.volume * ps.purchase_price # 2000 * 5.9 = 11,800

ps = p.port_stocks.second #(`id=18`)
second = ps.volume * ps.purchase_price # 1000 * 9.0 = 9,000

first + second = 19,800

I want to simply get 19,800. Ideally I would like to do this in a very Ruby way.

If I were simply summing up all the values in 1 total_spend, I know I could simply do: p.port_stocks.map(&:total_spend).sum and that would be that.

But not sure how to do something similar when I am first doing a math operation on each object, then adding up all the products from all the objects. This should obviously work for 2 objects or 500.

SRack
  • 11,495
  • 5
  • 47
  • 60
marcamillion
  • 32,933
  • 55
  • 189
  • 380
  • 1
    Try: `p.port_stocks.map{|ps| ps.volume * ps.purchase_price}.sum` – guitarman May 15 '18 at 08:08
  • 1
    Do you want to do the calculation in Ruby or in your database? – Stefan May 15 '18 at 08:08
  • 1
    Unrelated to your question but you shouldn't use floats for monetary values. – Stefan May 15 '18 at 08:09
  • @Stefan I want to do the calculation in Ruby. Also, why should I use floats for monetary values? I feel like I have heard that somewhere before, but can you give me a link or something I can read up more about that and explore the alternatives please? Thanks! – marcamillion May 15 '18 at 08:11
  • @guitarman Perfect. That works like a charm. Please add it as an answer and I will accept it. Thanks! – marcamillion May 15 '18 at 08:12
  • 2
    @marcamillion _"why should I use floats"_ – you should **not** use floats for monetary values. Because floats are inaccurate and can easily introduce rounding errors, e.g. `0.1 * 0.2` returns `0.020000000000000004`. See https://stackoverflow.com/q/3730019/477037 for a detailed explanation. Take a look a the [money](https://github.com/RubyMoney/money) gem and its [money-rails](https://github.com/RubyMoney/money-rails) integration. – Stefan May 15 '18 at 08:37
  • @Stefan - didn't know that, thanks for raising it and the explanation. Reading the linked post now. – SRack May 15 '18 at 08:47

2 Answers2

6

The best way of doing this using Rails is to pass a block to sum, such as the following:

p.port_stocks.sum do |port_stock|
  port_stock.volume * port_stock.purchase_price
end

That uses the method dedicated to totalling figures, and tends to be very fast and efficient - particularly when compared to manipulating the data ahead of calling a straight sum without a block.

A quick benchmark here typically shows it performing ~20% faster than the obvious alternatives.

I've not been able to test, but give that a try and it should resolve this for you.

Let me know how you get on!


Just a quick update as you also mention the best Ruby way, sum was introduced in 2.4, though on older versions of Ruby you can use reduce (also aliased to inject):

p.port_stocks.reduce(0) do |sum, port_stock|
  sum + (port_stock.volume * port_stock.purchase_price)
end

This isn't as efficient as sum, but thought I'd give you the options :)

SRack
  • 11,495
  • 5
  • 47
  • 60
2

You are right to use Array#map to iterate through all stocks, but instead to sum all total_spend values, you could calculate it for each stock. After, you sum all results and your done:

p.port_stocks.map{|ps| ps.volume * ps.purchase_price}.sum

Or you could use Enumerable#reduce like SRack did. This would return the result with one step/iteration.

guitarman
  • 3,290
  • 1
  • 17
  • 27
  • 1
    `collection.map { ... }.sum` => `collection.sum { ... }` – Stefan May 15 '18 at 11:29
  • Performs worse than using `sum` with a block, and is less readable IMHO. – SRack May 15 '18 at 11:34
  • @SRack there proof to suggest that it performs worse? – marcamillion May 15 '18 at 23:09
  • I've benchmarked it in the past @maramillion ([code here](https://repl.it/@SDRACK/QuickBenchmark) if you're interested), after answering a question similar to this with Guitarman's solution and seeing someone else post to use `sum` :) Was interested to see how `map`, `reduce` and `sum` compared, so I ran the numbers. `sum` generally comes out on top (usually by ~20%), and I'd maintain it's the most readable. – SRack May 16 '18 at 13:21