Volver al Centro de conocimiento
Huella digital

Intl.NumberFormat: modo de redondeo y signo

Una sonda sobre el formato numérico: qué modo de redondeo es el predeterminado, qué hace cada modo en un límite .5 y cómo un signo o un cero final desaparecen.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Huella digital?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

BotBrowser permite reproducir un fixture del mismo origen autorizado bajo un perfil declarado y guardar un registro acotado de la cadena exacta que una página mostró para un número, pero no puede hacer que un formateador redondee como el lector supone, no puede devolver un signo menos que una opción quitó, ni puede hacer que dos monedas muestren el mismo número de decimales. Esta revisión ejecuta una sonda sobre once casos fijos y registra qué modo de redondeo es el predeterminado en un límite .5, qué hace cada modo explícito con los mismos cuatro valores, cómo signDisplay y trailingZeroDisplay cambian la cadena mostrada, cómo roundingPriority arbitra entre dos límites, por qué los decimales por defecto de una moneda salen de su código, y qué informan resolvedOptions() y formatToParts() sobre el formateador que de verdad corrió.

Resumen

  • El redondeo predeterminado es half-expand, alejándose de cero: la sonda imprime default -> 3,4,-3,1,1 para 2.5, 3.5, -2.5, 0.5, 1.45 con cero decimales, así que 2.5 se vuelve 3, no 2.
  • halfEven es el modo que daría 2.5 -> 2, y la sonda imprime halfEven-vs-expand -> 2,4 vs 3,4, así que los dos modos difieren solo donde el dígito anterior al límite es par.
  • trunc, ceil y floor mueven un número negativo en direcciones distintas: -2.5 se vuelve -2 con trunc, -2 con ceil y -3 con floor.
  • signDisplay: 'never' quita el menos de un número negativo, así que la sonda imprime sign:never -> 5,0,5, y un consumidor que lee esa cadena no puede saber que venía de -5.
  • signDisplay: 'always' pone un + también en el cero, así que la sonda imprime sign:always -> +5,+0,-5.
  • trailingZeroDisplay: 'stripIfInteger' conserva los dos decimales de 3.10 pero quita los que solo existían por el mínimo, así que 3.00 se muestra como 3.
  • roundingPriority: 'morePrecision' ensancha la salida para satisfacer ambos límites, así que 12.345 imprime 12.35 con morePrecision mientras que auto y lessPrecision imprimen 12.3.
  • Los decimales por defecto de una moneda salen de su código, así que las mismas opciones imprimen $5.00 y ¥5.
  • Poner maximumFractionDigits: 0 en un formateador de moneda también fija minimumFractionDigits a 0, y resolvedOptions() informa min=0 max=0 mode=halfExpand.

Contenido

El primer panel muestra 2.5 y 3.5 redondeados a 3 y 4 con el modo predeterminado half-expand y a 2 y 4 con halfEven. El segundo muestra trunc, ceil y floor llevando -2.5 a -2, -2 y -3. El tercero muestra sign never convirtiendo -5 en 5 y sign always convirtiendo 5 en +5. El cuarto muestra morePrecision imprimiendo 12.35 donde auto imprime 12.3 y un predeterminado de moneda de dos decimales para USD frente a cero para JPY.

Qué modo de redondeo es el predeterminado

Un formateador numérico redondea tanto si quien llama nombra un modo como si no, y el predeterminado es una elección concreta que un lector casi nunca comprueba. La sonda formatea cinco valores con cero decimales y sin modo de redondeo:

node=v26.9.0
default -> 3,4,-3,1,1
mode:halfExpand -> 3,4,-3,2
mode:halfEven -> 2,4,-2,2
mode:trunc -> 2,3,-2,1
mode:ceil -> 3,4,-2,2
mode:floor -> 2,3,-3,1
mode:expand -> 3,4,-3,2
mode:halfTrunc -> 2,3,-2,2
halfEven-vs-expand -> 2,4 vs 3,4
sign:auto -> 5,0,-5
sign:never -> 5,0,5
sign:always -> +5,+0,-5
sign:exceptZero -> +5,0,-5
sign:negative -> 5,0,-5
trailing:default -> 3.00 3.10
trailing:strip -> 3 3.10
priority -> 12.345:auto=12.3 more=12.35 less=12.3 | 1234.5:auto=1230 more=1234.5 less=1230 | 456.78:auto=457 more=456.78 less=457
currency -> $5.00 ¥5
currency:min0 -> $5
percent -> 13% 13%
compact -> 1.2M 1.2M
digits -> ar-EG=١٢٣٤ ar-EG-latn=1234
grouping -> de-DE=1.234.567,89 en-US=1,234,567.89
resolved -> min=0 max=0 mode=halfExpand
parts -> minusSign:-|currency:USD|literal: |integer:5|decimal:.|fraction:00

La primera línea es la respuesta a la pregunta que casi todo el mundo falla. default -> 3,4,-3,1,1 significa que 2.5 redondea a 3 y -2.5 redondea a -3, así que el predeterminado es half-expand, que redondea un empate alejándose de cero. No es la regla «redondear el medio al par» que muchos desarrolladores suponen por otros lenguajes o por un curso de finanzas, y la diferencia aparece en cada valor .5, justo el tipo de número que un fixture de prueba suele contener.

El segundo valor de esa línea muestra la otra mitad de la misma regla. 1.45 con cero decimales imprime 1, no 2, porque 1.45 no es exactamente 1.45 en coma flotante binaria; es un valor justo por debajo del punto medio, así que no hay empate que romper y el resultado es el entero más cercano por debajo. Una página que supone que un literal decimal es exacto verá por tanto que un .45 redondea hacia abajo mientras un .5 redondea hacia arriba, y ambos comportamientos salen de la misma regla aplicada a entradas distintas.

El último valor de esa línea es el que sorprende a un informe. 0.5 se vuelve 1, así que un valor positivo pequeño redondea a una unidad entera aunque sea el menor número posible con un solo dígito distinto de cero. Una página que usa cero decimales para imprimir un recuento mostrará por tanto 1 para cualquier valor en [0.5, 1.5), y una página que lo usa para una tasa pequeña mostrará 1 para cualquier cosa desde medio hacia arriba, lo que puede leerse como un error del 100% en los valores más pequeños.

Los datos de idioma que gobiernan los separadores y las formas de dígitos que emite un formateador son un tema propio, y datos de idioma de la API Intl registra qué partes de una etiqueta de idioma lee un formateador. No hay dependencia de idioma en este resultado. La sonda pasa en-US y el modo es una propiedad del formateador, no del idioma; de-DE o ja-JP redondean igual y difieren solo en el separador y en las formas de dígitos que muestran líneas posteriores. Una página que prueba el redondeo en un solo idioma ha visto por tanto toda la regla, y una página que supone que otro idioma redondea distinto está leyendo una diferencia de agrupación como una diferencia de aritmética.

El predeterminado también se ve en resolvedOptions(), y esa es la comprobación que merece entrar en un registro. La sonda imprime resolved -> min=0 max=0 mode=halfExpand para un formateador que solo fijó maximumFractionDigits: 0, así que el modo resuelto nombra el predeterminado que quien llamó no escribió. Un registro que guarda las opciones de entrada pero no las resueltas no puede después explicar por qué 2.5 imprimió 3, porque la razón vive en un campo que quien llamó no fijó.

Qué hace cada modo de redondeo en un límite

La sonda fija luego cada modo que ofrece la especificación y formatea los mismos cuatro valores, lo que convierte la pregunta en una tabla. Cada línea es un modo y sus cuatro resultados para 2.5, 3.5, -2.5, 1.6.

mode:halfExpand -> 3,4,-3,2 es el predeterminado hecho explícito. mode:halfEven -> 2,4,-2,2 manda 2.5 al vecino par 2 y deja 3.5 en el vecino par 4, así que el empate siempre cae en un valor cuyo último dígito es par. La línea dedicada de la sonda hace concreta la diferencia: halfEven-vs-expand -> 2,4 vs 3,4, así que los dos modos coinciden en 3.5 y discrepan en 2.5, que es la definición de un empate roto por paridad en lugar de por dirección.

mode:trunc -> 2,3,-2,1 descarta todo dígito más allá del límite sin importar cuán cerca esté, así que 1.6 se vuelve 1 y -2.5 se vuelve -2. mode:ceil -> 3,4,-2,2 redondea hacia infinito positivo, así que 1.6 se vuelve 2 y -2.5 se vuelve -2 porque -2 es mayor que -2.5. mode:floor -> 2,3,-3,1 redondea hacia infinito negativo, así que 1.6 se vuelve 1 y -2.5 se vuelve -3. mode:expand -> 3,4,-3,2 redondea alejándose de cero en todo valor, así que coincide con ceil en positivos y con floor en negativos.

Los tres modos direccionales son donde un fallo de manejo de signo se introduce más fácilmente. Una página que quiere «redondear hacia arriba» y recurre a ceil obtiene -2 para -2.5, que es mayor pero más cercano a cero, así que un «redondear hacia arriba» que significa «alejándose de cero» necesita expand y no ceil. Una página que quiere la elección conservadora para una cantidad que no debe exagerar necesita floor en un positivo y ceil en un negativo, que son dos reglas y no una, y los nombres de los modos no las combinan.

mode:halfTrunc -> 2,3,-2,2 redondea un empate hacia cero y el resto al más cercano, así que 2.5 se vuelve 2, 3.5 se vuelve 4 y -2.5 se vuelve -2. Esa es la dirección de empate opuesta a halfExpand, y es el modo al que recurrir cuando una página informa un total que no debe redondear hacia arriba en un empate, como un recuento que un sistema posterior comparará contra un límite.

El valor 1.6 es el control en cada línea. No es un empate, así que todo modo que redondea al más cercano da 2 y solo los modos truncantes dan 1. Una prueba que formatea un valor no empate y un valor empate separa por tanto un modo que solo cambia empates de un modo que cambia todo valor, y una página que prueba solo 1.6 no puede distinguir halfExpand de halfEven en absoluto.

La consecuencia para un registro es que el modo le pertenece. Dos ejecuciones de la misma app pueden imprimir 2 y 3 para el mismo 2.5 si un formateador se construyó una vez con halfEven y otra con el predeterminado, y la cadena guardada no puede decir qué modo la produjo. Un registro que conserva el roundingMode resuelto junto a cada valor formateado hace la diferencia reproducible en lugar de un misterio que aparece solo en los valores que resultan ser empates.

Cómo el signo y el cero final cambian la cadena

Dos opciones de presentación cambian el ancho y el signo de la salida sin cambiar el valor, y las líneas de signo de la sonda muestran un caso en el que el signo se pierde.

sign:auto -> 5,0,-5 es el predeterminado: sin signo en un positivo ni en un cero, un menos en un negativo. sign:always -> +5,+0,-5 pone un más en el positivo y en el cero, así que un lector puede ver que un número se formateó con signo, y un diff de dos informes puede distinguir un positivo sin cambios de un negativo sin cambios. sign:exceptZero -> +5,0,-5 pone el más en un positivo pero deja el cero desnudo, que es la opción a elegir cuando un cero es un valor real que no debe parecer con signo.

sign:never -> 5,0,5 es la peligrosa. Quita el menos de -5 e imprime 5, así que un consumidor que analiza la cadena de vuelta a número obtiene un valor positivo de una entrada negativa. Una página que muestra un delta con sign:never suponiendo que «el signo se muestra en otro sitio» produce dos cadenas idénticas para +5 y -5, y un análisis posterior no puede recuperar cuál era. sign:negative -> 5,0,-5 es la opción que muestra un menos solo cuando el valor es negativo, que es la intención habitual detrás de recurrir a never, y conserva el signo en el negativo.

La opción de cero final cambia los decimales que solo existen por el mínimo. La sonda imprime trailing:default -> 3.00 3.10 para un formateador con mínimo y máximo de dos dígitos, y trailing:strip -> 3 3.10 tras poner stripIfInteger. Así que un número con valor entero pierde su fracción de ceros mientras que 3.10 conserva su cero final, porque 3.10 no es entero y solo el caso entero se recorta. Una columna de precios que usa el predeterminado muestra 3.00 y 4.00, y la misma columna con la opción de recorte muestra 3 junto a un 3.10 vecino, que es una columna de ancho mixto que una tabla puede alinear mal.

Ambas opciones interactúan con el signo y la parte fraccionaria del valor de formas que la lista de opciones no declara. stripIfInteger sobre -3.00 da -3, así que el menos se conserva; sign:always sobre -3.00 da -3.00, así que el signo y la fracción son independientes. Una página que compone ambas debe decidir qué quiere para -0, y el +0 de la sonda bajo sign:always muestra que el formateador imprimirá un signo en un cero, que es el valor que suele producir una resta de números iguales.

El ancho de la salida no es por tanto una propiedad del número solo. El mismo 5 se muestra como 5, +5, 5.00 o +5.00 según tres opciones, y un diseño que reserva un ancho de columna midiendo una variante recortará o desplazará las otras. Una página que formatea una tabla debe fijar las opciones una vez y usarlas para cada celda, en lugar de dejar que un predeterminado y una opción explícita se encuentren dentro de la misma columna.

Dónde chocan dos límites

Dos opciones pueden limitar los dígitos a la vez, y cuál gana depende de una tercera opción que casi ningún código fija. La sonda combina maximumSignificantDigits: 3 con maximumFractionDigits: 2 y formatea tres valores con cada roundingPriority; la transcripción registra esos resultados en la línea priority ->.

auto y lessPrecision coinciden en todos los valores: 12.345 se vuelve 12.3, 1234.5 se vuelve 1230 y 456.78 se vuelve 457. Ambos respetan el límite de dígitos significativos y tratan el límite de fracción como satisfecho por el resultado, así que gana la regla de tres dígitos y la regla de dos decimales nunca ata. morePrecision es el raro: imprime 12.35, 1234.5 y 456.78, que conserva más dígitos de los que permitiría cualquiera de los dos límites solo, porque redondea para que ambos límites queden satisfechos en lugar de dejar que uno domine.

El caso 1234.5 hace visible la sorpresa. Con auto el número se vuelve 1230, tres dígitos significativos y sin fracción, mientras que con morePrecision sigue siendo 1234.5, cuatro dígitos significativos y una fracción. Una página que fija ambos límites esperando «como mucho 3 dígitos significativos y como mucho 2 decimales» obtiene 1230 con el predeterminado, y una página que fija morePrecision esperando «el más estricto de los dos» obtiene el resultado más laxo. Ninguna lectura de las opciones coincide con el comportamiento, por lo que la salida resuelta pertenece al registro y no la intención detrás de ella.

Un formateador que quiere un límite debe fijar un límite. Fijar ambos y confiar en la prioridad predeterminada produce el comportamiento de dígitos significativos, así que una página que solo quería «dos decimales» debe fijar maximumFractionDigits solo, y una página que solo quería «tres dígitos significativos» debe fijar maximumSignificantDigits solo. La combinación es para el caso en que ambos deben respetarse, y eso es lo que morePrecision declara explícitamente en lugar de dejarlo al predeterminado.

Las líneas de moneda muestran un segundo límite que quien llama nunca fija. currency -> $5.00 ¥5 imprime dos fracciones para un dólar estadounidense y ninguna para un yen japonés, porque los decimales por defecto salen del código de la moneda, no del idioma ni de las opciones. currency:min0 -> $5 muestra que fijar minimumFractionDigits: 0 anula el predeterminado del código, así que el símbolo y los decimales son dos decisiones separadas. Una página que imprime una tabla de precios entre monedas obtiene por tanto un ancho distinto por moneda, y la única forma de saber cuál es leer resolvedOptions() para cada código.

percent -> 13% 13% y compact -> 1.2M 1.2M muestran la misma forma de sorpresa desde otra opción. 0.125 como porcentaje es 12.5%, y el predeterminado lo redondea a 13%, así que una tasa que un lector esperaba como medio punto se vuelve un punto entero. La notación compacta redondea a dígitos significativos en lugar de fracciones, así que 1234567 se vuelve 1.2M tanto con el predeterminado como con un maximumFractionDigits: 1 explícito, porque la notación ya limita los dígitos antes del sufijo.

Las líneas digits y grouping son el recordatorio de que el valor y la cadena son objetos distintos, y esos mismos datos de idioma también deciden cómo se muestra una zona horaria junto a un número formateado. digits -> ar-EG=١٢٣٤ ar-EG-latn=1234 muestra el mismo 1234 con dígitos árabe-índicos en un idioma y dígitos latinos con una extensión Unicode en el otro, y grouping -> de-DE=1.234.567,89 en-US=1,234,567.89 muestra el cambio de separador que viene solo del idioma. Una página que analiza una cadena formateada de vuelta a número tiene que conocer ambos, y una página que guarda solo la cadena ya perdió el número.

resolvedOptions() es el campo que cierra el círculo. La sonda imprime resolved -> min=0 max=0 mode=halfExpand tras fijar solo un máximo, así que el mínimo siguió al máximo hasta cero aunque quien llamó nunca lo pidió. Esa es la regla que una página suele pasar por alto cuando fija un máximo en un formateador de moneda: el mínimo por defecto de dos de la moneda se reemplaza, no se conserva, y la diferencia entre $5.00 y $5 es una sola opción que quien llamó no escribió.

formatToParts() es la otra mitad del registro. La sonda imprime parts -> minusSign:-|currency:USD|literal: |integer:5|decimal:.|fraction:00, así que cada pieza de la salida está nombrada y un consumidor puede reconstruir la cadena o reordenar las piezas conociendo el tipo de cada una. Una página que muestra un precio con la moneda después del número en un idioma y antes en otro puede hacerlo desde las piezas en lugar de desde la cadena entera, que es la forma fiable de manejar un idioma cuyo orden difiere.

Registrar una lectura de formato numérico

Una lectura de formato numérico sostiene una afirmación acotada: este tiempo de ejecución, este idioma, estas opciones, produjeron esta cadena. Cuatro campos la mantienen verificable:

  1. Las opciones resueltas, porque resolvedOptions() nombra el modo de redondeo, el mínimo y el máximo que de verdad corrieron, y la intención de quien llamó no está en la cadena.
  2. El modo de redondeo y los valores de entrada, porque un empate y un no empate pueden diferir en una unidad entera y un registro sin la entrada no puede decir cuál ocurrió.
  3. Las opciones de presentación de signo y fracción, porque sign:never borra un menos y stripIfInteger borra una fracción de ceros, y ninguna se recupera de la salida.
  4. Las piezas, porque el orden y las piezas de la salida de un idioma difieren, y una cadena guardada no puede reanalizarse sin el idioma que la produjo.

Fuentes públicas

Conclusión

Fija el modo de redondeo en lugar de confiar en el predeterminado, porque el predeterminado es half-expand y un valor .5 no redondeará como espera un lector de «redondear el medio al par». Conserva el signo menos con sign:negative en lugar de sign:never, porque never convierte un negativo en una cadena positiva que un analizador no puede recuperar. Elige un límite de dígitos en lugar de dos salvo que ambos deban atar, y lee resolvedOptions() después de construir un formateador de moneda porque un máximo también baja el mínimo. Para los datos de idioma que gobiernan los separadores y las formas de dígitos, lee datos de idioma de la API Intl; para la mitad de zona horaria de la misma familia de formateadores, lee zonas horarias e idioma.

#Intl.NumberFormat#Redondeo#Localización#Moneda#ECMAScript

Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?

Lleva BotBrowser de la investigación a producción

Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.