summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Seiler <cseiler@php.net>2008-12-02 16:27:15 +0000
committerChristian Seiler <cseiler@php.net>2008-12-02 16:27:15 +0000
commit2e32354456357d1d9adffb6dbdd49c281c5ccd58 (patch)
tree78305872c715b1c970b707b30b92d9129005ac14
parent04c528609a1849d19bed744f9f2bfcc0b7eab0ae (diff)
downloadphp-git-2e32354456357d1d9adffb6dbdd49c281c5ccd58.tar.gz
- MFH: Implemented http://wiki.php.net/rfc/rounding
-rw-r--r--ext/standard/basic_functions.c6
-rw-r--r--ext/standard/config.m425
-rw-r--r--ext/standard/math.c211
-rw-r--r--ext/standard/php_math.h17
-rw-r--r--ext/standard/tests/math/round_large_exp.phpt30
-rw-r--r--ext/standard/tests/math/round_modes.phpt38
-rw-r--r--ext/standard/tests/math/round_prerounding.phpt10
7 files changed, 265 insertions, 72 deletions
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
index d578b5cb4d..9742052e2a 100644
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
@@ -1632,6 +1632,7 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_round, 0, 0, 1)
ZEND_ARG_INFO(0, number)
ZEND_ARG_INFO(0, precision)
+ ZEND_ARG_INFO(0, mode)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO(arginfo_sin, 0)
@@ -3571,6 +3572,11 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
REGISTER_DOUBLE_CONSTANT("INF", php_get_inf(), CONST_CS | CONST_PERSISTENT);
REGISTER_DOUBLE_CONSTANT("NAN", php_get_nan(), CONST_CS | CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("PHP_ROUND_HALF_UP", PHP_ROUND_HALF_UP, CONST_CS | CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("PHP_ROUND_HALF_DOWN", PHP_ROUND_HALF_DOWN, CONST_CS | CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("PHP_ROUND_HALF_EVEN", PHP_ROUND_HALF_EVEN, CONST_CS | CONST_PERSISTENT);
+ REGISTER_LONG_CONSTANT("PHP_ROUND_HALF_ODD", PHP_ROUND_HALF_ODD, CONST_CS | CONST_PERSISTENT);
+
#if ENABLE_TEST_CLASS
test_class_startup();
#endif
diff --git a/ext/standard/config.m4 b/ext/standard/config.m4
index 125e7266a3..50bd1dc7af 100644
--- a/ext/standard/config.m4
+++ b/ext/standard/config.m4
@@ -223,31 +223,6 @@ AC_FUNC_FNMATCH
divert(5)dnl
dnl
-dnl round fuzz
-dnl
-AC_MSG_CHECKING([whether rounding works as expected])
-AC_TRY_RUN([
-#include <math.h>
- /* keep this out-of-line to prevent use of gcc inline floor() */
- double somefn(double n) {
- return floor(n*pow(10,2) + 0.5);
- }
- int main() {
- return somefn(0.045)/10.0 != 0.5;
- }
-],[
- PHP_ROUND_FUZZ=0.5
- AC_MSG_RESULT(yes)
-],[
- PHP_ROUND_FUZZ=0.50000000001
- AC_MSG_RESULT(no)
-],[
- PHP_ROUND_FUZZ=0.50000000001
- AC_MSG_RESULT(cross compile)
-])
-AC_DEFINE_UNQUOTED(PHP_ROUND_FUZZ, $PHP_ROUND_FUZZ, [ see #24142 ])
-
-dnl
dnl Check if there is a support means of creating a new process
dnl and defining which handles it receives
dnl
diff --git a/ext/standard/math.c b/ext/standard/math.c
index 47d3dc6f2b..3d76a6ee0d 100644
--- a/ext/standard/math.c
+++ b/ext/standard/math.c
@@ -24,65 +24,181 @@
#include "php.h"
#include "php_math.h"
#include "zend_multiply.h"
+#include "zend_float.h"
#include <math.h>
#include <float.h>
#include <stdlib.h>
+/* {{{ php_intlog10abs
+ Returns floor(log10(fabs(val))), uses fast binary search */
+static inline int php_intlog10abs(double value) {
+ int result;
+ value = fabs(value);
+ if (value < 1e-8 || value > 1e23) {
+ result = (int)floor(log10(value));
+ } else {
+ static const double values[] = {
+ 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1,
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7,
+ 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
+ 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
+ /* Do a binary search with 5 steps */
+ result = 16;
+ if (value < values[result]) {
+ result -= 8;
+ } else {
+ result += 8;
+ }
+ if (value < values[result]) {
+ result -= 4;
+ } else {
+ result += 4;
+ }
+ if (value < values[result]) {
+ result -= 2;
+ } else {
+ result += 2;
+ }
+ if (value < values[result]) {
+ result -= 1;
+ } else {
+ result += 1;
+ }
+ if (value < values[result]) {
+ result -= 1;
+ }
+ result -= 8;
+ }
+ return result;
+}
+/* }}} */
+
+/* {{{ php_intpow10
+ Returns pow(10.0, (double)power), uses fast lookup table for exact powers */
+static inline double php_intpow10(int power) {
+ static const double powers[] = {
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7,
+ 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
+ 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
+
+ /* Not in lookup table */
+ if (power < 0 || power > 22) {
+ return pow(10.0, (double)power);
+ }
+ return powers[power];
+}
+/* }}} */
+
+/* {{{ php_round_helper
+ Actually performs the rounding of a value to integer in a certain mode */
+static inline double php_round_helper(double value, int mode) {
+ ZEND_FLOAT_DECLARE
+ double tmp_value;
+
+ ZEND_FLOAT_ENSURE();
+ if (value >= 0.0) {
+ tmp_value = floor(value + 0.5);
+ if ((mode == PHP_ROUND_HALF_DOWN && value == (-0.5 + tmp_value)) ||
+ (mode == PHP_ROUND_HALF_EVEN && value == (0.5 + 2 * floor(tmp_value/2.0))) ||
+ (mode == PHP_ROUND_HALF_ODD && value == (0.5 + 2 * floor(tmp_value/2.0) - 1.0)))
+ {
+ tmp_value = tmp_value - 1.0;
+ }
+ } else {
+ tmp_value = ceil(value - 0.5);
+ if ((mode == PHP_ROUND_HALF_DOWN && value == (0.5 + tmp_value)) ||
+ (mode == PHP_ROUND_HALF_EVEN && value == (-0.5 + 2 * ceil(tmp_value/2.0))) ||
+ (mode == PHP_ROUND_HALF_ODD && value == (-0.5 + 2 * ceil(tmp_value/2.0) + 1.0)))
+ {
+ tmp_value = tmp_value + 1.0;
+ }
+ }
+
+ ZEND_FLOAT_RETURN(tmp_value);
+}
+/* }}} */
+
+/* {{{ _php_math_round */
/*
- * Pertains to some of the code found in the php_round() function
- * Ref: http://www.freebsd.org/cgi/query-pr.cgi?pr=59797
- *
- * Copyright (c) 2003, Steven G. Kargl
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- * notice unmodified, this list of conditions, and the following
- * disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * Rounds a number to a certain number of decimal places in a certain rounding
+ * mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding
*/
-static double php_round(double val, int places) {
- double t;
- double f = pow(10.0, (double) places);
- double x = val * f;
+PHPAPI double _php_math_round(double value, int places, int mode) {
+ ZEND_FLOAT_DECLARE
+ double f1, f2;
+ double tmp_value;
+ int precision_places;
+
+ ZEND_FLOAT_ENSURE();
+
+ precision_places = 14 - php_intlog10abs(value);
- if (zend_isinf(x) || zend_isnan(x)) {
- return val;
+ f1 = php_intpow10(abs(places));
+
+ /* If the decimal precision guaranteed by FP arithmetic is higher than
+ the requested places BUT is small enough to make sure a non-zero value
+ is returned, pre-round the result to the precision */
+ if (precision_places > places && precision_places - places < 15) {
+ f2 = php_intpow10(abs(precision_places));
+ if (precision_places >= 0) {
+ tmp_value = value * f2;
+ } else {
+ tmp_value = value / f2;
+ }
+ /* preround the result (tmp_value will always be something * 1e14,
+ thus never larger than 1e15 here) */
+ tmp_value = php_round_helper(tmp_value, mode);
+ /* now correctly move the decimal point */
+ f2 = php_intpow10(abs(places - precision_places));
+ /* because places < precision_places */
+ tmp_value = tmp_value / f2;
+ } else {
+ /* adjust the value */
+ if (places >= 0) {
+ tmp_value = value * f1;
+ } else {
+ tmp_value = value / f1;
+ }
+ /* This value is beyond our precision, so rounding it is pointless */
+ if (fabs(tmp_value) >= 1e15) {
+ ZEND_FLOAT_RETURN(value);
+ }
}
- if (x >= 0.0) {
- t = ceil(x);
- if ((t - x) > 0.50000000001) {
- t -= 1.0;
+ /* round the temp value */
+ tmp_value = php_round_helper(tmp_value, mode);
+
+ /* see if it makes sense to use simple division to round the value */
+ if (abs(places) < 23) {
+ if (places > 0) {
+ tmp_value = tmp_value / f1;
+ } else {
+ tmp_value = tmp_value * f1;
}
} else {
- t = ceil(-x);
- if ((t + x) > 0.50000000001) {
- t -= 1.0;
+ /* Simple division can't be used since that will cause wrong results.
+ Instead, the number is converted to a string and back again using
+ strtod(). strtod() will return the nearest possible FP value for
+ that string. */
+
+ /* 40 Bytes should be more than enough for this format string. The
+ float won't be larger than 1e15 anyway. But just in case, use
+ snprintf() and make sure the buffer is zero-terminated */
+ char buf[40];
+ snprintf(buf, 39, "%15fe%d", tmp_value, -places);
+ buf[39] = '\0';
+ tmp_value = zend_strtod(buf, NULL);
+ /* couldn't convert to string and back */
+ if (!zend_finite(tmp_value) || zend_isnan(tmp_value)) {
+ tmp_value = value;
}
- t = -t;
}
- x = t / f;
- return !zend_isnan(x) ? x : t;
+ ZEND_FLOAT_RETURN(tmp_value);
}
+/* }}} */
/* {{{ php_asinh
*/
@@ -210,20 +326,21 @@ PHP_FUNCTION(floor)
}
/* }}} */
-/* {{{ proto float round(float number [, int precision])
+/* {{{ proto float round(float number [, int precision [, int mode]])
Returns the number rounded to specified precision */
PHP_FUNCTION(round)
{
zval **value;
int places = 0;
long precision = 0;
+ long mode = PHP_ROUND_HALF_UP;
double return_val;
- if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z|l", &value, &precision) == FAILURE) {
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z|ll", &value, &precision, &mode) == FAILURE) {
return;
}
- if (ZEND_NUM_ARGS() == 2) {
+ if (ZEND_NUM_ARGS() >= 2) {
places = (int) precision;
}
convert_scalar_to_number_ex(value);
@@ -238,7 +355,7 @@ PHP_FUNCTION(round)
case IS_DOUBLE:
return_val = (Z_TYPE_PP(value) == IS_LONG) ? (double)Z_LVAL_PP(value) : Z_DVAL_PP(value);
- return_val = php_round(return_val, places);
+ return_val = _php_math_round(return_val, places, mode);
RETURN_DOUBLE(return_val);
break;
@@ -975,7 +1092,7 @@ PHPAPI char *_php_math_number_format(double d, int dec, char dec_point, char tho
}
dec = MAX(0, dec);
- d = php_round(d, dec);
+ d = _php_math_round(d, dec, PHP_ROUND_HALF_UP);
tmplen = spprintf(&tmpbuf, 0, "%.*F", dec, d);
diff --git a/ext/standard/php_math.h b/ext/standard/php_math.h
index b3fd245e75..d18bc6e1f9 100644
--- a/ext/standard/php_math.h
+++ b/ext/standard/php_math.h
@@ -152,4 +152,21 @@ PHP_FUNCTION(atanh);
#define M_SQRT3 1.73205080756887729352 /* sqrt(3) */
#endif
+/* Define rounding modes (all are round-to-nearest) */
+#ifndef PHP_ROUND_HALF_UP
+#define PHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */
+#endif
+
+#ifndef PHP_ROUND_HALF_DOWN
+#define PHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */
+#endif
+
+#ifndef PHP_ROUND_HALF_EVEN
+#define PHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */
+#endif
+
+#ifndef PHP_ROUND_HALF_ODD
+#define PHP_ROUND_HALF_ODD 0x04
+#endif
+
#endif /* PHP_MATH_H */
diff --git a/ext/standard/tests/math/round_large_exp.phpt b/ext/standard/tests/math/round_large_exp.phpt
new file mode 100644
index 0000000000..36db605e02
--- /dev/null
+++ b/ext/standard/tests/math/round_large_exp.phpt
@@ -0,0 +1,30 @@
+--TEST--
+round() works correctly for large exponents
+--FILE--
+<?php
+var_dump (2e-22 == round (2e-22, 22, PHP_ROUND_HALF_UP));
+var_dump (1e-22 == round (1e-22, 22, PHP_ROUND_HALF_UP));
+var_dump (2e-23 == round (2e-23, 23, PHP_ROUND_HALF_UP));
+var_dump (1e-23 == round (1e-23, 23, PHP_ROUND_HALF_UP));
+var_dump (2e-24 == round (2e-24, 24, PHP_ROUND_HALF_UP));
+var_dump (1e-24 == round (1e-24, 24, PHP_ROUND_HALF_UP));
+var_dump (2e22 == round (2e22, -22, PHP_ROUND_HALF_UP));
+var_dump (1e22 == round (1e22, -22, PHP_ROUND_HALF_UP));
+var_dump (2e23 == round (2e23, -23, PHP_ROUND_HALF_UP));
+var_dump (1e23 == round (1e23, -23, PHP_ROUND_HALF_UP));
+var_dump (2e24 == round (2e24, -24, PHP_ROUND_HALF_UP));
+var_dump (1e24 == round (1e24, -24, PHP_ROUND_HALF_UP));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/standard/tests/math/round_modes.phpt b/ext/standard/tests/math/round_modes.phpt
new file mode 100644
index 0000000000..03479c4d90
--- /dev/null
+++ b/ext/standard/tests/math/round_modes.phpt
@@ -0,0 +1,38 @@
+--TEST--
+round() with different rounding modes
+--FILE--
+<?php
+var_dump (round (2.5, 0, PHP_ROUND_HALF_UP));
+var_dump (round (2.5, 0, PHP_ROUND_HALF_DOWN));
+var_dump (round (2.5, 0, PHP_ROUND_HALF_EVEN));
+var_dump (round (2.5, 0, PHP_ROUND_HALF_ODD));
+var_dump (round (-2.5, 0, PHP_ROUND_HALF_UP));
+var_dump (round (-2.5, 0, PHP_ROUND_HALF_DOWN));
+var_dump (round (-2.5, 0, PHP_ROUND_HALF_EVEN));
+var_dump (round (-2.5, 0, PHP_ROUND_HALF_ODD));
+var_dump (round (3.5, 0, PHP_ROUND_HALF_UP));
+var_dump (round (3.5, 0, PHP_ROUND_HALF_DOWN));
+var_dump (round (3.5, 0, PHP_ROUND_HALF_EVEN));
+var_dump (round (3.5, 0, PHP_ROUND_HALF_ODD));
+var_dump (round (-3.5, 0, PHP_ROUND_HALF_UP));
+var_dump (round (-3.5, 0, PHP_ROUND_HALF_DOWN));
+var_dump (round (-3.5, 0, PHP_ROUND_HALF_EVEN));
+var_dump (round (-3.5, 0, PHP_ROUND_HALF_ODD));
+?>
+--EXPECT--
+float(3)
+float(2)
+float(2)
+float(3)
+float(-3)
+float(-2)
+float(-2)
+float(-3)
+float(4)
+float(3)
+float(4)
+float(3)
+float(-4)
+float(-3)
+float(-4)
+float(-3)
diff --git a/ext/standard/tests/math/round_prerounding.phpt b/ext/standard/tests/math/round_prerounding.phpt
new file mode 100644
index 0000000000..0419d6c735
--- /dev/null
+++ b/ext/standard/tests/math/round_prerounding.phpt
@@ -0,0 +1,10 @@
+--TEST--
+round() prerounds results to precision
+--INI--
+precision=14
+--FILE--
+<?php
+var_dump (round (0.285, 2, PHP_ROUND_HALF_UP));
+?>
+--EXPECT--
+float(0.29)