【发布时间】: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]。 (感谢mikera,kotarak):
/**
* 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看<<。我认为它也被移到了新的贡献中。不过,它只是编译时间。 -
@kotarak:看起来很有趣。希望它支持运行时替换,但即使只有编译时,它应该仍然有用。
-
@Ralph 您可以完全控制 Clojure 中的编译和评估,因此“编译时”解决方案通常是可用的,即使您的模板字符串是动态的,因此您需要“运行时”插值。