2011-05-24 78 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