DEV Community

Cover image for Normalización de Números para Datos Internacionales: Guía Práctica
circobit
circobit

Posted on

Normalización de Números para Datos Internacionales: Guía Práctica

Exportas una tabla de un sitio web alemán. La columna de ingresos muestra "1.234,56 €".

Lo pegas en Excel. Se convierte en "1.23456E" o, peor, una fecha.

Bienvenido al problema del formato numérico internacional.

El Problema Central: ¿Qué Significa "1.234"?

En EE.UU. y el Reino Unido: 1.234 = uno punto dos tres cuatro (decimal)

En Alemania, España y la mayor parte de Latinoamérica: 1.234 = mil doscientos treinta y cuatro

Mismos caracteres. Valores completamente diferentes.

Cuando extraes datos de sitios web internacionales, no puedes asumir con qué formato estás trabajando. Y equivocarte significa que tu análisis estará desfasado por órdenes de magnitud.

La Ambigüedad de Tres Dígitos

El caso más difícil es exactamente tres dígitos después de un separador.

Entrada Interpretación EE.UU. Interpretación UE
1.234 1.234 (decimal) 1.234 (miles)
1,234 1,234 (miles) 1.234 (decimal)
1.234.567 Inválido 1.234.567
1,234,567 1,234,567 Inválido

Con dos dígitos después, es claramente decimal: "1.23" o "1,23".
Con cuatro o más dígitos después, es claramente decimal: "3.14159".
¿Con tres dígitos? Podría ser cualquiera.

El Algoritmo de Detección

Aquí hay un enfoque heurístico que maneja la mayoría de los casos reales:

function normalizeNumberString(value) {
  if (typeof value !== "string") return value;
  const v = value.trim();
  if (!v || !/[0-9]/.test(v)) return value;

  // Paso 1: Limpiar símbolos de moneda, porcentajes, espacios
  let cleaned = v
    .replace(/ /g, " ")
    .replace(/^(USD|EUR|GBP|R\$|MXN)\s*/i, "")
    .replace(/[$€£¥₹₽₩]/g, "")
    .replace(/%/g, "")
    .replace(/\s+/g, "");

  // Paso 2: Manejar signos negativos
  const isNegative = cleaned.match(/^-/);
  if (isNegative) {
    cleaned = cleaned.substring(1).trim();
  }

  // Paso 3: ¿Solo dígitos? Listo
  if (/^\d+$/.test(cleaned)) {
    return isNegative ? `-${cleaned}` : cleaned;
  }

  // Paso 4: Detectar separadores
  const hasComma = cleaned.includes(",");
  const hasDot = cleaned.includes(".");

  // Paso 5: Aplicar heurística
  let decimalSeparator = ".";
  let thousandsSeparator = ",";

  if (hasComma && hasDot) {
    // Ambos presentes: el último es decimal
    const lastComma = cleaned.lastIndexOf(",");
    const lastDot = cleaned.lastIndexOf(".");

    if (lastComma > lastDot) {
      decimalSeparator = ",";
      thousandsSeparator = ".";
    }
  } else if (hasComma && !hasDot) {
    const parts = cleaned.split(",");

    if (parts.length === 2 && parts[1].length <= 2) {
      // "1,23" → decimal
      decimalSeparator = ",";
    } else {
      // "1,234" o "1,234,567" → miles
      decimalSeparator = null;
      thousandsSeparator = ",";
    }
  } else if (hasDot && !hasComma) {
    const parts = cleaned.split(".");

    if (parts.length === 2) {
      const afterDot = parts[1];

      if (afterDot.length <= 2 || afterDot.length >= 4) {
        // "1.23" o "3.14159" → decimal
        decimalSeparator = ".";
      } else {
        // "1.234" → MILES (decisión pragmática para tablas)
        decimalSeparator = null;
        thousandsSeparator = ".";
      }
    } else {
      // Múltiples puntos → miles
      decimalSeparator = null;
      thousandsSeparator = ".";
    }
  }

  // Paso 6: Limpiar separadores de miles
  if (thousandsSeparator) {
    cleaned = cleaned.replace(new RegExp(`\\${thousandsSeparator}`, "g"), "");
  }

  // Paso 7: Normalizar decimal a punto
  if (decimalSeparator && decimalSeparator !== ".") {
    cleaned = cleaned.replace(decimalSeparator, ".");
  }

  // Paso 8: Validar
  const num = Number(cleaned);
  if (!Number.isFinite(num)) return value;

  return isNegative ? `-${cleaned}` : cleaned;
}
Enter fullscreen mode Exit fullscreen mode

Por Qué Tres Dígitos = Miles

La decisión clave: cuando vemos exactamente tres dígitos después de un único separador (como "1.234"), lo interpretamos como miles, no como decimal.

¿Por qué? En tablas HTML:

  • Los datos financieros con miles son extremadamente comunes: $1.234, €1.234
  • Los decimales con exactamente tres dígitos son raros en la práctica
  • Los datos científicos con tres decimales (como "3.141") generalmente se escriben como "3.14159" o simplemente "3.14"

Esta heurística es una decisión pragmática que funciona para la mayoría de las tablas del mundo real.

La Matriz Completa de Formatos

Entrada Interpretación Salida Normalizada
1.234,56 Decimal UE 1234.56
1,234.56 Decimal EE.UU. 1234.56
1.234.567,89 Miles UE + decimal 1234567.89
1,234,567.89 Miles EE.UU. + decimal 1234567.89
1.234 Miles 1234
1,234 Miles 1234
3.14 Decimal 3.14
3,14 Decimal 3.14
3.14159 Decimal (4+ dígitos) 3.14159
$ 1.200,50 Moneda + UE 1200.50
-5.000 Miles negativos -5000
12.5% Porcentaje 12.5

Manejando Códigos de Moneda

Los datos internacionales vienen con prefijos y sufijos de moneda. Límpialos primero:

// Remover códigos de moneda ANTES de símbolos
.replace(/^(USD|EUR|GBP|JPY|CHF|CAD|AUD|CNY|INR|BRL|R\$|MXN|KRW)\s*/i, "")
// Luego remover símbolos
.replace(/[$€£¥₹₽₩₪฿₫₴₦]/g, "")
Enter fullscreen mode Exit fullscreen mode

El orden importa: "R$" (Real Brasileño) contiene "$", así que remover códigos de moneda primero evita coincidencias parciales.

Cuándo la Heurística Falla

Ninguna heurística es perfecta. Obtendrás resultados incorrectos cuando:

  1. Datos científicos con exactamente 3 decimales: "3.141" se convierte en "3141"
  2. Precios menores a $10 con 3 decimales: "$1.234" se convierte en "$1234"
  3. Formatos mixtos en una columna: Algunas filas UE, otras EE.UU.

Para el caso 3, necesitas detección de formato a nivel de columna:

function detectColumnFormat(values) {
  let euIndicators = 0;
  let usIndicators = 0;

  for (const v of values) {
    if (/\d\.\d{3},\d{2}$/.test(v)) euIndicators++;
    if (/\d,\d{3}\.\d{2}$/.test(v)) usIndicators++;
  }

  if (euIndicators > usIndicators) return "eu";
  if (usIndicators > euIndicators) return "us";
  return "auto"; // Usar heurística por celda
}
Enter fullscreen mode Exit fullscreen mode

Perfiles de Exportación para Diferentes Regiones

Cuando conoces el país de origen, puedes evitar la ambigüedad por completo usando perfiles específicos por región:

  • Formato europeo: Asume coma como separador decimal, usa punto y coma como delimitador CSV
  • Formato EE.UU./UK: Asume punto como separador decimal, usa coma como delimitador CSV

HTML Table Exporter incluye perfiles preconfigurados para ambos formatos, más perfiles especializados para herramientas como Pandas y DuckDB.

Errores Comunes

No Perder Precisión

// Mal: pierde precisión en números grandes
const num = parseFloat("12345678901234567890");
// 12345678901234567000 (límite de JavaScript)

// Mejor: mantener como string hasta el cálculo final
const cleaned = normalizeNumberString(value);
// Solo parseFloat cuando necesités calcular
Enter fullscreen mode Exit fullscreen mode

Cuidado con las Entidades HTML

Las tablas web a veces contienen &nbsp; (espacio sin separación) dentro de números:

// "1&nbsp;234&nbsp;567" debería convertirse en "1234567"
.replace(/&nbsp;/g, " ")
.replace(/\s+/g, "")
Enter fullscreen mode Exit fullscreen mode

Preservar el Original Ante la Duda

Si el valor no parece un número, devuélvelo sin cambios:

if (!/[0-9]/.test(v)) return value;
// ...
if (!Number.isFinite(num)) return value;
Enter fullscreen mode Exit fullscreen mode

Es mejor dejar "N/A" como "N/A" que intentar parsearlo.

Resumen

Escenario Detección Notas
Ambos . y , El último = decimal Universal
Solo coma, ≤2 dígitos después Decimal "1,23"
Solo coma, 3+ dígitos después Miles "1,234"
Solo punto, ≤2 o 4+ dígitos Decimal "1.23", "3.14159"
Solo punto, exactamente 3 dígitos Miles "1.234"

El caso de tres dígitos es la decisión pragmática. Funciona para la mayoría de los datos reales. Ante la duda, usa un perfil de exportación que coincida con tu región de origen.

Para más sobre copiar datos de sitios web a Excel sin problemas de formato, consulta nuestra guía sobre copiar tablas de sitios web a Excel.


¿Necesitas exportaciones de números limpias sin adivinanzas? Conoce más en gauchogrid.com/es/html-table-exporter o pruébalo gratis en la Chrome Web Store.

Top comments (0)