-
-
Save shicks/7a97ec6b3f10212e60a89a7f6d2d097d to your computer and use it in GitHub Desktop.
// NOTE: This implementation is incorrect (it doesn't break ties to even). See comments below for discussion. | |
Math.fround = Math.fround || function(arg) { | |
arg = Number(arg); | |
// Return early for ±0 and NaN. | |
if (!arg) return arg; | |
var sign = arg < 0 ? -1 : 1; | |
if (sign < 0) arg = -arg; | |
// Compute the exponent (8 bits, signed). | |
var exp = Math.floor(Math.log(arg) / Math.LN2); | |
var powexp = Math.pow(2, Math.max(-126, Math.min(exp, 127))); | |
// Handle subnormals: leading digit is zero if exponent bits are all zero. | |
var leading = exp < -127 ? 0 : 1; | |
// Compute 23 bits of mantissa, inverted to round toward zero. | |
var mantissa = Math.round((leading - arg / powexp) * 0x800000); | |
if (exp > 0 && mantissa <= -0x800000) return sign * Infinity; | |
return sign * powexp * (leading - mantissa / 0x800000); | |
}; |
it('should handle terminating numbers', function() { [5/32762] | |
// These cases all have terminating binary representations (i.e. integers | |
// or rationals with powers of two in the denominator). | |
assertPositiveZero(Math.fround(0)); | |
assertNegativeZero(Math.fround(-0)); | |
assertEquals(1, Math.fround(1)); | |
assertEquals(-1, Math.fround(-1)); | |
assertEquals(0.5, Math.fround(0.5)); | |
assertEquals(0.25, Math.fround(0.25)); | |
assertEquals(-0.75, Math.fround(-0.75)); | |
assertEquals(3, Math.fround(3)); | |
assertEquals(-20.375, Math.fround(-20.375)); | |
assertEquals(101.3125, Math.fround(101.3125)); | |
assertEquals(1 << 22, Math.fround(1 << 22)); | |
}); | |
it('should handle large numbers', function() { | |
assertEquals(1 << 30, Math.fround(1 << 30)); | |
assertEquals(2 ** 127, Math.fround(2 ** 127)); | |
assertEquals(-(2 ** 127), Math.fround(-(2 ** 127))); | |
assertEquals(1.875 * (2 ** 127), Math.fround(1.875 * (2 ** 127))); | |
assertEquals(-1.9375 * (2 ** 127), Math.fround(-1.9375 * (2 ** 127))); | |
assertEquals('a', Infinity, Math.fround(2 ** 128)); | |
assertEquals(-Infinity, Math.fround(-(2 ** 128))); | |
const maxFloat = 3.4028234663852886e38; | |
assertEquals(maxFloat, Math.fround(3.4028235e38)); | |
assertEquals(Infinity, Math.fround(3.4028236e38)); | |
assertEquals('b', Infinity, Math.fround(Infinity)); | |
assertEquals(-maxFloat, Math.fround(-3.4028235e38)); | |
assertEquals(-Infinity, Math.fround(-3.4028236e38)); | |
assertEquals(-Infinity, Math.fround(-Infinity)); | |
}); | |
it('should handle small numbers', function() { | |
// Smallest normal float32 | |
assertEquals(1.015625 * 2 ** -126, Math.fround(1.015625 * 2 ** -126)); | |
assertEquals(-1.015625 * 2 ** -126, Math.fround(-1.015625 * 2 ** -126)); | |
// Subnormal numbers | |
assertEquals(1.015625 * 2 ** -127, Math.fround(1.015625 * 2 ** -127)); | |
assertEquals(1.875 * 2 ** -128, Math.fround(1.875 * 2 ** -128)); | |
// Numbers exactly between two floats round toward zero | |
const minFloat = 2 ** -149; | |
assertEquals(12 * minFloat, Math.fround(12.5 * minFloat)); | |
assertEquals(13 * minFloat, Math.fround(12.5 * (1 + EPSILON) * minFloat)); | |
assertEquals(-12 * minFloat, Math.fround(-12.5 * minFloat)); | |
assertEquals(-13 * minFloat, Math.fround(-12.5 * (1 + EPSILON) * minFloat)); | |
// Smallest non-zero float32 | |
assertEquals(minFloat, Math.fround(minFloat)); | |
assertEquals(-minFloat, Math.fround(-minFloat)); | |
assertEquals(minFloat, Math.fround((1 + EPSILON) / 2 * minFloat)); | |
assertEquals(-minFloat, Math.fround(-(1 + EPSILON) / 2 * minFloat)); | |
assertPositiveZero(Math.fround(minFloat / 2)); | |
assertNegativeZero(Math.fround(-minFloat / 2)); | |
// Edge cases around mantissa === -0x800000 | |
assertEquals(2 ** -125, Math.fround(2 ** -125)); | |
assertEquals(2 ** -124, Math.fround(2 ** -124)); | |
}); | |
it('should handle non-numbers', function() { | |
assertEquals(1, Math.fround(noCheck('1'))); | |
assertExactlyNaN(Math.fround(noCheck([1, 2]))); | |
assertExactlyNaN(Math.fround(noCheck('a'))); | |
assertExactlyNaN(Math.fround(NaN)); | |
}); |
I don't fully understand the difference with the subnormals, nor why it should or shouldn't affect the results - I've long since paged this arcana out of my brain.
Nice find with the infinities. 2 ** -124
is also problematic. It looks like line 14 needs to also check exp > 0
. I added a new revision and included your test case.
This polyfill is the first one that shows up on a Google search, and seems to be the one featured on MDN. Yet, it is incorrect, as it does not break ties to-even when rounding. The following test cases fail:
function assertExactEquals(expected, actual) {
if (!Object.is(expected, actual)) {
throw new Error(`expected ${expected} but got ${actual}`);
}
}
it('some tests that fail', function() {
function test(expected, value) {
assertExactEquals(expected, Math.fround(value))
}
test(3.4000265159298767e-40, 3.400019509437555e-40) // even is upwards
test(1.1754943508222875e-38, 1.1754942807573643e-38) // even is upwards (it's min-normal)
test(3.40000057220459, 3.4000004529953003) // even is upwards
test(Infinity, 3.4028235677973366e38)
});
Here is a correct implementation that rounds-to-nearest and breaks-ties-to-even. It is the implementation used by Scala.js:
Math.fround = Math.fround || function(v) {
v = Number(v);
if (!v) return v;
var isNegative = v < 0;
var av = isNegative ? -v : v;
var absResult;
if (av >= 3.4028235677973366e38) { // also handles the case av === Infinity
absResult = Infinity;
} else if (av >= 1.1754943508222875e-38) { // threshold for normal representations
var e = Math.floor(Math.log(av) / 0.6931471805599453); // Math.LN2
var twoPowE = Math.pow(2, e);
var significand = av / twoPowE;
/* Because of loss of precision in its computation (Math.log is not
* guaranteed to return the best approximation), e might be 1 up or down,
* which causes twoPowE and significand to be a factor 2 up or down.
* We now adjust that so that significand is really in the range [1.0, 2.0).
*/
if (significand < 1) {
twoPowE = twoPowE / 2;
significand = significand * 2;
} else if (significand >= 2) {
twoPowE = twoPowE * 2;
significand = significand / 2;
};
// Round the significand to 23 bits of fractional part, and multiply back by twoPowE
// Break ties to even
// The constant is (min positive double value) / (1.0 / 2**23)
absResult = ((significand * 4.144523e-317) / 4.144523e-317) * twoPowE;
} else {
// Round the value to a multiple of the smallest Float ULP
// Break ties to even
// The constant is (min positive double value) / (min positive float value)
absResult = (av * 3.5257702653609953e-279) / 3.5257702653609953e-279;
};
return isNegative ? -absResult : absResult;
}
and here is the full tests of Scala.js, translated for the format above:
var FloatMin = 1.401298464324817e-45
var FloatMax = 3.4028234663852886e38
function assertExactEquals(expected, actual) {
if (!Object.is(expected, actual))
throw new Error(`expected ${expected} but got ${actual}`);
}
it('tests from Scala.js', function() {
function test(expected, value) {
assertExactEquals(expected, Math.fround(value))
}
// Values based on the limits of Doubles
// Normal forms
test(0.0, 2.2250738585072014e-308) // smallest pos normal form
test(Infinity, 1.7976931348623157e308) // largest pos normal form
test(1.879076684348498e23, 1.8790766677624812e23) // an arbitrary pos normal form
test(-0.0, -2.2250738585072014e-308) // smallest neg normal form
test(-Infinity, -1.7976931348623157e308) // largest neg normal form
test(-1.879076684348498e23, -1.8790766677624812e23) // an arbitrary neg normal form
// Some corner cases of doubleToLongBits
test(9.007199254740992e15, 9007199254740991.0)
test(8.988465623066525e30, 8.988465674311579e+30)
test(5.915260786662124e-27, 5.915260930833876e-27)
test(4.450147769049213e-30, 4.450147717014403e-30)
// Subnormal forms (they all underflow)
test(0.0, 4.9e-324) // smallest pos subnormal form
test(0.0, 2.225073858507201e-308) // largest pos subnormal form
test(0.0, 1.719471609939382e-308) // an arbitrary pos subnormal form
test(-0.0, -4.9e-324) // smallest neg subnormal form
test(-0.0, -2.225073858507201e-308) // largest neg subnormal form
test(-0.0, -1.719471609939382e-308) // an arbitrary neg subnormal form
// Values based on the limits of Floats
// Around Float.MinPositiveValue.toDouble / 2.0
test(0.0, 7.006492321624084e-46) // just below
test(0.0, 7.006492321624085e-46) // Float.MinPositiveValue.toDouble / 2.0
test(FloatMin, 7.006492321624087e-46) // just above
// Around FloatMin
test(FloatMin, 1.40129e-45)
test(FloatMin, 1.401298464324812e-45)
test(FloatMin, 1.401298464324832e-45)
test(FloatMin, 1.40131e-45)
// Around 3.4e-40f, which is a subnormal value
test(3.39999848996059e-40, 3.39999848996058e-40)
test(3.39999848996059e-40, 3.39999848996059e-40)
test(3.39999848996059e-40, 3.3999984899606e-40)
// Around 3.4000054964529118e-40, which is the midpoint between 3.4e-40f and 3.40001e-40f
test(3.39999848996059e-40, 3.4000054964529114e-40)
test(3.39999848996059e-40, 3.4000054964529118e-40) // even is downwards
test(3.4000125029452334e-40, 3.400005496452912e-40)
// Around 3.400019509437555e-40, which is the midpoint between 3.40001e-40f and 3.40003e-40f
test(3.4000125029452334e-40, 3.4000195094375546e-40)
test(3.4000265159298767e-40, 3.400019509437555e-40) // even is upwards
test(3.4000265159298767e-40, 3.4000195094375554e-40)
// Around 1.1754942807573643e-38, which is the midpoint between max-subnormal and min-normal
test(1.1754942106924411e-38, 1.1754942807573642e-38)
test(1.1754943508222875e-38, 1.1754942807573643e-38) // even is upwards (it's min-normal)
test(1.1754943508222875e-38, 1.1754942807573644e-38)
// Around 2.3509886e-38f, which is the max value with ulp == MinPosValue
test(2.3509885615147286e-38, 2.3509885615147283e-38)
test(2.3509885615147286e-38, 2.3509885615147286e-38)
test(2.3509885615147286e-38, 2.3509885615147283e-38)
// Around 2.3509887e-38f, which is the min value with ulp != MinPosValue
test(2.350988701644575e-38, 2.3509887016445748e-38)
test(2.350988701644575e-38, 2.350988701644575e-38)
test(2.350988701644575e-38, 2.3509887016445755e-38)
// Around 3.400000214576721, which is the midpoint between 3.4f and 3.4000003f (normals)
test(3.4000000953674316, 3.4000002145767207)
test(3.4000000953674316, 3.400000214576721) // even is downwards
test(3.4000003337860107, 3.4000002145767216)
// Around 3.4000004529953003, which is the midpoint between 3.4000003f and 3.4000006f (normals)
test(3.4000003337860107, 3.4000004529953)
test(3.40000057220459, 3.4000004529953003) // even is upwards
test(3.40000057220459, 3.4000004529953007)
// Around 3.4028235677973366e38, which is the midpoint between Float.MaxValue and Infinity
test(FloatMax, 3.4028235677973362e38)
test(Infinity, 3.4028235677973366e38)
test(Infinity, 3.402823567797337e38)
});
it('tests from Scala.js no loss', function() {
function test(value) {
assertExactEquals(value, Math.fround(value))
}
// Specials
test(+0.0)
test(-0.0)
test(Infinity)
test(-Infinity)
test(NaN)
// Other corner cases
test(FloatMin)
test(-FloatMin)
test(1.1754942106924411e-38) // max subnormal value
test(-1.1754942106924411e-38)
test(1.1754943508222875e-38) // min normal value
test(-1.1754943508222875e-38)
test(2.3509885615147286e-38) // max value with ulp == MinPosValue
test(-2.3509885615147286e-38)
test(2.350988701644575e-38) // min value with ulp != MinPosValue
test(-2.350988701644575e-38)
test(FloatMax)
test(-FloatMax)
// Some normal values
test(3.4000000953674316)
test(-3.4000000953674316)
test(3.423000153928881e36)
test(-3.423000153928881e36)
// Some subnormal values
test(3.39999848996059e-40)
test(-3.39999848996059e-40)
test(3.4191682529525537e-43)
test(-3.4191682529525537e-43)
});
/cc @shicks
EDIT(sdh): avoided braceless wrapped if
s (see #gotofail vulnerability)
Thanks for finding that - I added a comment to the top of the gist to hopefully direct anyone to your comment.
I couldn't think of any value where it would change the result, but the subnormal handling seems conceptually wrong to me.
I believe if we take 5.877473E-39, it should be handled as subnormal, with the binary mantissa 0x000001.
If we log (leading - arg / powexp) to give us the binary mantissa, (exp<=-127) handles it as subnormal with a binary mantissa of -400001, while (exp<-127) handles it as normalized with a binary mantissa of 0x3fffff.
Also, Math.fround(Math.pow(2,-125)) returns Infinity, while both Firefox and Chrome native Math.fround return 2.350988701644575e-38. Same with -Math.pow(2,-125), returning -Infinity instead of -2.350988701644575e-38.