diff --git a/core/lib/Drupal/Component/Utility/Number.php b/core/lib/Drupal/Component/Utility/Number.php index fd5207431c..87f71dab2d 100644 --- a/core/lib/Drupal/Component/Utility/Number.php +++ b/core/lib/Drupal/Component/Utility/Number.php @@ -9,6 +9,17 @@ */ class Number { + /** + * The minimum guaranteed precise number of floating point decimals. + * + * PHP's floating point implementation follows IEEE 754 doubles, which have + * 53-bit significands. For a significand with N bits, floor((N-1) * log10(2)) + * gives the minimum number of significant decimals (Kahan, 1997, retrieved + * from https://people.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF). + * For IEEE 754 doubles (PHP floats), this is floor((53-1) * log10(2)) = 15. + */ + const IEEE_754_GUARANTEED_PRECISION = 15; + /** * Gets a number's precision. * @@ -20,7 +31,7 @@ class Number { public static function getPrecision($number) { // Convert non-strings to strings, for consistent and lossless processing. if (is_float($number)) { - $number = rtrim(number_format($number, 13, '.', ''), '0'); + $number = rtrim(number_format($number, self::IEEE_754_GUARANTEED_PRECISION, '.', ''), '0'); } elseif (is_int($number)) { $number = (string) $number; @@ -66,7 +77,7 @@ public static function getPrecision($number) { * @see http://opensource.apple.com/source/WebCore/WebCore-1298/html/NumberInputType.cpp */ public static function validStep($value, $step, $offset = 0.0) { - $double_value = (double) abs($value - $offset); + $float_value = (float) abs($value - $offset); // Desired precision of comparison is one order of magnitude greater than // the precision of the step. @@ -74,12 +85,12 @@ public static function validStep($value, $step, $offset = 0.0) { // If the value is of higher precision than desired it isn't divisible by // step. - $value_precision = static::getPrecision((string) $double_value); + $value_precision = static::getPrecision((string) $float_value); if ($value_precision > $desired_precision) { return FALSE; } - $double_value = (double) round($double_value, $desired_precision); + $float_value = (float) round($float_value, $desired_precision); // The fractional part of a double has 53 bits. The greatest number that // could be represented with that is 2^53. If the given value is even bigger @@ -87,19 +98,19 @@ public static function validStep($value, $step, $offset = 0.0) { // remainder. Since that remainder can't even be represented with a single // precision float the following computation of the remainder makes no sense // and we can safely ignore it instead. - if ($double_value / pow(2.0, 53) > $step) { + if ($float_value / pow(2.0, 53) > $step) { return TRUE; } - $expected = (double) round($step * round($double_value / $step), $desired_precision); + $expected_float_value = (float) round($step * round($float_value / $step), $desired_precision); // Now compute that remainder of a division by $step. - $remainder = (double) abs($double_value - $expected); + $remainder = (float) abs($float_value - $expected_float_value); // $remainder is a double precision floating point number. Remainders that // can't be represented with single precision floats are acceptable. The // fractional part of a float has 24 bits. That means remainders smaller than // $step * 2^-24 are acceptable. - $computed_acceptable_error = (double) ($step / pow(2.0, 24)); + $computed_acceptable_error = (float) ($step / pow(2.0, 24)); return $computed_acceptable_error >= $remainder || $remainder >= ($step - $computed_acceptable_error); } diff --git a/core/tests/Drupal/Tests/Component/Utility/NumberTest.php b/core/tests/Drupal/Tests/Component/Utility/NumberTest.php index 5af129b68e..1cc66e0d98 100644 --- a/core/tests/Drupal/Tests/Component/Utility/NumberTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/NumberTest.php @@ -67,15 +67,15 @@ public function provideGetPrecision() { [1, '0.0'], [0, -0.0], [1, '-0.0'], - # The maximum supported precision is 13 decimals. + # The maximum supported precision is 15 decimals. [0, 0.0000000000000], - [13, '0.0000000000000'], + [15, '0.000000000000000'], [0, -0.0000000000000], - [13, '-0.0000000000000'], + [15, '-0.000000000000000'], [9, -0.0000000090000], - [13, '-0.0000000090000'], + [15, '-0.000000009000000'], [9, -0.0000000090000], - [13, '-0.0000000090000'], + [15, '-0.000000009000000'], ]; }