LCD Digits Kata with Scala
I recently attended one of the coding dojos run by the London Scala User Group. It was great fun, and I’d recommend getting involved to anyone who’s interested in meeting other developers and learning more about scala and functional programming. After breaking into groups, we set about tackling the ‘LCD Digits’ problem from cyber-dojo.org. Although fairly straightforward as a programming challenge, it offers a number of ways to experiment with different functional idioms. The goal is to write a program that takes an integer, and formats this as a string composed of the ‘.’, ‘_’, “|’ and ” ” characters so that the output resembles an LCD display.
For example, given the input 1234567890
, the program should return
... ._. ._. ... ._. ._. ._. ._. ._. ._.
..| ._| ._| |_| |_. |_. ..| |_| |_| |.|
..| |_. ._| ..| ._| |_| ..| |_| ..| |_|
In our solution, we started by defining a class to represent each digit as a set of three strings—representing each line of the LCD display. A case class is used here so that the productIterator
function can be used to format the display’s rows as a 3-line String:
case class LCDDisplay(firstRow: String, secondRow: String, thirdRow: String) {
override def toString: String = {
productIterator mkString "\n"
}
}
The digits 0-9 are then represented as instantiations of this class:
object zero extends LCDDisplay(
"._.",
"|.|",
"|_|"
)
object one extends LCDDisplay("...", "..|", "..|")
object two extends LCDDisplay("._.", "._|", "|_.")
object three extends LCDDisplay("._.", "._|", "._|")
object four extends LCDDisplay("...", "|_|", "..|")
object five extends LCDDisplay("._.", "|_.", "._|")
object six extends LCDDisplay("._.", "|_.", "|_|")
object seven extends LCDDisplay("._.", "..|", "..|")
object eight extends LCDDisplay("._.", "|_|", "|_|")
object nine extends LCDDisplay("._.", "|_|", "..|")
With the individual digits now defined, we can turn our attention to the business logic of this problem: converting arbitrary integers into LCDDisplay objects. We will look to solve this with an LCDFormatter
object, whose intended behaviour we can describe with the following test cases:
import org.scalatest.FunSuite
class LCDFormatterTest extends FunSuite {
test("three should be formatted correctly") {
val LCD_THREE = """._.
^._|
^._|""".stripMargin('^')
assert(LCDFormatter.format(3) == LCD_THREE)
}
test("ten should be formatted correctly") {
val LCD_TEN = """... ._.
^..| |.|
^..| |_|""".stripMargin('^')
assert(LCDFormatter.format(10) == LCD_TEN)
}
test("negative inputs should throw an exception") {
intercept[IllegalArgumentException]{LCDFormatter.format(-1)}
}
}
Working iteratively, we arrived at the following implementation:
object LCDFormatter {
val digitMapping = Map(
0 -> zero,
1 -> one,
2 -> two,
3 -> three,
4 -> four,
5 -> five,
6 -> six,
7 -> seven,
8 -> eight,
9 -> nine)
val invalidInputMsg = s"${getClass.getName} only displays positive numbers"
private def merge(a: LCDDisplay, b: LCDDisplay): LCDDisplay = {
LCDDisplay(a.firstRow + " " + b.firstRow,
a.secondRow + " " + b.secondRow,
a.thirdRow + " " + b.thirdRow)
}
private def parse(input: BigInt): LCDDisplay = (input compare 0).signum match {
case -1 => throw new IllegalArgumentException(invalidInputMsg)
case _ => input.toString().map(a => digitMapping(a.asDigit)).reduce((a, b) => merge(a, b))
}
def format(input: BigInt) = parse(input).toString
}
The reduce
function allows us to combine any number of LCDDigit
objects into one, and requires us only to specify how two such objects should be combined. We do this in the merge
function defined above. Note that reduce
works in a similar fashion to fold
for non-empty collections, but does not need to be provided with an initial value.
We can now write a simple application to complete the exercise, using arguments passed to the program as input:
object Main extends App {
args.foreach(i => (println(LCDFormatter.format(BigInt(i)))))
}
Runing the application (using sbt) confirms that it behaves as expected:
sbt "run 1234567890"
[info] Loading project definition from /Users/rich/dev/lsug-dojo/project
[info] Set current project to lsug-dojo (in build file:/Users/rich/dev/lsug-dojo/)
[info] Running lsug.Main 1234567890
... ._. ._. ... ._. ._. ._. ._. ._. ._.
..| ._| ._| |_| |_. |_. ..| |_| |_| |.|
..| |_. ._| ..| ._| |_| ..| |_| ..| |_|
[success] Total time: 4 s, completed 22-Dec-2015 21:44:38