Scala - shortening piece of code with higher level methods - scala

How can I shorten the following piece of code (the toText method) with higher level methods like map, takeWhile, filter, etc.:
/** Returns the text that is produced by this keypad from the given multi-tap input.
* For instance, the input `"33 3338881330000"` produces `"HIYAH!"`. The given string
* is assumed to consist of digits and spaces only.
*/
def toText(keysPressed: String): String = {
val pieces = keysPressed.split(" ")
var result = "" // gatherer
for (currentPiece <- pieces) { // most-recent holder
var remaining = currentPiece // gatherer
while (remaining.nonEmpty) {
val copyCount = countInitialCopies(remaining) // temporary
result += charFor(remaining(0), copyCount)
remaining = remaining.drop(copyCount)
}
}
result
}
where:
/** Returns the character produced by pressing the given number key (from '0' to '9')
* the given number of times on this keypad. If a key is pressed more times than there
* are characters assigned to the key, the result "wraps around".
*/
def charFor(keyPressed: Char, timesPressed: Int): Char = {
val charactersForKeys = Vector(" .,!?", "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "ÅÄÖ")
val key = charactersForKeys(keyPressed.asDigit)
key((timesPressed-1) % key.length)
}
and:
/** Determines the first letter of the given string, and returns the number of times
* the letter occurs consecutively at the beginning of the string. The given
* string must have at least one character.
*/
def countInitialCopies(str: String): Int = str.takeWhile(_ == str(0)).length
I tried to do the following, but it didn't go that far:
def toText(keysPressed: String): String = keysPressed.split(" ").foldLeft("")(charFor(_(0), countInitialCopies(_)))

Here's a slightly different approach to the problem. Uses takeWhile() but not much else.
val charsForKeys = Vector(" .,!?", "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ")
def toText(keysPressed: String): String = {
if (keysPressed.isEmpty) ""
else {
val kpHead = keysPressed.head
val kpStr = keysPressed.takeWhile(_ == kpHead)
val kpLen = kpStr.length
if (kpHead.isDigit)
charsForKeys(kpHead.asDigit)(kpLen-1) + toText(keysPressed.drop(kpLen))
else
toText(keysPressed.drop(kpLen)) //not a digit, skip these chars
}
}
toText("33 3338881330000") //res0: String = HIYAH!
2nd, shorter, attempt. Uses foldRight() and collect().
def toText(keysPressed: String): String = {
keysPressed.foldRight(List[String]()){
case (c, s::ss) => if (c == s.head) c+s :: ss
else c.toString :: s :: ss
case (c, Nil) => List(c.toString)
}.collect{
case s if s.head.isDigit => charsForKeys(s.head.asDigit)(s.length-1)
}.mkString
}

Not sure if it is really shorter but here is one:
def toText(keysPressed: String): String = {
def split(seq: Seq[Char]): Stream[Seq[Char]] = {
if (seq.isEmpty)
Stream.empty
else {
val lr = seq.span(ch => ch == seq.head)
Stream.cons(lr._1, split(lr._2))
}
}
split(keysPressed)
.filter(s => s.head.isDigit) // filter out spaces between different series of the same digits
.map(s => charFor(s.head, s.length))
.mkString("")
}
The idea behind this code is:
first split the keysPressed (treated as Seq[Char] thanks to implicit scala.collection.immutable.StringOps) into a Stream[Seq[Char]] in a way similar to your countInitialCopies (note that cons second argument is a "recursive" (actually delayed) call on the rest of the Seq!)
then filter out Seq[Char] that come from spaces for explicit groups separation,
then map that filtered Stream[Seq[Char]] using your charFor
finally accumulate the result from Stream into a String

Related

Iterate and trim string based on condition in spark Scala

I have dataframe 'regexDf' like below
id,regex
1,(.*)text1(.*)text2(.*)text3(.*)text4(.*)|(.*)text2(.*)text5(.*)text6(.*)
2,(.*)text1(.*)text5(.*)text6(.*)|(.*)text2(.*)
If the length of the regex exceeds some max length for example 50, then i want to remove the last text token in splitted regex string separated by '|' for the exceeded id. In the above data frame, id 1 length is more than 50 so that last tokens 'text4(.)' and 'text6(.)' from each splitted regex string should be removed. Even after removing that also length of the regex string in id 1 still more than 50, so that again last tokens 'text3(.)' and 'text5(.)' should be removed.so the final dataframe will be
id,regex
1,(.*)text1(.*)text2(.*)|(.*)text2(.*)
2,(.*)text1(.*)text5(.*)text6(.*)|(.*)text2(.*)
I am able to trim the last tokens using the following code
val reducedStr = regex.split("|").foldLeft(List[String]()) {
(regexStr,eachRegex) => {
regexStr :+ eachRegex.replaceAll("\\(\\.\\*\\)\\w+\\(\\.\\*\\)$", "\\(\\.\\*\\)")
}
}.mkString("|")
I tried using while loop to check the length and trim the text tokens in iteration which is not working. Also i want to avoid using var and while loop. Is it possible to achieve without while loop.
val optimizeRegexString = udf((regex: String) => {
if(regex.length >= 50) {
var len = regex.length;
var resultStr: String = ""
while(len >= maxLength) {
val reducedStr = regex.split("|").foldLeft(List[String]()) {
(regexStr,eachRegex) => {
regexStr :+ eachRegex
.replaceAll("\\(\\.\\*\\)\\w+\\(\\.\\*\\)$", "\\(\\.\\*\\)")
}
}.mkString("|")
len = reducedStr.length
resultStr = reducedStr
}
resultStr
} else {
regex
}
})
regexDf.withColumn("optimizedRegex", optimizeRegexString(col("regex")))
As per SathiyanS and Pasha suggestion, I changed the recursive method as function.
def optimizeRegex(regexDf: DataFrame): DataFrame = {
val shrinkString= (s: String) => {
if (s.length > 50) {
val extractedString: String = shrinkString(s.split("\\|")
.map(s => s.substring(0, s.lastIndexOf("text"))).mkString("|"))
extractedString
}
else s
}
def shrinkUdf = udf((regex: String) => shrinkString(regex))
regexDf.withColumn("regexString", shrinkUdf(col("regex")))
}
Now i am getting exception as "recursive value shrinkString needs type"
Error:(145, 39) recursive value shrinkString needs type
val extractedString: String = shrinkString(s.split("\\|")
.map(s => s.substring(0, s.lastIndexOf("text"))).mkString("|"));
Recursion:
def shrink(s: String): String = {
if (s.length > 50)
shrink(s.split("\\|").map(s => s.substring(0, s.lastIndexOf("text"))).mkString("|"))
else s
}
Looks like issues with function calling, some additional info.
Can be called as static function:
object ShrinkContainer {
def shrink(s: String): String = {
if (s.length > 50)
shrink(s.split("\\|").map(s => s.substring(0, s.lastIndexOf("text"))).mkString("|"))
else s
}
}
Link with dataframe:
def shrinkUdf = udf((regex: String) => ShrinkContainer.shrink(regex))
df.withColumn("regex", shrinkUdf(col("regex"))).show(truncate = false)
Drawbacks: Just basic example (approach) provided. Some edge cases (if regexp does not contains "text", if too many parts separated by "|", for ex. 100; etc.) have to be resolved by author of question, for avoid infinite recursion loop.
This is how I would do it.
First, a function for removing the last token from a regex:
def deleteLastToken(s: String): String =
s.replaceFirst("""[^)]+\(\.\*\)$""", "")
Then, a function that shortens the entire regex string by deleting the last token from all the |-separated fields:
def shorten(r: String) = {
val items = r.split("[|]").toSeq
val shortenedItems = items.map(deleteLastToken)
shortenedItems.mkString("|")
}
Then, for a given input regex string, create the stream of all the shortened strings you get by applying the shorten function repeatedly. This is an infinite stream, but it's lazily evaluated, so only as few elements as required will be actually computed:
val regex = "(.*)text1(.*)text2(.*)text3(.*)text4(.*)|(.*)text2(.*)text5(.*)text6(.*)"
val allShortened = Stream.iterate(regex)(shorten)
Finally, you can treat allShortened as any other sequence. For solving our problem, you can drop all elements while they don't satisfy the length requirement, and then keep only the first one of the remaining ones:
val result = allShortened.dropWhile(_.length > 50).head
You can see all the intermediate values by printing some elements of allShortened:
allShortened.take(10).foreach(println)
// Prints:
// (.*)text1(.*)text2(.*)text3(.*)text4(.*)|(.*)text2(.*)text5(.*)text6(.*)
// (.*)text1(.*)text2(.*)text3(.*)|(.*)text2(.*)text5(.*)
// (.*)text1(.*)text2(.*)|(.*)text2(.*)
// (.*)text1(.*)|(.*)
// (.*)|(.*)
// (.*)|(.*)
// (.*)|(.*)
// (.*)|(.*)
// (.*)|(.*)
// (.*)|(.*)
Just to add to #pasha701 answer. Here is the solution that works in spark.
val df = sc.parallelize(Seq((1,"(.*)text1(.*)text2(.*)text3(.*)text4(.*)|(.*)text2(.*)text5(.*)text6(.*)"),(2,"(.*)text1(.*)text5(.*)text6(.*)|(.*)text2(.*)"))).toDF("ID", "regex")
df.show()
//prints
+---+------------------------------------------------------------------------+
|ID |regex |
+---+------------------------------------------------------------------------+
|1 |(.*)text1(.*)text2(.*)text3(.*)text4(.*)|(.*)text2(.*)text5(.*)text6(.*)|
|2 |(.*)text1(.*)text5(.*)text6(.*)|(.*)text2(.*) |
+---+------------------------------------------------------------------------+
Now you can use the #pasha701 shrink function using udf
val shrink: String => String = (s: String) => if (s.length > 50) shrink(s.split("\\|").map(s => s.substring(0,s.lastIndexOf("text"))).mkString("|")) else s
def shrinkUdf = udf((regex: String) => shrink(regex))
df.withColumn("regex", shrinkUdf(col("regex"))).show(truncate = false)
//prints
+---+---------------------------------------------+
|ID |regex |
+---+---------------------------------------------+
|1 |(.*)text1(.*)text2(.*)|(.*)text2(.*) |
|2 |(.*)text1(.*)text5(.*)text6(.*)|(.*)text2(.*)|
+---+---------------------------------------------+

How to deal with decimal number in scala

I have a file like this:
1 4.146846
2 3.201141
3 3.016736
4 2.729412
I want to use toDouble but, it's not working as expected :
val rows = textFile.map { line =>
val fields = line.split("[^\\d]+")
((fields(0),fields(1).toDouble))
}
val Num = rows.sortBy(- _._2).map{case (user , num) => num}.collect.mkString("::")
println(Num)
The result print out is 4.0::3.0::3.0::2.0.
What I expect is 4.146846::3.201141::3.016736::2.729412
How do I do this?
Your regular expression is stopping at the decimal point in 4.146846.
Try line.split("[^\\d.]+")
What about splitting the lines by variant number of whitespaces? The regular expression would be like '[\s]+' . This resumes in two parts per line, one digit and one double string.
My whole program looks like:
object Application {
def parseDouble(s: String) =
try {
Some(s.toDouble)
} catch {
case _ => None
}
def main(args: Array[String]): Unit = {
val linesIt = "1 3.201141\n2 4.146846\n3 3.016736\n4 2.729412".lines
var doubles: List[Double] = List.empty
for (singleLine <- linesIt) {
val oneDouble = parseDouble(singleLine.split("[\\s]+")(1))
doubles = if (oneDouble != None)
oneDouble.get::doubles
else
doubles
}
val doublesArr = doubles.toArray
println("before sorting: " + doublesArr.mkString("::"))
scala.util.Sorting.quickSort(doublesArr)
println("after sorting: " + doublesArr.mkString("::"))
}
}

Filtering inside `for` with pattern matching

I am reading a TSV file and using using something like this:
case class Entry(entryType: Int, value: Int)
def filterEntries(): Iterator[Entry] = {
for {
line <- scala.io.Source.fromFile("filename").getLines()
} yield new Entry(line.split("\t").map(x => x.toInt))
}
Now I am both interested in filtering out entries whose entryType are set to 0 and ignoring lines with column count greater or lesser than 2 (that does not match the constructor). I was wondering if there's an idiomatic way to achieve this may be using pattern matching and unapply method in a companion object. The only thing I can think of is using .filter on the resulting iterator.
I will also accept solution not involving for loop but that returns Iterator[Entry]. They solutions must be tolerant to malformed inputs.
This is more state-of-arty:
package object liner {
implicit class R(val sc: StringContext) {
object r {
def unapplySeq(s: String): Option[Seq[String]] = sc.parts.mkString.r unapplySeq s
}
}
}
package liner {
case class Entry(entryType: Int, value: Int)
object I {
def unapply(s: String): Option[Int] = util.Try(s.toInt).toOption
}
object Test extends App {
def lines = List("1 2", "3", "", " 4 5 ", "junk", "0, 100000", "6 7 8")
def entries = lines flatMap {
case r"""\s*${I(i)}(\d+)\s+${I(j)}(\d+)\s*""" if i != 0 => Some(Entry(i, j))
case __________________________________________________ => None
}
Console println entries
}
}
Hopefully, the regex interpolator will make it into the standard distro soon, but this shows how easy it is to rig up. Also hopefully, a scanf-style interpolator will allow easy extraction with case f"$i%d".
I just started using the "elongated wildcard" in patterns to align the arrows.
There is a pupal or maybe larval regex macro:
https://github.com/som-snytt/regextractor
You can create variables in the head of the for-comprehension and then use a guard:
edit: ensure length of array
for {
line <- scala.io.Source.fromFile("filename").getLines()
arr = line.split("\t").map(x => x.toInt)
if arr.size == 2 && arr(0) != 0
} yield new Entry(arr(0), arr(1))
I have solved it using the following code:
import scala.util.{Try, Success}
val lines = List(
"1\t2",
"1\t",
"2",
"hello",
"1\t3"
)
case class Entry(val entryType: Int, val value: Int)
object Entry {
def unapply(line: String) = {
line.split("\t").map(x => Try(x.toInt)) match {
case Array(Success(entryType: Int), Success(value: Int)) => Some(Entry(entryType, value))
case _ =>
println("Malformed line: " + line)
None
}
}
}
for {
line <- lines
entryOption = Entry.unapply(line)
if entryOption.isDefined
} yield entryOption.get
The left hand side of a <- or = in a for-loop may be a fully-fledged pattern. So you may write this:
def filterEntries(): Iterator[Int] = for {
line <- scala.io.Source.fromFile("filename").getLines()
arr = line.split("\t").map(x => x.toInt)
if arr.size == 2
// now you may use pattern matching to extract the array
Array(entryType, value) = arr
if entryType == 0
} yield Entry(entryType, value)
Note that this solution will throw a NumberFormatException if a field is not convertible to an Int. If you do not want that, you'll have to encapsulate x.toInt with a Try and pattern match again.

Scala: how to traverse stream/iterator collecting results into several different collections

I'm going through log file that is too big to fit into memory and collecting 2 type of expressions, what is better functional alternative to my iterative snippet below?
def streamData(file: File, errorPat: Regex, loginPat: Regex): List[(String, String)]={
val lines : Iterator[String] = io.Source.fromFile(file).getLines()
val logins: mutable.Map[String, String] = new mutable.HashMap[String, String]()
val errors: mutable.ListBuffer[(String, String)] = mutable.ListBuffer.empty
for (line <- lines){
line match {
case errorPat(date,ip)=> errors.append((ip,date))
case loginPat(date,user,ip,id) =>logins.put(ip, id)
case _ => ""
}
}
errors.toList.map(line => (logins.getOrElse(line._1,"none") + " " + line._1,line._2))
}
Here is a possible solution:
def streamData(file: File, errorPat: Regex, loginPat: Regex): List[(String,String)] = {
val lines = Source.fromFile(file).getLines
val (err, log) = lines.collect {
case errorPat(inf, ip) => (Some((ip, inf)), None)
case loginPat(_, _, ip, id) => (None, Some((ip, id)))
}.toList.unzip
val ip2id = log.flatten.toMap
err.collect{ case Some((ip,inf)) => (ip2id.getOrElse(ip,"none") + "" + ip, inf) }
}
Corrections:
1) removed unnecessary types declarations
2) tuple deconstruction instead of ulgy ._1
3) left fold instead of mutable accumulators
4) used more convenient operator-like methods :+ and +
def streamData(file: File, errorPat: Regex, loginPat: Regex): List[(String, String)] = {
val lines = io.Source.fromFile(file).getLines()
val (logins, errors) =
((Map.empty[String, String], Seq.empty[(String, String)]) /: lines) {
case ((loginsAcc, errorsAcc), next) =>
next match {
case errorPat(date, ip) => (loginsAcc, errorsAcc :+ (ip -> date))
case loginPat(date, user, ip, id) => (loginsAcc + (ip -> id) , errorsAcc)
case _ => (loginsAcc, errorsAcc)
}
}
// more concise equivalent for
// errors.toList.map { case (ip, date) => (logins.getOrElse(ip, "none") + " " + ip) -> date }
for ((ip, date) <- errors.toList)
yield (logins.getOrElse(ip, "none") + " " + ip) -> date
}
I have a few suggestions:
Instead of a pair/tuple, it's often better to use your own class. It gives meaningful names to both the type and its fields, which makes the code much more readable.
Split the code into small parts. In particular, try to decouple pieces of code that don't need to be tied together. This makes your code easier to understand, more robust, less prone to errors and easier to test. In your case it'd be good to separate producing your input (lines of a log file) and consuming it to produce a result. For example, you'd be able to make automatic tests for your function without having to store sample data in a file.
As an example and exercise, I tried to make a solution based on Scalaz iteratees. It's a bit longer (includes some auxiliary code for IteratorEnumerator) and perhaps it's a bit overkill for the task, but perhaps someone will find it helpful.
import java.io._;
import scala.util.matching.Regex
import scalaz._
import scalaz.IterV._
object MyApp extends App {
// A type for the result. Having names keeps things
// clearer and shorter.
type LogResult = List[(String,String)]
// Represents a state of our computation. Not only it
// gives a name to the data, we can also put here
// functions that modify the state. This nicely
// separates what we're computing and how.
sealed case class State(
logins: Map[String,String],
errors: Seq[(String,String)]
) {
def this() = {
this(Map.empty[String,String], Seq.empty[(String,String)])
}
def addError(date: String, ip: String): State =
State(logins, errors :+ (ip -> date));
def addLogin(ip: String, id: String): State =
State(logins + (ip -> id), errors);
// Produce the final result from accumulated data.
def result: LogResult =
for ((ip, date) <- errors.toList)
yield (logins.getOrElse(ip, "none") + " " + ip) -> date
}
// An iteratee that consumes lines of our input. Based
// on the given regular expressions, it produces an
// iteratee that parses the input and uses State to
// compute the result.
def logIteratee(errorPat: Regex, loginPat: Regex):
IterV[String,List[(String,String)]] = {
// Consumes a signle line.
def consume(line: String, state: State): State =
line match {
case errorPat(date, ip) => state.addError(date, ip);
case loginPat(date, user, ip, id) => state.addLogin(ip, id);
case _ => state
}
// The core of the iteratee. Every time we consume a
// line, we update our state. When done, compute the
// final result.
def step(state: State)(s: Input[String]): IterV[String, LogResult] =
s(el = line => Cont(step(consume(line, state))),
empty = Cont(step(state)),
eof = Done(state.result, EOF[String]))
// Return the iterate waiting for its first input.
Cont(step(new State()));
}
// Converts an iterator into an enumerator. This
// should be more likely moved to Scalaz.
// Adapted from scalaz.ExampleIteratee
implicit val IteratorEnumerator = new Enumerator[Iterator] {
#annotation.tailrec def apply[E, A](e: Iterator[E], i: IterV[E, A]): IterV[E, A] = {
val next: Option[(Iterator[E], IterV[E, A])] =
if (e.hasNext) {
val x = e.next();
i.fold(done = (_, _) => None, cont = k => Some((e, k(El(x)))))
} else
None;
next match {
case None => i
case Some((es, is)) => apply(es, is)
}
}
}
// main ---------------------------------------------------
{
// Read a file as an iterator of lines:
// val lines: Iterator[String] =
// io.Source.fromFile("test.log").getLines();
// Create our testing iterator:
val lines: Iterator[String] = Seq(
"Error: 2012/03 1.2.3.4",
"Login: 2012/03 user 1.2.3.4 Joe",
"Error: 2012/03 1.2.3.5",
"Error: 2012/04 1.2.3.4"
).iterator;
// Create an iteratee.
val iter = logIteratee("Error: (\\S+) (\\S+)".r,
"Login: (\\S+) (\\S+) (\\S+) (\\S+)".r);
// Run the the iteratee against the input
// (the enumerator is implicit)
println(iter(lines).run);
}
}

Indentation preserving string interpolation in scala

I was wondering if there is any way of preserving indentation while doing string interpolation in scala. Essentially, I was wondering if I could interpose my own StringContext. Macros would address this problem, but I'd like to wait until they are official.
This is what I want:
val x = "line1 \nline2"
val str = s"> ${x}"
str should evaluate to
> line1
line2
Answering my question, and converting Daniel Sobral's very helpful answer to code. Hopefully it will be of use to someone else with the same issue. I have not used implicit classes since I am still pre-2.10.
Usage:
import Indenter._ and use string interpolation like so e" $foo "
Example
import Indenter._
object Ex extends App {
override def main(args: Array[String]) {
val name = "Foo"
val fields = "x: Int\ny:String\nz:Double"
// fields has several lines. All of them will be indented by the same amount.
print (e"""
class $name {
${fields}
}
""")
}
}
should print
class Foo
x: Int
y: String
z: Double
Here's the custom indenting context.
class IndentStringContext(sc: StringContext) {
def e(args: Any*):String = {
val sb = new StringBuilder()
for ((s, a) <- sc.parts zip args) {
sb append s
val ind = getindent(s)
if (ind.size > 0) {
sb append a.toString().replaceAll("\n", "\n" + ind)
} else {
sb append a.toString()
}
}
if (sc.parts.size > args.size)
sb append sc.parts.last
sb.toString()
}
// get white indent after the last new line, if any
def getindent(str: String): String = {
val lastnl = str.lastIndexOf("\n")
if (lastnl == -1) ""
else {
val ind = str.substring(lastnl + 1)
if (ind.trim.isEmpty) ind // ind is all whitespace. Use this
else ""
}
}
}
object Indenter {
// top level implicit defs allowed only in 2.10 and above
implicit def toISC(sc: StringContext) = new IndentStringContext(sc)
}
You can write your own interpolators, and you can shadow the standard interpolators with your own. Now, I have no idea what's the semantic behind your example, so I'm not even going to try.
Check out my presentation on Scala 2.10 on either Slideshare or SpeakerDeck, as they contain examples on all the manners in which you can write/override interpolators. Starts on slide 40 (for now -- the presentation might be updated until 2.10 is finally out).
For Anybody seeking a post 2.10 answer:
object Interpolators {
implicit class Regex(sc: StringContext) {
def r = new util.matching.Regex(sc.parts.mkString, sc.parts.tail.map(_ => "x"): _*)
}
implicit class IndentHelper(val sc: StringContext) extends AnyVal {
import sc._
def process = StringContext.treatEscapes _
def ind(args: Any*): String = {
checkLengths(args)
parts.zipAll(args, "", "").foldLeft("") {
case (a, (part, arg)) =>
val processed = process(part)
val prefix = processed.split("\n").last match {
case r"""([\s|]+)$d.*""" => d
case _ => ""
}
val argLn = arg.toString
.split("\n")
val len = argLn.length
// Todo: Fix newline bugs
val indented = argLn.zipWithIndex.map {
case (s, i) =>
val res = if (i < 1) { s } else { prefix + s }
if (i == len - 1) { res } else { res + "\n" }
}.mkString
a + processed + indented
}
}
}
}
Here's a short solution. Full code and tests on Scastie. There are two versions there, a plain indented interpolator, but also a slightly more complex indentedWithStripMargin interpolator which allows it to be a bit more readable:
assert(indentedWithStripMargin"""abc
|123456${"foo\nbar"}-${"Line1\nLine2"}""" == s"""|abc
|123456foo
| bar-Line1
| Line2""".stripMargin)
Here is the core function:
def indentedHelper(parts: List[String], args: List[String]): String = {
// In string interpolation, there is always one more string than argument
assert(parts.size == 1+args.size)
(parts, args) match {
// The simple case is where there is one part (and therefore zero args). In that case,
// we just return the string as-is:
case (part0 :: Nil, Nil) => part0
// If there is more than one part, we can simply take the first two parts and the first arg,
// merge them together into one part, and then use recursion. In other words, we rewrite
// indented"A ${10/10} B ${2} C ${3} D ${4} E"
// as
// indented"A 1 B ${2} C ${3} D ${4} E"
// and then we can rely on recursion to rewrite that further as:
// indented"A 1 B 2 C ${3} D ${4} E"
// then:
// indented"A 1 B 2 C 3 D ${4} E"
// then:
// indented"A 1 B 2 C 3 D 4 E"
case (part0 :: part1 :: tailparts, arg0 :: tailargs) => {
// If 'arg0' has newlines in it, we will need to insert spaces. To decide how many spaces,
// we count many characters after after the last newline in 'part0'. If there is no
// newline, then we just take the length of 'part0':
val i = part0.reverse.indexOf('\n')
val n = if (i == -1)
part0.size // if no newlines in part0, we just take its length
else
i // the number of characters after the last newline
// After every newline in arg0, we must insert 'n' spaces:
val arg0WithPadding = arg0.replaceAll("\n", "\n" + " "*n)
val mergeTwoPartsAndOneArg = part0 + arg0WithPadding + part1
// recurse:
indentedHelper(mergeTwoPartsAndOneArg :: tailparts, tailargs)
}
// The two cases above are exhaustive, but the compiler thinks otherwise, hence we need
// to add this dummy.
case _ => ???
}
}