Comparar cadenas ignorando marcas de acentuación

Aprende cómo comparar cadenas ignorando marcas diacríticas usando normalización de JavaScript e Intl.Collator

Introducción

Al construir aplicaciones que funcionan con múltiples idiomas, a menudo necesitas comparar cadenas que contienen acentos. Un usuario que busca "cafe" debería encontrar resultados para "café". Una verificación de nombre de usuario para "Jose" debería coincidir con "José". La comparación estándar de cadenas trata estos como cadenas diferentes, pero la lógica de tu aplicación necesita tratarlos como iguales.

JavaScript proporciona dos enfoques para resolver este problema. Puedes normalizar cadenas y eliminar los acentos, o utilizar la API de colación incorporada para comparar cadenas con reglas específicas de sensibilidad.

Qué son los acentos

Los acentos son símbolos colocados encima, debajo o a través de las letras para modificar su pronunciación o significado. Estas marcas se denominan diacríticos. Ejemplos comunes incluyen el acento agudo en "é", la tilde en "ñ" y la diéresis en "ü".

En Unicode, estos caracteres pueden representarse de dos maneras. Un único punto de código puede representar el carácter completo, o múltiples puntos de código pueden combinar una letra base con una marca de acento separada. La letra "é" puede almacenarse como U+00E9 o como "e" (U+0065) más un acento agudo combinado (U+0301).

Cuándo ignorar los acentos en las comparaciones

La funcionalidad de búsqueda es el caso de uso más común para la comparación insensible a los acentos. Los usuarios que escriben consultas sin acentos esperan encontrar contenido que contenga caracteres acentuados. Una búsqueda de "Muller" debería encontrar "Müller".

La validación de entrada del usuario requiere esta capacidad al verificar si los nombres de usuario, direcciones de correo electrónico u otros identificadores ya existen. Quieres evitar cuentas duplicadas para "maria" y "maría".

Las comparaciones insensibles a mayúsculas y minúsculas a menudo necesitan ignorar los acentos al mismo tiempo. Cuando compruebas si dos cadenas coinciden independientemente de las mayúsculas, normalmente también quieres ignorar las diferencias de acentos.

Eliminar marcas de acento usando normalización

El primer enfoque convierte las cadenas a una forma normalizada donde las letras base y las marcas de acento están separadas, luego elimina las marcas de acento.

La normalización Unicode convierte las cadenas a una forma estándar. La forma NFD (Descomposición Canónica) separa los caracteres combinados en sus letras base y marcas combinadas. La cadena "café" se convierte en "cafe" seguida de un carácter de acento agudo combinado.

Después de la normalización, puedes eliminar las marcas combinadas usando una expresión regular. El rango Unicode U+0300 a U+036F contiene marcas diacríticas combinadas.

function removeAccents(str) {
  return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

const text1 = 'café';
const text2 = 'cafe';

const normalized1 = removeAccents(text1);
const normalized2 = removeAccents(text2);

console.log(normalized1 === normalized2); // true
console.log(normalized1); // "cafe"

Este método te proporciona cadenas sin marcas de acento que puedes comparar usando operadores de igualdad estándar.

Puedes combinar esto con la conversión a minúsculas para comparaciones insensibles a mayúsculas y acentos.

function normalizeForComparison(str) {
  return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}

const search = 'muller';
const name = 'Müller';

console.log(normalizeForComparison(search) === normalizeForComparison(name)); // true

Este enfoque funciona bien cuando necesitas almacenar o indexar la versión normalizada de cadenas para búsquedas eficientes.

Comparar cadenas usando Intl.Collator

El segundo enfoque utiliza la API Intl.Collator, que proporciona comparación de cadenas con reconocimiento de configuración regional y niveles de sensibilidad configurables.

El objeto Intl.Collator compara cadenas según reglas específicas del idioma. La opción de sensibilidad controla qué diferencias importan al comparar cadenas.

El nivel de sensibilidad "base" ignora tanto las marcas de acento como las diferencias de mayúsculas y minúsculas. Las cadenas que difieren solo en acentos o capitalización se consideran iguales.

const collator = new Intl.Collator('en', { sensitivity: 'base' });

console.log(collator.compare('café', 'cafe')); // 0 (igual)
console.log(collator.compare('Café', 'cafe')); // 0 (igual)
console.log(collator.compare('café', 'caff')); // -1 (el primero va antes que el segundo)

El método compare devuelve 0 cuando las cadenas son iguales, un número negativo cuando la primera cadena va antes que la segunda, y un número positivo cuando la primera cadena va después de la segunda.

Puedes usar esto para verificaciones de igualdad o para ordenar arrays.

const collator = new Intl.Collator('en', { sensitivity: 'base' });

function areEqualIgnoringAccents(str1, str2) {
  return collator.compare(str1, str2) === 0;
}

console.log(areEqualIgnoringAccents('José', 'Jose')); // true
console.log(areEqualIgnoringAccents('naïve', 'naive')); // true

Para ordenar, puedes pasar el método compare directamente a Array.sort.

const names = ['Müller', 'Martinez', 'Muller', 'Márquez'];
const collator = new Intl.Collator('en', { sensitivity: 'base' });

names.sort(collator.compare);
console.log(names); // Agrupa variantes juntas

La API Intl.Collator proporciona otros niveles de sensibilidad para diferentes casos de uso.

El nivel "accent" ignora mayúsculas y minúsculas pero respeta las diferencias de acento. "Café" es igual a "café" pero no a "cafe".

const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (igual)
console.log(accentCollator.compare('café', 'cafe')); // 1 (no igual)

El nivel "case" ignora acentos pero respeta diferencias de mayúsculas y minúsculas. "café" es igual a "cafe" pero no a "Café".

const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (igual)
console.log(caseCollator.compare('café', 'Café')); // -1 (no igual)

El nivel "variant" respeta todas las diferencias. Este es el comportamiento predeterminado.

const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (no igual)

Elegir entre normalización y cotejo

Ambos métodos producen resultados correctos para comparaciones insensibles a acentos, pero tienen características diferentes.

El método de normalización crea nuevas cadenas sin marcas de acentos. Utiliza este enfoque cuando necesites almacenar o indexar las versiones normalizadas. Los motores de búsqueda y las bases de datos a menudo almacenan texto normalizado para búsquedas eficientes.

El método Intl.Collator compara cadenas sin modificarlas. Utiliza este enfoque cuando necesites comparar cadenas directamente, como al verificar duplicados o ordenar listas. El cotejador respeta las reglas de ordenación específicas del idioma que la comparación simple de cadenas no puede manejar.

Las consideraciones de rendimiento varían según el caso de uso. Crear un objeto cotejador una vez y reutilizarlo es eficiente para múltiples comparaciones. Normalizar cadenas es eficiente cuando normalizas una vez y comparas muchas veces.

El método de normalización elimina la información de acentos permanentemente. El método de cotejo preserva las cadenas originales mientras las compara según las reglas que especifiques.

Filtrar arrays usando búsqueda insensible a acentos

Un caso de uso común es filtrar un array de elementos basado en la entrada del usuario, ignorando las diferencias de acentos.

const products = [
  { name: 'Café Latte', price: 4.50 },
  { name: 'Crème Brûlée', price: 6.00 },
  { name: 'Croissant', price: 3.00 },
  { name: 'Café Mocha', price: 5.00 }
];

function searchProducts(query) {
  const collator = new Intl.Collator('en', { sensitivity: 'base' });

  return products.filter(product => {
    return collator.compare(product.name.slice(0, query.length), query) === 0;
  });
}

console.log(searchProducts('cafe'));
// Devuelve tanto Café Latte como Café Mocha

Para coincidencias de subcadenas, el enfoque de normalización funciona mejor.

function removeAccents(str) {
  return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

function searchProducts(query) {
  const normalizedQuery = removeAccents(query.toLowerCase());

  return products.filter(product => {
    const normalizedName = removeAccents(product.name.toLowerCase());
    return normalizedName.includes(normalizedQuery);
  });
}

console.log(searchProducts('creme'));
// Devuelve Crème Brûlée

Este enfoque verifica si el nombre normalizado del producto contiene la consulta de búsqueda normalizada como subcadena.

Manejar la coincidencia de entrada de texto

Cuando se valida la entrada del usuario contra datos existentes, se necesita una comparación insensible a los acentos para prevenir confusiones y duplicados.

const existingUsernames = ['José', 'María', 'François'];

function isUsernameTaken(username) {
  const collator = new Intl.Collator('en', { sensitivity: 'base' });

  return existingUsernames.some(existing =>
    collator.compare(existing, username) === 0
  );
}

console.log(isUsernameTaken('jose')); // true
console.log(isUsernameTaken('Maria')); // true
console.log(isUsernameTaken('francois')); // true
console.log(isUsernameTaken('pierre')); // false

Esto evita que los usuarios creen cuentas con nombres que difieren solo en acentos o mayúsculas de las cuentas existentes.

Compatibilidad con navegadores y entornos

El método String.prototype.normalize es compatible con todos los navegadores modernos y entornos Node.js. Internet Explorer no admite este método.

La API Intl.Collator es compatible con todos los navegadores modernos y versiones de Node.js. Internet Explorer 11 incluye soporte parcial.

Ambos enfoques funcionan de manera confiable en los entornos JavaScript actuales. Si necesitas compatibilidad con navegadores más antiguos, necesitarás polyfills o implementaciones alternativas.

Limitaciones de la eliminación de acentos

Algunos idiomas utilizan diacríticos para crear letras distintas, no solo variaciones de acentos. En turco, "i" y "ı" son letras diferentes. En alemán, "ö" es una vocal distinta, no una "o" acentuada.

Eliminar los acentos cambia el significado en estos casos. Considera si la comparación insensible a los acentos es apropiada para tu caso de uso e idiomas objetivo.

El enfoque de cotejo maneja mejor estos casos porque sigue reglas específicas del idioma. Especificar el idioma correcto en el constructor Intl.Collator asegura comparaciones culturalmente apropiadas.

const turkishCollator = new Intl.Collator('tr', { sensitivity: 'base' });
const germanCollator = new Intl.Collator('de', { sensitivity: 'base' });

Siempre considera los idiomas que tu aplicación soporta al elegir una estrategia de comparación.