Adding a nice trim for multi-line strings in Scala
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)
}
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.