-
-
Save eamodeorubio/768329 to your computer and use it in GitHub Desktop.
# StringCalculator in Ruby 1.8.x | |
class StringCalculator | |
DEFAULT_DELIMITER_DETECTOR = /\,|\n/ | |
CUSTOM_DELIMITER_PARSERS = [/^\/\/(.)\n/, /^\/\/((?:\[[^\]]+\])+)\n/] | |
def initialize(numbers) | |
@numbers=numbers | |
@result=nil | |
@errors='' | |
@delimiterDetectorRegExp=DEFAULT_DELIMITER_DETECTOR | |
detect_and_consume_custom_delimiter_if_any | |
end | |
def total_sum | |
calculate_sum unless @result | |
raise "Negative numbers detected: #{@errors}" unless @errors.empty? | |
@result | |
end | |
private | |
def calculate_sum | |
@result = 0 | |
@numbers.split(@delimiterDetectorRegExp).each do |currentNumber| | |
currentNumber = currentNumber.to_i | |
@errors << " #{currentNumber}" if currentNumber < 0 | |
@result += currentNumber if currentNumber <= 1000 | |
end | |
end | |
def detect_and_consume_custom_delimiter_if_any | |
CUSTOM_DELIMITER_PARSERS.detect do |customDelimiterDetectorRegExp| | |
if customDelimiterMatch = customDelimiterDetectorRegExp.match(@numbers) | |
extractAllDelimiters(customDelimiterMatch[1]) | |
@numbers = @numbers[customDelimiterMatch.end(0)..-1] | |
end | |
end | |
end | |
def extractAllDelimiters(delimiterExpression) | |
return @delimiterDetectorRegExp = Regexp.union(@delimiterDetectorRegExp, Regexp.new(Regexp.escape(delimiterExpression))) if delimiterExpression.length == 1 | |
delimiterExpression.scan(/\[([^\]]+)\]/) do |delimiter| | |
@delimiterDetectorRegExp = Regexp.union(@delimiterDetectorRegExp, Regexp.new(Regexp.escape(delimiter[0]))) | |
end | |
end | |
end | |
describe StringCalculator do | |
context "Sums 0, 1 or 2 numbers" do | |
it "returns 0 when passed an empty string" do | |
StringCalculator.new('').total_sum.should == 0 | |
end | |
it "returns the number when passed only one number" do | |
StringCalculator.new('1').total_sum.should == 1 | |
StringCalculator.new('12').total_sum.should == 12 | |
StringCalculator.new('24').total_sum.should == 24 | |
StringCalculator.new('35').total_sum.should == 35 | |
end | |
it "returns the sum of two numbers separated by commas" do | |
StringCalculator.new('1,2').total_sum.should == 3 | |
StringCalculator.new('12,34').total_sum.should == 46 | |
StringCalculator.new('24,10').total_sum.should == 34 | |
StringCalculator.new('46,34').total_sum.should == 80 | |
end | |
end | |
context "Sums any number of integers" do | |
it "returns 13 when passed '1,10,2'" do | |
StringCalculator.new("1,10,2").total_sum.should == 13 | |
end | |
it "returns 254 when passed '21,1,232'" do | |
StringCalculator.new("21,1,232").total_sum.should == 254 | |
end | |
it "returns 112 when passed '100,5,6,1'" do | |
StringCalculator.new("100,5,6,1").total_sum.should == 112 | |
end | |
it "returns 153 when passed '10,11,102,30'" do | |
StringCalculator.new("10,11,102,30").total_sum.should == 153 | |
end | |
end | |
context "It deals with \\n as a delimiter" do | |
it "returns 46 when passed '12\\n34'" do | |
StringCalculator.new("12\n34").total_sum.should == 46 | |
end | |
it "returns 13 when passed '1,10\\n2'" do | |
StringCalculator.new("1,10\n2").total_sum.should == 13 | |
end | |
it "returns 254 when passed '21\\n1,232'" do | |
StringCalculator.new("21\n1,232").total_sum.should == 254 | |
end | |
it "returns 112 when passed '100\\n5\\n6\\n1'" do | |
StringCalculator.new("100\n5\n6\n1").total_sum.should == 112 | |
end | |
it "returns 153 when passed '10,11\\n102\\n30'" do | |
StringCalculator.new("10,11\n102\n30").total_sum.should == 153 | |
end | |
it "returns 112 when passed '100\\n5\\n6,1'" do | |
StringCalculator.new("100\n5\n6,1").total_sum.should == 112 | |
end | |
it "returns 153 when passed '10\\n11,102\\n30'" do | |
StringCalculator.new("10\n11,102\n30").total_sum.should == 153 | |
end | |
end | |
context "It deals with an arbitrary delimiter specified with '//delimiter\\n'" do | |
it "returns 46 when passed '//;\\n12;34'" do | |
StringCalculator.new("//;\n12;34").total_sum.should == 46 | |
end | |
it "returns 13 when passed '//*\\n1*10\\n2'" do | |
StringCalculator.new("//*\n1*10\n2").total_sum.should == 13 | |
end | |
it "returns 13 when passed '//*\\n1\\n10*2'" do | |
StringCalculator.new("//*\n1\n10*2").total_sum.should == 13 | |
end | |
it "returns 153 when passed '//-\\n10-11\\n102-30'" do | |
StringCalculator.new("//-\n10-11\n102-30").total_sum.should == 153 | |
end | |
end | |
context "It raises an error when passed negative numbers including the offending numbers" do | |
it "raises an error containing -22 when passed '1,-22'" do | |
lambda { StringCalculator.new("1,-22").total_sum }.should raise_error { |error| error.message.should include("-22") } | |
end | |
it "raises an error containing -22,-2 and -15 when passed '1,-22,33,-2,44,-15'" do | |
lambda { StringCalculator.new("1,-22,33,-2,44,-15").total_sum }.should raise_error { |error| error.message.should include("-22","-2","-15") } | |
end | |
end | |
context "Ignores numbers above 1000" do | |
it "returns 35 when passed '1,1002,34'" do | |
StringCalculator.new("1,1002,34").total_sum.should == 35 | |
end | |
it "returns 135 when passed '1,1002,34,50,2353,50'" do | |
StringCalculator.new("1,1002,34,50,2353,50").total_sum.should == 135 | |
end | |
end | |
context "It deals with a custom delimiter of variable length specified with '//[delimiter]\\n'" do | |
it "returns 46 when passed '//[;**]\\n12;**34'" do | |
StringCalculator.new("//[;**]\n12;**34").total_sum.should == 46 | |
end | |
it "returns 13 when passed '//[*-*]\\n1*-*10\\n2'" do | |
StringCalculator.new("//[*-*]\n1*-*10\n2").total_sum.should == 13 | |
end | |
it "returns 13 when passed '//[iop]\\n1\\n10iop2'" do | |
StringCalculator.new("//[iop]\n1\n10iop2").total_sum.should == 13 | |
end | |
it "returns 153 when passed '//[-99-]\\n10-99-11\\n102-99-30'" do | |
StringCalculator.new("//[-99-]\n10-99-11\n102-99-30").total_sum.should == 153 | |
end | |
end | |
context "It deals with a several custom delimiter '//[delimiter1][delimiter2]\\n'" do | |
it "returns 76 when passed '//[;**][%]\\n12;**34%10\n20'" do | |
StringCalculator.new("//[;**][%]\n12;**34%10\n20").total_sum.should == 76 | |
end | |
it "returns 53 when passed '//[*-][$$][&]\\n1*-*10\\n2$$15&25'" do | |
StringCalculator.new("//[*-*][$$][&]\n1*-*10\n2$$15&25").total_sum.should == 53 | |
end | |
end | |
end |
Test and implementation for only one number
Added test and implementation for two numbers. Need code clean up !
First refactor. Removed some duplication but I don't really like this code. Need to think more.
I added tests for any count of numbers to sum. I implemented it in a copy&paste way to show a clear pattern of duplication. Time to refactor.
We clearly were looking for commas in the string, extract the number and sum it, then look the next comma, extract the number and sum. It can be clearly be replaced by a loop, an iteration or a recursive call. I choose a recursivity.
Added test for allowing \n as a separator. Instead of looking for comma now we looks for the regular expression meaning "comma or \n"
Implemented single character custom delimiter. Notice I've moved the recursive algorithm to an auxiliar method. There are two reasons:
- Single Responsability Principle. I do not want a method doing two unrelated things: parsing the custom delimiter, and calculating the sum.
- The sum algorithm is recursive and I want to detect the delimiter only once
Refactored to improve readability
Refactored to use only one base case for recursivity.
Thanks to last change I can now refactor. I extract a method from add_numbers, called consume_next_number that extract the next number in the string. Returns the next number in the string and the rest of the string. This way the code is more readable and the methods are more focused.
Raising error when only a negative number is passed. Simple implementation
Test and implementation for several negative numbers. Notice I added the parameter errors to collect all the negative numbers found. If at the end (base case) there are errors then raise the exception
Ignoring numbers above 1000
A bit of refactoring. Emphasize that negative and greater than 1000 numbers are treated specially
Arbitrary length delimiters. Time to refactor !
Forgot to paste the test ! I'm sorry
Refactored. The two different parsing expressions for custom delimiters are stored in a constant array. This constant is iterated looking for the first one matching. If match calculate delimiter detector reg exp and consume the first line. If not return default delimiter detector (that is now a reg exp too instead a string)
Refactor to delete duplication in method arguments. To do so we do a more OO design. Created a constructor with number and the delimiter regexp as instance variables. They can now be removed from the method signature. Tests fails, need to be refactored too.
Tests refactored to the new design. Note the method is called total_sum instead of add_numbers, it is more readable in the new design.
Refactored again. Changed recursive design to an iterative one. It is more compact and we need to instantiate only one object eliminating the need to pass the regexp from object to object. This design hides better the information.
The recursive algorithm was spliting the string using the separator reg exp. I use the split method that do the same and returns an array.
Ooops ! Bad formatted code. Now its fine. Damned copy&paste !
One last refactor. Transform error and the result in instance variables allows us to:
- Compute only once the result. Once computed cache it.
- Remove the "process_special_...." method since no more clarifies the intent.
Final version. Supporting several custom delimiters of arbitrary length.
Initial contents