Skip to content

Instantly share code, notes, and snippets.

@eamodeorubio
Created January 6, 2011 18:51
Show Gist options
  • Save eamodeorubio/768329 to your computer and use it in GitHub Desktop.
Save eamodeorubio/768329 to your computer and use it in GitHub Desktop.
The string calculator kata step by step
# 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
@eamodeorubio
Copy link
Author

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)

@eamodeorubio
Copy link
Author

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.

@eamodeorubio
Copy link
Author

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.

@eamodeorubio
Copy link
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.

@eamodeorubio
Copy link
Author

Ooops ! Bad formatted code. Now its fine. Damned copy&paste !

@eamodeorubio
Copy link
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.

@eamodeorubio
Copy link
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