Are there any standard library facilities to do string interpolation/formatting at runtime? I\'d like the formatting to behave exactly the same as the macro based s\"s
There are no silly questions. Only Sunday mornings.
First, don't use String.format.
scala> val s = "Count to %d"
s: String = Count to %d
scala> String format (s, 42)
<console>:9: error: overloaded method value format with alternatives:
(x$1: java.util.Locale,x$2: String,x$3: Object*)String <and>
(x$1: String,x$2: Object*)String
cannot be applied to (String, Int)
String format (s, 42)
^
scala> s format 42
res1: String = Count to 42
But formatting can be expensive. So with your choice of escape handling:
scala> StringContext("Hello, {}. Today is {}." split "\\{}" : _*).s("Bob", "Tuesday")
res2: String = Hello, Bob. Today is Tuesday.
scala> StringContext("""Hello, \"{}.\" Today is {}.""" split "\\{}" : _*).s("Bob", "Tuesday")
res3: String = Hello, "Bob." Today is Tuesday.
scala> StringContext("""Hello, \"{}.\" Today is {}.""" split "\\{}" : _*).raw("Bob", "Tuesday")
res4: String = Hello, \"Bob.\" Today is Tuesday.
It turns out that split doesn't quite hack it.
scala> StringContext("Count to {}" split "\\{}" : _*) s 42
java.lang.IllegalArgumentException: wrong number of arguments (1) for interpolated string with 1 parts
at scala.StringContext.checkLengths(StringContext.scala:65)
at scala.StringContext.standardInterpolator(StringContext.scala:121)
at scala.StringContext.s(StringContext.scala:94)
... 33 elided
So given
scala> val r = "\\{}".r
r: scala.util.matching.Regex = \{}
scala> def parts(s: String) = r split s
parts: (s: String)Array[String]
Maybe
scala> def f(parts: Seq[String], args: Any*) = (parts zip args map (p => p._1 + p._2)).mkString
f: (parts: Seq[String], args: Any*)String
So
scala> val count = parts("Count to {}")
count: Array[String] = Array("Count to ")
scala> f(count, 42)
res7: String = Count to 42
scala> f(parts("Hello, {}. Today is {}."), "Bob", "Tuesday")
res8: String = Hello, Bob. Today is Tuesday
Hey, wait!
scala> def f(parts: Seq[String], args: Any*) = (parts.zipAll(args, "", "") map (p => p._1 + p._2)).mkString
f: (parts: Seq[String], args: Any*)String
scala> f(parts("Hello, {}. Today is {}."), "Bob", "Tuesday")
res9: String = Hello, Bob. Today is Tuesday.
or
scala> def f(parts: Seq[String], args: Any*) = (for (i <- 0 until (parts.size max args.size)) yield (parts.applyOrElse(i, (_: Int) => "") + args.applyOrElse(i, (_: Int) => ""))).mkString
f: (parts: Seq[String], args: Any*)String
or
scala> def f(parts: Seq[String], args: Any*) = { val sb = new StringBuilder ; for (i <- 0 until (parts.size max args.size) ; ss <- List(parts, args)) { sb append ss.applyOrElse(i, (_: Int) => "") } ; sb.toString }
f: (parts: Seq[String], args: Any*)String
scala> f(parts("Hello, {}. Today is {}. {}"), "Bob", "Tuesday", "Bye!")
res16: String = Hello, Bob. Today is Tuesday. Bye!
A. As of Scala 2.10.3, you can't use StringContext.f
unless you know the number of arguments at compile time since the .f
method is a macro.
B. Use String.format
, just like you would in the good ol' days of Java.
I had a similar requirement where I was loading a Seq[String]
from a config file which would become a command to be executed (using scala.sys.process
). To simplify the format and ignore any potential escaping problems I also made the variable names a configurable option too.
The config looked something like this:
command = ["""C:\Program Files (x86)\PuTTY\pscp.exe""", "-P", "2222", "-i",
".vagrant/machines/default/virtualbox/private_key", "$source", "~/$target"]
source = "$source"
target = "$target"
I couldn't find a nice (or efficient) way of using the StringContext
or "string".format
so I rolled my own VariableCommand which is quite similar to StringContext
however a single variable can appear zero or more times in any order and in any of the items.
The basic idea was to create a function which took the variable values and then would either take part of the string (e.g. "~/"
) or take the variable value (e.g. "test.conf"
) repeatedly to build up the result (e.g. "~/test.conf"
). This function is created once which is where all the complexity is and then at substitution time it is really simple (and hopefully fast although I haven't done any performance testing, or much testing at all for that matter).
For those that might wonder why I was doing this it was for running automation tests cross platform using ansible (which doesn't support Windows control machines) for provisioning. This allowed me to copy the files to the target machine and run ansible locally.