Solving the Greetings kata
Statement of the kata
The formulation of this kata is very simple. We have to create a pure function greet() that returns a string with a greeting. As a parameter, it must accept the name of the person that we want to greet.
Next, we’ll add requirements that will force us to extend the algorithm to support them, just through the function’s input and output. Each requisite will have be accompanied by an example. They are the following:
| Requirements | input | output |
|---|---|---|
| 1. Interpolate name in a simple greeting | “Bob” | Hello, Bob. |
| 2. If no name is passed, return some generic formula | null | Hello, my friend. |
| 3. If we’re yelled at, yell back | “JERRY” | HELLO, JERRY! |
| 4. Handle two names | “Jill”, “Jane” | Hello, Jill and Jane. |
| 5. Handle any number of names, using Oxford commas | “Amy”, “Brian”, “Charlotte” | Hello, Amy, Brian, and Charlotte. |
| 6. Allow mixing regular and yelled names, but separate the answers | “Amy”, “BRIAN”, “Charlotte” | Hello, Amy and Charlotte. AND HELLO BRIAN! |
| 7. If a name contains a comma, split it | “Bob”, “Charlie, Dianne” | Hello, Bob, Charlie, and Dianne. |
| 8. Allow escaping the commas of #7 | “Bob”, “\“Charlie, Dianne\”” | Hello, Bob and Charlie, Dianne. |
Language and approach
We’re going to solve this kata with Scala and the FunSite framework. We’ll write it using a functional approach.
Basic greeting
The way this kata is presented provides us with practically all the test cases we might need. At this point, I believe that we can start with a relatively long jump.
This is our first test, in which we assume that the function will be a method from the Greetings class in the greetings package.
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8 }
In any case, when using very strongly typed languages, many times we won’t be able to begin with smaller tests, as the compiler itself would force us to introduce more code. But, on the other hand, the strict typing lets us safely ignore those very same tests. In fact, you may consider that the strongly typed system is, in a certain way, a testing system.
The test will fail, as expected. In this case, we’ll create the minimum necessary code to make it pass in one go:
1 package greetings
2
3 object Greetings {
4 def greet(value: String): String = {
5 "Hello, Bob."
6 }
7 }
Scala doesn’t let us define a function without arguments and then call it with one, so we’re forced to include it in the signature. Otherwise, we return the string expected by the test so that it turns green.
Generic greeting
The second case consists in handling the situation where no name is passed, so the greeting should be a generic one.
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12 }
The first thing we do is observe that the test fails due to the fact that greet expects a parameter that we’re not passing. This indicates that it should be optional.
Our first impulse would be to correct that and allow an optional parameter to be passed. But, we have to take into account that if we do that the test will continue to fail.
Therefore, what we’re going to do, is to discard this last test momentarily, and refactor our current code while we keep the first test passing.
Use the parameter
We deactivate the test:
1 import org.scalatest.FunSuite
2
3 class GreetingTest extends FunSuite {
4 test("Require the function") {
5 assert(Greetings.greet("Bob") === "Hello, Bob.")
6 }
7
8 // test("Act when name is null") {
9 // assert(Greetings.greet() === "Hello, my friend.")
10 // }
11 }
And we do the refactoring. In Scala it’s possible to set default values, eliminating the need to pass a parameter.
1 package greetings
2
3 object Greetings {
4 def greet(name: String = "Bob"): String = {
5 "Hello, Bob."
6 }
7 }
The only thing left would be to make effective use of the parameter, this time, through an interpolation.
1 package greetings
2
3 object Greetings {
4 def greet(name: String = "Bob"): String = {
5 s"Hello, $name."
6 }
7 }
Back to the generic greeting
We turn the second test on again to be able to implement requirement number two, which consists in a generic greeting when no names are passed.
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12 }
The test won’t pass, but the necessary change to make it do so is very simple:
1 package greetings
2
3 object Greetings {
4 def greet(name: String = "my friend"): String = {
5 s"Hello, $name."
6 }
7 }
It’s very important to take note of this detail. We’ve made a very small change, but in order for it to be small, we performed a refactoring while protecting ourselves with the previous test. It’s very common to go and try to do that refactoring while the new test isn’t passing, but that’s bad practice because that way we can never be sure about what we’re doing and why it could be failing.
Answering with a yell
This third test introduces the new requirement of responding in a different manner to those names expressed in all capitals:
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16 }
We make sure that the test fails for the right reason before starting to write the production code. This is one possible approach:
1 package greetings
2
3 object Greetings {
4 def greet(name: String = "my friend"): String = {
5 if (name.toUpperCase == name) {
6 return s"HELLO, $name!"
7 }
8 s"Hello, $name."
9 }
10 }
Having arrived at this point, let’s see what refactoring opportunities we have. This leads us to this very simple solution:
1 package greetings
2
3 object Greetings {
4 def greet(name: String = "my friend"): String = {
5 if (name.toUpperCase() == name) s"HELLO, $name!" else s"Hello, $name."
6 }
7 }
For the time-being there’s not much else that we can do with the information that we have, so let’s move on and examine the next requisite.
Be able to greet two people
Requisite number four asks us to handle two name, which changes the greeting chain slightly. Of course, it provides us with an example with which to design a test.
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20 }
It’s possible that, while you’re writing the test, the IDE itself warns you that it’s not right to pass two arguments when the function’s signature only allows one, which on top of that is optional. In any case, the execution of the test will fail due to a compilation error.
As we’ve already seen in other occasions, the best way to tackle this it to go back to the previous test and fo a refactoring with which to prevent the problem. So, we temporarily cancel the test that we’ve just introduced.
Getting ready for several names
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 // test("Should manage two names") {
18 // assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 // }
20 }
And we refactor towards an implementation that allows us to introduce two parameters. The easiest way to do so is to use splat parameters. However, this will force us to change the algorithm, as the parameters will be presented as a Seq of Strings object. On top of that, we change the name of the parameter.
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 if (person.isEmpty) return "Hello, my friend."
6
7 val name = person.last
8
9 if (name.toUpperCase() == name) s"HELLO, $name!" else s"Hello, $name."
10 }
11 }
This is a naive reimplementation, it’s sufficient to let us pass the test, but it could be further developed to better match the style of the language. One of the best things about TDD is precisely this, a great facility to sketch out functional implementations, that might be rough but help us reflect on the problem and experiment with alternative solutions.
To improve it a bit, first we’re going to extract the if condition to a nested function, which is not only more expressive, but also easier to reuse if needed:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val name = person.last
11
12 if (isShouting(name)) s"HELLO, $name!" else s"Hello, $name."
13 }
14 }
The question now is, should we reintroduce the fourth test, or should we keep on refactoring to support the changes that we need?
A refactoring before proceeding
The last piece of refactoring has allowed us to support lists of names, but we’d need to change the approach to be able to handle lists of shouted names.
Up until now, we don’t decide if we need to yell until we’re building the greeting. However, it’s possible that we’re interested in separating the names first by whether they have to be shouted or not.
So, what we do is split the name list in two -yelled and regular names- and adapt the rest of the code to fit this.
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 s"Hello, ${normal.last}."
14 else if (shout.nonEmpty)
15 s"HELLO, ${shout.last}!"
16 else ""
17 }
18 }
With this, we should be better prepared to tackle the fourth test, so we reactivate it.
Reintroducing a test
When we turn the fourth test back on again, what we could have predicted happens: the greeting will be directed towards just one person, which will be precisely the last of the two.
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20 }
The result is:
1 Expected :"Hello, J[ill and J]ane."
2 Actual :"Hello, J[]ane."
That is, the test fails for the correct reason, indicating that we have to introduce a new change in the code to process and concatenate the list of names. Thanks to the previous refactorings, it’s easy to introduce:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 s"Hello, ${normal.mkString(" and ")}."
14 else if (shout.nonEmpty)
15 s"HELLO, ${shout.last}!"
16 else ""
17 }
18 }
It’s important to note that, at this point, we’re not trying to get ahead of the next requirements, we’re just solving the current problem. Only when we introduce the next test and learn new thing about the behavior that we have to implement to the function, we’ll maybe consider going back and refactor the previous changes as we need.
Handle an indeterminate amount of numbers
The fifth requirements consists in handling an arbitrary number of names, with a small change in the greeting format. We introduce a new test to specify it:
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20
21 test("Should manage several names") {
22 assert(Greetings.greet("Amy", "Brian", "Charlotte") === "Hello, Amy, Brian, and \
23 Charlotte.")
24 }
25 }
The result of the test is:
1 Expected :"Hello, Amy[, Brian,] and Charlotte."
2 Actual :"Hello, Amy[ and Brian] and Charlotte."
We can start from the next change:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 s"Hello, ${normal.mkString(", ")}."
14 else if (shout.nonEmpty)
15 s"HELLO, ${shout.last}!"
16 else ""
17 }
18 }
This breaks the previous test and doesn’t pass the new one, which indicates us that the last element of the list requires special treatment:
1 Expected :"Hello, Amy, Brian, [and ]Charlotte."
2 Actual :"Hello, Amy, Brian, []Charlotte."
Let’s do this literally, that is: let’s separate the last element:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 s"Hello, ${normal.init.mkString(", ")}, and ${normal.last}."
14 else if (shout.nonEmpty)
15 s"HELLO, ${shout.last}!"
16 else ""
17 }
18 }
However, this change makes the last test pass, at the same time that it breaks the previous and first one. The problem is that the case of the normal greeting and the two-people greeting can’t follow the same pattern. We’re robbing Peter to pay Paul.
Since we’re starting to break tests that we’re already in green, it’s best to go back to the stage in the code at which the four previous tests were passing.
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 s"Hello, ${normal.mkString(" and ")}."
14 else if (shout.nonEmpty)
15 s"HELLO, ${shout.last}!"
16 else ""
17 }
18 }
What this back and forth tells us is that there’s two kinds of cases that have a different treatment.
- Lists of 2 or less names.
- Lists of more than 2 names.
It’s simplest to acknowledge and embrace this in the code itself:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 if (normal.nonEmpty)
13 if (normal.length <= 2)
14 s"Hello, ${normal.mkString(" and ")}."
15 else
16 s"Hello, ${normal.init.mkString(", ")}, and ${normal.last}."
17 else if (shout.nonEmpty)
18 s"HELLO, ${shout.last}!"
19 else ""
20 }
21 }
Again, a rough and naive implementation lets us pass all of the tests, invoking such a simple mechanism as postponing the generalization might be. It’s now, after having achieved the desired behavior, when we can start trying to analyze the problem and search for a more general algorithm.
As we want to focus in the part of the algorithm that concatenates the names inside of the greeting, we’ll first do the following refactoring, extracting the target block of code to an inline function:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 def concatenate = {
13 if (normal.length <= 2)
14 s"${normal.mkString(" and ")}."
15 else
16 s"${normal.init.mkString(", ")}, and ${normal.last}."
17 }
18
19 if (normal.nonEmpty)
20 s"Hello, ${concatenate}"
21 else if (shout.nonEmpty)
22 s"HELLO, ${shout.last}!"
23 else ""
24 }
25 }
The most interesting part is to have specifically isolated the name concatenation. Let’s do a couple more changes. Right now we’re directly acting on the normal sequence that’s in the greet function’s namespace, and therefore, is global within the inner concatenate function:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 def concatenate(names: Seq[String]) = {
13 if (names.length <= 2)
14 s"${names.mkString(" and ")}"
15 else
16 s"${names.init.mkString(", ")}, and ${names.last}"
17 }
18
19 if (normal.nonEmpty)
20 s"Hello, ${concatenate(normal)}."
21 else if (shout.nonEmpty)
22 s"HELLO, ${shout.last}!"
23 else ""
24 }
25 }
After having made sure that the tests keep passing, let’s make the different cases that they handle explicit. Right now, the idea of “only one name” is covered implicitly by the two-name case. Our objective here is to better understand the regularities in the three situations:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 def concatenate(names: Seq[String]) = {
13 if (names.length == 1)
14 s"${names.last}"
15 else if (names.length == 2)
16 s"${names.mkString(" and ")}"
17 else
18 s"${names.init.mkString(", ")}, and ${names.last}"
19 }
20
21 if (normal.nonEmpty)
22 s"Hello, ${concatenate(normal)}."
23 else if (shout.nonEmpty)
24 s"HELLO, ${shout.last}!"
25 else ""
26 }
27 }
Let’s take a small step further in the case of the two names:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8 if (person.isEmpty) return "Hello, my friend."
9
10 val (shout, normal) = person.partition(isShouting)
11
12 def concatenate(names: Seq[String]) = {
13 if (names.length == 1)
14 s"${names.last}"
15 else if (names.length == 2)
16 s"${names.head} and ${names.last}"
17 else
18 s"${names.init.mkString(", ")}, and ${names.last}"
19 }
20
21 if (normal.nonEmpty)
22 s"Hello, ${concatenate(normal)}."
23 else if (shout.nonEmpty)
24 s"HELLO, ${shout.last}!"
25 else ""
26 }
27 }
In Scala this can be expressed more succinctly using match... case:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 names.length match {
11 case 1 => s"${names.last}"
12 case 2 => s"${names.head} and ${names.last}"
13 case _ => s"${names.init.mkString(", ")}, and ${names.last}"
14 }
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val (shout, normal) = person.partition(isShouting)
20
21 if (normal.nonEmpty)
22 s"Hello, ${concatenate(normal)}."
23 else if (shout.nonEmpty)
24 s"HELLO, ${shout.last}!"
25 else ""
26 }
27 }
And a little bit more:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val (shout, normal) = person.partition(isShouting)
20
21 if (normal.nonEmpty)
22 s"Hello, ${concatenate(normal)}."
23 else if (shout.nonEmpty)
24 s"HELLO, ${shout.last}!"
25 else ""
26 }
27 }
Shout to the shouters, but only to them
In the previous test we’ve tackled the problem of generalizing the algorithm for any number of names and make it more expressive without breaking the achieved functionality. It’s time to introduce a new requirement through a new test:
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20
21 test("Should manage several names") {
22 assert(Greetings.greet("Amy", "Brian", "Charlotte") === "Hello, Amy, Brian, and \
23 Charlotte.")
24 }
25
26 test("Should shout to shouters") {
27 assert(Greetings.greet("Amy", "BRIAN", "Charlotte") === "Hello, Amy and Charlott\
28 e. AND HELLO, BRIAN!")
29 }
30 }
This test fails, as we could expect. It’s interesting to note that we were already prepared for this case and we were already treating the “screaming” greetings separately. According to what we deducted from the example, we could treat the same way we did the “non-screaming”, taking into account that the two cases may appear simultaneously. After a couple of tries, we arrive at this:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val (shout, normal) = person.partition(isShouting)
20
21 s"${if (normal.nonEmpty) s"Hello, ${concatenate(normal)}." else ""}${if (shout.n\
22 onEmpty) s"${if (normal.nonEmpty) " AND " else ""}HELLO, ${concatenate(shout)}!" els\
23 e ""}"
24 }
25 }
Separate names that contain commas
The next requirement that we’re asked is to separate the names that contain commas. To get a better hold of this, it’s basically like allowing the user to pass the names as an indeterminate number of strings, as well as in the shape of one long string that contains several names. This doesn’t actually alter the way in which we generate the greeting, but rather the way in which we prepare the input data.
Therefore, it’s time to add a test to exemplify this new requisite:
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20
21 test("Should manage several names") {
22 assert(Greetings.greet("Amy", "Brian", "Charlotte") === "Hello, Amy, Brian, and \
23 Charlotte.")
24 }
25
26 test("Should shout to shouters") {
27 assert(Greetings.greet("Amy", "BRIAN", "Charlotte") === "Hello, Amy and Charlott\
28 e. AND HELLO, BRIAN!")
29 }
30
31 test("Should separate names with comma") {
32 assert(Greetings.greet("Bob", "Charlie, Dianne") === "Hello, Bob, Charlie, and D\
33 ianne.")
34 }
35 }
We run the test to check that it doesn’t pass, and we wonder about how to solve this new case.
In principle, we could go through the list of people and split each of them alongside the comma. As this will generate a collection of collections, we flatten it. There are methods in Scala to do so:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val personsList = person.flatMap(name => name.split(",").map(_.trim))
20
21 val (shout, normal) = personsList.partition(isShouting)
22
23 s"${if (normal.nonEmpty) s"Hello, ${concatenate(normal)}." else ""}${if (shout.n\
24 onEmpty) s"${if (normal.nonEmpty) " AND " else ""}HELLO, ${concatenate(shout)}!" els\
25 e ""}"
26 }
27 }
And here’s the test, that passes without a problem.
Once we’ve checked that the solution works, we refactor the code a little:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val (shout, normal) = person
20 .flatMap(_.split(",").map(_.trim))
21 .partition(isShouting)
22
23 s"${if (normal.nonEmpty) s"Hello, ${concatenate(normal)}." else ""}${if (shout.n\
24 onEmpty) s"${if (normal.nonEmpty) " AND " else ""}HELLO, ${concatenate(shout)}!" els\
25 e ""}"
26 }
27 }
Escaping commas
The eighth requisite is to allow the previous behavior to be avoided if the input text is escaped. Let’s see the case in the shape of a test:
1 import greetings.Greetings
2 import org.scalatest.FunSuite
3
4 class GreetingTest extends FunSuite {
5 test("Require the function") {
6 assert(Greetings.greet("Bob") === "Hello, Bob.")
7 }
8
9 test("Act when name is null") {
10 assert(Greetings.greet() === "Hello, my friend.")
11 }
12
13 test("Should manage shout") {
14 assert(Greetings.greet("JERRY") === "HELLO, JERRY!")
15 }
16
17 test("Should manage two names") {
18 assert(Greetings.greet("Jill", "Jane") === "Hello, Jill and Jane.")
19 }
20
21 test("Should manage several names") {
22 assert(Greetings.greet("Amy", "Brian", "Charlotte") === "Hello, Amy, Brian, and \
23 Charlotte.")
24 }
25
26 test("Should shout to shouters") {
27 assert(Greetings.greet("Amy", "BRIAN", "Charlotte") === "Hello, Amy and Charlott\
28 e. AND HELLO, BRIAN!")
29 }
30
31 test("Should separate names with comma") {
32 assert(Greetings.greet("Bob", "Charlie, Dianne") === "Hello, Bob, Charlie, and D\
33 ianne.")
34 }
35
36 test("Should not separate names with comma if escaped") {
37 assert(Greetings.greet("Bob", "\"Charlie, Dianne\"") === "Hello, Bob and Charlie\
38 , Dianne.")
39 }
40 }
Again, this affects the preparation of the data before the assembly of the greeting. The solution that comes to mind is to detect the situation in which the string comes escaped first, and then replace the comma for an arbitrary character before doing the split. Once done, we restore the original comma.
In this case, we achieve it with a regular expression, replacing the comma for the # symbol, and restoring it after.
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val escaped = "^\"([^,]+),(.+)\"$".r
20 val personsList = person
21 .map(input => escaped.replaceAllIn(input, "$1#$2"))
22 .flatMap(_.split(",").map(_.trim))
23 .map(_.replace("#", ","))
24
25 val (shout, normal) = personsList.partition(isShouting)
26
27 s"${if (normal.nonEmpty) s"Hello, ${concatenate(normal)}." else ""}${if (shout.n\
28 onEmpty) s"${if (normal.nonEmpty) " AND " else ""}HELLO, ${concatenate(shout)}!" els\
29 e ""}"
30 }
31 }
With this, we complete all of the requirements. We can do a small refactoring:
1 package greetings
2
3 object Greetings {
4 def greet(person: String*): String = {
5 def isShouting(name: String): Boolean = {
6 name.toUpperCase() == name
7 }
8
9 def concatenate(names: Seq[String]) = {
10 s"${names.length match {
11 case 1 => ""
12 case 2 => s"${names.head} and "
13 case _ => s"${names.init.mkString(", ")}, and "
14 }}${names.last}"
15 }
16
17 if (person.isEmpty) return "Hello, my friend."
18
19 val escaped = "^\"([^,]+),(.+)\"$".r
20 val (shout, normal) = person
21 .map(input => escaped.replaceAllIn(input, "$1#$2"))
22 .flatMap(_.split(",").map(_.trim))
23 .map(_.replace("#", ","))
24 .partition(isShouting)
25
26 s"${if (normal.nonEmpty) s"Hello, ${concatenate(normal)}." else ""}${if (shout.n\
27 onEmpty) s"${if (normal.nonEmpty) " AND " else ""}HELLO, ${concatenate(shout)}!" els\
28 e ""}"
29 }
30 }
One of the things that comes into attention in this kata is that the functional approach allows us to achieve relatively large behavioral changes with comparatively small changes in the production code.
What have we learned in this kata
- In this kata we’ve learned to postpone the generalization until we have more information about the algorithm that we’re developing
- We’ve applied the techniques that we’ve learned in previous kata
- We’ve checked that a strong type system can save us a few tests