String format with named values

2019-09-09 19:58发布

问题:

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.

回答1:

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



回答2:

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


回答3:

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



标签: string scala