4

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
Sriram Srinivasan
  • 1,255
  • 17
  • 16

4 Answers4

8

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)
}
Community
  • 1
  • 1
Sriram Srinivasan
  • 1,255
  • 17
  • 16
  • Thanks for this interesting example! And here is a link to docs on StringContext and string interpolation: http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.StringContext – KajMagnus Dec 13 '12 at 22:05
5

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).

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • Thanks. This is beautiful! There's not much semantics behind my example ... I'm outputting formatted source code, and I would like to be able to say `s"class Foo:\n ${fields}"` to get the results of fields lined up. – Sriram Srinivasan Jul 11 '12 at 04:44
0

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
      }
    }
  }
}
Busti
  • 5,492
  • 2
  • 21
  • 34
0

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 _ => ???
    }
  }
Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88