Expresiones regulares y grupos nombrados en Dart

RegExp es la clase que implementa el motor de expresiones regulares siguiendo el estándar ECMAScript. A diferencia de una comparación de strings simple, RegExp permite definir patrones complejos para buscar, validar o extraer información dentro de bloques de texto de forma estructurada. Debido a que implementa la interfaz Pattern, puedes pasar una instancia de RegExp directamente a métodos como String.split(), String.contains() o String.replaceAll(), integrándose de forma nativa en cualquier operación de manipulación de texto.

Su funcionamiento se basa en un motor de búsqueda que recorre la cadena de caracteres buscando coincidencias con el patrón definido. Usamos RegExp cuando necesitamos ir más allá de la coincidencia literal; por ejemplo, para extraer un ID de usuario de un log desordenado o para validar que un correo electrónico cumpla con una estructura específica. Debes tener especial cuidado con la sintaxis: si olvidas usar raw strings (r'pattern') para definir el patrón, tendrás que escapar cada barra invertida (\), lo que hace que el código sea prácticamente ilegible. Además, si intentas acceder a los resultados de firstMatch sin verificar si la coincidencia fue nula, el programa fallará en tiempo de ejecución.

void main() {
  // Datos simulados provenientes de un log de auditoría
  final logEntries = [
    '2023-10-25 | USER: admin_01 | ACTION: LOGIN | STATUS: success',
    '2023-10-25 | USER: guest_user | ACTION: VIEW_PAGE | STATUS: failed',
    'ERROR: Corrupción de datos detectada en el sector 0x4F2',
    '2023-10-26 | USER: dev_expert | ACTION: DELETE_RECORDS | STATUS: success',
  ];

  // Definimos el patrón usando raw strings (r'') para evitar el doble escape de \
  // Usamos grupos nombrados (?<name>...) para extraer datos de forma semántica
  // \s+ busca uno o más espacios, \w+ busca caracteres alfanuméricos
  final logPattern = RegExp(
    r'^(?<date>\d{4}-\d{2}-\d{2}) \| USER: (?<user>\w+) \| ACTION: (?<action>\w+) \| STATUS: (?<status>\w+)',
    caseSensitive: true,
  );

  print('--- Procesando Logs ---');

  for (final line in logEntries) {
    // allMatches es ideal cuando esperamos múltiples ocurrencias, 
    // pero aquí usamos firstMatch porque cada línea debería tener un único match estructural.
    final match = logPattern.firstMatch(line);

    if (match != null) {
      // match.group(0) siempre retorna la coincidencia completa de la expresión
      print('Línea procesada: ${match.group(0)}');

      // Accedemos a los grupos mediante sus nombres definidos en el patrón
      // namedGroup devuelve un String? (puede ser null si el grupo no capturó nada)
      final user = match.namedGroup('user');
      final action = match.namedGroup('action');
      final status = match.namedGroup('status');

      print('   -> Usuario: $user | Acción: $action | Estado: $status');
    } else {
      // Si el patrón no coincide (como en la línea de error), match es null
      print('Línea ignorada (no cumple el formato): "$line"');
    }
  }
}

Análisis del código

En el ejemplo anterior, la variable logPattern encapsula la lógica de búsqueda. Al usar r'...', le indicamos a Dart que trate las barras invertidas como caracteres literales de la expresión regular y no como secuencias de escape de Dart (como \n). El patrón utiliza grupos nombrados como (?<user>\w+). Esta es una práctica fundamental en producción porque hace que el código sea mucho más robusto que usar índices numéricos como group(1), group(2), etc., los cuales rompen si decides añadir un nuevo grupo al principio de la expresión.

Cuando ejecutamos logPattern.firstMatch(line), el motor de la VM busca la coincidencia desde el inicio de la cadena. El resultado es un objeto RegExpMatch (que hereda de Match) que contiene no solo el texto encontrado, sino metadatos críticos. match.start y match.end nos indican la posición exacta de la coincidencia en la cadena original, lo cual es vital para tareas de reemplazo o segmentación de archivos de gran tamaño. En el caso de las líneas que no coinciden con el patrón, firstMatch devuelve null, por lo que el chequeo if (match != null) es obligatorio para evitar errores de puntero nulo.

El uso de allMatches en lugar de firstMatch es una decisión de diseño: allMatches devuelve un Iterable<RegExpMatch>, permitiendo iterar sobre todas las ocurrencias encontradas en una sola cadena, mientras que firstMatch se detiene en la primera coincidencia, siendo más eficiente si solo nos interesa la primera aparición.

El error frecuente

Un error común al trabajar con firstMatch es encadenar llamadas asumiendo que la coincidencia siempre existirá. Esto provoca un error de tipo Null check operator used on a null value.

void main() {
  final input = "Texto sin coincidencia";
  final regex = RegExp(r'\d+'); // Busca dígitos

  // ERROR: firstMatch puede ser null si no hay dígitos
  // Esto lanzará una excepción si no se valida antes
  final firstDigit = regex.firstMatch(input)!.group(0); 
  print(firstDigit);
}

Para evitar esto, siempre usa el operador de control de flujo if (match != null) o el operador de encadenamiento opcional ?. si solo necesitas el valor.

40

Dejar un comentario

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

Scroll al inicio