gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 13:41:58 26 4
gpt4 key购买 nike

我刚刚开始使用Scala,希望更好地了解解决问题的功能方法。
我有成对的字符串,第一个具有用于参数的占位符,而这对具有可替换的值。例如
“从tab1中选择col1,其中id> $ 1,名称类似$ 2”
“参数:$ 1 ='250',$ 2 ='some%'”

可能有两个以上的参数。

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

谁能指出我要一种更整洁,更不易出错的功能性方法?

最佳答案

严格说来替换问题,我的首选解决方案是通过一项功能启用的解决方案,该功能可能会在即将到来的Scala 2.8中提供,该功能可以使用功能替换正则表达式模式。使用它,问题可以简化为:

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


这可以将问题减少到您实际打算执行的操作:将所有$ N模式替换为列表的第N个值。

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

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


如果您只想要这些,可以在这里停止。但是,如果您对如何以功能性方式解决此类问题感兴趣,而缺少聪明的库函数,请继续阅读。

从功能上进行思考意味着对功能的思考。您有一个字符串,一些值,并且想要一个字符串。在静态类型的功能语言中,这意味着您需要这样的东西:

(String, List[String]) => String


如果您认为这些值可以以任何顺序使用,我们可能会要求一种更适合的类型:

(String, IndexedSeq[String]) => String


这对于我们的功能应该足够了。现在,我们如何分解工作?有几种标准的方法可以做到:递归,理解,折叠。

递归

让我们从递归开始。递归意味着将问题分为第一步,然后对其余数据重复进行。对我来说,这里最明显的划分是:


替换第一个占位符
与其余占位符重复


实际上,这很简单,因此让我们进一步了解细节。如何替换第一个占位符?不可避免的一件事是,我需要知道那个占位符,因为我需要从中获取索引到我的值中。所以我需要找到它:

(String, Pattern) => String


找到后,我可以将其替换为字符串并重复:

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
}


这是低效的,因为它会反复生成新的字符串,而不是仅仅串联每个部分。让我们尝试对此更加聪明。

为了通过串联有效地构建字符串,我们需要使用 StringBuilder。我们还希望避免创建新的字符串。 StringBuilder可以接受 CharSequence,我们可以从 String获得。我不确定是否实际创建了新字符串-如果是,我们可以以查看 CharSequence的方式滚动自己的 String而不是创建新的 String。确保我们可以根据需要轻松更改此设置,我将假设不是。

因此,让我们考虑一下我们需要什么功能。自然地,我们需要一个将索引返回到第一个占位符的函数:

String => Int


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

(String, Int) => Int


不过,有一个小细节。如果还有其他占位符怎么办?这样就不会有任何索引要返回。 Java重用索引以返回该异常。但是,在进行函数式编程时,最好总是返回您的意思。我们的意思是我们可能会返回索引,也可能不会。签名是这样的:

(String, Int) => Option[Int]


让我们构建这个功能:

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
}


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

要跳过占位符,我们还需要知道其长度,签名为 (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
}


接下来,我们还想知道占位符代表的确切值的索引。签名有点模棱两可:

(String, Int) => Int


第一个 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 "$"
}


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

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
}


需要注意的是,常规的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)
}


使用 StringBuilder效率更高,功能也一样。

理解

在最基本的层面上,Scala理解意味着在给定函数 T[A]的情况下将 T[B]转换为 A => B,这被称为函子。谈到收藏时,很容易理解。例如,我可以通过函数 List[String]将名称的 List[Int]转换为名称长度的 String => Int,该函数返回字符串的长度。那是一个列表理解。

给定具有签名 A => T[B]的功能,这些功能还可以通过理解来完成,这些功能与monad或 A => Boolean有关。

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

(List[String], String => String) => String


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

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


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

def valuesMatcher(values: IndexedSeq[String]): String => String = (input: String) => values(input.substring(1).toInt - 1)


我们还需要一个功能 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
}


我使用 @unchecked是因为如果我的正则表达式模式正确构建,则除了上述两种模式之外,不应有其他模式。但是,编译器并不知道这一点,因此我使用该注释来使它产生的警告静音。如果引发异常,则正则表达式模式中存在错误。

然后,最终功能将所有功能统一起来:

def replaceComprehension(input: String, values: IndexedSeq[String]) =
comprehension(tokenize(input), valuesMatcher(values)).mkString


此解决方案的一个问题是,我两次应用了正则表达式模式:一次用于拆分字符串,另一次用于标识占位符。另一个问题是令牌的 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


折叠式

折叠有点类似于递归和理解。折叠时,我们接受一个可以理解的 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)
}


或者,作为单线:

def factorial2(n: Int) = (2 to n).foldLeft(1)(_ * _)


好的,那么我们将如何解决折叠问题呢?结果当然应该是我们要产生的字符串。因此,种子应为空字符串。让我们使用来自 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
}


并且,至此,我完成了以功能性方式展示解决此问题的最常用方法。我求助于 StringBuilder,因为 String的连接很慢。如果不是这种情况,我可以轻松地用 StringBuilder替换上面函数中的 String。我还可以将 Iterator转换为 Stream,并完全消除可变性。

不过,这就是Scala,Scala的目的是平衡需求和手段,而不是纯粹的解决方案。当然,您可以自由选择纯正。 :-)

关于scala - 用Scala中的占位符替换字符串中的值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2183503/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com