Scala runtime string interpolation/formatting

前端 未结 3 1934
青春惊慌失措
青春惊慌失措 2021-01-20 11:10

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

相关标签:
3条回答
  • 2021-01-20 11:51

    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!
    
    0 讨论(0)
  • 2021-01-20 12:05

    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.

    0 讨论(0)
  • 2021-01-20 12:05

    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.

    0 讨论(0)
提交回复
热议问题