Last active
January 31, 2022 19:01
-
-
Save shicks/7a97ec6b3f10212e60a89a7f6d2d097d to your computer and use it in GitHub Desktop.
Universal polyfill for Math.fround, does not depend on Float32Array.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}); |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 checkexp > 0
. I added a new revision and included your test case.