Week 2, day 5

Our cohort was issued a double-barreled weekend challenge today.

Write your own method to replicate the inject method in Ruby.

Write a take-away restaurant program that presents menus, takes orders and sends an SMS to customers to confirm delivery using Twilio

I had a pretty relaxed day at Makers Academy, touching base with a lot of people I haven't had enough time to interact with, then in the afternoon I set to work rewriting inject... and quickly stalled!
This was going to be tough. I continued in the evening and by about 11pm I had something that passed the tests and that I was somewhat ok with. I'm not sure how long the take-away challenge is going to take me, but if I can squeeze in more time around my social plans then I'll have another go at it, but for now here's what I came up with:

 1 class Array
 2   def my_inject(arg = nil, arg_sym = nil)
 3     arg.nil? || arg.is_a?(Symbol) ? result = self[0] : result = arg
 4     if arg.nil?
 5       self[1..-1].each { |value| result = yield(result, value) }
 6       result
 7     else
 8       if arg.is_a?(Symbol)
 9         self[1..-1].each { |value| result = result.send(arg, value) }
10         result
11       else
12         if arg && arg_sym
13           each { |value| result = result.send(arg_sym, value) }
14           result
15         else
16           each { |value| result = yield(result, value) }
17           result
18         end
19       end
20     end
21   end
22 end

Join me after the break for a look at how my code evolved from terrible to satisfactory.

So, what is inject? According to Ruby-doc:

Inject combines all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator.
If you specify a block, then for each element in enum the block is passed an accumulator value (memo) and the element. If you specify a symbol instead, then each element in the collection will be passed to the named method of memo. In either case, the result becomes the new value for memo. At the end of the iteration, the final value of memo is the return value for the method.
If you do not explicitly specify an initial value for memo, then the first element of collection is used as the initial value of memo.

It's basically magic. It takes optional blocks and arguments and performs the block on each value in an array and intelligently returns an accumulator at the end of it. If that doesn't make sense then dont worry, you're probably still sane. If you want to go nuts then fire up IRB and play with it.

Here's where my code started:

1 class Array
2   def my_inject starting = 0
3     result ||= starting + self.first
4     self[1..-1].each do |value|
5       result = yield(result, value)
6     end
7   result
8 end

It passed some of the tests, the ones that didn't have arguments passed to it, but that just wasn't going to cut it. Now I needed to figoure out how to handle arguments, I started with just 1 arg and no symbol, below is where that lead me:

 1 class Array
 2   def my_inject arg = nil, &block
 3     if !arg.nil?
 4       my_inject_with_arg arg, &block
 5     else
 6       my_inject_without_arg &block
 7     end
 8   end
 9 
10   def my_inject_with_arg arg, &block
11     result = arg
12     each do |value|
13       result = yield(result, value)
14     end
15     result
16   end
17 
18   def my_inject_without_arg &block
19     result = self.first
20     self[1..-1].each do |value|
21       result = yield(result, value)
22     end
23     result
24   end
25 end

Ok, that's not too bad, we're getting somewhere. Now I need to handle symbols passed as args and also both symbols and args, below is my functioning code:

 1 class Array
 2   def my_inject(arg = nil, arg_sym = nil, &block)
 3     if arg.nil?
 4       my_inject_without_arg(&block)
 5     else
 6       if arg.is_a?(Symbol)
 7         my_inject_with_symbol(arg, &block)
 8       else
 9         if arg && arg_sym
10           my_inject_with_arg_and_symbol(arg, arg_sym, &block)
11         else
12          my_inject_with_arg(arg, &block)
13         end
14       end
15     end
16   end
17  
18  def my_inject_with_arg(arg)
19    result = arg
20    each do |value|
21      result = yield(result, value)
22    end
23    result
24  end
25  
26  def my_inject_without_arg
27    result = self.first
28    self[1..-1].each do |value|
29      result = yield(result, value)
30    end
31    result
32  end
33  
34  def my_inject_with_symbol(arg_is_sym, &block)
35    result = self.first
36    self[1..-1].each do |value|
37      result = result.send(arg_is_sym, value)
38    end
39   result
40  end
41  
42  def my_inject_with_arg_and_symbol(arg, arg_sym)
43    result = arg
44    each do |value|
45      result = result.send(arg_sym, value)
46    end
47    result
48  end
49 end

But functional does not equate to pretty or well-designed. So after this I began refactoring and ended up with the code shown at the top of this post. Below are the tests I set up, and passed:

 1 require 'array'
 2 
 3 describe 'Array' do
 4   context 'without arguments' do
 5     it 'can add' do
 6       expect([1, 2, 3].inject { |sum, n| sum + n }).to eq 6
 7       expect([1, 2, 3].my_inject { |sum, n| sum + n }).to eq 6
 8     end
 9 
10     it 'can multiply' do
11       expect([1, 2, 3].inject { |sum, n| sum * n }).to eq 6
12       expect([1, 2, 3].my_inject { |sum, n| sum * n }).to eq 6
13     end
14 
15     it 'can subtract' do
16       expect([10, 5, 2].inject { |sum, n| sum - n }).to eq 3
17       expect([10, 5, 2].my_inject { |sum, n| sum - n }).to eq 3
18     end
19 
20     it 'can divide' do
21       expect([1, 2, 3].inject { |sum, n| sum / n }).to eq 0
22       expect([1, 2, 3].my_inject { |sum, n| sum / n }).to eq 0
23     end
24   end
25 
26   context 'with starting point' do
27     it 'can add with starting point' do
28       expect([1, 2, 3].inject(10) { |sum, n| sum + n }).to eq 16
29       expect([1, 2, 3].my_inject(10) { |sum, n| sum + n }).to eq 16
30     end
31 
32     it 'can multiply with a starting point' do
33       expect([1, 2, 3].inject(10) { |sum, n| sum * n }).to eq 60
34       expect([1, 2, 3].my_inject(10) { |sum, n| sum * n }).to eq 60
35     end
36 
37     it 'can subtract with a starting point' do
38       expect([1, 2, 3].inject(10) { |sum, n| sum - n }).to eq 4
39       expect([1, 2, 3].my_inject(10) { |sum, n| sum - n }).to eq 4
40     end
41 
42     it 'can divide with a starting point' do
43       expect([2, 2, 2].inject(80) { |sum, n| sum / n }).to eq 10
44       expect([2, 2, 2].my_inject(80) { |sum, n| sum / n }).to eq 10
45     end
46   end
47 
48   context 'with a symbol' do
49     it 'can add with a symbol' do
50       expect([1, 2, 3].inject(:+)).to eq 6
51       expect([1, 2, 3].my_inject(:+)).to eq 6
52     end
53 
54     it 'can multiply with a symbol' do
55       expect([2, 5, 10].inject(:*)).to eq 100
56       expect([2, 5, 10].my_inject(:*)).to eq 100
57     end
58 
59     it 'can subtract with a symbol' do
60       expect([50, 20, 3].inject(:-)).to eq 27
61       expect([50, 20, 3].my_inject(:-)).to eq 27
62     end
63 
64     it 'can divide with a symbol' do
65       expect([80, 2, 2].inject(:/)).to eq 20
66       expect([80, 2, 2].my_inject(:/)).to eq 20
67     end
68   end
69 
70   context 'with a starting point and a symbol' do
71     it 'can add with a starting point and symbol' do
72       expect([1, 2, 3].inject(10, :+)).to eq 16
73       expect([1, 2, 3].my_inject(10, :+)).to eq 16
74     end
75 
76     it 'can multiply with a starting point and a symbol' do
77       expect([2, 5, 10].inject(10, :*)).to eq 1000
78       expect([2, 5, 10].my_inject(10, :*)).to eq 1000
79     end
80 
81     it 'can subtract with a starting point and a symbol' do
82       expect([5, 20, 3].inject(50, :-)).to eq 22
83       expect([5, 20, 3].my_inject(50, :-)).to eq 22
84     end
85 
86     it 'can divide with a starting point and a symbol' do
87       expect([2, 2, 2].inject(80, :/)).to eq 10
88       expect([2, 2, 2].my_inject(80, :/)).to eq 10
89     end
90   end
91 end
Written on March 27, 2015