Test Driven Development (series) - String calculator #3

It feels nice when adding new features, but don't forget about refactoring! 💯

Brief

On top of the current implementation, we want to add a new feature.

We want to give the users the opportunity to use their own delimiters between numbers.

  • To add a custom delimiter, the beginning of the string will contain a separate line. So the input will look like this: //[delimiter]\n[numbers…]
    • for example //;\n1;2 should return 3 where the custom delimiter is ;
  • The first line //[delimiter]\n should be optional. All existing scenarios should still be supported.

Implementation

As we're already used by now, we'll start with a test:

 @Test
 void testCalculator_CustomDelimiter() {
     StringCalculator stringCalculator = new StringCalculator();
     String input = "//;\n1;2";
     Assertions.assertEquals(3, stringCalculator.add(input));
 }

🔴  Of course, the test will fail, telling us that the input is not something the application can handle.

🔵  At this moment, we start thinking about an implementation.


The first thing coming to mind is to use a RegEx to match the new type of input.

If the input matches the pattern, we'll extract the new delimiter and we'll use it to split the numbers.

If not, we'll assume that the input came in the old format, and we'll use the current implementation.


🔵  To do that, we need to:

  1. Define the actual regular expression used to extract the delimiter and the numbers: //(.*)\\n(?<numbers>\\S*)
  2. Add a new SplitStringStrategy to the StringSplitter based on the extracted delimiter.
  3. Split numbers and calculate the sum like we did until now.

🔵   Add a method to the StringSplitter so that we are able to add a new StringSplitStrategy , next to the default ones.

 public class StringSplitter {
  ---
    public void addStrategy(StringSplitStrategy stringSplitStrategy) {
        splitStrategyList.add(stringSplitStrategy);
    }
    
}
StringSplitter.java

🔵   Add the logic to the StringCalculator to match the pattern.

public class StringCalculator {

    private static final String NEW_DELIMITER = "//(?<delimiter>.*)\\n(?<numbers>\\S*)";

    private final StringSplitter stringSplitter = new StringSplitter();

    public int add(String input) {

        if (input.isEmpty()) {
            return 0;
        }

        Pattern pattern = Pattern.compile(NEW_DELIMITER, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(input);

        if (matcher.find()) {
            String delimiter = matcher.group("delimiter");
            stringSplitter.addStrategy(new StringSplitStrategy(delimiter));

            input = matcher.group("numbers");
        }

        int sum = 0;

        String[] splitNumbers = stringSplitter.splitNumbers(input);

        for (String number : splitNumbers) {
            sum += Integer.parseInt(number);
        }

        return sum;
    }

}
StringCalculator.java

🟢  Run all the tests again and see them turning green. 🚀


Refactoring

We can now go a step further and do a bit of refactoring.

Let's create a DelimiterMatcher class which will hold the responsibility of matching and extracting the groups from the input.

Because we want to return both delimiter and numbers values, we will need a DTO to store the two.

public class DelimiterMatchResult {
    private Boolean found;
    private String delimiter;
    private String numbers;

    private DelimiterMatchResult(String delimiter, String numbers, Boolean found) {
        this.delimiter = delimiter;
        this.numbers = numbers;
        this.found = found;
    }

    public static DelimiterMatchResult NOT_FOUND(String numbers) {
        DelimiterMatchResultBuilder delimiterMatchResultBuilder =
                new DelimiterMatchResultBuilder()
                .withNumbers(numbers)
                .isFound(false);
        return delimiterMatchResultBuilder.build();
    }

    public static DelimiterMatchResult FOUND(String delimiter, String numbers) {
        DelimiterMatchResultBuilder delimiterMatchResultBuilder =
                new DelimiterMatchResultBuilder()
                .withNumbers(numbers)
                .withDelimiter(delimiter)
                .isFound(true);
        return delimiterMatchResultBuilder.build();
    }

    private static class DelimiterMatchResultBuilder {
        private String delimiter;
        private String numbers;
        private Boolean found;

        public DelimiterMatchResultBuilder withDelimiter(String delimiter) {
            this.delimiter = delimiter;
            return this;
        }

        public DelimiterMatchResultBuilder withNumbers(String numbers) {
            this.numbers = numbers;
            return this;
        }

        public DelimiterMatchResultBuilder isFound(Boolean found) {
            this.found = found;
            return this;
        }

        public DelimiterMatchResult build() {
            return new DelimiterMatchResult(this.delimiter, this.numbers, this.found);
        }
    }

    public String getDelimiter() {
        return delimiter;
    }

    public String getNumbers() {
        return numbers;
    }

    public Boolean isFound() {
        return found;
    }
}
DelimiterMatchResult.java
public class DelimiterMatcher {

    private static final String NEW_DELIMITER = "//(?<delimiter>.*)\\n(?<numbers>\\S*)";

    public static DelimiterMatchResult matchExpression(String input) {
        Pattern pattern = Pattern.compile(NEW_DELIMITER, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(input);
   
        if (matcher.find()) {
            String delimiter = matcher.group("delimiter");
            String numbers = matcher.group("numbers");
            return DelimiterMatchResult.FOUND(delimiter, numbers);
        }
        return DelimiterMatchResult.NOT_FOUND(input);
    }

}
DelimiterMatcher.java

A bit of cleaning in the StringSplitter too.

public class StringSplitter {

    public static final String DEFAULT_DELIMITER_1 = ",";
    public static final String DEFAULT_DELIMITER_2 = "\n";
    private final static String CUSTOM_DELIMITER = ";";

    private final static StringSplitStrategy DEFAULT_1_STRATEGY = new StringSplitStrategy(DEFAULT_DELIMITER_1);
    private final static StringSplitStrategy DEFAULT_2_STRATEGY = new StringSplitStrategy(DEFAULT_DELIMITER_2);

    private List<StringSplitStrategy> splitStrategyList;

    public StringSplitter() {
        this.splitStrategyList = new ArrayList<>(List.of(DEFAULT_1_STRATEGY, DEFAULT_2_STRATEGY));
    }

---

}
StringSplitter.java

And now, putting it all together.

public class StringCalculator {

    public int add(String input) {

       ---

        StringSplitter stringSplitter = new StringSplitter();

        DelimiterMatchResult delimiterMatchResult = DelimiterMatcher.matchExpression(input);

        if (delimiterMatchResult.isFound()) {
            stringSplitter.addStrategy(new StringSplitStrategy(delimiterMatchResult.getDelimiter()));
        }

        String[] splitNumbers = stringSplitter.splitNumbers(delimiterMatchResult.getNumbers());
        

       ---
      
    }

}
StringCalculator.java

You can find the full project with the rest of the tests here: https://github.com/andreiszu/tdd-kata


💡
Don't miss out on more posts like this! Susbcribe to our free newsletter!
💡
Currently I am working on a Java Interview e-book designed to successfully get you through any Java technical interview you may take.
Stay tuned! 🚀