Expresiones regulares en Kotlin: Regex, grupos, reemplazar y validar

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.

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 coincidenciar.find(texto)MatchResult?
  • Todasr.findAll(texto)Sequence<MatchResult>
  • Reemplazarr.replace(texto, "\$1") o con lambdaString
  • Dividirtexto.split(r)List<String>
  • Texto literalRegex.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.