To prevent bias for max, I'm recalculating each time the decimal range falls above max, instead of stripping decimals. Using random() introduces some bias I think, but that is acceptable.
Optional arg decimal is an integer denoting how many decimal places to include.
Optional arg exclude is handy for excluding a specific number (typically 0) from the result.
Optionally returns true or false randomly if all args are omitted.
Basically, I'm just wondering if there's any way to increase the speed and efficiency without adding and more bias (or even reducing bias). It seems the while loops might slow it down. I'm using this for animation, and don't want biased results (especially over small ranges like 1.0 to 2.0).
function random(min, max, decimal, exclude) {
if (min == null) return (Math.random()*2 >= 1) ? true : false
var decimal = (decimal == null) ? 1 : Math.pow(10,decimal), result = exclude
while (result == exclude) {
result = max+1
while (result > max) var result = Math.round((Math.random()*(max-min+1)+min)*decimal)/decimal
}
return result
}
Here's a bit cleaner implementation:
function random(min, max, decimal, exclude) {
// if no min and max is passed, return true or false
if (arguments.length < 2) return(Math.random() >= 0.5);
// calc decimal multiplier
var factor = 1, result;
if (typeof decimal === "number") {
factor = Math.pow(10, decimal);
}
// loop until we get a value that isn't our exclude value
do {
// calc rand value in proper range
result = Math.random() * (max - min) + min;
// adjust to proper number of decimal digits
result = Math.round(result * factor) / factor;
} while (result === exclude);
return result;
}
Working demo with decimals: http://jsfiddle.net/jfriend00/SjgaW/
Working demo as integers (with exclude value): http://jsfiddle.net/jfriend00/GgkJv/
Changes:
Change decimal local variable to not be same name as argument so the argument will work
Remove one while loop by using proper range scaling
Change to do/while so condition isn't tested until after the calculation
Use === to avoid type conversions
Remove multiplication and ternary operator from true/false calculation since the comparison already returns true/false
More explicit checking of arguments that doesn't require them to be passed or rely on type conversions to null
Add relevant comments
The distribution is uniform, so instead of repeating if the number falls outside your range, you can scale/move the pseudorandom number.
var rand = Math.random() * (max - min) + min;
That removes one of your while loops, and should speed up the code.
Related
Here is a sample function:
function step(x, min, max) {
return x >= min && x <= max ? x : 0;
}
console.log(step(-3 - Number.EPSILON, -3, 5)); // Expected 0, actual -3
console.log(step(5 + Number.EPSILON, -3, 5)); // Expected 0, actual 5
I need to check, that it returns zero for values outside [min, max] interval. For sure I can subtract/add a bigger number, for example 1. But I'm pretty sure, that there should exist a function returning previous/next floating point number. Could you please suggest the function or how to implement it?
Not all adjacent representable numbers are the same mathematical distance from one another. Floating point arcana isn't my strong suit, but if you want to find the next representable number, I think you need to keep increasing what you add/subtract from it by Number.EPSILON for as long as you keep getting the same number.
The very naive, simplistic approach would look like this (but keep reading):
// DON'T USE THIS
function next(x) {
const ep = x < 0 ? -Number.EPSILON : Number.EPSILON;
let adder = ep;
let result;
do {
result = x + adder;
adder += ep;
} while (result === x);
return result;
}
console.log(`Next for -3: ${next(-3)}`);
console.log(`Next for 5: ${next(5)}`);
(That's assuming direction based on the sign of the number given, which is probably not what you really want, but is easily switched up.)
But, that would take hours (at least) to handle next(Number.MAX_SAFE_INTEGER).
When I posted my caveat on the above originally, I said a better approach would take the magnitude of x into account "...or do bit twiddling (which definitely takes us into floating point arcana land)..." and you pointed to Java's Math.nextAfter operation, so I had to find out what they do. And indeed, it's bit twiddling, and it's wonderfully simple. Here's a re-implementation of the OpenJDK's version from here (the line number in that link will rot):
// A JavaScript implementation of OpenJDK's `Double.nextAfter` method.
function nextAfter(start, direction) {
// These arrays share their underlying memory, letting us use them to do what
// Java's `Double.doubleToRawLongBits` and `Double.longBitsToDouble` do.
const f64 = new Float64Array(1);
const b64 = new BigInt64Array(f64.buffer);
// Comments from https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Math.java:
/*
* The cases:
*
* nextAfter(+infinity, 0) == MAX_VALUE
* nextAfter(+infinity, +infinity) == +infinity
* nextAfter(-infinity, 0) == -MAX_VALUE
* nextAfter(-infinity, -infinity) == -infinity
*
* are naturally handled without any additional testing
*/
/*
* IEEE 754 floating-point numbers are lexicographically
* ordered if treated as signed-magnitude integers.
* Since Java's integers are two's complement,
* incrementing the two's complement representation of a
* logically negative floating-point value *decrements*
* the signed-magnitude representation. Therefore, when
* the integer representation of a floating-point value
* is negative, the adjustment to the representation is in
* the opposite direction from what would initially be expected.
*/
// Branch to descending case first as it is more costly than ascending
// case due to start != 0.0d conditional.
if (start > direction) {
// descending
if (start !== 0) {
f64[0] = start;
const transducer = b64[0];
b64[0] = transducer + (transducer > 0n ? -1n : 1n);
return f64[0];
} else {
// start == 0.0d && direction < 0.0d
return -Number.MIN_VALUE;
}
} else if (start < direction) {
// ascending
// Add +0.0 to get rid of a -0.0 (+0.0 + -0.0 => +0.0)
// then bitwise convert start to integer.
f64[0] = start + 0;
const transducer = b64[0];
b64[0] = transducer + (transducer >= 0n ? 1n : -1n);
return f64[0];
} else if (start == direction) {
return direction;
} else {
// isNaN(start) || isNaN(direction)
return start + direction;
}
}
function test(start, direction) {
const result = nextAfter(start, direction);
console.log(`${start} ${direction > 0 ? "up" : "down"} is ${result}`);
}
test(-3, -Infinity);
test(5, Infinity);
test(Number.MAX_SAFE_INTEGER, Infinity);
test(Number.MAX_SAFE_INTEGER + 2, Infinity);
I need numbers to have only 2 decimals (as in money), and I was using this:
Number(parseFloat(Math.trunc(amount_to_truncate * 100) / 100));
But I can no longer support the Math library.
How can I achieve this without the Math library AND withou rounding the decimals?
You can use toFixed
Number(amount_to_truncate.toFixed(2))
If you are sure that your input always will be lower or equal than 21474836.47 ((2^31 - 1) / 100) (32bit) then:
if you need as string (to make sure result will have 2 decimals)
((amount_to_truncate * 100|0)/100).toFixed(2)
Otherwise
((amount_to_truncate * 100|0)/100)
Else: See Nina Schols's answer
console.log((((15.555 * 100)|0)/100)) // will not round: 15.55
console.log((((15 * 100)|0)/100).toFixed(2)) // will not round: 15.55
Make it simple
const trunc = (n, decimalPlaces) => {
const decimals = decimalPlaces ? decimalPlaces : 2;
const asString = n.toString();
const pos = asString.indexOf('.') != -1 ? asString.indexOf('.') + decimals + 1 : asString.length;
return parseFloat(n.toString().substring(0, pos));
};
console.log(trunc(3.14159265359));
console.log(trunc(11.1111111));
console.log(trunc(3));
console.log(trunc(11));
console.log(trunc(3.1));
console.log(trunc(11.1));
console.log(trunc(3.14));
console.log(trunc(11.11));
console.log(trunc(3.141));
console.log(trunc(11.111));
The only thing I see wrong with toFixed is that it rounds the precision which OP specifically states they don't want to do. Truncate is more equivalent to floor for positive numbers and ceil for negative than round or toFixed. On the MDN page for the Math.trunc there is a polyfill replacement function that would do what OP is expecting.
Math.trunc = Math.trunc || function(x) {
return x - x % 1;
}
If you just used that, then the code wouldn't have to change.
You could use parseInt for a non rounded number.
console.log(parseInt(15.555 * 100, 10) / 100); // 15.55 no rounding
console.log((15.555 * 100 | 0) / 100); // 15.55 no rounding, 32 bit only
console.log((15.555).toFixed(2)); // 15.56 rounding
Try using toFixed:
number.toFixed(2)
Truncate does also a rounding, so your statement: "I need numbers to have only 2 decimals ... without rounding the decimals" seems to me a little bit convoluted and would lead to a long discussion.
Beside this, when dealing with money, the problem isn't Math but how you are using it. I suggest you read the Floating-point cheat sheet for JavaScript - otherwise you will fail even with a simple calculation like 1.40 - 1.00.
The solution to your question is to use a well-tested library for arbitrary-precision decimals like bignumber.js or decimals.js (just as an example).
EDIT:
If you absolutely need a snippet, this is how i did it some time ago:
function round2(d) { return Number(((d+'e'+2)|0)+'e-'+2); }
You could parseInt to truncate, then divide by 100 and parseFloat.
var num = 123.4567;
num=parseInt(num*100);
num=parseFloat(num/100);
alert(num);
See fiddle
Edit: in order to deal with javascript math craziness, you can use .toFixed and an additional digit of multiplication/division:
var num = 123.4567;
num = (num*1000).toFixed();
num = parseInt(num/10);
num = parseFloat(num/100);
alert(num);
Updated fiddle
This was a lot easier than I thought:
const trunc = (number, precision) => {
let index = number.toString().indexOf(".");
let subStr;
// in case of no decimal
if (index === -1) {
subStr = number.toString();
}
// in case of 0 precision
else if (precision === 0) {
subStr = number.toString().substring(0, index);
}
// all else
else {
subStr = number.toString().substring(0, index + 1 + precision);
}
return parseFloat(subStr);
};
let x = trunc(99.12, 1);
console.log("x", x);
You can try this
function trunc(value){
return (!!value && typeof value == "number")? value - value%1 : 0;
}
console.log(trunc(1.4));
console.log(trunc(111.9));
console.log(trunc(0.4));
console.log(trunc("1.4"));
I have a number var x = 2.305185185185195;
x = x.toFixed(5);
x = 2.30519 but I require this without rounding i.e. 2.30518
I read some thread with two decimal places but could not find for five decimal places.
Any help would be appreciated.
You can use an apropriate factor and floor it and return the result of the division.
Basically this solution moves the point to the left with a factor of 10^d and gets an integer of that and divided the value with the former factor to get the right digits.
function getFlooredFixed(v, d) {
return (Math.floor(v * Math.pow(10, d)) / Math.pow(10, d)).toFixed(d);
}
var x = 2.305185185185195;
document.write(getFlooredFixed(x, 5));
If you need only a "part" of a number with a floating point without rounding, you can just "cut" it:
function cutNumber(number, digitsAfterDot) {
const str = `${number}`;
return str.slice(0, str.indexOf('.') + digitsAfterDot + 1);
}
const x = 2.305185185185195;
console.log(cutNumber(x, 5)); // 2.30518
This method is fast (https://jsfiddle.net/93m8akzo/1/) and its execution time doesn't depend on number or digitsAfterDot values.
You can also "play around" with both functions in a given fiddle for a better understanding of what they do.
You can read more about slice() method here - MDN documentation
NOTE This function is only an example, don't use it in production applications.
You should definitely add input values validation and errors handling!
The Math.trunc() function returns the integer part of a number by
removing any fractional digits
So you can multiply the number by 10^n where n is the desired number of precision, truncate the decimal part using Math.trunc(), divide by the same number (10^n) and apply toFixed() to format it (in order to get the form of 2.30 instead of 2.3 for example)
var x = 2.305185185185195;
console.log((Math.trunc(x*100000)/100000).toFixed(5));
I have sorted it out by adding a small amount if the decimal is 5, then rounding as usual:
function(value, decimals) {
var decimals = decimals || 2;
if( isNaN(value) ){ return 0; }
var decimalPart = value.toString().trim().split('.').pop(),
extra = decimalPart.substr(decimals, decimalPart.length - decimals);
if( extra == '5' &&
decimalPart.length > decimals
){
value = parseFloat(value) + (1 / ( Math.pow(10, decimals + 5) ) );
}
return Number( parseFloat( value ).toFixed( decimals ) );
}
Introduction
For some calculations I need to find the smallest possible number I can add/subtract from a specified number without JavaScript getting in trouble with the internal used data type.
Goal
I tried to write a function which is able to return the next nearest number to VALUE in the direction of value DIR.
function nextNearest(value, direction) {
// Special cases for value==0 or value==direction removed
if (direction < value) {
return value - Number.MIN_VALUE;
} else {
return value + Number.MIN_VALUE;
}
}
The problem with this is, that JavaScript uses a 64-bit float type (I think) which has different minimum step sizes depending on its current exponent.
Problem in detail
The problem is the step size depending on its current exponent:
var a = Number.MIN_VALUE;
console.log(a);
// 5e-324
console.log(a + Number.MIN_VALUE);
// 1e-323 (changed, as expected)
var a = Number.MAX_VALUE;
console.log(a);
// 1.7976931348623157e+308
console.log(a - Number.MIN_VALUE);
// 1.7976931348623157e+308 (that's wrong)
console.log(a - Number.MIN_VALUE == a);
// true (which also is wrong)
Summary
So how can I find the smallest possible number I can add/subtract from a value specified in a parameter in any direction? In C++ this would be easily possible by accessing the numbers binary values.
I tried to implement Pointy's suggestion from the comments (using typed arrays). This is loosely adapted from glibc's implementation of nextafter. Should be good enough.
You can actually just increment/decrement the 64-bit integer representation of a double to get the wanted result. A mantissa overflow will overflow to the exponent which happens to be just what you want.
Since JavaScript doesn't provide a Uint64Array I had to implement a manual overflow over two 32-bit integers.
This works on little-endian architectures, but I've left out big-endian since I have no way to test it. If you need this to work on big-endian architectures you'll have to adapt this code.
// Return the next representable double from value towards direction
function nextNearest(value, direction) {
if (typeof value != "number" || typeof direction != "number")
return NaN;
if (isNaN(value) || isNaN(direction))
return NaN;
if (!isFinite(value))
return value;
if (value === direction)
return value;
var buffer = new ArrayBuffer(8);
var f64 = new Float64Array(buffer);
var u32 = new Uint32Array(buffer);
f64[0] = value;
if (value === 0) {
u32[0] = 1;
u32[1] = direction < 0 ? 1 << 31 : 0;
} else if ((value > 0) && (value < direction) || (value < 0) && (value > direction)) {
if (u32[0]++ === 0xFFFFFFFF)
u32[1]++;
} else {
if (u32[0]-- === 0)
u32[1]--;
}
return f64[0];
}
var testCases = [0, 1, -1, 0.1,
-1, 10, 42e42,
0.9999999999999999, 1.0000000000000002,
10.00000762939453, // overflows between dwords
5e-324, -5e-324, // minimum subnormals (around zero)
Number.MAX_VALUE, -Number.MAX_VALUE,
Infinity, -Infinity, NaN];
document.write("<table><tr><th>n</th><th>next</th><th>prev</th></tr>");
testCases.forEach(function(n) {
var next = nextNearest(n, Infinity);
var prev = nextNearest(n, -Infinity);
document.write("<tr><td>" + n + "</td><td>" + next + "</td><td>" + prev + "</td></tr>");
});
document.write("</table>");
Number.MIN_VALUE is the smallest possible representable number, not the smallest possible difference between representable numbers. Because of the way javascript handles floating point numbers the smallest possible difference between representable numbers changes with the size of the number. As the number gets larger the precision gets smaller. Thus there is no one number that will solve your problem. I suggest you either rethink how you're going to solve your problem or choose a subset of numbers to use and not the full range of MAX and MIN values.
for example: 1.7976931348623156e+308 == 1.7976931348623155e+308 //true
Also, by subtracting MIN_VALUE from MAX_VALUE you're trying to get javascript to accurately represent a number with over 600 significant figures. That's a bit too much.
To find the (smallest) increment value of a given float:
For example, useful to set the step attribute of an html input type=number on the fly!
function getIncrement(number) {
function getDecimals(number) {
let d = parseFloat(number).toString().split('.')[1]
if (d) {return d.length}
return 0
}
return (0 + "." + Array(getDecimals(number)-1).fill(0).join("") + "1").toLocaleString('fullwide', {useGrouping:false})
}
// Tests
console.log(
getIncrement(0.00000105),
getIncrement(455.105),
getIncrement(455.050000)
)
// Test
// Create input type number
function createInput(value){
let p = document.createElement("input")
p.type = "number"
p.step = getIncrement(value)
p.min = 0
p.max = 1000
p.value = value
panel.appendChild(p)
}
// Tests
createInput(0.00000105)
createInput(455.105)
createInput(455.050000)
<b>TEST.</b><br>
<div id="panel"></div>
The step attribute is set based on the base value increment.
<hr>
<b>TEST DEFAULT BEHAVIOR. </b><br>
<input type="number" min="0" max="100" value="0.00000105">
<input type="number" step="0.001" min="0" max="100" value="0.00000105">
<input type="number" step="0.000001" min="0" max="100" value="0.00000105">
<br>
This is not usable. The step atttribute needs to be set based on the value it will contains. But this require to know the value in advance and if a new dynamic value has a different decimal place, it become also unusable.
Get only the decimal place.
function getDecimalplace(number) {
let d = parseFloat(number).toString().split('.')[1]
return d.length
}
// Tests
console.log(
getDecimalplace(8.0001) // 4
)
console.log(
getDecimalplace(4.555433) // 6
)
Here's a quick and dirty method using Math.log2() that doesn't require heap allocation and is much faster:
function getNext(num) {
return num + 2 ** (Math.log2(num) - 52);
}
or
function getPrevious(num) {
return num - 2 ** (Math.log2(num) - 52);
}
The disadvantage is that it's not perfect, just very close. It doesn't work well for absurdly small values as a result. (num < 2 ** -1000)
It's possible that there's failure case for a number > 9.332636185032189e-302, but I haven't found one. There are failure cases for numbers smaller than the above, for example:
console.log(getNext(Number.MIN_VALUE) === Number.MIN_VALUE); // true
However, the following works just fine:
let counter = 0;
// 2 ** -1000 === 9.332636185032189e-302
for (let i = -1000; i < 1000; i += 0.00001) {
counter++;
if (getNext(2 ** i) === 2 ** i)
throw 'err';
if (getPrevious(2 ** i) === 2 ** i)
throw 'err';
}
console.log(counter.toLocaleString()); // 200,000,001
But note that "a bunch of test cases work" is not a proof of correctness.
If you don't want the hacky way,
function precision( n ) {
return Math.max( Number.MIN_VALUE, 2 ** Math.floor( Math.log2( n ) ) * Number.EPSILON )
}
works aswell.
This gives you the increment to the next (larger absolute value) floating point number. Stepping to zero always works as the precision increases.
Since Number.EPSILON is the floating point precision at n=1, we can from this calculate all epsilon values by using 2**x and log2() this function will return the smallest safe increment from a number for all floating point numbers.
Only edge case are denormalized numbers (smaller than 2**-1023) where the step will be larger.
What I would like to have is the almost opposite of Number.prototype.toPrecision(), meaning that when i have number, how many decimals does it have? E.g.
(12.3456).getDecimals() // 4
For anyone wondering how to do this faster (without converting to string), here's a solution:
function precision(a) {
var e = 1;
while (Math.round(a * e) / e !== a) e *= 10;
return Math.log(e) / Math.LN10;
}
Edit: a more complete solution with edge cases covered:
function precision(a) {
if (!isFinite(a)) return 0;
var e = 1, p = 0;
while (Math.round(a * e) / e !== a) { e *= 10; p++; }
return p;
}
One possible solution (depends on the application):
var precision = (12.3456 + "").split(".")[1].length;
If by "precision" you mean "decimal places", then that's impossible because floats are binary. They don't have decimal places, and most values that have a small number of decimal places have recurring digits in binary, and when they're translated back to decimal that doesn't necessarily yield the original decimal number.
Any code that works with the "decimal places" of a float is liable to produce unexpected results on some numbers.
There is no native function to determine the number of decimals. What you can do is convert the number to string and then count the offset off the decimal delimiter .:
Number.prototype.getPrecision = function() {
var s = this + "",
d = s.indexOf('.') + 1;
return !d ? 0 : s.length - d;
};
(123).getPrecision() === 0;
(123.0).getPrecision() === 0;
(123.12345).getPrecision() === 5;
(1e3).getPrecision() === 0;
(1e-3).getPrecision() === 3;
But it's in the nature of floats to fool you. 1 may just as well be represented by 0.00000000989 or something. I'm not sure how well the above actually performs in real life applications.
Basing on #blackpla9ue comment and considering numbers exponential format:
function getPrecision (num) {
var numAsStr = num.toFixed(10); //number can be presented in exponential format, avoid it
numAsStr = numAsStr.replace(/0+$/g, '');
var precision = String(numAsStr).replace('.', '').length - num.toFixed().length;
return precision;
}
getPrecision(12.3456); //4
getPrecision(120.30003300000); //6, trailing zeros are truncated
getPrecision(15); //0
getPrecision(120.000)) //0
getPrecision(0.0000005); //7
getPrecision(-0.01)) //2
Try the following
function countDecimalPlaces(number) {
var str = "" + number;
var index = str.indexOf('.');
if (index >= 0) {
return str.length - index - 1;
} else {
return 0;
}
}
Based on #boolean_Type's method of handling exponents, but avoiding the regex:
function getPrecision (value) {
if (!isFinite(value)) { return 0; }
const [int, float = ''] = Number(value).toFixed(12).split('.');
let precision = float.length;
while (float[precision - 1] === '0' && precision >= 0) precision--;
return precision;
}
Here are a couple of examples, one that uses a library (BigNumber.js), and another that doesn't use a library. Assume you want to check that a given input number (inputNumber) has an amount of decimal places that is less than or equal to a maximum amount of decimal places (tokenDecimals).
With BigNumber.js
import BigNumber from 'bignumber.js'; // ES6
// const BigNumber = require('bignumber.js').default; // CommonJS
const tokenDecimals = 18;
const inputNumber = 0.000000000000000001;
// Convert to BigNumber
const inputNumberBn = new BigNumber(inputNumber);
// BigNumber.js API Docs: http://mikemcl.github.io/bignumber.js/#dp
console.log(`Invalid?: ${inputNumberBn.dp() > tokenDecimals}`);
Without BigNumber.js
function getPrecision(numberAsString) {
var n = numberAsString.toString().split('.');
return n.length > 1
? n[1].length
: 0;
}
const tokenDecimals = 18;
const inputNumber = 0.000000000000000001;
// Conversion of number to string returns scientific conversion
// So obtain the decimal places from the scientific notation value
const inputNumberDecimalPlaces = inputNumber.toString().split('-')[1];
// Use `toFixed` to convert the number to a string without it being
// in scientific notation and with the correct number decimal places
const inputNumberAsString = inputNumber.toFixed(inputNumberDecimalPlaces);
// Check if inputNumber is invalid due to having more decimal places
// than the permitted decimal places of the token
console.log(`Invalid?: ${getPrecision(inputNumberAsString) > tokenDecimals}`);
Assuming number is valid.
let number = 0.999;
let noOfPlaces = number.includes(".") //includes or contains
? number.toString().split(".").pop().length
: 0;
5622890.31 ops/s (91.58% slower):
function precision (n) {
return (n.toString().split('.')[1] || '').length
}
precision(1.0123456789)
33004904.53 ops/s (50.58% slower):
function precision (n) {
let e = 1
let p = 0
while(Math.round(n * e) / e !== n) {
e *= 10
p++
}
return p
}
precision(1.0123456789)
62610550.04 ops/s (6.25% slower):
function precision (n) {
let cur = n
let p = 0
while(!Number.isInteger(cur)) {
cur *= 10
p++
}
return p
}
precision(1.0123456789)
66786361.47 ops/s (fastest):
function precision (n) {
let cur = n
let p = 0
while(Math.floor(cur) !== cur) {
cur *= 10
p++
}
return p
}
precision(1.0123456789)
Here is a simple solution
First of all, if you pass a simple float value as 12.1234 then most of the below/above logics may work but if you pass a value as 12.12340, then it may exclude a count of 0. For e.g, if the value is 12.12340 then it may give you a result of 4 instead of 5. As per your problem statement, if you ask javascript to split and count your float value into 2 integers then it won't include trailing 0s of it.
Let's satisfy our requirement here with a trick ;)
In the below function you need to pass a value in string format and it will do your work
function getPrecision(value){
a = value.toString()
console.log('a ->',a)
b = a.split('.')
console.log('b->',b)
return b[1].length
getPrecision('12.12340') // Call a function
For an example, run the below logic
value = '12.12340'
a = value.toString()
b = a.split('.')
console.log('count of trailing decimals->',b[1].length)
That's it! It will give you the exact count for normal float values as well as the float values with trailing 0s!
Thank you!
This answer adds to Mourner's accepted solution by making the function more robust. As noted by many, floating point precision makes such a function unreliable. For example, precision(0.1+0.2) yields 17 rather than 1 (this might be computer specific, but for this example see https://jsfiddle.net/s0v17jby/5/).
IMHO, there are two ways around this: 1. either properly define a decimal type, using e.g. https://github.com/MikeMcl/decimal.js/, or 2. define an acceptable precision level which is both OK for your use case and not a problem for the js Number representation (8 bytes can safely represent a total of 16 digits AFAICT). For the latter workaround, one can write a more robust variant of the proposed function:
const MAX_DECIMAL_PRECISION = 9; /* must be <= 15 */
const maxDecimalPrecisionFloat = 10**MAX_DECIMAL_PRECISION;
function precisionRobust(a) {
if (!isFinite(a)) return 0;
var e = 1, p = 0;
while ( ++p<=MAX_DECIMAL_PRECISION && Math.round( ( Math.round(a * e) / e - a ) * maxDecimalPrecisionFloat ) !== 0) e *= 10;
return p-1;
}
In the above example, the maximum precision of 9 means this accepts up to 6 digits before the decimal point and 9 after (so this would work for numbers less than one million and with a maximum of 9 decimal points). If your use-case numbers are smaller then you can choose to make this precision even greater (but with a maximum of 15). It turns out that, for calculating precision, this function seems to do OK on larger numbers as well (though that would no longer be the case if we were, say, adding two rounded numbers within the precisionRobust function).
Finally, since we now know the maximum useable precision, we can further avoid infinite loops (which I have not been able to replicate but which still seem to cause problems for some).