使用Scala中的占位符替换字符串中的值

Gav*_*vin 17 functional-programming scala

我刚开始使用Scala,希望更好地理解解决问题的功能方法.我有一对字符串,第一个字符串有参数的占位符,它的对有要替换的值.例如"从tab1中选择col1,其中id> $ 1,名称如$ 2""参数:$ 1 ='250',$ 2 ='some%'"

可能有多于2个参数.

我可以通过逐步执行并在每一行上使用regex.findAllIn(line)构建正确的字符串,然后通过迭代器来构造替换,但这似乎相当不优雅且程序驱动.

任何人都可以指出我的功能方法更整洁,更不容易出错吗?

Dan*_*ral 29

严格说来是替换问题,我首选的解决方案是一个功能,该功能应该可以在即将推出的Scala 2.8中使用,它可以使用函数替换正则表达式模式.使用它,问题可以减少到这个:

def replaceRegex(input: String, values: IndexedSeq[String]) =  
  """\$(\d+)""".r.replaceAllMatchesIn(input, {
    case Regex.Groups(index) => values(index.toInt)
  })
Run Code Online (Sandbox Code Playgroud)

这将问题简化为您实际要执行的操作:将所有$ N模式替换为列表的相应第N个值.

或者,如果您实际上可以为输入字符串设置标准,则可以这样做:

"select col1 from tab1 where id > %1$s and name like %2$s" format ("one", "two")
Run Code Online (Sandbox Code Playgroud)

如果这就是你想要的,你可以在这里停下来.但是,如果您对如何以功能性方式解决此类问题感兴趣,如果没有聪明的库函数,请继续阅读.

从功能上思考它意味着思考功能.你有一个字符串,一些值,你想要一个字符串.在静态类型的函数式语言中,这意味着你想要这样的东西:

(String, List[String]) => String
Run Code Online (Sandbox Code Playgroud)

如果考虑到这些值可以按任何顺序使用,我们可能会要求更适合的类型:

(String, IndexedSeq[String]) => String
Run Code Online (Sandbox Code Playgroud)

这应该足够我们的功能.现在,我们如何分解工作?有几种标准方法:递归,理解,折叠.

递推

让我们从递归开始.递归意味着将问题分成第一步,然后在剩余数据上重复.对我来说,这里最明显的部门如下:

  1. 替换第一个占位符
  2. 重复其余占位符

这实际上是非常简单的,所以让我们进一步了解细节.如何更换第一个占位符?有一点是无法避免的,我需要知道占位符是什么,因为我需要从中获取索引到我的值中.所以我需要找到它:

(String, Pattern) => String
Run Code Online (Sandbox Code Playgroud)

一旦找到,我可以在字符串上替换它并重复:

val stringPattern = "\\$(\\d+)"
val regexPattern = stringPattern.r
def replaceRecursive(input: String, values: IndexedSeq[String]): String = regexPattern findFirstIn input match {
  case regexPattern(index) => replaceRecursive(input replaceFirst (stringPattern, values(index.toInt)))
  case _ => input // no placeholder found, finished
}
Run Code Online (Sandbox Code Playgroud)

这是低效的,因为它反复产生新的字符串,而不是仅仅连接每个部分.让我们试着更加聪明一点.

要通过连接有效地构建字符串,我们需要使用StringBuilder.我们还想避免创建新字符串.StringBuilder可以接受CharSequence,我们可以得到String.我不知道,如果实际创建或不是一个新字符串-如果是这样,我们可以推出我们自己CharSequence在充当视图分成的方式String,而不是创建一个新的String.确保我们可以根据需要轻松更改,我会继续假设它不是.

那么,让我们考虑一下我们需要什么功能.当然,我们需要一个将索引返回到第一个占位符的函数:

String => Int
Run Code Online (Sandbox Code Playgroud)

但是我们也想跳过我们已经看过的字符串的任何部分.这意味着我们还需要一个起始索引:

(String, Int) => Int
Run Code Online (Sandbox Code Playgroud)

但是有一个小细节.如果还有占位符怎么办?然后就没有任何索引可以返回.Java重用索引来返回该异常.然而,在进行函数式编程时,最好还是返回你的意思.我们的意思是我们可能会返回一个索引,或者我们可能不会.签名是这样的:

(String, Int) => Option[Int]
Run Code Online (Sandbox Code Playgroud)

让我们构建这个函数:

def indexOfPlaceholder(input: String, start: Int): Option[Int] = if (start < input.lengt) {
  input indexOf ("$", start) match {
    case -1 => None
    case index => 
      if (index + 1 < input.length && input(index + 1).isDigit)
        Some(index)
      else
        indexOfPlaceholder(input, index + 1)
  }
} else {
  None
}
Run Code Online (Sandbox Code Playgroud)

这相当复杂,主要是为了处理边界条件,例如索引超出范围,或者在寻找占位符时出现误报.

要跳过占位符,我们还需要知道它的长度,签名(String, Int) => Int:

def placeholderLength(input: String, start: Int): Int = {
  def recurse(pos: Int): Int = if (pos < input.length && input(pos).isDigit)
    recurse(pos + 1)
  else
    pos
  recurse(start + 1) - start  // start + 1 skips the "$" sign
}
Run Code Online (Sandbox Code Playgroud)

接下来,我们还想知道占位符所代表的值的索引.这个签名有点含糊不清:

(String, Int) => Int
Run Code Online (Sandbox Code Playgroud)

第一个Int是输入的索引,而第二个是值的索引.我们可以做些什么,但不是那么容易或有效,所以让我们忽略它.这是一个实现:

def indexOfValue(input: String, start: Int): Int = {
  def recurse(pos: Int, acc: Int): Int = if (pos < input.length && input(pos).isDigit)
    recurse(pos + 1, acc * 10 + input(pos).asDigit)
  else
    acc
  recurse(start + 1, 0) // start + 1 skips "$"
}
Run Code Online (Sandbox Code Playgroud)

我们也可以使用长度,并实现更简单的实现:

def indexOfValue2(input: String, start: Int, length: Int): Int = if (length > 0) {
  input(start + length - 1).asDigit + 10 * indexOfValue2(input, start, length - 1)
} else {
  0
}
Run Code Online (Sandbox Code Playgroud)

作为一个注释,在简单表达式周围使用大括号,如上所述,是传统的Scala样式不赞成的,但我在这里使用它,因此可以很容易地粘贴在REPL上.

因此,我们可以将索引获取到下一个占位符,其长度和值的索引.这几乎是更高效版本所需的一切replaceRecursive:

def replaceRecursive2(input: String, values: IndexedSeq[String]): String = {
  val sb = new StringBuilder(input.length)
  def recurse(start: Int): String = if (start < input.length) {
    indexOfPlaceholder(input, start) match {
      case Some(placeholderIndex) =>
        val placeholderLength = placeholderLength(input, placeholderIndex)
        sb.append(input subSequence (start, placeholderIndex))
        sb.append(values(indexOfValue(input, placeholderIndex)))
        recurse(start + placeholderIndex + placeholderLength)
      case None => sb.toString
    }
  } else {
    sb.toString
  }
  recurse(0)
}
Run Code Online (Sandbox Code Playgroud)

效率更高,功能更强大StringBuilder.

理解

在最基本的层面上,理解意味着转变T[A]T[B]给定的功能A => B.这是一个monad的东西,但它在收集时很容易理解.例如,我可以转换一个List[String]名字为List[Int]通过函数名长度的String => Int返回字符串的长度.这是列表理解.

在给定具有签名A => T[B]或函数的函数的情况下,还可以通过理解来完成其他操作A => Boolean.

这意味着我们需要将输入字符串视为一个T[A].我们不能Array[Char]用作输入,因为我们想要替换整个占位符,它大于单个char.因此,让我们提出这种类型的签名:

(List[String], String => String) => String
Run Code Online (Sandbox Code Playgroud)

由于我们收到的输入是String,我们String => List[String]首先需要一个函数,它将我们的输入分为占位符和非占位符.我建议这个:

val regexPattern2 = """((?:[^$]+|\$(?!\d))+)|(\$\d+)""".r
def tokenize(input: String): List[String] = regexPattern2.findAllIn(input).toList
Run Code Online (Sandbox Code Playgroud)

我们遇到的另一个问题是我们得到了一个IndexedSeq[String],但我们需要一个String => String.有很多方法,但让我们解决这个问题:

def valuesMatcher(values: IndexedSeq[String]): String => String = (input: String) => values(input.substring(1).toInt - 1)
Run Code Online (Sandbox Code Playgroud)

我们还需要一个功能List[String] => String,但ListmkString的确已经.所以除了编写所有这些东西之外别无他法:

def comprehension(input: List[String], matcher: String => String) = 
  for (token <- input) yield (token: @unchecked) match {
    case regexPattern2(_, placeholder: String) => matcher(placeholder)
    case regexPattern2(other: String, _) => other
  }
Run Code Online (Sandbox Code Playgroud)

我使用的@unchecked是因为除了上面这两个之外不应该有任何模式,如果我的正则表达式模式是正确构建的.但是,编译器不知道,所以我使用该注释来静默它将产生的警告.如果抛出异常,则正则表达式模式中存在错误.

然后,最终的功能统一了所有:

def replaceComprehension(input: String, values: IndexedSeq[String]) =
  comprehension(tokenize(input), valuesMatcher(values)).mkString
Run Code Online (Sandbox Code Playgroud)

这个解决方案的一个问题是我应用了两次正则表达式模式:一次打破字符串,另一次识别占位符.另一个问题是List令牌是不必要的中间结果.我们可以通过这些变化解决这个问题

def tokenize2(input: String): Iterator[List[String]] = regexPattern2.findAllIn(input).matchData.map(_.subgroups)

def comprehension2(input: Iterator[List[String]], matcher: String => String) = 
  for (token <- input) yield (token: @unchecked) match {
    case List(_, placeholder: String) => matcher(placeholder)
    case List(other: String, _) => other
  }

def replaceComprehension2(input: String, values: IndexedSeq[String]) =
  comprehension2(tokenize2(input), valuesMatcher(values)).mkString
Run Code Online (Sandbox Code Playgroud)

折叠

折叠有点类似于递归和理解.通过折叠,我们T[A]可以获得可以理解的输入,B"种子"和功能(B, A) => B.我们使用函数来理解列表,总是B从最后一个元素处理得到的结果(第一个元素获取种子).最后,我们返回最后一个被理解元素的结果.

我承认我很难以一种不那么模糊的方式解释它.当你试图保持抽象时会发生这种情况.我这样解释,以便所涉及的类型签名变得清晰.但是,让我们看一下折叠的一个简单例子来理解它的用法:

def factorial(n: Int) = {
  val input = 2 to n
  val seed = 1
  val function = (b: Int, a: Int) => b * a
  input.foldLeft(seed)(function)
}
Run Code Online (Sandbox Code Playgroud)

或者,作为一个单行:

def factorial2(n: Int) = (2 to n).foldLeft(1)(_ * _)
Run Code Online (Sandbox Code Playgroud)

好的,那么我们如何解决折叠问题呢?当然,结果应该是我们想要生成的字符串.因此,种子应该是空的字符串.让我们使用结果tokenize2作为可理解的输入,并执行以下操作:

def replaceFolding(input: String, values: IndexedSeq[String]) = {
  val seed = new StringBuilder(input.length)
  val matcher = valuesMatcher(values)
  val foldingFunction = (sb: StringBuilder, token: List[String]) => {
    token match {          
      case List(_, placeholder: String) => sb.append(matcher(placeholder))
      case List(other: String, _) => sb.append(other)
    }
    sb
  }
  tokenize2(input).foldLeft(seed)(foldingFunction).toString
}
Run Code Online (Sandbox Code Playgroud)

并且,通过这种方式,我完成了以功能性方式展示最常用的方式.我求助于StringBuilder因为连接String缓慢.如果不是这样的话,我可以轻松地替换StringBuilder上面的函数String.我也可以转换Iterator成一个Stream,并完全消除可变性.

这是Scala,而Scala是关于平衡需求和手段,而不是纯粹的解决方案.当然,你可以自由地去纯粹主义.:-)


oxb*_*kes 14

您可以使用标准Java String.format样式:

"My name is %s and I am %d years of age".format("Oxbow", 34)
Run Code Online (Sandbox Code Playgroud)

在Java当然这看起来像:

String.format("My name is %s and I am %d years of age", "Oxbow", 34)
Run Code Online (Sandbox Code Playgroud)

这两种样式之间的主要区别(我更喜欢Scala)是概念上这意味着每个String都可以被认为是Scala中的格式字符串(即格式方法似乎是类中的实例方法String).虽然这可能被认为在概念上是错误的,但它会导致更直观和可读的代码.

这种格式化样式允许您根据需要格式化浮点数,日期等.它的主要问题是格式字符串中的占位符与参数之间的"绑定"纯粹基于顺序,与任何名称无关.方式(像"My name is ${name}")虽然我没有看到如何...

interpolate("My name is ${name} and I am ${age} years of age", 
               Map("name" -> "Oxbow", "age" -> 34))
Run Code Online (Sandbox Code Playgroud)

...在我的代码中嵌入了更多可读性.这种东西对于文本替换更有用,其中源文本嵌入在单独的文件中(例如在i18n中),您需要这样的文件:

"name.age.intro".text.replacing("name" as "Oxbow").replacing("age" as "34").text
Run Code Online (Sandbox Code Playgroud)

要么:

"My name is ${name} and I am ${age} years of age"
     .replacing("name" as "Oxbow").replacing("age" as "34").text
Run Code Online (Sandbox Code Playgroud)

我认为这很容易使用,只需几分钟即可编写(我似乎无法使用我的Scala 2.8版本编译Daniel的插值):

object TextBinder {
  val p = new java.util.Properties
  p.load(new FileInputStream("C:/mytext.properties"))

  class Replacer(val text: String) {
    def replacing(repl: Replacement) = new Replacer(interpolate(text, repl.map))
  }

  class Replacement(from: String, to: String) {
    def map = Map(from -> to)
  }
  implicit def stringToreplacementstr(from: String) = new {
    def as(to: String) = new Replacement(from, to)
    def text = p.getProperty(from)
    def replacing(repl: Replacement) = new Replacer(from)
  }

  def interpolate(text: String, vars: Map[String, String]) = 
    (text /: vars) { (t, kv) => t.replace("${"+kv._1+"}", kv._2)  }
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我是一个流畅的API的傻瓜!无论他们多么无形!

  • 似乎很多人都不知道Java格式字符串中的位置参考模式.例如:'%3 $ d`.在此格式规范中,第3个参数显式引用,而不是相同格式字符串中的其他格式规范.当然,也可能出现对任何给定参数的多个引用. (3认同)