Created
January 6, 2011 18:51
-
-
Save eamodeorubio/768329 to your computer and use it in GitHub Desktop.
The string calculator kata step by step
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
Author
Author
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.
Author
Ooops ! Bad formatted code. Now its fine. Damned copy&paste !
Author
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.
Author
Final version. Supporting several custom delimiters of arbitrary length.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.