trubidata
Lección 10 de 14·16 min

Strings

En esta guía (11)

En esta lección vas a trabajar con texto. Vas a ver cómo declarar y concatenar strings, consultar su longitud y sus caracteres, buscar, reemplazar, dividir y unir, formatear salidas con interpolación, y transformar mayúsculas, espacios y rellenos. También vas a dar un primer vistazo a las expresiones regulares (que profundizás en la lección 12) y a la codificación de caracteres. Al final vas a resolver cuatro problemas típicos de datos bancarios: nombres, saldos, números de cuenta y datos sensibles.

En Scala, un String es el mismo java.lang.String de Java, con todos sus métodos, más un montón de métodos extra que agrega Scala. Igual que en Java, los strings son inmutables: cada operación devuelve un string nuevo.

Declarar y concatenar

scala
val cadena1 = "Hola, "
val cadena2 = "Mundo!"
val resultado = cadena1 + cadena2
println(resultado) // Hola, Mundo!

Para textos de varias líneas se usan triples comillas. Con stripMargin, el | marca dónde empieza cada línea y se ignora la indentación del código:

scala
val mensaje = """Estimado cliente:
                |Su resumen ya está disponible.
                |Saludos.""".stripMargin
println(mensaje)

Métodos básicos

scala
val cadena = "Ejemplo"

println(cadena.length)           // 7
println(cadena(0))               // E: el carácter en la posición 0
println(cadena == "Ejemplo")     // true: compara contenido
println(cadena.equalsIgnoreCase("EJEMPLO")) // true
println(cadena.isEmpty)          // false
println("".isEmpty)              // true

cadena(0) devuelve un Char, no un String. Y como viste en la lección 9, en Scala == compara contenido también entre strings, a diferencia de Java.

Operaciones comunes

scala
val texto = "Esta es una cadena de ejemplo"

println(texto.substring(5, 11))                 // es una: de la posición 5 a la 10
println(texto.replace("ejemplo", "prueba"))     // Esta es una cadena de prueba
println(texto.contains("cadena"))               // true
println(texto.startsWith("Esta"))               // true
println(texto.indexOf("una"))                   // 8

val partes = texto.split(" ")                   // Array(Esta, es, una, cadena, de, ejemplo)
println(partes.length)                          // 6
println(partes.mkString("-"))                   // Esta-es-una-cadena-de-ejemplo
  • substring(desde, hasta) incluye la posición desde y excluye hasta.
  • split divide por un separador y devuelve un Array[String]; mkString hace lo contrario, une con el separador que le pases.
  • Ojo: el argumento de split es una expresión regular. Para dividir por un carácter especial como | o ., hay que escaparlo (split("\\|")) o pasarlo como Char (split('|')). Lo vas a necesitar en la lección 11.

Formatear strings

Scala tiene tres interpoladores de strings, que se activan con una letra antes de las comillas:

scala
val nombre = "Alice"
val edad = 30
val saldo = 1234.5

// s: inserta valores
println(s"Mi nombre es $nombre y tengo $edad años.")
println(s"El año que viene voy a tener ${edad + 1}.") // con llaves para expresiones

// f: inserta valores con formato, al estilo printf
println(f"Pi es aproximadamente ${math.Pi}%.2f")     // Pi es aproximadamente 3.14
println(f"Saldo: $saldo%.2f")                        // Saldo: 1234.50
println(f"Saldo: $saldo%10.2f|")                     // Saldo:    1234.50|
println(f"Cuenta: ${42}%05d")                        // Cuenta: 00042

// raw: no interpreta las secuencias de escape
println(raw"C:\nueva\carpeta")                        // C:\nueva\carpeta
  • Con $nombre se inserta una variable; para cualquier otra cosa (una expresión, un acceso con punto), van llaves: ${math.Pi}. Si escribís $math.Pi, Scala inserta math y deja .Pi como texto, y el resultado no es el esperado.
  • En f, el formato va pegado después del valor: %.2f (dos decimales), %10.2f (ancho 10), %05d (entero con ceros a la izquierda), %-20s (texto alineado a la izquierda en 20 caracteres).
  • Para escribir un $ literal dentro de un interpolador, se duplica: $$.

Transformar strings

scala
val original = "  Cadena de Ejemplo  "

println(original.toLowerCase)      // "  cadena de ejemplo  "
println(original.toUpperCase)      // "  CADENA DE EJEMPLO  "
println(original.trim)             // "Cadena de Ejemplo": sin espacios en los extremos

val limpia = original.trim
println(limpia.take(6))            // Cadena: los primeros 6 caracteres
println(limpia.takeRight(7))       // Ejemplo: los últimos 7
println(limpia.drop(7))            // de Ejemplo
println("42".padTo(6, '*'))        // 42****: rellena hasta 6 caracteres
println("*" * 5)                   // *****: repetir un string
println(limpia.reverse)            // olpmejE ed anedaC

Los strings tienen además casi todas las operaciones de colecciones que viste en el módulo 2, porque se pueden tratar como una secuencia de caracteres:

scala
val texto = "Cuenta 1234-5678"

println(texto.filter(_.isDigit))       // 12345678
println(texto.count(_.isLetter))       // 6
println(texto.map(_.toUpper))          // CUENTA 1234-5678
println(texto.exists(_.isWhitespace))  // true

Convertir entre texto y números

scala
println("42".toInt + 1)          // 43
println("1500.75".toDouble)      // 1500.75
println("abc".toIntOption)       // None: no se pudo convertir
println("42".toIntOption)        // Some(42)
println(42.toString + "!")       // 42!

toInt lanza una NumberFormatException si el texto no es un número. Cuando el dato viene de afuera (un archivo, lo que escribe un usuario), es más seguro toIntOption o toDoubleOption, que devuelven un Option.

Expresiones regulares, un adelanto

Para buscar patrones en un texto, y no solo un texto fijo, se usan expresiones regulares. En Scala se crean agregando .r a un string:

scala
val texto = "Mi número de teléfono es 123-456-7890."
val patron = "\\d{3}-\\d{3}-\\d{4}".r

println(patron.findAllIn(texto).toList) // List(123-456-7890)

\d significa "un dígito" y {3} "tres veces". Dentro de un string común la barra invertida se escribe doble (\\d); con triples comillas ("""\d{3}""") se escribe una sola. Las vas a ver a fondo en la lección 12.

Codificación de caracteres

Un string es una secuencia de caracteres; para guardarlo en un archivo o mandarlo por la red hay que convertirlo en bytes, según una codificación. La más usada es UTF-8:

scala
val cadena = "Café"
val bytes = cadena.getBytes("UTF-8")
val decodificado = new String(bytes, "UTF-8")

println(cadena.length)        // 4 caracteres
println(bytes.length)         // 5 bytes: la é ocupa dos
println(decodificado)         // Café

Si un archivo se lee con una codificación distinta de la que se usó para escribirlo, aparecen los caracteres raros del tipo Café. Cuando trabajes con archivos, indicá siempre la codificación de forma explícita.

Practicá

  1. Concatenación de nombres. Escribí una función que reciba el nombre y el apellido de un cliente y los devuelva unidos en un solo string.

  2. Formato de saldo. Escribí una función que reciba un saldo y lo devuelva como texto con el símbolo de la moneda y dos decimales.

  3. Extracción de números de cuenta. Dado un texto con números de cuenta de 10 dígitos, escribí una función que devuelva la lista de todos los números de cuenta válidos.

    scala
    val texto = "Mi número de cuenta es 1234567890 y otro es 9876543210."
    
  4. Máscara de información sensible. Escribí una función que reciba un dato sensible, como un número de tarjeta, y reemplace por asteriscos todos los dígitos excepto los últimos cuatro.

    scala
    val tarjeta = "1234-5678-9876-5432"
    

Soluciones

scala
// Ejercicio 1
def concatenarNombres(nombre: String, apellido: String): String =
  s"$nombre $apellido"

println(concatenarNombres("John", "Doe")) // John Doe

// Ejercicio 2
def formatearSaldo(saldo: Double): String =
  f"$$ $saldo%.2f"

println(formatearSaldo(1000))    // $ 1000.00
println(formatearSaldo(1000.5))  // $ 1000.50

// Ejercicio 3
def extraerNumerosCuenta(cadena: String): List[String] = {
  val patron = "\\b\\d{10}\\b".r
  patron.findAllIn(cadena).toList
}

val texto = "Mi número de cuenta es 1234567890 y otro es 9876543210."
println(extraerNumerosCuenta(texto))              // List(1234567890, 9876543210)
println(extraerNumerosCuenta("Inválido: 12345678901")) // List(): tiene 11 dígitos

// Ejercicio 4, versión simple
def enmascararInformacion(cadena: String): String = {
  val ultimosCuatro = cadena.takeRight(4)
  val asteriscos = "*" * (cadena.length - 4)
  asteriscos + ultimosCuatro
}

val tarjeta = "1234-5678-9876-5432"
println(enmascararInformacion(tarjeta)) // ***************5432

// Ejercicio 4, conservando los separadores
def enmascararDigitos(cadena: String): String = {
  val cantidadDigitos = cadena.count(_.isDigit)
  var vistos = 0
  cadena.map { c =>
    if (c.isDigit) {
      vistos += 1
      if (vistos <= cantidadDigitos - 4) '*' else c
    } else c
  }
}

println(enmascararDigitos(tarjeta)) // ****-****-****-5432

Algunos detalles para revisar:

  • En el ejercicio 2, f"$$ $saldo%.2f" produce $ 1000.00: el $$ es el signo pesos literal y %.2f fuerza los dos decimales aunque el saldo sea entero.
  • En el ejercicio 3, \b marca un límite de palabra. Sin él, \d{10} también encontraría 10 dígitos dentro de un número más largo, como 12345678901, y lo daría por válido.
  • En el ejercicio 4, la versión simple tapa todo menos los últimos 4 caracteres, guiones incluidos: el resultado tiene 15 asteriscos, uno por cada carácter tapado. La segunda versión recorre el string con map, cuenta los dígitos y solo reemplaza los que no están entre los últimos cuatro, así el número enmascarado conserva su formato.

Resumen

  • Los strings son inmutables; se concatenan con + y se escriben en varias líneas con """...""" y stripMargin.
  • length, charAt/s(i), substring, contains, indexOf, replace, split y mkString cubren las operaciones básicas. El argumento de split es una expresión regular.
  • s"..." interpola, f"..." interpola con formato (%.2f, %05d, %-20s) y raw"..." no interpreta escapes. Las expresiones van entre llaves: ${math.Pi}.
  • trim, toUpperCase, take, takeRight, padTo y las operaciones de colecciones (filter, map, count) transforman texto.
  • toIntOption y toDoubleOption convierten texto a número sin lanzar excepciones.
  • Las expresiones regulares se crean con .r; los bytes de un string dependen de la codificación, y UTF-8 es la más común.

← Volver al temario