Cómo obtener partes individuales de un número formateado para visualización personalizada

Divide los números formateados en componentes para aplicar estilos personalizados y construir interfaces complejas

Introducción

El método format() devuelve una cadena formateada completa como "$1,234.56" o "1.5M". Esto funciona bien para visualizaciones simples, pero no permite aplicar estilos diferentes a partes individuales. No puedes hacer que el símbolo de moneda esté en negrita, colorear la parte decimal de manera diferente o aplicar marcado personalizado a componentes específicos.

JavaScript proporciona el método formatToParts() para resolver este problema. En lugar de devolver una sola cadena, devuelve un array de objetos, cada uno representando una parte del número formateado. Cada parte tiene un tipo como currency, integer o decimal, y un valor como $, 1234 o .. Luego puedes procesar estas partes para aplicar estilos personalizados, construir diseños complejos o integrar números formateados en interfaces de usuario enriquecidas.

Por qué las cadenas formateadas son difíciles de personalizar

Cuando recibes una cadena formateada como "$1,234.56", no puedes identificar fácilmente dónde termina el símbolo de moneda y dónde comienza el número. Diferentes locales colocan los símbolos en diferentes posiciones. Algunos locales utilizan separadores diferentes. Analizar estas cadenas de manera fiable requiere una lógica compleja que duplica las reglas de formato ya implementadas en la API Intl.

Considera un panel que muestra cantidades monetarias con el símbolo de moneda en un color diferente. Con format(), necesitarías:

  1. Detectar qué caracteres son el símbolo de moneda
  2. Tener en cuenta los espacios entre el símbolo y el número
  3. Manejar diferentes posiciones de símbolos en distintos locales
  4. Analizar la cadena cuidadosamente para evitar romper el número

Este enfoque es frágil y propenso a errores. Cualquier cambio en las reglas de formato del locale rompe tu lógica de análisis.

El método formatToParts() elimina este problema proporcionando los componentes por separado. Recibes datos estructurados que te indican exactamente qué parte es cada una, independientemente del locale.

Uso de formatToParts para obtener componentes de números

El método formatToParts() funciona de manera idéntica a format() excepto por su valor de retorno. Creas un formateador con las mismas opciones, luego llamas a formatToParts() en lugar de format().

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
console.log(parts);

Esto produce un array de objetos:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

Cada objeto contiene una propiedad type que identifica lo que representa la parte y una propiedad value que contiene la cadena real. Las partes aparecen en el mismo orden en que aparecerían en la salida formateada.

Puedes verificar esto uniendo todos los valores:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Salida: "$1,234.56"

Las partes concatenadas producen exactamente la misma salida que al llamar a format().

Entendiendo los tipos de partes

La propiedad type identifica cada componente. Diferentes opciones de formato producen diferentes tipos de partes.

Para el formato básico de números:

const formatter = new Intl.NumberFormat("en-US");
const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

El tipo integer representa la parte entera del número. Aparecen múltiples partes integer cuando los separadores de grupo dividen el número. El tipo group representa el separador de miles. El tipo decimal representa el punto decimal. El tipo fraction representa los dígitos después del decimal.

Para el formato de moneda:

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "EUR"
});

const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "currency", value: "€" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

El tipo currency aparece antes o después del número dependiendo de las convenciones de la localización.

Para porcentajes:

const formatter = new Intl.NumberFormat("en-US", {
  style: "percent"
});

const parts = formatter.formatToParts(0.1234);
console.log(parts);
// [
//   { type: "integer", value: "12" },
//   { type: "percentSign", value: "%" }
// ]

El tipo percentSign representa el símbolo de porcentaje.

Para notación compacta:

const formatter = new Intl.NumberFormat("en-US", {
  notation: "compact"
});

const parts = formatter.formatToParts(1500000);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "5" },
//   { type: "compact", value: "M" }
// ]

El tipo compact representa el indicador de magnitud como K, M o B.

Aplicando estilos personalizados a partes de números

El caso de uso principal para formatToParts() es aplicar diferentes estilos a diferentes componentes. Puedes procesar el array de partes para envolver tipos específicos en elementos HTML.

Haciendo que el símbolo de moneda esté en negrita:

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
const html = parts
  .map(part => {
    if (part.type === "currency") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>$</strong>1,234.56"

Este enfoque funciona para cualquier lenguaje de marcado. Puedes generar HTML, JSX o cualquier otro formato procesando el array de partes.

Estilizando las partes decimales de manera diferente:

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2
});

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "1,234<span class="text-gray-500">.50</span>"

Este patrón es común en visualizaciones de precios donde la parte decimal aparece más pequeña o más clara.

Codificación por colores de números negativos

Las aplicaciones financieras a menudo muestran números negativos en rojo. Con formatToParts(), puedes detectar el signo menos y aplicar estilos en consecuencia.

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatWithColor(number) {
  const parts = formatter.formatToParts(number);
  const hasMinusSign = parts.some(part => part.type === "minusSign");

  const html = parts
    .map(part => part.value)
    .join("");

  if (hasMinusSign) {
    return `<span class="text-red-600">${html}</span>`;
  }

  return html;
}

console.log(formatWithColor(-1234.56));
// Output: "<span class="text-red-600">-$1,234.56</span>"

console.log(formatWithColor(1234.56));
// Output: "$1,234.56"

Este enfoque detecta números negativos de manera confiable en todas las configuraciones regionales, incluso aquellas que utilizan diferentes símbolos o posiciones para los indicadores negativos.

Creación de visualizaciones de números personalizadas con múltiples estilos

Las interfaces complejas a menudo combinan múltiples reglas de estilo. Puedes aplicar diferentes clases o elementos a diferentes tipos de partes simultáneamente.

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatCurrency(number) {
  const parts = formatter.formatToParts(number);

  return parts
    .map(part => {
      switch (part.type) {
        case "currency":
          return `<span class="currency-symbol">${part.value}</span>`;
        case "integer":
          return `<span class="integer">${part.value}</span>`;
        case "group":
          return `<span class="group">${part.value}</span>`;
        case "decimal":
          return `<span class="decimal">${part.value}</span>`;
        case "fraction":
          return `<span class="fraction">${part.value}</span>`;
        case "minusSign":
          return `<span class="minus">${part.value}</span>`;
        default:
          return part.value;
      }
    })
    .join("");
}

console.log(formatCurrency(1234.56));
// Output: "<span class="currency-symbol">$</span><span class="integer">1</span><span class="group">,</span><span class="integer">234</span><span class="decimal">.</span><span class="fraction">56</span>"

Este control granular permite un estilo preciso para cada componente. Puedes usar CSS para aplicar estilos diferentes a cada clase.

Todos los tipos de partes disponibles

La propiedad type puede tener estos valores dependiendo de las opciones de formato utilizadas:

  • integer: Dígitos de números enteros
  • fraction: Dígitos decimales
  • decimal: Separador decimal
  • group: Separador de miles
  • currency: Símbolo de moneda
  • literal: Espaciado u otro texto literal añadido por el formato
  • percentSign: Símbolo de porcentaje
  • minusSign: Indicador de número negativo
  • plusSign: Indicador de número positivo (cuando signDisplay está configurado)
  • unit: Cadena de unidad para formato de unidades
  • compact: Indicador de magnitud en notación compacta (K, M, B)
  • exponentInteger: Valor del exponente en notación científica
  • exponentMinusSign: Signo negativo en el exponente
  • exponentSeparator: Símbolo que separa la mantisa del exponente
  • infinity: Representación de infinito
  • nan: Representación de no-es-un-número
  • unknown: Tokens no reconocidos

No todas las opciones de formato producen todos los tipos de partes. Las partes que recibes dependen del valor numérico y de la configuración del formateador.

La notación científica produce partes relacionadas con el exponente:

const formatter = new Intl.NumberFormat("en-US", {
  notation: "scientific"
});

const parts = formatter.formatToParts(1234);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "234" },
//   { type: "exponentSeparator", value: "E" },
//   { type: "exponentInteger", value: "3" }
// ]

Los valores especiales producen tipos de partes específicos:

const formatter = new Intl.NumberFormat("en-US");

console.log(formatter.formatToParts(Infinity));
// [{ type: "infinity", value: "∞" }]

console.log(formatter.formatToParts(NaN));
// [{ type: "nan", value: "NaN" }]

Creación de visualizaciones de números accesibles

Puedes usar formatToParts() para añadir atributos de accesibilidad a números formateados. Esto ayuda a los lectores de pantalla a anunciar los valores correctamente.

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatAccessibleCurrency(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessibleCurrency(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

Esto asegura que los lectores de pantalla anuncien tanto el valor de visualización formateado como el valor numérico subyacente con el contexto adecuado.

Resaltado de rangos específicos de números

Algunas aplicaciones resaltan números que caen dentro de ciertos rangos. Con formatToParts(), puedes aplicar estilos basados en el valor mientras mantienes un formato adecuado.

const formatter = new Intl.NumberFormat("en-US");

function formatWithThreshold(number, threshold) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  if (number >= threshold) {
    return `<span class="text-green-600 font-bold">${formatted}</span>`;
  }

  return formatted;
}

console.log(formatWithThreshold(1500, 1000));
// Output: "<span class="text-green-600 font-bold">1,500</span>"

console.log(formatWithThreshold(500, 1000));
// Output: "500"

El número recibe el formato adecuado para la configuración regional mientras se aplica el estilo condicional basado en la lógica de negocio.

Cuándo usar formatToParts versus format

Usa format() cuando necesites una cadena formateada simple sin ninguna personalización. Este es el caso común para la mayoría de las visualizaciones de números.

Usa formatToParts() cuando necesites:

  • Aplicar diferentes estilos a diferentes partes del número
  • Construir HTML o JSX con números formateados
  • Añadir atributos o metadatos a componentes específicos
  • Integrar números formateados en diseños complejos
  • Procesar la salida formateada programáticamente

El método formatToParts() tiene un poco más de sobrecarga que format() porque crea un array de objetos en lugar de una sola cadena. Esta diferencia es insignificante para aplicaciones típicas, pero si formateas miles de números por segundo, format() tiene mejor rendimiento.

Para la mayoría de las aplicaciones, elige según tus necesidades de estilo en lugar de preocupaciones de rendimiento. Si no necesitas personalizar la salida, usa format(). Si necesitas estilos personalizados o marcado, usa formatToParts().

Cómo las partes preservan el formato específico del idioma

El array de partes mantiene automáticamente las reglas de formato específicas del idioma. Los diferentes idiomas colocan los símbolos en diferentes posiciones y utilizan diferentes separadores, pero formatToParts() maneja estas diferencias.

const usdFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

El formato alemán coloca la moneda después del número con un espacio. El separador de grupos es un punto, y el separador decimal es una coma. Tu código de estilo procesa el array de partes de la misma manera independientemente del idioma, y el formato se adapta automáticamente.

El tipo literal representa cualquier espacio o texto insertado por el formateador que no encaja en otras categorías. En el formato de moneda alemán, representa el espacio entre el número y el símbolo de moneda.

Combinando formatToParts con componentes de frameworks

Los frameworks modernos como React pueden utilizar formatToParts() para construir componentes de manera eficiente.

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

Este componente aplica diferentes estilos a diferentes partes mientras mantiene el formato adecuado para cualquier idioma y moneda.