Yesterday, @Krzysztof Atłasik helped me figure out how to reduce redundancy in matching by using partial functions, so what used to look like:
i match {
case x if x == 0 ⇒
romanNumeral
case x if x >= 1000 ⇒
this.roman(i - 1000, s"${romanNumeral}M")
case x if x >= 900 ⇒
this.roman(i - 900, s"${romanNumeral}CM")
// etc.
now looks like:
object RomanNumerals {
def roman(i: Int)(implicit romanNumeral: String = ""): String =
this.tryRoman(romanNumeral)
.orElse(this.tryRoman(1000, "M", romanNumeral))
.orElse(this.tryRoman(900, "CM", romanNumeral))
.orElse(this.tryRoman(500, "D", romanNumeral))
.orElse(this.tryRoman(400, "CD", romanNumeral))
.orElse(this.tryRoman(100, "C", romanNumeral))
.orElse(this.tryRoman(90, "XC", romanNumeral))
.orElse(this.tryRoman(50, "L", romanNumeral))
.orElse(this.tryRoman(40, "XL", romanNumeral))
.orElse(this.tryRoman(10, "X", romanNumeral))
.orElse(this.tryRoman(9, "IX", romanNumeral))
.orElse(this.tryRoman(5, "V", romanNumeral))
.orElse(this.tryRoman(4, "IV", romanNumeral))
.orElse(this.tryRoman(1, "I", romanNumeral))
.apply(i)
private def tryRoman(romanNumeral: String = ""): PartialFunction[Int, String] = {
case value if value == 0 => romanNumeral
}
private def tryRoman(
upperGuard: Int,
token: String,
romanNumeral: String
): PartialFunction[Int, String] = {
case value if value >= upperGuard =>
this.roman(value - upperGuard)(s"$romanNumeral$token")
}
}
Okay, it is more concise and considerably drier, but I'm thinking I'd like to take this even further.
I've put all my values into a ListMap, like:
val romanNumeralByValue: ListMap[Int, String] = ListMap(
1000 → "M",
900 → "CM",
500 → "D",
400 → "CD",
100 → "C",
90 → "XC",
50 → "L",
40 → "XL",
10 → "X",
9 → "IX",
5 → "V",
4 → "IV",
1 → "I"
)
Now, I am trying to figure how how to transform this map into the series of parial functions.
I thought it would be something like:
def roman(i: Int)(implicit romanNumeral: String = ""): String = {
romanNumeralByValue.reduce(tryRoman){
case (keyvalue, accumulator) ⇒
accumulator
.orElse(this.tryRoman(keyvalue._1, keyvalue._2, romanNumeral))
}.apply(i)
}
But this doesn't compile.
Any ideas how to make this work?
Thanks!
Here is the basic idea of how to build the function.
(You still need to adapt this to your use case)
def checkLowerLimit(lowerLimit: Int, result: String): PartialFunction[Int, String] = {
case value if (value >= lowerLimit) => result
}
val limits: ListMap[Int, String] = ListMap(
10 -> "ten",
0 -> "zero"
)
val foo: PartialFunction[Int, String] =
limits.map((checkLowerLimit _).tupled).reduce {
(acc, f) => acc.orElse(f)
}
Which you can test like:
foo(11)
// res: String = "ten"
foo(10)
// res: String = "ten"
foo(3)
// res: String = "zero"
foo(-1)
// scala.MatchError: -1 (of class java.lang.Integer)
Applying the technique to the problem.
import scala.collection.immutable.ListMap
import scala.collection.mutable.StringBuilder
object RomanNumerals {
private val romanNumeralByValue: ListMap[Int, String] = ListMap(
1000 → "M",
900 → "CM",
500 → "D",
400 → "CD",
100 → "C",
90 → "XC",
50 → "L",
40 → "XL",
10 → "X",
9 → "IX",
5 → "V",
4 → "IV",
1 → "I"
)
private val tryRomanStep: (Int, String) => PartialFunction[Int, (Int, String)] =
(upperLimit, result) => {
case value if (value >= upperLimit) =>
upperLimit -> result
}
private val tryRoman: PartialFunction[Int, (Int, String)] =
romanNumeralByValue.map(tryRomanStep.tupled).reduce {
(acc, f) => acc.orElse(f)
}
def roman(i: Int): String = {
@annotation.tailrec
def loop(remainingValue: Int, acc: StringBuilder): String =
if (remainingValue == 0)
acc.result()
else {
val (removedValue, newToken) = tryRoman(remainingValue)
loop(remainingValue - removedValue, acc.append(newToken))
}
loop(
remainingValue = i,
acc = new StringBuilder()
)
}
}