Buscar un número de pedido en un texto, validar un código postal o cambiar el formato de las fechas: las expresiones regulares lo resuelven en una línea. Kotlin las envuelve en la clase Regex, con métodos claros y cadenas crudas que te ahorran escribir cada barra invertida dos veces.
También te puede interesar
Buscar con Regex
En Kotlin una expresión regular es un objeto Regex. Sus métodos responden a las preguntas de siempre: ¿aparece?, ¿dónde está la primera?, ¿cuáles son todas?, ¿el texto entero cumple el patrón?
fun main() {
val texto = "Pedido 4512: 3 tazas y 12 cucharas, enviado el 2026-11-09"
val numero = Regex("""\d+""")
println(numero.containsMatchIn(texto))
println(numero.find(texto)?.value)
println(numero.findAll(texto).map { it.value }.toList())
println(numero.matches(texto))
println(numero.matches("4512"))
}salidatrue 4512 [4512, 3, 12, 2026, 11, 09] false true
Fíjate en la diferencia entre las dos últimas: containsMatchIn busca el patrón en cualquier parte, y matches exige que todo el texto lo cumpla, como si el patrón llevara ^ al principio y $ al final. find devuelve un MatchResult?: es null si no hay coincidencia, por eso va con ?.. Y findAll devuelve una Sequence, que se recorre cuando la necesitas; con toList() la conviertes en lista.
La barra invertida: cadenas crudas
Los patrones están llenos de barras invertidas (\d, \s, \w), y en una cadena normal de Kotlin la barra invertida sirve para escapar caracteres. Si escribes el patrón tal cual, el compilador no te deja:
fun main() {
val numero = Regex("\d+")
println(numero.find("Pedido 4512")?.value)
}kotlincescape.kt:2:25: error: unsupported escape sequence.
val numero = Regex("\d+")
^^«unsupported escape sequence»: \d no es un escape válido en una cadena de Kotlin. Hay dos soluciones: escribir cada barra dos veces, como en Java, o usar una cadena cruda entre tres comillas, donde nada se escapa. Las dos dan el mismo patrón:
fun main() {
val conDobles = Regex("\\d+") // cadena normal: la barra se escribe dos veces
val cruda = Regex("""\d+""") // cadena cruda: tal cual
println(conDobles.find("Pedido 4512")?.value)
println(cruda.find("Pedido 4512")?.value)
println(conDobles.pattern == cruda.pattern)
}salida4512 4512 true
En el resto de la página usamos cadenas crudas: el patrón se lee igual que en la documentación o en un probador de expresiones regulares.
Grupos: sacar partes de la coincidencia
fun main() {
val fecha = Regex("""(?<anio>\d{4})-(?<mes>\d{2})-(?<dia>\d{2})""")
val m = fecha.find("Entregado el 2026-11-09 a las 10:30") ?: return
println(m.value)
println(m.groupValues)
println(m.groups["mes"]?.value)
val (anio, mes, dia) = m.destructured
println("$dia/$mes/$anio")
}salida2026-11-09 [2026-11-09, 2026, 11, 09] 11 09/11/2026
Cada paréntesis es un grupo. groupValues da la coincidencia completa en la posición 0 y luego cada grupo; con (?<nombre>...) también se pueden pedir por nombre. Y destructured permite repartir los grupos en variables de una vez, igual que se hace con una data class.
Reemplazar
fun main() {
val fecha = Regex("""(\d{4})-(\d{2})-(\d{2})""")
val texto = "Del 2026-11-09 al 2026-12-24"
println(fecha.replace(texto, "\$3/\$2/\$1"))
println(fecha.replace(texto) { m ->
val (anio, mes, dia) = m.destructured
"$dia de ${MESES[mes.toInt() - 1]} de $anio"
})
println(" muchos espacios aquí ".trim().replace(Regex("""\s+"""), " "))
}
val MESES = listOf("enero", "febrero", "marzo", "abril", "mayo", "junio", "julio",
"agosto", "septiembre", "octubre", "noviembre", "diciembre")salidaDel 09/11/2026 al 24/12/2026 Del 09 de noviembre de 2026 al 24 de diciembre de 2026 muchos espacios aquí
En el texto de reemplazo, $1, $2… son los grupos. Como en Kotlin el signo $ también sirve para meter variables en una cadena, hay que escaparlo: "\$3/\$2/\$1". Para algo más que reordenar, pásale una lambda que recibe cada coincidencia y devuelve el texto nuevo: ahí puedes convertir, calcular o buscar en una lista, como el nombre del mes.
Validar datos
val CODIGO_POSTAL = Regex("""\d{5}""")
val TELEFONO = Regex("""\d{10}""")
val CORREO = Regex("""[\w.+-]+@[\w-]+(\.[\w-]+)+""")
fun main() {
for (cp in listOf("06600", "6600", "06600-1")) {
println("CP $cp: ${CODIGO_POSTAL.matches(cp)}")
}
val tel = "55 1234 5678"
println("Teléfono $tel: ${TELEFONO.matches(tel.replace(" ", ""))}")
for (c in listOf("ana@example.com", "ana.lopez+pedidos@correo.example.mx", "ana@", "ana example.com")) {
println("Correo $c: ${CORREO.matches(c)}")
}
}salidaCP 06600: true CP 6600: false CP 06600-1: false Teléfono 55 1234 5678: true Correo ana@example.com: true Correo ana.lopez+pedidos@correo.example.mx: true Correo ana@: false Correo ana example.com: false
Para validar, matches: el código postal de México tiene exactamente 5 dígitos, así que «6600» y «06600-1» no pasan. Antes de comprobar el teléfono quitamos los espacios, porque la gente los escribe de mil formas. El patrón del correo es deliberadamente sencillo: comprueba la forma general, nada más. Ninguna expresión regular sabe si un correo existe; para eso hay que mandarle un mensaje.
Dividir un texto
fun main() {
val linea = "manzana , pera,uva ; kiwi"
println(linea.split(Regex("""\s*[,;]\s*""")))
println("uno dos tres".split(" "))
println("uno dos tres".split(Regex("""\s+""")))
}salida[manzana, pera, uva, kiwi] [uno, , dos, , , tres] [uno, dos, tres]
split(" ") corta en cada espacio y deja cadenas vacías donde había dos seguidos. Con una expresión regular (\s+, «uno o más espacios») el resultado es el que normalmente quieres.
Opciones: mayúsculas y varias líneas
fun main() {
val texto = """
Error: disco lleno
aviso: memoria baja
ERROR: sin red
""".trimIndent()
val error = Regex("""^error: (.+)$""", setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
error.findAll(texto).forEach { println(it.groupValues[1]) }
println(Regex("""^error""").containsMatchIn(texto))
}salidadisco lleno sin red false
IGNORE_CASE no distingue mayúsculas de minúsculas, y MULTILINE hace que ^ y $ marquen el principio y el final de cada línea, no solo del texto entero. Sin esa opción, ^error no encuentra nada: el texto empieza con «Error», en mayúscula.
El error típico: meter texto del usuario en el patrón
Si construyes un patrón con un texto que viene de fuera, cualquier carácter especial (( ) [ ] . * + ?) se interpreta como parte de la expresión. Un paréntesis sin cerrar no se detecta al compilar, sino al ejecutar:
fun main() {
val usuario = "precio (con IVA"
val r = Regex(usuario)
println(r.containsMatchIn("el precio (con IVA) es 116"))
}salida con errorException in thread "main" java.util.regex.PatternSyntaxException: Unclosed group near index 15
Para buscar un texto literal, pásalo por Regex.escape. Y si no necesitas una expresión regular, no la uses: replace con un String reemplaza el texto tal cual.
fun main() {
val buscado = "precio (con IVA"
val r = Regex(Regex.escape(buscado))
println(r.containsMatchIn("el precio (con IVA) es 116"))
println("1+1=2".replace(Regex.escape("1+1"), "dos"))
println("1+1=2".replace("1+1", "dos"))
}salidatrue 1+1=2 dos=2
- ParaEscribeDevuelve
- ¿Aparece en alguna parte?
r.containsMatchIn(texto)Boolean - ¿Todo el texto cumple?
r.matches(texto)Boolean - La primera coincidencia
r.find(texto)MatchResult? - Todas
r.findAll(texto)Sequence<MatchResult> - Reemplazar
r.replace(texto, "\$1")o con lambdaString - Dividir
texto.split(r)List<String> - Texto literal
Regex.escape(s)patrón seguro
Para ejecutar cualquiera de estos programas basta con el compilador, sin bibliotecas: kotlinc basico.kt -include-runtime -d basico.jar y java -jar basico.jar. Por dentro, Regex usa el motor de Java (java.util.regex), así que la sintaxis de los patrones es la misma que en Java.
Todo el código de esta página se compiló con Kotlin 2.4.21 y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.