TDD Basics : String Calculator Kata

Objectives


  • Triangulate to solve the problem
  • Experiment to learn and explore possible solution
  • Refactoring when there is no duplication to write intent revealing code
  • Simplifying method signature

Difficulty Level


Medium

Problem Statement


  • Try not to read ahead.
  • Do one task at a time. The trick is to learn to work incrementally.
  • Make sure you only test for correct inputs. There is no need to test for invalid inputs for this kata.
1. Create a simple String Calculator with a method add(string containing numbers)
  - The method can take 0, 1 or 2 numbers, and will return their sum (for an empty string it will return 0) for example '', '1' or '1,2'.
  - Start with the simplest test case of an empty string and move to 1 and two numbers.
  - Remember to solve things as simply as possible so that you force yourself to write tests you did not think about.
  - Remember to refactor after each passing test.
2. Allow the add method to handle an unknown amount of numbers.
3. Allow the add method to handle newlines between numbers (instead of commas)
    - The following input is ok: '1\n2,3' (will equal 6)
    - The following input is NOT ok: '1,\n' (no need to prove it - just clarifying)
4. Support different delimiters
    - To change a delimiter, the beginning of the string will contain a separate line that looks like this: '//[delimiter]\n[numbers...]' for example '//;\n1;2' should return three where the default delimiter is ';'
    - The first line is optional. All existing scenarios should still be supported.

This TDD Kata is by Roy Osherove found at : http://osherove.com/tdd-kata-1/. Follow the guidelines and write the specs. Compare your solution to the following solution.

Steps


Step 1

class Calculator
  def calculate(input)
      input.to_i
  end
end

describe Calculator do
  let(:calculator) {  Calculator.new }

  it "returns 0 for an empty string" do
    result = calculator.calculate("")

    result.should == 0
  end

  it "returns 1 for a string containing 1" do
    result = calculator.calculate("1")

    result.should == 1    
  end

end

Discussion


If I get stuck and I don’t know how a complex algorithm should work I’ll write a test for an error case. Then I’ll write a test for the simplest non-error case I can think of and return a hard coded value. Then I’ll write another test case and see if I can figure out the algorithm at that point. In doing so I gain some momentum and perhaps some insight in how the algorithm should behave on an edge case and a few normal cases.

This is called triangulation and it was used in celestial navigation for thousands of years. It is easier to see you are moving when you compare your position to two or more points on the horizon rather than just one. The same applies to coding; it is often easier to figure out the behavior of an algorithm by examining a couple of test cases instead of just one.

From a blog post on Triangulation by David Bernstein. Let's now triangulate and implement the real solution.

Step 2

class Calculator
  def calculate(input)
    strings = input.split(',')
    numbers = strings.map{|x| x.to_i}
    numbers.inject{|sum, n| sum + n}
  end
end

describe Calculator do
  let(:calculator) {  Calculator.new }

  it "returns 0 for an empty string" do
    result = calculator.calculate("")

    result.should == 0
  end

  it "returns 1 for a string containing 1" do
    result = calculator.calculate("1")

    result.should == 1    
  end

  it "returns the sum of the numbers for '1,2'" do
    result = calculator.calculate("1,2")

    result.should == 3        
  end

end

Started with the simplest test case of an empty string and moved to 1 and two numbers. Experimented in irb to get the generic solution working. Copied the code to calculate method to get the test passing. This broke the test #1. Let's fix that now.

Step 3

Added a guard condition to handle the blank string edge case.

class Calculator
  def calculate(input)
    if input.include?(',')
      strings = input.split(',')
      numbers = strings.map{|x| x.to_i}
      numbers.inject{|sum, n| sum + n}
    else
      input.to_i
    end
  end
end

Step 4

Refactored in green state. Made the methods smaller. Method names expressive and focused on doing just one thing.

class Calculator
  def calculate(input)
    if input.include?(',')
      numbers = convert_string_to_integers(input)
      calculate_sum(numbers)
    else
      input.to_i
    end
  end

  private

  def convert_string_to_integers(input)
    strings = input.split(',')
    strings.map{|x| x.to_i}
  end

  def calculate_sum(numbers)
    numbers.inject{|sum, n| sum + n}
  end
end

Note that this refactoring was not about duplication. The focus was to write intent revealing code.

Step 5

From the requirements, the spec for the next task:

it 'can add unknown amount of numbers' do
  result = calculator.calculate("1,2,3,4")

  result.should == 10           
end

Step 6

This test passes without failing. So we mutate the code to make the test fail:

def calculate_sum(numbers)
  return 0 if numbers.size == 4
  numbers.inject{|sum, n| sum + n}
end

Step 7

Now we make the test pass by removing the short-circuit statement :

return 0 if numbers.size == 4
def calculate_sum(numbers)
  numbers.inject{|sum, n| sum + n}
end

Step 8

Add the following statement to the calculator_spec.rb:

require_relative 'calculator' 

Step 9

Move the calculator class to its own file. All specs should pass.

Step 10

it 'allows new line also as a delimiter' do
  result = calculator.calculate("1\n2,3")

  result.should == 6
end

This test fails.

Step 11

To make it pass the calculator method now calls normalize_delimiter() method:

class Calculator

  def calculate(input)
    normalize_delimiter(input)
    if input.include?(',')
      numbers = convert_string_to_integers(input)
      calculate_sum(numbers)
    else
      input.to_i
    end
  end

  private

  def normalize_delimiter(input)
    input.gsub!("\n", ',')
  end
  ... Other methods are the same ...
end

Step 12

After experimenting in the irb and learning about the String API, the quick and dirty implementation looks like this:

class Calculator

  def calculate(input)
    if input.start_with?('//')
      @delimiter = input[2]
      @string = input[4, input.length - 1]
    else
      @delimiter = "\n"
      @string = input
    end

    normalize_delimiter
    if @string.include?(',')
      numbers = convert_string_to_integers
      calculate_sum(numbers)
    else
      @string.to_i
    end
  end

  private

  def convert_string_to_integers
    strings = @string.split(',')
    strings.map{|x| x.to_i}
  end

  def calculate_sum(numbers)
    numbers.inject{|sum, n| sum + n}
  end

  def normalize_delimiter
    @string.gsub!(@delimiter, ',')
  end
end

Step 13

After Cleanup :

class Calculator

  def calculate(input)
    initialize_delimiter_and_input(input)  
    normalize_delimiter
    if @string.include?(',')
      numbers = convert_string_to_integers
      calculate_sum(numbers)
    else
      @string.to_i
    end
  end

  private

  def initialize_delimiter_and_input(input)
    if input.start_with?('//')
      @delimiter = input[2]
      @string = input[4, input.length - 1]
    else
      @delimiter = "\n"
      @string = input
    end
  end

  def convert_string_to_integers
    strings = @string.split(',')
    strings.map{|x| x.to_i}
  end

  def calculate_sum(numbers)
    numbers.inject{|sum, n| sum + n}
  end

  def normalize_delimiter
    @string.gsub!(@delimiter, ',')
  end
end

We are not passing in the string to be processed into methods anymore. Since it is needed by most of the methods, it is now an instance variable. We removed the argument to the private methods to simplify the interface.

Summary


In this article you learned about Triangulation. When you apply Triangulation, you write tests for 0, 1, 2 element cases and finally generalize by extending the solution to any number of n elements.


Related Articles

Watch this Article as Screencast

You can watch this as a screencast TDD Basics : String Calculator Kata


Software Compatibility Best Practices

I spoke to some of the most talented and experienced software developers. I have created a guide that is filled with valuable insights and actionable ideas to boost developer productivity.

You will gain a better understanding of what's working well for other developers and how they address the software compatibility problems.

Get the Guide Now