2

Using minitest, I'm trying to make this test pass, which is asking for the sum of 5!:

def test_sum_of_factorials

  sum_of_factorials = 0
  numbers = [1, 2, 3, 4, 5]


  assert_equal 153, sum_of_factorials
end

I wrote a passing solution but it is long and repetitive:

fractorial_5 = numbers.inject(1) {|aggregate, num| aggregate * num}
fractorial_4 = numbers[0..3].inject(1) {|aggregate, num| aggregate * num}
fractorial_3 = numbers[0..2].inject(1) {|aggregate, num| aggregate * num}
fractorial_2 = numbers[0..1].inject(1) {|aggregate, num| aggregate * num}
fractorial_1 = 1

fractorials_array = [fractorial_1, fractorial_2, fractorial_3, fractorial_4, fractorial_5]

fractorials_array.each {|fractorial| sum_of_factorials += fractorial}

Does anyone have a cleaner, simpler solution that they'd be willing to explain?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303

5 Answers5

1
def sum_of_factorials(n)
  (1..n).reduce([1, 0]) { |(f, sum), e| [(f *= e), (sum + f)] }.last
end

produces

sum_of_factorials(5) #=> 153

Explanation:

def sum_of_factorials(n)
  (1..n).             # n times
    reduce([1, 0]) do # iterate and use [factorial, sum] as an accumulator
    |(f, sum), e|     # splat the accumulator into f and sum variables
      [               # each time recalculate the accumulator as
        f *= e,       # product of the element and previous factorial,
        sum + f       # sum of current factorial and previous sum
      ]
    end.last          # and return only last part(sum) of the accumulator
end
Pavel Mikhailyuk
  • 2,757
  • 9
  • 17
  • Nice and clean. – Cary Swoveland Jan 15 '20 at 20:22
  • @CarySwoveland I'm not sure, what is the best way to show, that the output was taken in REPL. – Pavel Mikhailyuk Jan 16 '20 at 05:50
  • If you simply write `sum_of_factorials(5) #=> 153` (one line or two, with or without '#') it's understood that if the method is executed with an argument of `5` the return value is `153`, regardless of how it is executed (e.g, IRB, PRY, `ruby t.rb`). – Cary Swoveland Jan 16 '20 at 05:57
  • @CarySwoveland As for me, Copy-Paste from REPL is some kind of "guarantee" that the code really works as claimed. Like "protection" from typos. That is why I'm not sure. – Pavel Mikhailyuk Jan 16 '20 at 06:28
1

It appears you are given:

def test_sum_of_factorials
  sum_of_factorials = 0
  numbers = [1, 2, 3, 4, 5]
  <...missing bits...>
  assert_equal 153, sum_of_factorials
end

and are being asked to fill in the missing bits. I think something like the following is being asked for.

def test_sum_of_factorials
  sum_of_factorials = 0
  numbers = [1, 2, 3, 4, 5]
  fac = 1
  numbers.each do |n|
    fac *= n
    sum_of_factorials += fac
  end
  assert_equal 153, sum_of_factorials
end

We can instead write that as:

def test_sum_of_factorials
  numbers = [1, 2, 3, 4, 5]      
  assert_equal 153, sum_of_factorials(numbers)
end

def sum_of_factorials(numbers)
  fac_sum = 0
  fac = 1
  numbers.each do |n|
    fac *= n
    fac_sum += fac
  end
  fac_sum
end

where

sum_of_factorials([1,2,3,4,5])
  #=> 153

It would be more Ruby-like, however, to use Array#sum to write sum_of_factorials as follows:

def sum_of_factorials(numbers)
  fac = 1
  numbers.sum { |n| fac *= n }
end

Together with the title of your question, this is why the authors of other answers assume you are asking how the method sum_of_factorials can be improved. Firstly, it can be passed the argument numbers.max, rather than the array numbers.

Another way to write sum_of_factorials is to make use of the method Enumerator::produce, which made its debut in v2.7.

def sum_of_factorials(n)
  enum = Enumerator.produce([1,1]) { |n0, n1| [n0+1, (n0+1)*n1] }
  n.times.sum { enum.next.last }
end

(1..8).each { |n| puts "%d: %6d" % [n, sum_of_factorials(n)] }
1:      1
2:      3
3:      9
4:     33
5:    153
6:    873
7:   5913
8:  46233

Note that if:

enum = Enumerator.produce([1,1]) { |n0, n1| [n0+1, (n0+1)*n1] }
  #=> #<Enumerator: #<Enumerator::Producer:0x000059d490c742a0>:each> 

then

enum.next #=> [1, 1] 
enum.next #=> [2, 2] 
enum.next #=> [3, 6] 
enum.next #=> [4, 24] 

so the factorials of 1 through 4 are given by (after redefining or rewinding enum):

enum.next.last #=>  1 
enum.next.last #=>  2 
enum.next.last #=>  6 
enum.next.last #=> 24 

If n could equal zero add the line return 1 if n.zero? at the beginning of the method.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • You are correct, I should have been more clear in the formatting of my question to show the 'missing bits' as the section I needed. Your first solution is perfect, exactly what I was looking for. – tylerpporter Jan 16 '20 at 03:39
1

Another option is to define a factorial function (stolen here: https://stackoverflow.com/a/12415362/5239030), maybe as a patch for the Integer class:

module MyMathFunctions
  def factorial
    (1..self).reduce(1, :*)
  end
end

Integer.include MyMathFunctions

So you can use it this way: 10.factorial #=> 3628800

Then, just call:

[1, 2, 3, 4, 5].sum &:factorial
#=> 153
iGian
  • 11,023
  • 3
  • 21
  • 36
1

I would like to simply as below,

[1, 2, 3, 4, 5].map { |x| (1..x).inject(1, :*) }.sum
# => 153
ray
  • 5,454
  • 1
  • 18
  • 40
0

Since you can write 5!+4!+3!+2!+1!, generally 1!+2!+3!+...+n! as 1 + 2(1 + 3(1 + 4(1 + 5(1+...(1+n))))), you can do this in O(n)

sum_of_factorials = 1
numbers.drop(1).reverse_each { |i| sum_of_factorials = 1 + i * sum_of_factorials }
Dinu
  • 129
  • 2
  • 6
  • Thanks for explaining, makes sense. But I am supposed to make the test pass without changing any of the initial given parameters, in this case sum_of_factorials = 0 – tylerpporter Jan 15 '20 at 19:14