Métodos para aplicar a números reales

Figura
Figura. Generador de formato local del método toLocaleString()

Dedicaremos este tema a ver métodos que aplican espcialmente a números reales, mejor dicho, a números decimales con parte fraccionaria. Uno de ellos es Number.parseFloat() que convierte un string en un number. El método num.toExponential() nos permitirán poner un número en formato exponencial mientras que num.toFixed() y num.toPrecision() sirven para ajustar el número de dígitos fraccionarios y significativos respectivamente.

También hablaremos de los métodos heredados de Object que son num.toString() y num.toLocaleString() que tienen un especial comportamiento con los números. Con el último haremos un generador de formato para presentar números con formato local de moneda o porcentajes.

No comentaremos sobre el método num.valueOf(), remitiéndole al tema sobre ese método en objetos {}.valueOf(). En ese tema hacíamos un ejemplo para implementar un tipo de números romanos.

El método genérico Number.parseFloat()

El método window.parseFloat() está disponible desde la primera versión de JavaScript. Como con otros métodos del objeto global windows, podemos omitir la referencia y escribir directamente parseFloat().

En ES6 se incorpora Number.parseFloat() debido que podría no estar disponible si usamos un objeto global que no sea windows. Pero ambos son la misma función:

console.log(window.parseFloat); //function parseFloat(){[native code]}
console.log(parseFloat); //function parseFloat(){[native code]}
console.log(parseFloat === window.parseFloat); // true
console.log(Number.parseFloat); //function parseFloat(){[native code]}
console.log(parseFloat === Number.parseFloat); // true
    

Básicamente parseFloat(str) parsea el string del argumento y devuelve un tipo number. Empieza leyendo caracteres esperando encontrar los que se permiten en un número. Si llega a un caracter que no reconoce como en 24px o que no es correcto como el segundo punto en 1.23.4, devolverá lo encontrado hasta ese momento: 24 y 1.23 respectivamente. Si el primer caracter no se reconoce devolverá un NaN.

console.log(parseFloat("1.234")); // 1.234
console.log(parseFloat("+1.234")); // 1.234
console.log(parseFloat("-1.234")); // -1.234
console.log(parseFloat(".123")); // 0.123
console.log(parseFloat("1.2e34")); // 1.2e+34
console.log(parseFloat("24px"));  // 24
console.log(parseFloat("1,234"));  // 1
console.log(parseFloat("1.23.4"));  // 1.23
console.log(parseFloat("a789"));  // NaN
    

El método espera un string en el argumento. Si no lo es tratará de convertirlo usando el método toString() del tipo que tenga el argumento. Si pasamos un número aplicara num.toString() obteniendo la representación como cadena del número. Esto puede dar lugar a un diferente comportamiento como se observa en este ejemplo, donde el número 1.23e4 es convertido al string "12300" y luego se aplica el parseado resultando en 12300. Si en cambio pasamos un string del formato exponencial nos devolverá el número en ese formato:

console.log(parseFloat("1.2e34")); // 1.2e+34
console.log(1.23e4 .toString()); // "12300"
console.log(parseFloat(1.23e4)); // 12300
    

Si le pasamos un Array también lo parseará sin causar ningún error. Si el Array tiene un primer elemento que puede ser convertido a número lo hará sin más, pues la presentacion a string de un Array es una lista de elementos separados por comas:

let arr = [1.5, -2.7, 0.3];
console.log(arr.toString()); // 1.5,-2.7,0.3
console.log(parseFloat(arr)); // 1.5
    

El método del prototipo num.toFixed()

El método num.toFixed(digitos) nos devuelve un string con la representación del número en coma fija usando el número de dígitos fraccionarios especificados en el argumento. Si no se especifica se toman cero dígitos. El máximo es 20 dígitos. Cuando hay menos dígitos que los especificados se completa con ceros. Con más dígitos que los especificados se redondea al más cercano.

//Completa con ceros
let num = (123).toFixed(2);
console.log(num); // "123.00"
//El resultado es un string
console.log(typeof num); // string
//Redondea abajo
console.log((43.21499999).toFixed(2)); // "43.21"
//Redondea arriba
console.log((43.215).toFixed(2)); // "43.22"
//Las notaciones exponenciales se convierten a coma fija
console.log((4.32578e2).toFixed(2)); //"432.58"
    

Aunque se pueden producir redondeos es importante recalcar que toFixed() no aplica la regla redondeo estándar. En ese tema se comenta como JavaScript aplica el redondeo con este método.

Los métodos toFixed(), toPrecision() y toExponential devuelven un string. Pero hemos de tener en cuenta que si le ponemos un + o - a un string actúa como un operador de precedencia convirtiendo ese string en un number. Ejecuta lo mismo que Number() actuando como conversor de tipos:

//El operador de precedencia convierte un string en un number
let num = +"1234";
console.log(num); // 1234
console.log(typeof num); // number
//El operador de precedencia actúa como el conversor Number()
num = Number("1234");
console.log(num); // 1234
console.log(typeof num); // number
    

En el siguiente ejemplo obtenemos un number y no un string, en caso de que esperemos obtener el mismo tipo que devuelve toFixed():

let x = -1.2345 .toFixed(2); 
console.log(x); // -1.23
console.log(typeof x); // number
    

Lo que está pasando es que primero ejecuta 1.2345 .toFixed(2) obteniendo el string "1.23" y luego aplica el operador de precedencia volviendo a obtener un number -1.23. Para evitarlo es mejor siempre encerrar en paréntesis el número o expresión a la que vamos a aplicar un método:

let x = (-1.2345).toFixed(2);
console.log(x); // "-1.23"
console.log(typeof x); // string
    

En todo caso si lo que buscamos es que toFixed() devuelva un number en lugar de un string, tendría sentido anteponer el operador de precedencia + al resultado para evitar tener que usar Number():

//Con Number()
let num = Number((123.4567).toFixed(2));
console.log(num, typeof num); // 123.46 "number"
//Con operador precedencia
num = +((123.4567).toFixed(2));
console.log(num, typeof num); // 123.46 "number"
num = +((-123.4567).toFixed(2)).toFixed(2);
console.log(res, typeof num);// -123.46 "number"
    

El argumento del método toFixed(digitos) nos permitirá especificar hasta 20 dígitos. En el siguiente ejemplo verá que el número 1/3 se presenta con 16 dígitos fraccionarios, pero sabemos que tiene infinitos dígitos 3. Un toFixed() a 21 dígitos causa error:

let num = 1/3;
console.log(num);  // 0.3333333333333333
console.log(num.toFixed(21)); // RangeError: toFixed() digits 
    //argument must be between 0 and 20(…)
    

Pero a 20 dígitos obtenemos más de 16 dígitos fraccionarios que no son 3 ¿De dónde sale los dígitos finales 1483? ¿Por qué no es 3333?

//¿Por qué si 1/3 es 0.333333... con infinitos dígitos 3  
//el metodo toFixed() devuelve los dígitos finales 1483?
console.log((1/3).toFixed(20)); // 0.33333333333333331483
    

Esto tiene que ver con lo que expusimos en otro tema sobre la precisión. El número 1/3 se representa con el formato IEEE754 siguiente, como puede obtenerse en el convertidor IEEE754 del tema que explica ese formato usando un número como 0.3333333333333333 con 16 o más dígitos fraccionarios 3:

1/3 ≈ 1.0101010101010101010101010101010101010101010101010101 × 2-2 0.3333333333333333

Hay una representación para ese número, pero se observa que sólo podemos precisar 16 dígitos decimales en el resultado, pero hay más dígitos ¿cuáles son?. Calcularemos como hicimos en aquel apartado pasando el fraccionario a un entero, obteniendo el valor con el convertidor binario-decimal:

1/3 ≈ 10101010101010101010101010101010101010101010101010101 × 2-52 × 2-2 = 6004799503160661 × 2-54 = 0.33333333333333331482961625624739

Este último cálculo hemos de hacerlo con una calculadora con mayor precisión que la que puede darnos JavaScript, porque sino obtendríamos otra vez 0.3333333333333333 dado que redondea con 16 decimales por debajo al ser el siguiente dígito menor que cinco. Vemos que los dígitos 17 al 20 son precisamente 1483 tras redondear arriba dado que el siguiente es un 9.

Así que el método num.toFixed(20) nos permite obtener más precisión que la del formato IEEE754. Pero no nos servirá de mucho pues dado que obtenemos un string si lo convertimos a number perdemos ese exceso de precisión.

let str = (1/3).toFixed(20);
console.log(str); // "0.33333333333333331483"
let num = Number(str);
console.log(num); // 0.3333333333333333
    

El método por lo tanto sólo tiene utilidad para presentar visualmente una representación del número. Pero podría resultar desconcertante decir en un texto que genere automáticamente con JavaScript que 1/3 = 0.33333333333333331483, cuando esperaríamos ver dígitos 3 en lugar de esos finales.

El método del prototipo num.toPrecision()

El método num.toPrecision(digitos) nos devuelve un string con la representación del número en coma fija usando el número de dígitos significativos especificados en el argumento. Si no se especifica se toman cero dígitos. El máximo es 21 dígitos. Cuando hay menos dígitos que los especificados se completa con ceros. Con más dígitos que los especificados se redondea al más cercano. La definición es igual que para num.toFixed(digitos) sólo que usando dígitos 21 significativos en lugar de 20 fraccionarios.

//5 dígitos significativos
console.log((123.4567).toPrecision(5)); // "123.46"
//5 dígitos fraccionarios
console.log((123.4567).toFixed(5)); // "123.45670"
    

Si el número tiene más dígitos significativos que los del método perderemos precisión. En algunos casos se devuelve un formato exponencial normalizado. Por otro lado los ceros a la izquierda en la parte fraccionaria no cuentan como significativos. En este ejemplo se observan estos casos:

//Devuelve un formato exponencial
console.log((1234).toPrecision(2)); // "1.2e+3"
//Los ceros a la izquierda no son significativos
console.log((0.0001234).toPrecision(2)); // "0.00012"
    

El número de dígitos significativos puede ser hasta 21. Por encima dará error (aunque un navegador podría permitir valores superiores). Como vimos para toFixed() y luego veremos para toExponential(), el exceso de dígitos 14830 surgen de la representación más cercana del número en el formato IEEE754.

let num = 1/3; console.log(num);  // 0.3333333333333333
console.log(num.toPrecision(21)); // 0.333333333333333314830
console.log(num.toPrecision(22)); // RangeError: toPrecision() 
    //argument must be between 1 and 21(…)
    

Igual que con toFixed() no ejecuta el redondeo estándar. Vea que 1.295 debería ser redondeado a 1.30 y sin embargo tanto toFixed(2) como toPrecision(3) no ejecutan el redondeo esperado:

let num = 1.295;
console.log(num.toFixed(2)); // 1.29
console.log(num.toPrecision(3)); // 1.29
    

El método del prototipo num.toExponential()

El método del prototipo num.toExponential(digitos) devuelve una representación string del número en formato exponencial normalizado, también llamado notación científica, que es de la forma m × 10e, siendo m un número real en el intervalo [1, 10) y e el exponente entero. El argumento especifica el número de dígitos que deseamos en la parte fraccionaria, entre 0 y 20, aunque podrían permitirse valores superiores. Si es menor que los existentes se aplicará con redondeo. Si no se aporta argumento usará tantos dígitos decimales como sea necesario para representar el número:

console.log(123.45678 .toExponential()); // "1.2345678e+2"
let x = 123.45678 .toExponential(4);
console.log(x); // "1.2346e+2"
console.log(typeof x); // string
    

Como ya hemos comentado, hemos de cuidar los operadores de precedencia cuando apliquemos métodos a números, pues en el caso siguiente vemos que no está convirtiendo el número a exponencial:

let num = -123 .toExponential(2);
console.log(num); // -123
console.log(typeof num); // number
    

Lo que está pasando es que primero obtiene el string al aplicar el método toExponential() y luego aplica el operador con lo que vuelve a obtener el mismo número sin formato exponencial:

let str = 123 .toExponential(2); 
console.log(str); // "1.23e+2"
let num = -str;
console.log(num); // -123
console.log(typeof num); // number
    

Se soluciona envolviendo en paréntesis el número con el operador y luego aplicar el método, por lo que es mejor usar siempre paréntesis para aplicar métodos a números literales:

console.log((-123).toExponential(2)); // "-1.23e+2"
    

Si no se especifican número de dígitos fraccionarios, usará los necesarios. En este ejemplo el número resultado de 1/3 tiene infinitos dígitos, usándose 15 dígitos fraccionarios, que junto al de la parte entera hacen los 16 dígitos significativos necesarios para la representación de ese número en el formato IEEE754:

let num = 1/3;
console.log(num.toExponential()); // "3.333333333333333e-1"
console.log(num.toExponential(3)); // "3.333e-1"
    

El número de dígitos debe estar entre 0 y 20. Si el número no tiene más digitos fraccionarios que los que se indican en el argumento se rellenará con ceros. Pero eso es un poco desconcertante porque un número en formato IEEE754 tiene como mucho 17 dígitos en total, incluyendo parte entera y fraccional ¿a qué viene poner 20 dígitos fraccionarios en este formato exponencial? Entre los siguientes casos vemos que si especificamos 21 dígitos fraccionarios nos dará error:

let num = 1.5;
console.log(num.toExponential(20)); // "1.5000 0000 0000 0000 0000e+0"
num = 1/3;
console.log(num.toExponential(20)); // "3.3333 3333 3333 3331 4830e-1" 
console.log(num.toExponential(21)); // RangeError: toExponential() 
    //argument must be between 0 and 20(…)
    

En el ejemplo anterior separamos los dígitos para poder contar los 20 que hay en la parte fraccionaria. El número 1.5 sólo tiene un dígito fraccionario y se rellena el resto con ceros. Para el número 1/3 vemos que hay 16 dígitos 3, pues la representación del formato IEEE754 para ese número es 0.3333 3333 3333 3333, con 16 dígitos fraccionarios y no más. Así que si solicitamos más no obtendremos más dígitos 3, que sería lo esperable, sino los últimos 14830 ¿De dónde salen esos dígitos?.

El resultado tiene que ver con lo que ya expusimos en el apartado del método num.toFixed(), donde el valor con más precisión para 1/3 es 0.33333333333333331482961625624739. El valor que usa JavaScript es redondeado al inferior tomando 16 dígitos fraccionarios. Se observa la aproximacion 14830 cuando corremos la coma 20 espacios como resultado de num.toExponential(20). Observe en el siguiente ejemplo aproximaciones de 1/3 aplicando toExponential() con 15 a 20 dígitos fraccionarios:

let num = 1/3;
console.log(num.toExponential(15)); // 3.333 3333 3333 3333       e-1
console.log(num.toExponential(16)); // 3.333 3333 3333 3333 1     e-1
console.log(num.toExponential(17)); // 3.333 3333 3333 3333 15    e-1
console.log(num.toExponential(18)); // 3.333 3333 3333 3333 148   e-1
console.log(num.toExponential(19)); // 3.333 3333 3333 3333 1483  e-1
console.log(num.toExponential(20)); // 3.333 3333 3333 3333 14830 e-1
    

Utilizamos este método en el convertidor ieee754 pues siempre representará el número en formato exponencial normalizado. Esto nos asegura contar siempre con 21 dígitos significativos, cosa que con el método toFixed() no pasa pues es una representación de coma fija.

let num = 1/7; console.log(num);    // 0.14285714285714285
console.log(num.toExponential(20)); // 1.42857142857142849213e-1
console.log(num.toFixed(20));       // 0.14285714285714284921

num = 1/700; console.log(num);      // 0.0014285714285714286
console.log(num.toExponential(20)); // 1.42857142857142857019e-3
console.log(num.toFixed(20));       // 0.00142857142857142857
    

El objetivo de usarlo en el convertidor IEEE754 es que ese número obtenido con num.toExponential() es el valor de la representación más cercana al número, tema que explicamos en el apartado sobre la precisión del formato IEEE754.

El método del prototipo num.toString()

El método toString() se establece inicialmente en el prototipo de Object, de tal forma que es sobrescrito en otros built-in como el Number que nos ocupa. Su finalidad es devolver una representación string del número.

console.log((123).toString()); // 123
console.log(typeof((123).toString())); // string
    

Con formatos en notación científica hasta un exponente 20 lo representa como un número con 21 dígitos, uno más de la parte entera. A partir de un exponente 21 conserva la notación científica:

console.log((1.5e10).toString()); // 15000000000
console.log((1.5e20).toString()); // 150000000000000000000
console.log((1.5e21).toString()); // 1.5e+21
console.log((1.5e22).toString()); // 1.5e+22
    

Aunque pueda representar 21 dígitos significativos en formato exponencial con un exponente 20, realmente sólo puede representar 17 dígitos significativos, pues en este caso llevará a cabo un redondeo desde el formato IEEE754 más cercano, aproximando el 17º y rellenando con ceros el resto: ···5678901 ⇒ ···5683968 ≈ ···5680000:

let num = 1.23456789012345678901e+20;
//toString() no nos dará más de 17 dígitos significativos
console.log(num.toString());        // 123456789012345680000
//toExponential() podría darnos hasta 20 en la parte fraccionaria, 
//pero aproximado al formato IEEE754 más cercano
console.log(num.toExponential(20)); // 1.23456789012345683968e+20
    

Como se observa en el código anterior, toString() nunca nos dará más de 17 dígitos significativos, pues en el fondo no hay más dígitos en el formato IEEE754. La única forma de obtener más dígitos es usando toExponential() que vimos en el apartado anterior. Aunque realizará una aproximación al IEEE754 más cercano.

Cuando aplicamos el método a un número literal, previamente a devolvernos el string se realiza una conversión del número literal al formato IEEE754. Por ejemplo, con un número con 21 dígitos significativos, el formato IEEE754 sólo aceptará los 17 primeros. El resto de dígitos se desechan, no se redondean tampoco, es como si no existieran:

//Podemos pasar a String un número literal
console.log((1234567890.12345678901).toString()); //1234567890.1234567
//pero está implícito siempre una conversión al formato IEEE754
let num = 1234567890.12345678901;
console.log(num.toString()); // 1234567890.1234567
//El resto de dígitos que superan 17 se deseachan y no se redondan
let num2 = 1234567890.123456799999999999999999999;
console.log(num2.toString()); //1234567890.1234567
console.log(num===num2); // true
    

En algún caso deberíamos evitar usar identificadores de registros con números. Supongamos que tenemos un sistema que registra personas con un identificador que tuviera 20 dígitos y un nombre. Si registraramos el identificador 12345678901234567890 podríamos encontrarnos que ese número sería transformado en 12345678901234567000:

class Persona {
    constructor(id, nombre){
        this.id = id;
        this.nombre = nombre
    }
    ver(){
        return this.id + " " + this.nombre;
    }
}
let user = new Persona(12345678901234567890, "John Doe");
console.log(user.ver()); // 12345678901234567000 John Doe
    

El método num.toString(base) tiene un argumento para especificar la base númerica del número sobre el que se está aplicando. Si no se especifica argumento se tomará la base decimal (10). Las bases permitidas son de 2 a 36. Junto al método parseInt() nos servira para hacer conversiones de bases:

//Convertir decimal a binario
console.log((1234).toString(2));// 10011010010
//Convertir binario a decimal
console.log(parseInt("10011010010", 2)); // 1234
//Convertir decimal a hexadecimal
console.log((1234).toString(16));// 4d2
//Convertir hexadecimal a decimal
console.log(parseInt("4d2", 16)); // 1234
//Convertir hexadecimal a binario (pasando por decimal)
console.log(parseInt("4d2", 16).toString(2)); //10011010010
//Convertir hexadecimal a binario directamente
console.log((0x4d2).toString(2)); //10011010010
    

El método del prototipo num.toLocaleString()

El built-in Intl es un objeto cuya finalidad es servir de API para la internacionalización de JavaScript, lo que permite comparar cadenas, formatear números, fechas y horas de forma sensitiva con cada país o región internacional. Estos son algunos enlaces de documentos de los que he extraido algunas cosas para este apartado:

  1. Mozilla: Intl, ECMAScript Internationalization API
  2. The ECMAScript Internationalization API
  3. BCP47: IANA Language Subtag Registry
  4. Unicode UTS35 - LDML part 3 Numbers
  5. LDML: ldmlBCP47
  6. LDML: supplementalData (numberingSystems)
  7. ISO 4217 currency codes

Meterse de lleno con la Internacionalización requiere algo más que lo que pienso dedicar a este apartado. Sólo me limitaré a aplicar algunas cosas para ver cómo funciona el método toLocaleString() aplicado al formato local de números.

El método toLocaleString([locales [,opciones]]) está disponible en Object para ser sobrescrito en los built-in como este caso de Number. Nos devolverá una representación string del número en un formato local. Tiene dos argumentos opcionales: locales y opciones que iremos desgranando en este apartado.

En primer lugar vemos que si aplicamos toString() a un número nos lo muestra con un punto separador de decimales, sin separar los miles. Si aplicamos toLocaleString() sin argumentos se usará el formato local del sistema. En este ejemplo ejecutado en un sistema en español vemos que usa la coma como separador decimal y el punto como separador de miles. Si usamos el argumento locales con el tag "en" de idioma inglés vemos que lo hace al revés, tal como es usual en ese idioma.

let n = 1234567890.12;
// Sin formato
console.log(n.toString()); // 1234567890.12
// Con formato local
console.log(n.toLocaleString()); // 1.234.567.890,12
// Inglés (en)
console.log(n.toLocaleString("en")); // 1,234,567,890.12
    

El argumento locales tiene una estructura con tres tags iniciales separados por guión. Son el idioma, el modo de escritura y la región. Por ejemplo "zh-Hans-CN" especifica el idioma chino (zh), con modo de escritura "Hans" y región China (CN). Estos tags se encuentran en el documento BCP47: IANA Language Subtag Registry cuyo enlace indiqué antes, encontrándose estos registros:

Type: language
Subtag: zh
Description: Chinese
Added: 2005-10-16
Scope: macrolanguage
....
Type: script
Subtag: Hans
Description: Han (Simplified variant)
Added: 2005-10-16
...
Type: region
Subtag: CN
Description: China
Added: 2005-10-16

Estos tags se utilizan en muchas aplicaciones, no sólo para formatear números. Por ejemplo el atributo de HTML lang utiliza ese mismo registro BCP47 para indicar el idioma de la página. Esta página que está viendo tiene en la cabecera el elemento <html lang="es"> con ese atributo con valor "es" que indica idioma español.

El tipo script o modo de escritura se puede obviar en muchos idiomas, especialmente en todos los basados en Latín. Se observa el campo Suppress-Script: Latn que significa que se omite y se sobreentiende el tipo de escritura "Latn".

Type: language
Subtag: es
Description: Spanish
Description: Castilian
Added: 2005-10-16
Suppress-Script: Latn 
...
Type: script
Subtag: Latn
Description: Latin
Added: 2005-10-16

Así que si queremos forzar a que el número use separaciones como en español ignorando el formato del sistema, tendríamos que usar el tag "es". Así sea cual sea el ordenador que se use, el formato siempre saldrá en español. A no ser que haya alguna región cuyo idioma es español pero no se use ese formato de separar decimales con coma y miles con punto. De hecho México ("MX") utiliza el mismo formato inglés, como se observa en esta ejecución poniendo el tag idioma y región "es-MX". Vea como Chile sin embargo usa el mismo que el general "es":

// Español (es)
console.log(n.toLocaleString("es")); // 1.234.567.890,12
// Español de España (es-ES)
console.log(n.toLocaleString("es-ES")); // 1.234.567.890,12
// Español de México (es-MX)
console.log(n.toLocaleString("es-MX")); // 1,234,567,890.12
// Español de Chile (es-CL)
console.log(n.toLocaleString("es-CL")); // 1.234.567.890,12
// Francés (fr)
console.log(n.toLocaleString("fr")); // 1 234 567 890,12
// Ruso (ru)
console.log(n.toLocaleString("ru")); // 1 234 567 890,12
// Inglés de la India (en-IN)
console.log(n.toLocaleString("en-IN")); // 1,23,45,67,890.12
    

Por lo tanto para forzar el formato español siempre tendríamos que usar "es-ES" o algún otro tag que saque ese mismo formato. Observe en el código anterior como en idioma francés ("fr") o ruso ("ru") los miles se separan con espacios. Mientras en inglés hablado en India ("en-IN") tienen una especial separación para la parte entera propia de ese idioma. A continuación hay unas muestras con otros idiomas que tienen sus propios caracteres numéricos:

// Árabe (ar)
console.log(n.toLocaleString("ar")); // ١٬٢٣٤٬٥٦٧٬٨٩٠٫١٢
// Chino (zh) más Unicode numerales nativo
console.log(n.toLocaleString("zh-u-nu-native")); // 一,二三四,五六七,八九〇.一二
// Hindú (hi) de la India (IN) 
console.log(n.toLocaleString("hi-IN")); // 1,23,45,67,890.12
// Hindú (hi) de la India (IN) más Unicode numerales nativo (u-nu-native)
console.log(n.toLocaleString("hi-IN-u-nu-native")); // १,२३,४५,६७,८९०.१२
// Thai (th) más Unicode numerales nativo (u-nu-native)
console.log(n.toLocaleString("th-u-nu-native")); // ๑,๒๓๔,๕๖๗,๘๙๐.๑๒
// Tamil (ta) más Unicode numerales nativo (u-nu-native)
console.log(n.toLocaleString("ta-u-nu-native")); // ௧,௨௩,௪௫,௬௭,௮௯௦.௧௨
    

Observe que "hi-IN" tiene el mismo formato que "en-IN" que vimos más arriba. Pero además vemos "hi-IN-u-nu-native" que presenta unos caracteres numéricos nativos específicos del idioma hindú de la India. El tag "hi-IN" se complementa con la parte "u-nu-native", donde la "u" es una extensión para referirse a caracteres Unicode, la "nu" referencia caracteres numéricos y "native" hace referencia a recuperar los caracteres nativos del idioma.

El subtag "u-nu-" nos permite también dar formato visual a los números, como los que vemos aquí, claves que he tomado del documento supplementalData (numberingSystems) cuyo enlace indiqué más arriba:

console.log(n.toLocaleString("es")); // 1.234.567.890,12
console.log(n.toLocaleString("es-u-nu-fullwide")); // 1.234.567.890,12
console.log(n.toLocaleString("es-u-nu-mathbold")); // 𝟏.𝟐𝟑𝟒.𝟓𝟔𝟕.𝟖𝟗𝟎,𝟏𝟐
console.log(n.toLocaleString("es-u-nu-mathdbl")); // 𝟙.𝟚𝟛𝟜.𝟝𝟞𝟟.𝟠𝟡𝟘,𝟙𝟚
console.log(n.toLocaleString("es-u-nu-mathsanb")); // 𝟭.𝟮𝟯𝟰.𝟱𝟲𝟳.𝟴𝟵𝟬,𝟭𝟮
console.log(n.toLocaleString("es-u-nu-mathsans")); // 𝟣.𝟤𝟥𝟦.𝟧𝟨𝟩.𝟪𝟫𝟢,𝟣𝟤
    

El segundo argumento de toLocaleString() sirve para agregar opciones de formato. Se trata de un objeto al que le podemos pasar los siguientes parámetros:

  • localeMatcher: Con dos valores, "lookup" y "best fit" que es el valor predeterminado. Definen la forma en que debe llevarse a cabo la búsqueda del formato local a aplicar al número.
  • style: El estilo del formato a aplicar. El valor "decimal" es el predeterminado, siendo "currency" para moneda y "percent" para porcentajes los otros dos valores posibles.
  • currency: Indicando el estilo de moneda, podemos aquí precisar posibles valores de la lista ISO 4217 currency codes, como "EUR" para euro o "USD" para dólares de USA. No hay un valor predeterminado.
  • currencyDisplay: Cadena para representar la moneda como símbolo (valor "symbol" predeterminado), usar el código de la lista ISO 4217 ("code") o usar el nombre de moneda de esa misma lista ("name").
  • useGrouping: Valor booleano true o false para indicar si se separan miles. El valor predeterminado es true para que se separen según como indique el primer argumento de idioma locales.
  • Dígitos enteros y fraccionarios:
    • minimumIntegerDigits: El número mínimo de dígitos enteros a presentar. El valor predeterminado es uno. El mayor valor posible es 21.
    • minimumFractionDigits: El número mínimo de dígitos fraccionarios a presentar. Valores van desde cero hasta 20. El valor predeterminado para moneda lo especifica la lista ISO 4217, o valor 2 si esa lista no especifica nada al respecto. Para números y porcentajes el valor predeterminado es cero.
    • maximumFractionDigits: El número máximo de dígitos fraccionarios a presentar. Valores van desde cero hasta 20. El valor predeterminado es el más grande entre minimumFractionDigits y:
      • el de ISO 4217 para moneda
      • 3 para números
      • 0 para porcentajes
  • Dígitos significativos. Si se especifica alguno de estos, los del grupo anterior serán ignorados.
    • minimumSignificantDigits: El número mínimo de dígitos significativos, valores desde 1 hasta 21, valor predeterminado 1.
    • maximumSignificantDigits: El número máximo de dígitos significativos, valores desde 1 hasta 21, valor predeterminado es minimumSignificantDigits.

En este ejemplo interactivo podrá probar las opciones anteriores:

Ejemplo: Generador de formato local de números

Argumento locale
Unicode
Argumento options
localeMatcher
Estilo
Presentación moneda
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Abril 2020: Cambios en el formato númerico español: nuevo valor 2 para minimumGroupingDigits

Se han producido unos cambios en la ejecución del formato en español que ya se van traladando a la ejecución de JavaScript. En la RAE: uso del punto podemos ver esto:

5.2. Aunque todavía es práctica común en los números escritos con cifras separar los millares, millones, etc., mediante un punto (o una coma, en los países en que se emplea el punto para separar la parte entera de la decimal), la norma internacional establece que se prescinda de él. Para facilitar la lectura de estos números, cuando constan de más de cuatro cifras se recomienda separar estas mediante espacios por grupos de tres, contando de derecha a izquierda: 52 345, 6 462 749. Esta recomendación no debe aplicarse en documentos contables ni en ningún tipo de escrito en que la separación arriesgue la seguridad.

Viene a decir que podemos usar el punto o la coma como separador de decimales. Para la representación de la parte entera debe usarse el espacio, pero cuando el número de digitos de la parte entera sea de cuatro o más. Por ejemplo, 1234.56 ⇒ 1234,56 y 12345.67 ⇒ 12 345,67, de tal forma que si hay cuatro dígitos enteros no hacemos nada con la parte entera y si hay más los separamos con un espacio.

Esta propiedad se llama minimumGroupingDigits y se define como el mínimo número de dígitos a partir del cual se aplica el separador de enteros. Si el separador de enteros es por miles, es decir, separamos cada tres dígitos,entonces la primera inserción del separador de miles se calcula sumando dos a estos tres, es decir, cuando tengamos cinco o más digitos enteros. Dígamos que minimumGroupingDigits toma ahora el nuevo valor 2 cuando antes su valor era 1 y separábamos con 3 + 1 = 4 dígitos enteros.

El nuevo valor 2 fue decidido en una encuesta Unicode para números en español. En la página Unicode numbers expone unos ejemplos usando la coma como separador de miles. Esa propiedad minimumGroupingDigits no es de JavaScript, sino de la estructura de formatos de UNICODE, el organismo que se encarga de estandarizar los formatos de números. Por lo tanto y por ahora no puede modificarse ese comportamiento.

Si ejecutamos un formato de número en español ("es") verá que ahora no aparece el punto separador de miles para 4 dígitos y si a partir de 5. Se observa que no usa el espacio como separador sino el punto. En cambio en alemán ("de") sigue usando el formato que antes teníamos separando desde los 4 dígitos.

(1234.56).toLocaleString("es", {style:"decimal", useGrouping: true})
"1234,56"
(12345.67).toLocaleString("de", {style:"decimal", useGrouping: true})
"12.345,67"
(1234.56).toLocaleString("de", {style:"decimal", useGrouping: true})
"1.234,56"

Veamos primero algunos ejemplos con el estilo moneda. Tras especificar style: "currency" hemos de acompañar un código de moneda, en este caso currency: "EUR" (o "eur" pues es indiferente a mayúsculas). El valor por defecto de currencyDisplay es como símbolo "€". Pero también podemos usar "code" para sacar el propio código "EUR" o incluso "name" para el nombre "euros":

let n = 1234.5678;
let idioma = "es-ES";
let formato = {style: "currency", currency: "eur"};
//El valor predeterminado de currencyDisplay es "symbol"
console.log(n.toLocaleString(idioma, formato)); // 1.234,57 €
formato.currencyDisplay = "code";
console.log(n.toLocaleString(idioma, formato)); // 1.234,57 EUR
formato.currencyDisplay = "name";
console.log(n.toLocaleString(idioma, formato)); // 1.234,57 euros
    

El mismo ejemplo que antes sólo que usando idioma inglés ("en"). Observe las diferencias en la posición del símbolo y código de moneda:

let n = 1234.5678;
let idioma = "en";
let formato = {style: "currency", currency: "eur"};
//El valor predeterminado de currencyDisplay es "symbol"
console.log(n.toLocaleString(idioma, formato)); // €1,234.57
formato.currencyDisplay = "code";
console.log(n.toLocaleString(idioma, formato)); // EUR1,234.57
formato.currencyDisplay = "name";
console.log(n.toLocaleString(idioma, formato)); // 1,234.57 euros
    

Observe como ahora con localeString() se produce un redondeo estándar a dos decimales cuando con los otros métodos no lo hacía:

console.log((1.295).toFixed(2)); // 1.29
console.log((1.295).toExponential(2)); // 1.29e+0
console.log((1.295).toPrecision(3)); // 1.29
console.log((1.295).toLocaleString("es", 
    {style: "currency", currency: "EUR"})); // 1,30 €
    

Toda la información que requiere el formato está en el XML del ISO 4217. Aunque está agrupada por países, se observa que el código ("code") de moneda CCy es un indentificador único, por ejemplo "EUR" para Francia y España. El código numérico CCyNbr es un alias del identificador CCy. El nombre ("name") es CCyNm mientras que el campo CCyMnrUnts describe el número de dígitos fraccionarios.

<CcyNtry>
    <CtryNm>FRANCE</CtryNm>
    <CcyNm>Euro</CcyNm>
    <Ccy>EUR</Ccy>
    <CcyNbr>978</CcyNbr>
    <CcyMnrUnts>2</CcyMnrUnts>
</CcyNtry>
...
<CcyNtry>
    <CtryNm>SPAIN</CtryNm>
    <CcyNm>Euro</CcyNm>
    <Ccy>EUR</Ccy>
    <CcyNbr>978</CcyNbr>
    <CcyMnrUnts>2</CcyMnrUnts>
</CcyNtry>
    

El ajuste del formato es más que interesante. Por ejemplo, decimos 1 euro con el nombre en singular y 2 euros, en plural. Pero también en plural 0.5 euros. La ejecución del método recoge esto:

let formato = {
    style: "currency", 
    currency: "eur", 
    currencyDisplay: "name"
};
//En español escribe correctamente "1 euro" y "2 euros" 
console.log((0.5).toLocaleString("es", formato)); // 0,50 euros
console.log((1).toLocaleString("es", formato)); // 1,00 euro
console.log((2).toLocaleString("es", formato)); // 2,00 euros
//En inglés siempre pondrá "N euros"
console.log((0.5).toLocaleString("en", formato)); // 0,50 euros
console.log((1).toLocaleString("en", formato)); // 1.00 euros
console.log((2).toLocaleString("en", formato)); // 2,00 euros
    

El parámetro useGrouping tiene valor predeterminado true. Se separarán los miles según el idioma elegido. Si lo forzamos a falso vemos que se ignoran los separadores de miles:

let n = 1234.567;
let formato = {style: "currency", currency: "eur"};
//useGrouping tiene valor predeterminado verdadero
console.log(n.toLocaleString("es", formato)); // 1.234,57 euros
formato.useGrouping = false;
console.log(n.toLocaleString("es", formato)); // 1234,57 euros
    

El ajuste por defecto de los dígitos fraccionarios para los tres estilos decimal, moneda y porcentaje se pueden ver en esta ejecución:

let n = 5.6789, formato = {};
//Decimal es el formato por defecto y ajusta a 3 dígitos fraccionarios
formato.style = "decimal";
console.log(n.toLocaleString("es", formato)); // 5,679;
//Moneda ajusta a 2 dígitos fraccionarios del Euro en este caso
formato.style = "currency"; formato.currency = "eur";
console.log(n.toLocaleString("es", formato)); // 5,68 €
//Porcentaje ajusta a 0 dígitos fraccionarios
n = 0.5689;
formato.style = "percent";
console.log(n.toLocaleString("es", formato)); // 57%
    

Podemos cambiar este comportamiento con las propiedades que limitan el mínimo o máximo número de dígitos fraccionarios o enteros. En este código ajustamos que tanto la parte entera como la fraccionaria tengan dos dígitos para un formato de porcentaje. Se observa que si el número tiene menos dígitos enteros o fraccionarios que los especificados, entonces se rellenará con ceros, como con "00,06 %" y "08,73 %" donde se rellena la parte entera y "11,00 %" que rellena la parte decimal.

let ratios = [0.11, 0.0006134, 0.087269, 0.55987];
let formato = {
    style: "percent", 
    minimumFractionDigits: 2,
    minimumIntegerDigits: 2
};
arr = ratios.map(v => v.toLocaleString("es", formato));
console.log(arr); // ["11,00 %", "00,06 %", "08,73 %", "55,99 %"]
    

Ejemplo especificando un número mínimo y máximo de dígitos fraccionarios. En lugar de especificar dígitos enteros o fraccionarios también podemos hacerlo sobre los significativos, observándose que en ese caso se ignoran los límites anteriores:

let formato = {style: "decimal"};
//Número de dígitos fraccionarios
formato.minimumFractionDigits = 3;
console.log((1+1/2).toLocaleString("es", formato));  // 1,500
formato.maximumFractionDigits = 4;
console.log((1+2/3).toLocaleString("es", formato));  // 1,6667
//Especificando dígitos significativos, los límites sobre 
//dígitos fraccionarios serán ignorados
formato.minimumSignificantDigits = 6;
console.log((1+1/2).toLocaleString("es", formato));  // 1,50000
formato.maximumSignificantDigits = 7;
console.log((1+2/3).toLocaleString("es", formato));  // 1,666667
    

En un tema posterior hablaremos sobre cómo realizar los cálculos para una factura usando métodos de redondeo, con la posibilidad de usar toLocaleString() para eso, aunque con algún inconveniente.