2011-05-24 55 views
9

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

例如:

斯卡拉:

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代码?

UPDATE:请参阅我的follow-up

更新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) 
+0

的关键是在编译时已知?如果是这样,这太复杂了 – 2011-05-24 12:26:27

+0

@Kim Stebel:这是怎么回事?我如何改进它? – Ralph 2011-05-24 12:32:33

+0

@ralph从clojure.contrib.strint看['<<'](http://clojure.github.com/clojure-contrib/strint-api.html#clojure.contrib.strint/%3C%3C) '。我认为它也转移到了新的贡献。不过,这只是编译时间。 – kotarak 2011-05-24 12:41:40

回答

8

我觉得您可以创建的最佳算法是输入字符串长度中的O(n),并且会类似于:

  1. 初始化一个空的StringBuilder
  2. 扫描字符串以找到第一个“{”,将之前的任何子字符串添加到您的Stringbuilder中。如果没有找到“{”,你就完成了!
  3. 扫描直到下一个“}”。使用无论是在大括号之间做一个地图查找在与字符串>字符串HashMap和从“}”

后的结果添加到您的StringBuilder

  • 回到2,继续扫描转换为Scala/Clojure留作练习:-)

  • +0

    非常好!我将执行(s)并发布更新。 – Ralph 2011-05-24 13:29:24

    7

    下面是使用正则表达式进行替换的clojure实现的一个版本。它比你的版本快(运行你Lorum存有测试用例100次,见进一步下跌),而且也维护更少的代码:

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

    实现是快速和肮脏的,但它的作品。关键是我认为你应该使用正则表达式来解决这个问题。


    更新:

    尝试了一下用一个时髦的方式做substringing,并得到了一个令人惊讶的表现结果。下面的代码:

    (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" 
    
    1

    我不知道的Clojure,所以我只能为斯卡拉效力:

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

    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 
    } 
    

    或用正则表达式(短,但速度较慢):

    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 
    } 
    
    +0

    请参阅上面的mikera算法。似乎相似。我尝试了一个Clojure实现(http://stackoverflow.com/questions/6112534/follow-up-to-simple-string-template-replacement-in-scala-and-clojure),它运行比前一个更慢。不知道为什么 - 仍在调查。 – Ralph 2011-05-24 15:11:17

    7

    我写了Clojure的字符串插值库被带进的Clojure-contrib请为clojure.contrib.strint。 I 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

    +0

    只是想感谢你的strint的东西,只是喜欢它!比cl-format更容易使用。 – 2011-05-24 13:45:01

    +0

    @Torbjørn你很受欢迎,我很高兴你发现它很有用。 :-) – cemerick 2011-05-25 15:13:33

    6

    有些人在遇到问题时会想“我会用正则表达式!”。现在他们有两个问题。但是,其他人决定而不是使用正则表达式 - 现在他们有三个问题:实现和维护半正则表达式的特设实现,加上另外两个。

    无论如何,考虑一下:

    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可以避免这种情况,但会需要某种类型的注释,因为该方法已经过载。

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

    +0

    这个测试似乎失败了:'assertEquals(“this is {a FOO test} {bar”,replaceTemplates(“this is {a {foo} test} {bar”,Map(“foo” - >“” FOO“,”bar“ - >”BAR“)))' – Ralph 2011-05-24 19:53:52

    +0

    嵌套表达式是正则表达式的祸根。杰米·扎文斯基似乎是对的:-)。 – Ralph 2011-05-24 19:59:57

    +0

    @Ralph固定。它不会处理嵌套模式,但是,再次,您自己的代码也不会正确执行它(这取决于模板的处理顺序)。另一方面,您可以多次应用它以获得结果。 – 2011-05-24 21:01:52

    1

    正则表达式+ 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)) 
    
    6

    Torbjørns答案是非常好的,可读。使用butlast可能会很好,可以摆脱双重反转,以及string/join而不是apply'ing str。另外使用地图作为功能。 所以Clojure的代码可以进一步简化为:

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

    更短! :) – Zubair 2013-07-27 06:37:21