/
osherove-tdd-1-05-16-12.rb
161 lines (132 loc) · 3.42 KB
/
osherove-tdd-1-05-16-12.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class StringCalculatorError < StandardError
attr_reader :invalid_values
def initialize(invalid_values)
@invalid_values = invalid_values
super("negatives not allowed: #{invalid_values.join(",")}")
end
end
class StringCalculator
attr_writer :parse_source
def add(str)
parse(str).reduce(0, :+)
end
private
def parse(str)
res = parse_source.call(str)
validate res
end
def validate(list)
unless valid?(list)
raise StringCalculatorError.new(errors)
end
list
end
def errors
@errors
end
def valid?(list)
@errors = list.select {|i| i < 0}
@errors.empty?
end
def parse_source
@parse_source ||= lambda {|str| Parser.new(str).numbers}
end
end
class Parser
attr_reader :input
def initialize(input)
@input = input
end
def numbers
res = items.map(&:to_i)
res.empty? ? [0] : res
end
def delimiter
matched = input.match(%r|//(.*)|)
(matched && matched.captures[0]) || ','
end
private
def lines
input.sub(%r|//.*|,'').split("\n")
end
def items
lines.map {|line| line.split(delimiter)}.flatten
end
end
# After Kata notes
#
# * finished on time
# * Having minitest in a guard makes a world of difference
# * Parser feels right. Using MatchData instead of String#[] feels
# better
# * the private methods on StringCalculator bug me, but the intention
# was to not add methods to the "public api" that were just helpers
# * I like the flexibility that the dependency injection (thanks to
# @avdi for the inspiration in Object on Rails) brings to the system.
# I didn't take full advantage of it in the tests, but not coupling
# feels correct
# * using a specialized exception feels right as well (is `raise str`
# a smell?)
require 'minitest/autorun'
describe StringCalculator do
before do
@obj = StringCalculator.new
end
it "can be newed up" do
@obj.wont_be_nil
end
it "can add empty string" do
@obj.add('').must_equal 0
end
it "can add single digit" do
@obj.add('1').must_equal 1
end
it "can add two numbers" do
@obj.add('1,2').must_equal 3
end
it "can add three numbers" do
@obj.add('1,2,3').must_equal 6
end
it "can handle newlines" do
@obj.add("1,2\n3").must_equal 6
end
it "parses numbers" do
called = false
@obj.parse_source = ->(str) { called = true; [1,2,3] }
@obj.add("1,2,3")
called.must_equal true
end
it "throws errors if a number is negative" do
lambda { @obj.add("1,2,-3")}.must_raise StringCalculatorError
end
it "includes the error numbers if a number is negative" do
begin
@obj.add("1,2,-3,0,-1")
rescue StringCalculatorError => e
e.invalid_values.must_equal [-3,-1]
end
end
end
describe Parser do
it "gets inited with a string" do
Parser.new("").input.wont_be_nil
end
it "returns a list of numbers from the string" do
Parser.new("1,2").numbers.must_equal [1,2]
end
it "returns 0 for ''" do
Parser.new('').numbers.must_equal [0]
end
it "returns a list of numbers when newlines are in the string" do
Parser.new("1,2\n3").numbers.must_equal [1,2,3]
end
it "defaults to ',' as a delimiter" do
Parser.new('').delimiter.must_equal ','
end
it "switches delimiter if the first line is '//<new delimiter>" do
Parser.new("//ab\n1").delimiter.must_equal 'ab'
end
it "parses lines with delimiter flag" do
Parser.new("//ab\n1ab2\n3").numbers.must_equal [1,2,3]
end
end