Adding a nice trim for multi-line strings in Scala

Adding a nice trim for multi-line strings in Scala
Photo by chester wade / Unsplash

As already mentioned in my PHP article on this topic, I frequently store multi-line strings in my programs. Today, let's use ascii based game art for our example.

Language version used in this article: Scala 3.6.2

The Artwork

Here is the artwork of a book that I want to display in the console:

|\\\\ ////|
| \\\V/// |
|  |~~~|  |
|  |===|  |
 \ |   | /
  \|===|/

Basic Multi-Line String

In Scala, regular strings have to be defined on a single line. For example, the following results in a compiler error:

val a = "
  Hello World
  "

To define multi-line strings in this fashion, we have to use triple quotes """. This is how that would look like:

@main
def main(): Unit = {
  val sprite = """
    |\\\\ ////|
    | \\\V/// |
    |  |~~~|  |
    |  |===|  |
     \ |   | /
      \|===|/
  """

  println(sprite)
}

The output is kind of OK, but not really:


    |\\\\ ////|
    | \\\V/// |
    |  |~~~|  |
    |  |===|  |
     \ |   | /
      \|===|/
  

We get additional blank lines at the top and bottom and hardcoded whitespace on the left, that mirrors the indentation level of the source code. For our game this is not what we want. Each sprite should render without any additional margins.

Scala Book solution

If you listen to the Scala Book, then the following is a solution:

@main
def main(): Unit = {
  val sprite = 
    """|\\\\ ////|
      k| \\\V/// |
      k|  |~~~|  |
      k|  |===|  |
      k \ |   | /
      k  \|===|/""".stripMargin('k')

  println(sprite)
}

Not what I would call elegant.

But to explain what we did:

  • eliminated the top blank line by immediately starting the string
  • eliminated the bottom blank line by immediately closing the string
  • removed left whitespace by adding a trim character and then calling stripMargin() on the string. The default trim character is |, but here we cannot use that as it is part of the artwork

Output:

|\\\\ ////|
| \\\V/// |
|  |~~~|  |
|  |===|  |
 \ |   | /
  \|===|/

So this works. But at what cost?! I want to be able to copy-paste artworks directly into my code without mangling it up in multiple ways in the source code, so I need something better!

Custom String Extension

When I lamented about this on the Scala Discord server, I immediately got some suggestions from the very helpful community, and chose to solve it with an extension method. Scala lets you add new methods to existing types from the outside.

In this case, I add a nice() method to the Java.lang.String type:

package StringExtensions

extension (input: String) {
  def nice(): String = {
    val contentLines = input
      .split("\\r?\\n")          
      .dropWhile(_.trim.isEmpty) // cutting off blank lines from the top
      .reverse
      .dropWhile(_.trim.isEmpty) // cutting off blank lines from the bottom
      .reverse

    val indent = contentLines
      .filter(_.trim.nonEmpty)           
      .map(_.takeWhile(_ == ' ').length) // getting the length of initial spaces for each line
      .minOption                         
      .getOrElse(0)

    contentLines
      .map(line => if line.length >= indent then line.drop(indent) else line)
      .mkString("\n")
  }
}

What it does:

  • removes all the top and bottom blank lines
  • finds the minimum indentation
  • cuts off excessive whitespace from all the lines

And then we can use it from another package like this:

import StringExtensions.nice

@main
def main(): Unit = {
  val sprite = """
    |\\\\ ////|
    | \\\V/// |
    |  |~~~|  |
    |  |===|  |
     \ |   | /
      \|===|/
  """.nice()

  println(sprite)
}
💡
If you prefer, you can define and call the nice method without parentheses, because it doesn't take any arguments.

And once again we get the proper output:

|\\\\ ////|
| \\\V/// |
|  |~~~|  |
|  |===|  |
 \ |   | /
  \|===|/

Conclusion

Initially I was quite disappointed with multi-line strings in Scala. But this challenge gave me a good reason to try and extend the language myself, which was surprisingly easy and a lot of fun.

Read more