Furoya ajusta cuentas con Javascript 4.5.16
Furoya dedica este artículo a repasar las principales operaciones o cálculos matemáticos realizados con javascript. Tanto las las diversas formas de algunas como los problemas o particularidades de otras al realizar desde sumas y restas hasta calcular números primos pasando por redondeos.
Furoya ajusta cuentas con Javascript
Este artículo es de los "atípicos", porque no encaramos un proyecto a resolver sino que vamos a probar varios modos de hacer operaciones matemáticas con javascript.
Pude notar en entregas anteriores que más de una vez se necesitaron cálculos para obtener algún efecto, y si bien eran muy conocidos por todos, al pasar por javascript nos encontramos con operadores distintos a los que usamos en una calculadora de mano o una hoja de papel; por no hablar de las funciones prehechas que generan números u operaciones complejas en un solo paso.
Acá hay una breve colección de lo más común. Evité los métodos y funciones fuera del estándar aprobado y los que no son compatibles aún. Tampoco están los de cálculo de perímetro o superficie o conversión de unidades, que son operaciones combinadas de entre las que sí están en la tabla.
See the Pen Calculations in javascript. by solipsistaCP (@solipsistacp) on CodePen.
Los escripts están inline, para facilitar su ubicación durante la lectura; además hay un tooltip en cada celda de la primera columna con un ejemplo para escribir su JS:
<tr>
<td title="sumando1 + sumando2"> <!-- tooltip -->
<input type="number" name="sumando1" value="2">
+ <!-- operación -->
<input type="number" name="sumando2" value="1">
</td>
<td> <!-- script en línea -->
<input type="button" value="="
onclick="this.form.suma.value = Number(this.form.sumando1.value) +
Number(this.form.sumando2.value)">
</td>
<td> <!-- resultado -->
<input type="text" name="suma" readonly>
</td>
<td>
SUMA
</td>
</tr>
Una incomodidad para probarlos es que usé input
's del tipo number
. Se supone que si ingresamos un valor dentro, JS lo va a interpretar como número; pero no necesariamente es así.
También se supone que si tipeamos desde el teclado numérico se va a interpretar correctamente el símbolo decimal, y tampoco; por alguna razón se saltea la configuración regional para el ingreso de datos, pero no para la validación, por lo que en algunos países hay que salir del numberpad y darle con el pulgar derecho a la coma del teclado alfanumérico, o lo rechazará por el formato no-válido (*). De cualquier modo, no todos los navegadores tienen campos de número; y si se les complica mucho el uso, los cambian por uno de texto.
(*)Hay un modo de forzar a que interprete un formato de número "inglés" pisando la configuración local ... ¡pero es sólo para input type=text
!.
Hay cosas que son increíbles.
Veamos entonces algunos detalles del ejemplo.
- Los botones con el signo "=" contienen el escript que asigna el valor de resultado final a cada celda en la tercera columna. Como toda la tabla está encerrada en un mismo formulario, el modo de ubicar a sus
input
es a través del mismo botón y "subiendo" a su formulario (conthis.form
). Después se vuelve a "bajar" a cada elemento por suname
y se capturan o se asignan valores según el caso. - Las operaciones de suma, resta, multiplicación y división tienen como operadores a
+; -; *; /
respectivamente. Ya comentamos que+
tiene doble (o triple) uso, y si tratamos con texto concatena en vez de sumar. Por eso nos aseguramos que nuestro número sea tratado como tal con algún método como el constructorNumber(número)
, o algo más impresentable comonúmero*1
, ya que cualquier número multiplicado por 1 es siempre igual a sí mismo, y para javascript, si lo multiplicamos tiene que ser número, porque no puede ser otra cosa.
Pero más atrás corregí con un "triple uso" entre paréntesis, porque resulta que el signo de suma, si lo ponemos delante de una variable o una línea para texto, sirve justamente para convertir a número en base 10 (siempre que tenga caracteres de número, claro).
document.getElementById("miInput").value
//es texto+document.getElementById("miInput").value
//es número - Volviendo a las cuentas, en la última división ya vemos un operador extraño. El
%
aquí no saca porcentajes sino módulos. Divide el primer número por el segundo, pero no devuelve el cociente sino el resto, que será distinto de cero cuando no tratemos una división entera. Por una cuestión de prolijidad, quitamos los decimales que hubiese en el resultado de la división conparseInt(entera.decimal)
que devuelve sólo la parte entera y descarta la decimal. - Para hacer potenciación lo más práctico es usar una función de matemáticas :
Math.pow(base, exponente)
, cuyo mecanismo es evidente. Lo mismo con la raíz cuadrada :Math.sqrt(radicando)
, que tiene un sólo valor, porque el "2" está implícito. - Para las demás raíces, no. Aquí debemos echar mano de una operación aritmética ya conocida, pero que no está incorporada como función de
Math
. Por ejemplo, para sacar la raíz cúbica de 27 debemos hacer
Que en JS sería3√27 = 3√271 = 27(1/3)
Math.pow(radicando,(1/indice))
. - Los porcentajes son más conocidos para todos,
porque siempre había que sacar alguno cuando no disponíamos de una calculadora comercial
. Los tanto por mil, también.
10% de 50 →
Las sumas y restas de porcentajes no las repito acá porque son fáciles de entender.(10 / 100) * 50
es 10 · 0.01 · 50
10‰ de 50 →(10 / 1000) * 50
es 10 · 0.001 · 50
razon * .01 * número;
razon * .001 * número; - Los redondeos de número merecen un capítulo solamente para ellos. Tienen demasiadas particularidades.
Empecemos diciendo que
Math.round(entero.decimal)
redondea al entero más próximo, si el decimal es menor a 0.5 va para abajo, si es 0.5 o mayor, va para arriba.
La parte decimal también se pueden redondear, para eso existe.toFixed(máximo_decimales)
, que por supuesto fija una cantidad de decimales para saber dónde redondear. Como adicional, si la cantidad original tiene menos decimales, rellena con ceros. Esto es útil cuando trabajamos con formato de moneda, que usa centavos y ¤1.5 es en realidad ¤1.50
Cuando no queremos dejar a criterio del navegador la dirección de redondeo a entero, aprovechamos dos métodos ya conocidos de otros artículos :Math.ceil(número)
que va al entero superior yMath.floor(número)
que va al entero inferior.
Más abajo vamos a ver lo poco confiables que son estos cálculos; de momento, seguimos con nuestra tabla. - Las funciones trigonométricas son autoexplicativas. El único detalle a mencionar es que no trabajan con ángulos sino con radianes.
- Por eso agregué una calculadora de conversión (la única de la tabla) para pasar de grados angulares a radianes. Y ya que estamos, les presento a
Math.PI()
. - Resisto la tentación de contar aquí algo de los logaritmos neperianos que según parece Napier no usó nunca, así que es preferible llamarlos "naturales"; si para nuestro concepto de "natural" cabe basarse en un número irracional infinito al hacer las cuentas. 8P
En cualquier caso, dejo también varias formas de calcular logaritmos.
Una vez estudiadas estas operaciones vamos a agregar algunas más que quizá sean útiles en algún momento. No lo muestro aquí, pero les dejo un enlace al pen que tiene varios códigos para editar y experimentar con los siguientes ejemplos:
See the Pen Calculations made in javascript. by solipsistaCP (@solipsistacp) on CodePen.
Divisores de cociente entero
Como vimos arriba, existe un operador que devuelve los restos de una división, y si es cero estamos confirmando que tenemos un cociente entero. Recorriendo todos los números entre 1 y el dividendo sabemos cuáles son todos sus divisores exactos o submúltiplos. El escript sería así.
<body>
<form>
<input type="number" oninput="divisible(this)">
es divisible por <input name="divisibilidad">
</form>
<script type="text/javascript">
function divisible(T) {
var numero = Math.abs(T.value);
var enumera = document.forms[0].divisibilidad;
enumera.value = "";
for(i=1; i<numero; i++) {
if(numero%i==0) enumera.value += i+"; ";
}
}
</script>
</body>
Si al dividir por i
el resto es 0 (cero) entonces nuestro número sí es divisible (representado como i|número ) y agregamos ese i
a la lista final separado por un punto y coma, y un espacio.
Números primos
De la misma forma, podemos confirmar si un número es o no primo, ya que los primos sólo son divisibles por 1 y por sí mismos; si en cualquier otro caso la división tiene resto 0, entonces no son primos.
<body>
<form>
<input type="number" oninput="buscaPrimos(this)" min="1">
<input name="primos" size=1> es primo.
</form>
<script type="text/javascript">
function buscaPrimos(T) {
var numero = Math.abs(T.value);
var prueba = document.forms[0].primos;
prueba.value = "SÍ";
for(i=2; i<numero; i++) {
//console.log(numero+" / "+i+" resto "+numero%i);
if(numero%i==0) {prueba.value = "NO"; return; }
}
}
</script>
</body>
Aquí ponemos un "SÍ" como default, ya que lo que comprobamos después es si no es primo, y en ese caso cambiamos el valor a "NO".
El bucle y el condicional merecen algunas explicaciones. La variable incremental empieza por 2, y esto es porque si comenzara por 0 ó 1 el resto siempre va a dar cero y entonces la prueba no nos sirve. Como la condición pide que i
sea menor que el número, si ingresamos un 1 ó un 2 no va a probar nada porque son mayor e igual respectivamente, y el resultado queda como "SÍ"; lo que es correcto, porque da la casualidad que 1 y 2 son números primos. A partir de allí, el resultado se cambia solamente si aparece un resto cero.
Hay un detalle en el if(){}
, y es que termina con return
. En realidad no retorna nada, o mejor dicho, no retorna a ninguna parte. El motivo para usarlo es que resulta un buen mecanismo para detener un bucle o una función. Lo que buscamos es que al aparecer el primer resto cero, ya no siga probando con los demás valores incrementales, con tener al menos un divisor submúltiplo nos alcanza para saber que no es primo, entonces el return
"devuelve lo que hay" y el escript ya no sigue.
Para estos casos es mejor otra palabra reservada como break
, pero aprovecho para mencionar a las dos y los dejo estudiar cada caso. La prueba de que funciona se ve en la consola descomentando su línea.
Ajuste de redondeo
Prometí escribir algo más sobre redondeos, porque hay casos un poco más ajustados que los presentados más arriba. Uno es el redondeo a valores de múltiplos. Y un ejemplo sería subir o bajar a múltiplos enteros de 10.
<body>
<form>
Redondea cada <input name="paso" value="10"> a
<input type="number" oninput="redonPaso(this)" step="1" value="233"> =
<input name="redonEnt">
</form>
<script type="text/javascript">
function redonPaso(T) {
var numero = parseInt(T.value);
var pasoRedondeo = parseInt(document.forms[0].paso.value);
document.forms[0].redonEnt.value = Math.round(numero/pasoRedondeo)*pasoRedondeo;
}
</script>
</body>
Este mecanismo lo vamos a ver seguido. Se usa uno de los métodos de redondeo con nuestro número multiplicado o dividido por algún valor, y luego a su resultado se le hace la operación inversa (dividido o multiplicado, respectivamente) por el mismo valor. Esto crea un ajuste adicional y más preciso que podemos usar también para redondear de a 0.5 hacia arriba o hacia abajo.
<body>
<form>
Redondea abajo <input name="pasoAba" size="2" value=".5"> a
<input type="number" oninput="redonPasoAba(this)" step=".1" value="5.4"> =
<input name="redonAba"><br><br>
Redondea arriba <input name="pasoArr" size="2" value=".5"> a
<input type="number" oninput="redonPasoArr(this)" step=".1" value="5.6"> =
<input name="redonArr">
</form>
<script type="text/javascript">
function redonPasoAba(T) {
var numero = parseFloat(T.value);
var pasoRedondeo = parseFloat(document.forms[0].pasoAba.value);
document.forms[0].redonAba.value = Math.floor(numero/pasoRedondeo)*pasoRedondeo;
}
function redonPasoArr(T) {
var numero = parseFloat(T.value);
var pasoRedondeo = parseFloat(document.forms[0].pasoArr.value);
document.forms[0].redonArr.value = Math.ceil(numero/pasoRedondeo)*pasoRedondeo;
}
</script>
</body>
Y más ajuste de operaciones
Resulta que javascript no es muy exacto para hacer las cuentas. Algunos problemas están en los números de entrada, como por ejemplo que parseInt()
interpretaba una cadena "08" o "09" en base octal, donde no existen los números "08" y "09". Esto tenía que preverse usando el segundo argumento (radix) que es justamente la base declarada
parseInt('08', 10);
y así convertido a base 10 se evitaba el error.
Es una buena práctica para todos los casos, pero ya va siendo un poco innecesaria para la base decimal, porque los navegadores hacen la corrección internamente desde los años 2012 ó 2013.
Para otros casos todavía no tenemos esa suerte y debemos corregir algún que otro pequeño desvío.
Por ejemplo, si dividimos 10.7 / 1.07
el resultado debería ser 10; pero javascript devuelve 9.999999999999998
. Según me explicaron, el problema esta en la conversión de decimal a binario y de vuelta a decimal en números que no son potencia de 2. Porque las cuentas se hacen siempre en binario, que es el lenguaje que entienden las máquinas. Y en las fracciones se pierden.
Les muestro un ejemplo evidente:
<script>
alert( (0.155).toFixed(2) ) //→ 0.15
alert( (1.155).toFixed(2) ) //→ 1.16
</script>
En el primer caso, el resultado debió ser 0.16 . Y una de las formas de corregirlo es:
<script>
alert( (Math.round(0.155*100)/100).toFixed(2) )
</script>
Siendo puntillosos, digamos que en el ejemplo anterior hay información redundante. Usamos el valor 100 justamente para trabajar con 2 decimales; si quisiéramos ajustar a 3, el valor a multiplicar y luego dividir sería 1000. Sabiendo eso, podemos declarar la cantidad de decimales en algún lado y hacer una misma operación para cualquier caso.
<script>
var dec = 3; alert((0.1055).toFixed(3)); //→ 0.105 ¡ERROR! alert((Math.round(0.1055* 1000 )/ 1000 ).toFixed( 3 )); //→ 0.106 alert((Math.round(0.1055*Math.pow(10,dec))/Math.pow(10,dec)).toFixed(dec)); //→ 0.106
</script>
Por supuesto que para el número de ejemplo, si elegimos 5 decimales solamente le va a agregar un cero al final, y el redondeo no tiene mucho sentido ni como número, porque los ceros a la derecha de la parte decimal carecen de valor numérico. Pero no de valor gráfico; lo que nos lleva al siguiente ejemplo.
Relleno con ceros
Existen muchos casos en que necesitamos que el valor de salida tenga un tamaño o formato muy específico. Puede ocurrir con los documentos o números de serie, además de las monedas con centavos que ya mencionamos arriba. En la mayoría se hace un relleno de ceros hasta alcanzar el largo necesario, pero fuera de toFixed()
en las fracciones, no hay una función nativa para los ceros a la izquierda de los enteros. Así que acá tienen una, que seguramente ya vimos al completar hexadecimales para crear colores a partir de un número.
<body>
<form>
Fija <input name="cantLargo" size="2" value="6"> caracteres a
<input oninput="fijaLargo(this)" value="12A"> =
<input name="largoFijado">
</form>
<script type="text/javascript">
function fijaLargo(T) {
var numero = T.value;
var caracteres = document.forms[0].cantLargo.value;
T.setAttribute("maxlength", caracteres);
while(numero.length < caracteres){
numero = "0"+numero;
}
document.forms[0].largoFijado.value = numero;
}
</script>
</body>
Cambio de base numérica
Ya que mencionamos al hexadecimal, hay conversiones que no sé si llamarlas "operaciones". Son las que cambian de base un número, y las más usadas en diseño web son justamente las que van de decimal a hexa y viceversa.
<body>
<form>
<input type="number" step="1" oninput="cambiaBaseDec(this)" value="255"> decimal a base
<input type="number" name="base1" value="16" size="2" min="0" max="32"> =
<input name="nuevaBase1">
<br><br>
<input oninput="cambiaBaseCad(this)" value="FF">
en base <input type="number" name="base2" value="16" size="2" min="0" max="32">
a decimal = <input name="nuevaBase2">
</form>
<script type="text/javascript">
function cambiaBaseDec(T) {
var numero = T.value;
var base = document.forms[0].base1.value;
document.forms[0].nuevaBase1.value = Number(numero).toString(base);
}
function cambiaBaseCad(T) {
var cadena = T.value;
var base = document.forms[0].base2.value;
document.forms[0].nuevaBase2.value = parseInt(cadena,base);
}
</script>
</body>
Los números base 10 pueden ir en un campo de número, pero los otros son vistos como cadena de texto, y van en un campo de texto. De ahí que en el primer caso se tome un número y se lo pase por una función que lo convierte en string, y que acepta un argumento para la base. Si ese argumento no existe, termina como ASCII (o Unicode), como texto común y corriente. En el segundo es justamente al revés, porque se ingresa una cadena y la función lo convierte en un número entero (integer number) con la base como segundo argumento.
Códigos para caracteres Unicode
Ya que mencionamos al Unicode, también hay una función para obtener el valor Unicode decimal de un caracter.
alert("R".charCodeAt()) //CÓDIGO ASCII DECIMAL "R"
alert("QWERTY".charCodeAt(3)) //CÓDIGO ASCII DECIMAL DE 4º CARACTER "R"
O, al revés, convertir en caracteres su código decimal
alert(String.fromCharCode(82)) //CARACTER DE CÓDIGO DECIMAL "82"
alert(String.fromCharCode(81,87,69,82,84,89)) //CARACTERES DE CÓDIGOS "81,87,69,82,84,89"
Las cadenas de texto en javascript pueden tener errores de interpretación si contienen caracteres que estén reservados como operadores o delimitadores. Un caso típico son las comillas, porque si se rodea una string con comillas dobles, ya no se pueden usar como caracter dentro de esa cadena. La forma simple de resolver el conflicto es usar el caracter de escape \
barra invertida
de este modo:
alert("Usando \"comillas dobles\" dentro de una cadena de texto.")
Pero hay también otras maneras, como es usar sus códigos ASCII o Unicode
alert("Usando \x22comillas dobles\x22 dentro de una cadena de texto.")
alert("Usando \u0022comillas dobles\u0022 dentro de una cadena de texto.")
\U0000 → FORMATO UNICODE (HEXA 4 CARACTERES)
\x00 → ASCII (HEXA 2 CARACTERES, SÓLO 00 A FF)
\x22 → "
\x27 → '
Suma y resta de fechas
Otro cálculo que vimos cuando ensayamos almanaques es el de fechas. Como dijimos, se usa de referencia un valor que representa una fecha, que suele ser la actual capturada en el objeto Date()
. Internamente se compone con los valores de año, mes, día, hora, minuto, segundo, milisegundo, y alguno más. Como existen objetos que capturan exclusivamente uno de estos valores, o que los reasignan a nuestro Date
, los cálculos son demasiado fáciles una vez que entendemos la mecánica.
Si tenemos que sumar un día al de hoy y ver el resultado en una cadena, simplemente capturamos la fecha actual, extraemos el día, le sumamos 1 y lo asignamos de nuevo antes de leer el objeto "traducido" a texto.
<script type="text/javascript">
(function(){
var ahora = new Date();
ahora.setHours(ahora.getHours()+1); //SUMA UNA HORA A LA ACTUAL
alert(ahora);
})();
</script>
El ejemplo está puesto en una función anónima autoejecutable por comodidad para hacer las pruebas, y nada más. Por supuesto que podemos sumar o restar donde querramos. Si tenemos un vencimiento dentro de 60 días, para saber la fecha final exacta usamos la siguiente operación
var ahora = new Date();
alert(ahora, ahora.setDate(ahora.getDate()+60)); //SUMA 60 DÍAS AL ACTUAL
Y de paso probamos una manera de integrar funciones a los mensajes modales, ya que la alerta muestra la variable ahora
, modificada por la linea que está después de la coma.
Huelga decir que en el resultado se ajustan automáticamente el día de semana, el mes y el año si fuese el caso. No hay ninguna necesidad de calcularlos uno a uno.
Comprobación de la ausencia del azar en la entropía temporal de Eddington
Cerramos aquí un paseo por las cuentas más usuales a la hora de empezar proyectos sencillos cómo los que estoy exponiendo en este blog.
Para muestra de lo que se puede llegar a hacer, les propongo una situación real: en plantillas prehechas como las que se usan en estos sitios no siempre hay previsto un identificador para los subtítulos. Eso hace un poco complicado buscar una referencia para agregar a un enlace y que termine exactamente en lo que queremos recomendar de un artículo. El autor debe escribirlo en cada uno como un "servicio" al lector y compartidor, y uno de los inconvenientes es crear id
's diferentes en forma automática para ponerlos a mano o usando algún escript.
Entre los métodos de Date
hay uno que cuenta los milisegundos desde las cero horas del 1º de enero de 1970 hasta una fecha que puede ser la actual. De esa forma, es imposible que un número se repita, no importa cuando lo pidamos(**). Si al resultado lo convertimos a base 36 para mezclar letras y números, al fin tenemos un lindo identificador para los subtítulos de nuestros artículos.
prompt( "", " id=\""+( Date.parse( Date() ).toString(36) ).toUpperCase()+"\"" );
O también:
prompt( ""," id=\""+( new Date().setTime(new Date()).toString(36) ).toUpperCase()+"\"" );
O más corto:
prompt( ""," id=\""+( new Date().getTime().toString(36) ).toUpperCase()+"\"" );
O todavía más corto:
prompt( ""," id=\""+( Date.now().toString(36) ).toUpperCase()+"\"" );
Sí. Todos se pueden meter en un bookmarklet y usarlos con los editores online desde el navegador. Da lo mismo cualquiera; ya comentamos que javascript ofrece montones de formas para hacer la misma cosa.
(**) No se repiten siempre que esperemos al menos 15 milisegundos entre llamadas al código cuando estemos usando Windows, por ese clásico retraso que jamás corrigieron.
Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.
Vuelvo por acá para contarles que al fin agregué una operación matemática que en su momento evité publicar en estas colecciones. Es el factorial, que terminó en el pen Cálculos en Javascrpt que pueden ver más arriba.
ResponderEliminarHubo dos motivos para la omisión : en primer lugar, esperaba que alguien lo notara y propusiera su versión, y no ocurrió; la segunda es que claramente debía ir en los cálculos de la primera demo, pero la operación es lo bastante compleja como para ponerla como función en la segunda demo (de hecho, en el tooltip ni aparece el código sino que remite al source). Y esa disyuntiva retrasó la publicación hasta hoy, que me decidí.
Existen dos modos clásicos de resolver un factorial en JS. Como (p.ej.) para encontrar el 4! hay que muliplicar 1·2·3·4 ; o usamos una función recursiva que se llame a sí misma tantas veces como el número que factoreamos y en cada llamada que lo multiplique; o usamos un bucle [code]for()[/code] que haga el mismo trabajo en cada vuelta. Usé el segundo, con una sintaxis poco común, sin cuerpo, sólo para que la estudien.
Y no quise poner un [code]for...of[/code] porque tenía problemas con algunas versiones de Chrome, que no sé si ya resolvieron.