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