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
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:
val mensaje = """Estimado cliente:
|Su resumen ya está disponible.
|Saludos.""".stripMargin
println(mensaje)
Métodos básicos
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
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óndesdey excluyehasta.splitdivide por un separador y devuelve unArray[String];mkStringhace lo contrario, une con el separador que le pases.- Ojo: el argumento de
splites una expresión regular. Para dividir por un carácter especial como|o., hay que escaparlo (split("\\|")) o pasarlo comoChar(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:
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
$nombrese inserta una variable; para cualquier otra cosa (una expresión, un acceso con punto), van llaves:${math.Pi}. Si escribís$math.Pi, Scala insertamathy deja.Picomo 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
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:
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
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:
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:
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á
-
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.
-
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.
-
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.
scalaval texto = "Mi número de cuenta es 1234567890 y otro es 9876543210." -
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.
scalaval tarjeta = "1234-5678-9876-5432"
Soluciones
// 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%.2ffuerza los dos decimales aunque el saldo sea entero. - En el ejercicio 3,
\bmarca un límite de palabra. Sin él,\d{10}también encontraría 10 dígitos dentro de un número más largo, como12345678901, 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"""..."""ystripMargin. length,charAt/s(i),substring,contains,indexOf,replace,splitymkStringcubren las operaciones básicas. El argumento desplites una expresión regular.s"..."interpola,f"..."interpola con formato (%.2f,%05d,%-20s) yraw"..."no interpreta escapes. Las expresiones van entre llaves:${math.Pi}.trim,toUpperCase,take,takeRight,padToy las operaciones de colecciones (filter,map,count) transforman texto.toIntOptionytoDoubleOptionconvierten 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.