Saltar al contenido

Sustituir valores en una cadena con marcadores de posición en Scala

Luego de tanto batallar pudimos hallar la contestación de este atasco que algunos usuarios de este sitio web han tenido. Si deseas aportar algún dato puedes compartir tu conocimiento.

Solución:

Hablando estrictamente del problema de reemplazo, mi solución preferida es una habilitada por una característica que probablemente debería estar disponible en el próximo Scala 2.8, que es la capacidad de reemplazar patrones de expresiones regulares usando una función. Usándolo, el problema se puede reducir a esto:

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

Lo que reduce el problema a lo que realmente pretende hacer: reemplazar todos $ N patrones por el correspondiente Enésimo valor de una lista.

O, si realmente puede establecer los estándares para su cadena de entrada, puede hacerlo así:

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

Si eso es todo lo que quieres, puedes parar aquí. Sin embargo, si está interesado en cómo resolver estos problemas de manera funcional, sin funciones de biblioteca inteligentes, continúe leyendo.

Pensar funcionalmente en él significa pensar en la función. Tiene una cadena, algunos valores y quiere una cadena de vuelta. En un lenguaje funcional de tipo estático, eso significa que quieres algo como esto:

(String, List[String]) => String

Si se considera que esos valores pueden usarse en cualquier orden, podemos solicitar un tipo más adecuado para eso:

(String, IndexedSeq[String]) => String

Eso debería ser lo suficientemente bueno para nuestra función. Ahora bien, ¿cómo desglosamos el trabajo? Hay algunas formas estándar de hacerlo: recursividad, comprensión, plegado.

RECURSION

Comencemos con la recursividad. Recurrencia significa dividir el problema en un primer paso y luego repetirlo sobre los datos restantes. Para mí, la división más obvia aquí sería la siguiente:

  1. Reemplazar el primer marcador de posición
  2. Repita con los marcadores de posición restantes

En realidad, es bastante sencillo de hacer, así que entremos más detalles. ¿Cómo reemplazo el primer marcador de posición? Una cosa que no se puede evitar es que necesito saber qué es ese marcador de posición, porque necesito obtener el índice en mis valores a partir de él. Entonces necesito encontrarlo:

(String, Pattern) => String

Una vez encontrado, puedo reemplazarlo en la cadena y repetir:

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

Eso es ineficiente, porque produce repetidamente nuevas cadenas, en lugar de simplemente concatenar cada parte. Intentemos ser más inteligentes al respecto.

Para construir una cadena de manera eficiente a través de la concatenación, necesitamos usar StringBuilder. También queremos evitar la creación de nuevas cadenas. StringBuilder puede acepta CharSequence, que podemos obtener de String. No estoy seguro de si realmente se crea una nueva cadena o no; si es así, podríamos lanzar la nuestra CharSequence de una manera que actúa como una vista de String, en lugar de crear una nueva String. Con la seguridad de que podemos cambiar esto fácilmente si es necesario, continuaré asumiendo que no es así.

Entonces, consideremos qué funciones necesitamos. Naturalmente, queremos una función que devuelva el índice al primer marcador de posición:

String => Int

Pero también queremos omitir cualquier parte de la cadena que ya hemos visto. Eso significa que también queremos un índice inicial:

(String, Int) => Int

Sin embargo, hay un pequeño detalle. ¿Qué pasa si hay un marcador de posición adicional? Entonces no habría ningún índice para devolver. Java reutiliza el índice para devolver esa excepción. Sin embargo, al hacer programación funcional, siempre es mejor devolver lo que quiere decir. Y lo que queremos decir es que nosotros mayo devolver un índice, o es posible que no. La firma para eso es esta:

(String, Int) => Option[Int]

Construyamos esta función:

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

Eso es bastante complejo, principalmente para lidiar con condiciones de límite, como que el índice esté fuera de rango o falsos positivos al buscar marcadores de posición.

Para omitir el marcador de posición, también necesitaremos saber su longitud, firma (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

A continuación, también queremos saber cuál es, exactamente, el índice del valor que representa el marcador de posición. La firma de esto es un poco ambigua:

(String, Int) => Int

El primero Int es un índice en la entrada, mientras que el segundo es un índice en los valores. Podríamos hacer algo al respecto, pero no tan fácil o eficientemente, así que ignorémoslo. Aquí hay una implementación para ello:

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 "$"

También podríamos haber usado la longitud y lograr una implementación más simple:

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

Como nota, el uso de corchetes alrededor de expresiones simples, como el anterior, está mal visto por el estilo convencional de Scala, pero lo uso aquí para que se pueda pegar fácilmente en REPL.

Entonces, podemos llevar el índice al siguiente marcador de posición, su longitud y el índice del valor. Eso es prácticamente todo lo que se necesita para una versión más eficiente de 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)

Mucho más eficiente y funcional como se puede usar StringBuilder.

COMPRENSIÓN

Scala Comprehensions, en su nivel más básico, significa transformar T[A] dentro T[B] dada una función A => B, algo conocido como functor. Se puede entender fácilmente cuando se trata de colecciones. Por ejemplo, puedo transformar un List[String] de nombres en un List[Int] de longitudes de nombres a través de una función String => Int que devuelve la longitud de una cadena. Esa es una lista de comprensión.

Hay otras operaciones que se pueden realizar mediante comprensiones, funciones dadas con firmas A => T[B], que está relacionado con las mónadas, o A => Boolean.

Eso significa que necesitamos ver la cadena de entrada como un T[A]. No podemos usar Array[Char] como entrada porque queremos reemplazar todo el marcador de posición, que es más grande que un solo carácter. Propongamos, por tanto, este tipo de firma:

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

Dado que la entrada que recibimos es String, necesitamos una función String => List[String] primero, que dividirá nuestra entrada en marcadores de posición y no marcadores de posición. Propongo esto:

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

Otro problema que tenemos es que tenemos un IndexedSeq[String], pero necesitamos un String => String. Hay muchas formas de evitar eso, pero conformémonos con esto:

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

También necesitamos una función List[String] => String, pero List's mkString ya lo hace. Así que queda poco por hacer aparte de componer todas estas cosas:

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
  

yo suelo @unchecked porque allí no debería ser cualquier patrón distinto de estos dos anteriores, si mi patrón de expresiones regulares se construyó correctamente. Sin embargo, el compilador no lo sabe, así que utilizo esa anotación para silenciar la advertencia que produciría. Si se lanza una excepción, hay un error en el patrón de expresiones regulares.

La función final, entonces, unifica todo eso:

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

Un problema con esta solución es que aplico el patrón de expresiones regulares dos veces: una para dividir la cadena y la otra para identificar los marcadores de posición. Otro problema es que el List de tokens es un resultado intermedio innecesario. Podemos solucionar eso con estos cambios:

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

PLEGABLE

El plegado es un poco similar tanto a la recursividad como a la comprensión. Con plegado, tomamos un T[A] entrada que se puede comprender, una B "semilla" y una función (B, A) => B. Comprendemos la lista utilizando la función, siempre tomando el B que resultó del último elemento procesado (el primer elemento toma la semilla). Finalmente, devolvemos el resultado del último elemento comprendido.

Admito que difícilmente podría explicarlo de una manera menos oscura. Eso es lo que sucede cuando intentas mantenerte abstracto. Lo expliqué de esa manera para que las firmas de tipo involucradas queden claras. Pero veamos un ejemplo trivial de plegado para comprender su uso:

def factorial(n: Int) = 
  val input = 2 to n
  val seed = 1
  val function = (b: Int, a: Int) => b * a
  input.foldLeft(seed)(function)

O, como una sola línea:

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

Bien, entonces, ¿cómo solucionaríamos el problema del plegado? El resultado, por supuesto, debería ser la cadena que queremos producir. Por lo tanto, la semilla debe ser una cadena vacía. Usemos el resultado de tokenize2 como entrada comprensible, y haga esto:

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

Y, con eso, termino de mostrar las formas más habituales en que se abordaría esto de manera funcional. He recurrido a StringBuilder porque la concatenación de String es lento. Si ese no fuera el caso, podría reemplazar fácilmente StringBuilder en funciones arriba por String. Yo también podría convertir Iterator en un Streamy acabar con la mutabilidad.

Sin embargo, esto es Scala y Scala se trata de equilibrar necesidades y medios, no de soluciones puristas. Aunque, por supuesto, eres libre de volverte purista. 🙂

Puede utilizar el estándar Java String.format estilo con un toque:

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

En Java, por supuesto, esto se habría visto así:

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

La principal diferencia entre estos dos estilos (prefiero el de Scala) es que conceptualmente esto significa que cada cadena se puede considerar una cadena de formato en Scala (es decir, el método de formato parece ser un método de instancia en el String clase). Si bien se podría argumentar que esto es conceptualmente incorrecto, conduce a un código más intuitivo y legible.

Este estilo de formato le permite formatear números de punto flotante como desee, fechas, etc. El principal problema con él es que el "enlace" entre los marcadores de posición en la cadena de formato y los argumentos se basa puramente en el orden, no relacionado con los nombres de ninguna manera. camino (como "My name is $name") aunque no veo cómo ...

interpolate("My name is $name and I am $age years of age", 
               Map("name" -> "Oxbow", "age" -> 34))

... es más legible incrustado en mi código. Este tipo de cosas es mucho más útil para el reemplazo de texto donde el texto de origen está incrustado en archivos separados (en i18n por ejemplo) donde querrías algo como:

"name.age.intro".text.replacing("name" as "Oxbow").replacing("age" as "34").text

O:

"My name is $name and I am $age years of age"
     .replacing("name" as "Oxbow").replacing("age" as "34").text

Creo que esto sería bastante fácil de usar y solo tomaría unos minutos escribirlo (parece que no puedo obtener la interpolación de Daniel para compilar con la versión de Scala 2.8 que tengo):

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)  

Por cierto, soy un fanático de las API fluidas. ¡No importa cuán faltos de rendimiento sean!

Esta no es una respuesta directa a su pregunta, sino más bien un truco de Scala. Puede interpolar cadenas en Scala usando xml:

val id = 250
val value = "some%"
select col1 from tab1 where id > id and name like value.text
// res1: String = select col1 from tab1 where id > 250 and name like some%

Eric.

Te invitamos a añadir valor a nuestro contenido informacional dando tu experiencia en las ilustraciones.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *