【问题标题】:Simple String template replacement in Scala and ClojureScala 和 Clojure 中的简单字符串模板替换
【发布时间】:2011-05-24 12:00:50
【问题描述】:

以下是用 Scala 和 Clojure 编写的函数,用于简单替换字符串中的模板。每个函数的输入是一个String,其中包含{key} 形式的模板和一个从符号/关键字到替换值的映射。

例如:

斯卡拉:

replaceTemplates("This is a {test}", Map('test -> "game"))

Clojure:

(replace-templates "This is a {test}" {:test "game"})

将返回"This is a game"

输入映射使用符号/关键字,因此我不必处理字符串中的模板包含大括号的极端情况。

很遗憾,算法效率不是很高。

这是 Scala 代码:

def replaceTemplates(text: String,
                     templates: Map[Symbol, String]): String = {
  val builder = new StringBuilder(text)

  @tailrec
  def loop(key: String,
           keyLength: Int,
           value: String): StringBuilder = {
    val index = builder.lastIndexOf(key)
    if (index < 0) builder
    else {
      builder.replace(index, index + keyLength, value)
      loop(key, keyLength, value)
    }
  }

  templates.foreach {
    case (key, value) =>
      val template = "{" + key.name + "}"
      loop(template, template.length, value)
  }

  builder.toString
}

这是 Clojure 代码:

(defn replace-templates
  "Return a String with each occurrence of a substring of the form {key}
   replaced with the corresponding value from a map parameter.
   @param str the String in which to do the replacements
   @param m a map of keyword->value"
  [text m]
  (let [sb (StringBuilder. text)]
    (letfn [(replace-all [key key-length value]
              (let [index (.lastIndexOf sb key)]
                (if (< index 0)
                  sb
                  (do
                    (.replace sb index (+ index key-length) value)
                    (recur key key-length value)))))]
      (doseq [[key value] m]
        (let [template (str "{" (name key) "}")]
          (replace-all template (count template) value))))
    (.toString sb)))

这是一个测试用例(Scala 代码):

replaceTemplates("""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
elit nisi, egestas et tincidunt eget, {foo} mattis non erat. Aenean ut
elit in odio vehicula facilisis. Vestibulum quis elit vel nulla
interdum facilisis ut eu sapien. Nullam cursus fermentum
sollicitudin. Donec non congue augue. {bar} Vestibulum et magna quis
arcu ultricies consectetur auctor vitae urna. Fusce hendrerit
facilisis volutpat. Ut lectus augue, mattis {baz} venenatis {foo}
lobortis sed, varius eu massa. Ut sit amet nunc quis velit hendrerit
bibendum in eget nibh. Cras blandit nibh in odio suscipit eget aliquet
tortor placerat. In tempor ullamcorper mi. Quisque egestas, metus eu
venenatis pulvinar, sem urna blandit mi, in lobortis augue sem ut
dolor. Sed in {bar} neque sapien, vitae lacinia arcu. Phasellus mollis
blandit commodo.
""", Map('foo -> "HELLO", 'bar -> "GOODBYE", 'baz -> "FORTY-TWO"))

和输出:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
elit nisi, egestas et tincidunt eget, HELLO mattis non erat. Aenean ut
elit in odio vehicula facilisis. Vestibulum quis elit vel nulla
interdum facilisis ut eu sapien. Nullam cursus fermentum
sollicitudin. Donec non congue augue. GOODBYE Vestibulum et magna quis
arcu ultricies consectetur auctor vitae urna. Fusce hendrerit
facilisis volutpat. Ut lectus augue, mattis FORTY-TWO venenatis HELLO
lobortis sed, varius eu massa. Ut sit amet nunc quis velit hendrerit
bibendum in eget nibh. Cras blandit nibh in odio suscipit eget aliquet
tortor placerat. In tempor ullamcorper mi. Quisque egestas, metus eu
venenatis pulvinar, sem urna blandit mi, in lobortis augue sem ut
dolor. Sed in GOODBYE neque sapien, vitae lacinia arcu. Phasellus mollis
blandit commodo.

该算法遍历输入映射,并且对于每一对,在输入 String 中进行替换,暂时保存在 StringBuilder 中。对于每个键/值对,我们搜索最后一次出现的键(括在大括号中)并将其替换为值,直到不再出现为止。

如果我们在 StringBuilder 中使用 .lastIndexOf.indexOf 是否会产生任何性能差异?

如何改进算法?有没有更惯用的方式来编写 Scala 和/或 Clojure 代码?

更新:见我的follow-up

UPDATE 2:这是一个更好的 Scala 实现;字符串长度为 O(n)。请注意,我根据几个人的建议将Map 修改为[String, String] 而不是[Symbol, String]。 (感谢mikerakotarak):

/**
 * Replace templates of the form {key} in the input String with values from the Map.
 *
 * @param text the String in which to do the replacements
 * @param templates a Map from Symbol (key) to value
 * @returns the String with all occurrences of the templates replaced by their values
 */
def replaceTemplates(text: String,
                     templates: Map[String, String]): String = {
  val builder = new StringBuilder
  val textLength = text.length

  @tailrec
  def loop(text: String): String = {
    if (text.length == 0) builder.toString
    else if (text.startsWith("{")) {
      val brace = text.indexOf("}")
      if (brace < 0) builder.append(text).toString
      else {
        val replacement = templates.get(text.substring(1, brace)).orNull
          if (replacement != null) {
            builder.append(replacement)
            loop(text.substring(brace + 1))
          } else {
            builder.append("{")
            loop(text.substring(1))
          }
      }
    } else {
      val brace = text.indexOf("{")
      if (brace < 0) builder.append(text).toString
      else {
        builder.append(text.substring(0, brace))
        loop(text.substring(brace))
      }
    }
  }

  loop(text)
}

更新 3:这是一组 Clojure 测试用例(Scala 版本留作练习 :-)):

(use 'clojure.test)

(deftest test-replace-templates
  (is (=        ; No templates
        (replace-templates "this is a test" {:foo "FOO"})
        "this is a test"))

  (is (=        ; One simple template
        (replace-templates "this is a {foo} test" {:foo "FOO"})
        "this is a FOO test"))

  (is (=        ; Two templates, second at end of input string
        (replace-templates "this is a {foo} test {bar}" {:foo "FOO" :bar "BAR"})
        "this is a FOO test BAR"))

  (is (=        ; Two templates
        (replace-templates "this is a {foo} test {bar} 42" {:foo "FOO" :bar "BAR"})
        "this is a FOO test BAR 42"))

  (is (=        ; Second brace-enclosed item is NOT a template
        (replace-templates "this is a {foo} test {baz} 42" {:foo "FOO" :bar "BAR"})
        "this is a FOO test {baz} 42"))

  (is (=        ; Second item is not a template (no closing brace)
        (replace-templates "this is a {foo} test {bar" {:foo "FOO" :bar "BAR"})
        "this is a FOO test {bar"))

  (is (=        ; First item is enclosed in a non-template brace-pair
        (replace-templates "this is {a {foo} test} {bar" {:foo "FOO" :bar "BAR"})
        "this is {a FOO test} {bar")))

(run-tests)

【问题讨论】:

  • 在编译时是否知道键?如果是这样,这太复杂了
  • @Kim Stebel:怎么会这样?我该如何改进它?
  • @ralph 从clojure.contrib.strint&lt;&lt;。我认为它也被移到了新的贡献中。不过,它只是编译时间。
  • @kotarak:看起来很有趣。希望它支持运行时替换,但即使只有编译时,它应该仍然有用。
  • @Ralph 您可以完全控制 Clojure 中的编译和评估,因此“编译时”解决方案通常是可用的,即使您的模板字符串是动态的,因此您需要“运行时”插值。

标签: scala string clojure


【解决方案1】:

我认为您可以构建的最佳算法是输入字符串的长度为 O(n),并且类似于:

  1. 初始化一个空的 StringBuilder
  2. 扫描字符串以找到第一个“{”,将在此之前的任何子字符串添加到您的 Stringbuilder。如果没有找到“{”,你就完成了!
  3. 扫描到下一个“}”。使用大括号之间的任何内容在 String->String hashmap 中进行映射查找,并将结果添加到您的 StringBuilder
  4. 回到 2. 并从“}”之后继续扫描

转换为 Scala/Clojure 作为练习:-)

【讨论】:

    【解决方案2】:

    我为 Clojure 编写了一个字符串插值库,它作为 clojure.contrib.strint 被引入 clojure-contrib。我blogged about it;你会在那里找到该方法的描述。它的最新来源可以是viewed here on githubclojure.contrib.strint 和这里的方法之间的最大区别在于后者都在运行时执行插值。根据我的经验,运行时插值在很大程度上是不必要的,并且使用像 clojure.contrib.strint 这样在编译时执行插值的东西通常会为您的应用程序带来实实在在的性能优势。

    请注意,clojure.contrib.strint 希望是 migrating to clojure.core.strint under Clojure's "new-contrib" organization

    【讨论】:

    • 只是想感谢你提供的东西,只是喜欢它!比 cl-format 更容易使用。
    • @Torbjørn 不客气,我很高兴你发现它很有用。 :-)
    【解决方案3】:

    这是使用正则表达式进行替换的 clojure 实现版本。它比您的版本更快(运行 Lorum ipsum 测试用例 100 次,请往下看),并且需要维护的代码更少:

    (defn replace-templates2 [text m]
      (clojure.string/replace text 
                              #"\{\w+\}" 
                              (fn [groups] 
                                  ((keyword (subs groups 
                                                  1 
                                                  (dec (.length groups)))) m))))
    

    实现起来又快又脏,但它确实有效。关键是我认为你应该使用正则表达式来解决这个问题。


    更新:

    用一种时髦的方式进行子字符串化的实验,得到了令人惊讶的性能结果。代码如下:

    (defn replace-templates3 [text m]
      (clojure.string/replace text 
                              #"\{\w+\}" 
                              (fn [groups] 
                                  ((->> groups
                                        reverse
                                        (drop 1)
                                        reverse
                                        (drop 1)
                                        (apply str)
                                        keyword) m))))
    

    以下是我的机器上针对您的版本、我的第一个版本以及最后的这个版本(100 次迭代)的结果:

    "Elapsed time: 77.475072 msecs"
    "Elapsed time: 50.238911 msecs"
    "Elapsed time: 38.109875 msecs"
    

    【讨论】:

      【解决方案4】:

      有些人在遇到问题时会想“我会使用正则表达式!”。现在他们有两个问题。然而,其他人决定使用正则表达式——现在他们有三个问题:实施和维护半正则表达式的临时实施,加上另外两个。 p>

      无论如何,考虑一下:

      import scala.util.matching.Regex
      
      def replaceTemplates(text: String,
                           templates: Map[String, String]): String = 
          """\{([^{}]*)\}""".r replaceSomeIn ( text,  { case Regex.Groups(name) => templates get name } )
      

      它使用字符串生成器来搜索和替换。该映射使用String 而不是Symbol,因为这样更快,并且代码不会替换没有有效映射的匹配项。使用replaceAllIn 可以避免这种情况,但需要一些类型注释,因为该方法已重载。

      您可能想从 Regex 的 scaladoc API 浏览 Scala 的源代码,看看发生了什么。

      【讨论】:

      • 这个测试似乎失败了:assertEquals("this is {a FOO test} {bar", replaceTemplates("this is {a {foo} test} {bar", Map("foo" -&gt; "FOO", "bar" -&gt; "BAR")))
      • 嵌套表达式是正则表达式的祸根。看来 Jamie Zawinski 是对的 :-)。
      • @Ralph 已修复。它不会处理嵌套模式,但同样,您自己的代码也不会正确处理(这取决于模板的处理顺序)。另一方面,您可以多次应用它以获得结果。
      • 谢谢。我会看看它。我不打算让模板替换支持嵌套替换,只是它忽略了大括号括起来的非模板。我在 Java 代码生成器中使用它,显然非模板大括号 必须 得到支持。我查看了RegexScaladocs。您能否使用将(Match) =&gt; Option[String] 作为第二个参数的replaceSomeIn 方法并避免引用(非线程安全?)Regex.Groups 对象?或者 Regex.Groups 仅用于其 unapplySeq 方法是否重要?
      • @Ralph 您可以随时单击源链接并亲自查看源代码。 Regex.Groups 是线程安全的,因为它不包含可变状态。无论如何,我只是用它来使与子组的匹配更容易。
      【解决方案5】:

      Torbjørns 的答案非常好且易读。使用 butlast 摆脱双重反转以及字符串/连接而不是应用 str 可能会很好。另外使用地图作为一个功能。 所以clojure代码可以进一步缩短为:

      (defn replace-template [text m] 
            (clojure.string/replace text #"\{\w+\}" 
                                    (comp m keyword clojure.string/join butlast rest)))
      

      【讨论】:

        【解决方案6】:

        我不知道 Clojure,所以我只能说 Scala:

        foreach 循环很慢,因为您在每个循环周期中遍历整个字符串。这可以通过首先搜索模板然后替换它们来改进。此外,数据应始终附加到 StringBuilder。这是因为每次在 StringBuilder 内部替换某些内容时,新内容和 StringBuilder 的结尾都会被复制到一个新的字符数组中。

        def replaceTemplates(s: String, templates: Map[String, String]): String = {
          type DataList = List[(Int, String, Int)]
          def matchedData(from: Int, l: DataList): DataList = {
            val end = s.lastIndexOf("}", from)
            if (end == -1) l
            else {
              val begin = s.lastIndexOf("{", end)
              if (begin == -1) l
              else {
                val template = s.substring(begin, end+1)
                matchedData(begin-1, (begin, template, end+1) :: l)
              }
            }
          }
        
          val sb = new StringBuilder(s.length)
          var prev = 0
          for ((begin, template, end) <- matchedData(s.length, Nil)) {
            sb.append(s.substring(prev, begin))
            val ident = template.substring(1, template.length-1)
            sb.append(templates.getOrElse(ident, template))
            prev = end
          }
          sb.append(s.substring(prev, s.length))
          sb.toString
        }
        

        或者使用 RegEx(更短但更慢):

        def replaceTemplates(s: String, templates: Map[String, String]): String = {
          val sb = new StringBuilder(s.length)
          var prev = 0
          for (m <- """\{.+?\}""".r findAllIn s matchData) {
            sb.append(s.substring(prev, m.start))
            val ms = m.matched
            val ident = ms.substring(1, ms.length-1)
            sb.append(templates.getOrElse(ident, ms))
            prev = m.end
          }
          sb.append(s.substring(prev, s.length))
          sb.toString
        }
        

        【讨论】:

        • 见上面 mikera 的算法。看起来很相似。我尝试了一个 Clojure 实现 (stackoverflow.com/questions/6112534/…),它的运行速度比前一个慢。不知道为什么——仍在调查。
        【解决方案7】:

        正则表达式 + replaceAllIn + 折叠:

        val template = "Hello #{name}!"
        val replacements = Map( "name" -> "Aldo" )
        replacements.foldLeft(template)((s:String, x:(String,String)) => ( "#\\{" + x._1 + "\\}" ).r.replaceAllIn( s, x._2 ))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-09-18
          • 1970-01-01
          • 1970-01-01
          • 2011-09-08
          • 1970-01-01
          • 2014-02-11
          • 1970-01-01
          • 2018-09-16
          相关资源
          最近更新 更多