I'm searching for idiomatic scala way to format string with named arguments. I'm aware of the String method format, but it does not allow to specify named arguments, only positional are available.
Simple example:
val bob = "Bob"
val alice = "Alice"
val message = "${b} < ${a} and ${a} > ${b}"
message.format(a = alice, b = bob)
Defining message as separate value is crucial, since I want to load it from resource file and not specify in the code directly. There are plenty of similar questions that was answered with the new scala feature called String Interpolation. But this does not address my case: I could not let the compiler do all the work, since resource file is loaded in the run time.
It's not clear whether by "positional args" you mean the usual sense or the sense of "argument index" as used by Formatter
.
scala> val bob = "Bob"
bob: String = Bob
scala> val alice = "Alice"
alice: String = Alice
scala> val message = "%2$s likes %1$s and %1$s likes %2$s" format (bob, alice)
message: String = Alice likes Bob and Bob likes Alice
You'd like:
scala> def f(a: String, b: String) = s"$b likes $a and $a likes $b"
f: (a: String, b: String)String
scala> f(b = bob, a = alice)
res2: String = Bob likes Alice and Alice likes Bob
You can compile the interpolation with a reflective toolbox or the scripting engine.
scala> import scala.tools.reflect._
import scala.tools.reflect._
scala> val tb = reflect.runtime.currentMirror.mkToolBox()
tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@162b3d47
scala> val defs = s"""val a = "$alice" ; val b = "$bob""""
defs: String = val a = "Alice" ; val b = "Bob"
scala> val message = "${b} < ${a} and ${a} > ${b}"
message: String = ${b} < ${a} and ${a} > ${b}
scala> val msg = s"""s"$message""""
msg: String = s"${b} < ${a} and ${a} > ${b}"
scala> tb eval (tb parse s"$defs ; $msg")
res3: Any = Bob < Alice and Alice > Bob
or
scala> def f(a: String, b: String) = tb eval (tb parse s"""val a = "$a"; val b = "$b"; s"$message"""")
f: (a: String, b: String)Any
scala> f(a = alice, b = bob)
res4: Any = Bob < Alice and Alice > Bob
It's a little overmuch.
But consider:
https://www.playframework.com/documentation/2.0/ScalaTemplates
I don't know whether it's possible to use the same parser the compiler uses.
But Regex
can handle the example you've given.
val values = Map("a" -> "Alice", "b" -> "Bob")
val message = "${b} < ${a} and ${a} > ${b}"
def escapeReplacement(s: String): String =
s.replace("\\", "\\\\").replace("$", "\\$")
"\\$\\{([^\\}]*)\\}".r.replaceAllIn(message,
m => escapeReplacement(values(m.group(1))))
One approach might be to have a list of substitutions (no limit on how many variables) and then go with something like:
def substituteVar(s:String,subs:Seq[(String,String)]):String=
subs.foldLeft(s)((soFar,item) => soFar replaceAll("\\$\\{"+item._1+"}", item._2))
substituteVar(message,Seq(("a",alice),("b",bob)))
res12: String = Bob < Alice and Alice > Bob
Could be extended as well...