Простая замена шаблона строки в Scala и Clojure - PullRequest
9 голосов
/ 24 мая 2011

Ниже приведены функции, написанные на Scala и Clojure для простой замены шаблонов в строках.Входными данными для каждой функции является String, содержащий шаблоны вида {key} и карту из символа / ключевого слова в значение замены.

Например:

Scala:

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.Для каждой пары ключ / значение мы ищем последнее вхождение ключа (заключенное в фигурные скобки) и заменяем его значением до тех пор, пока не будет больше вхождений.

Имеет ли это какое-либо различие в производительности, если мы используем.lastIndexOf против .indexOf в StringBuilder?

Как улучшить алгоритм?Есть ли более идиоматический способ написания кода Scala и / или Clojure?

ОБНОВЛЕНИЕ : см. Мое продолжение .

ОБНОВЛЕНИЕ 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)
}

UPDATE 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)

Ответы [ 7 ]

8 голосов
/ 24 мая 2011

Я думаю, что лучший алгоритм, который вы можете построить, это O (n) по длине входной строки, и он будет выглядеть примерно так:

  1. Инициализировать пустой StringBuilder
  2. Сканироватьстрока, чтобы найти первое «{», добавьте любую подстроку перед этим в ваш Stringbuilder.Если "{" не найдено, вы закончили!
  3. Сканирование до следующего "}".Используйте все, что находится между фигурными скобками, чтобы выполнить поиск карты в хэш-карте String-> String и добавьте результат в StringBuilder
  4. . Вернитесь к 2. и продолжите сканирование после "}"

Преобразование в Scala / Clojure слева в качестве упражнения: -)

7 голосов
/ 24 мая 2011

Я написал библиотеку интерполяции строк для Clojure, которая была переведена в clojure-contrib как clojure.contrib.strint написал об этом в блоге ;вы найдете описание подхода там.Самый последний источник для этого может быть просмотрен здесь на github .Большая разница между clojure.contrib.strint и подходами здесь состоит в том, что последние все выполняют интерполяцию во время выполнения.По моему опыту, интерполяция во время выполнения в основном не нужна, и использование чего-то вроде clojure.contrib.strint, который выполняет интерполяцию во время компиляции, часто дает ощутимые преимущества в производительности для вашего приложения.

Обратите внимание, что clojure.contrib.strint, мы надеемся, будет Миграция в clojure.core.strint под организацией Clojure "new-contrib" .

7 голосов
/ 24 мая 2011

Вот версия реализации 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"
6 голосов
/ 26 июля 2013

Ответ Торбьерна очень приятный и читаемый.Было бы неплохо использовать 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)))
6 голосов
/ 24 мая 2011

Некоторые люди, столкнувшись с проблемой, думают: «Я буду использовать регулярные выражения!».Теперь у них две проблемы.Другие, однако, решают , а не использовать регулярное выражение - и теперь у них есть три проблемы: реализация и поддержка специальной реализации половины регулярного выражения плюс два других.

В любом случае, учтите следующее:

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 позволит избежать этого, но потребует аннотации некоторых типов, поскольку этот метод перегружен.

Возможно, вы захотите просмотреть исходный код Scala из API-интерфейса scaladoc для Regex и посмотреть, что происходит.

1 голос
/ 02 января 2013

Regex + replaceAllIn + Fold:

val template = "Hello #{name}!"
val replacements = Map( "name" -> "Aldo" )
replacements.foldLeft(template)((s:String, x:(String,String)) => ( "#\\{" + x._1 + "\\}" ).r.replaceAllIn( s, x._2 ))
1 голос
/ 24 мая 2011

Я не знаю Clojure, поэтому могу говорить только за Скала:

Цикл 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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...