27
27
#include <float.h>
28
28
#include <math.h>
29
29
#include <stdlib.h>
30
+ #include <fenv.h>
30
31
31
32
#include "basic_functions.h"
32
33
33
- /* {{{ php_intlog10abs
34
- Returns floor(log10(fabs(val))), uses fast binary search */
35
- static inline int php_intlog10abs (double value ) {
36
- value = fabs (value );
37
-
38
- if (value < 1e-8 || value > 1e22 ) {
39
- return (int )floor (log10 (value ));
40
- } else {
41
- /* Do a binary search with 5 steps */
42
- int result = 15 ;
43
- static const double values [] = {
44
- 1e-8 , 1e-7 , 1e-6 , 1e-5 , 1e-4 , 1e-3 , 1e-2 , 1e-1 , 1e0 , 1e1 , 1e2 ,
45
- 1e3 , 1e4 , 1e5 , 1e6 , 1e7 , 1e8 , 1e9 , 1e10 , 1e11 , 1e12 , 1e13 ,
46
- 1e14 , 1e15 , 1e16 , 1e17 , 1e18 , 1e19 , 1e20 , 1e21 , 1e22 };
47
-
48
- if (value < values [result ]) {
49
- result -= 8 ;
50
- } else {
51
- result += 8 ;
52
- }
53
- if (value < values [result ]) {
54
- result -= 4 ;
55
- } else {
56
- result += 4 ;
57
- }
58
- if (value < values [result ]) {
59
- result -= 2 ;
60
- } else {
61
- result += 2 ;
62
- }
63
- if (value < values [result ]) {
64
- result -= 1 ;
65
- } else {
66
- result += 1 ;
67
- }
68
- if (value < values [result ]) {
69
- result -= 1 ;
70
- }
71
- result -= 8 ;
72
- return result ;
73
- }
74
- }
75
- /* }}} */
76
-
77
34
/* {{{ php_intpow10
78
35
Returns pow(10.0, (double)power), uses fast lookup table for exact powers */
79
36
static inline double php_intpow10 (int power ) {
@@ -90,22 +47,30 @@ static inline double php_intpow10(int power) {
90
47
}
91
48
/* }}} */
92
49
93
- /* {{{ php_round_helper
94
- Actually performs the rounding of a value to integer in a certain mode */
95
- static inline double php_round_helper (double value , int mode ) {
96
- double integral , fractional ;
50
+ static zend_always_inline double php_round_get_basic_edge_case (double integral , double exponent , int places )
51
+ {
52
+ return (places > 0 )
53
+ ? fabs ((integral + copysign (0.5 , integral )) / exponent )
54
+ : fabs ((integral + copysign (0.5 , integral )) * exponent );
55
+ }
97
56
98
- /* Split the input value into the integral and fractional part.
99
- *
100
- * Both parts will have the same sign as the input value. We take
101
- * the absolute value of the fractional part (which will not result
102
- * in branches in the assembly) to make the following cases simpler.
103
- */
104
- fractional = fabs (modf (value , & integral ));
57
+ static zend_always_inline double php_round_get_zero_edge_case (double integral , double exponent , int places )
58
+ {
59
+ return (places > 0 )
60
+ ? fabs ((integral ) / exponent )
61
+ : fabs ((integral ) * exponent );
62
+ }
63
+
64
+ /* {{{ php_round_helper
65
+ Actually performs the rounding of a value to integer in a certain mode */
66
+ static inline double php_round_helper (double integral , double value , double exponent , int places , int mode ) {
67
+ double value_abs = fabs (value );
68
+ double edge_case ;
105
69
106
70
switch (mode ) {
107
71
case PHP_ROUND_HALF_UP :
108
- if (fractional >= 0.5 ) {
72
+ edge_case = php_round_get_basic_edge_case (integral , exponent , places );
73
+ if (value_abs >= edge_case ) {
109
74
/* We must increase the magnitude of the integral part
110
75
* (rounding up / towards infinity). copysign(1.0, integral)
111
76
* will either result in 1.0 or -1.0 depending on the sign
@@ -120,21 +85,24 @@ static inline double php_round_helper(double value, int mode) {
120
85
return integral ;
121
86
122
87
case PHP_ROUND_HALF_DOWN :
123
- if (fractional > 0.5 ) {
88
+ edge_case = php_round_get_basic_edge_case (integral , exponent , places );
89
+ if (value_abs > edge_case ) {
124
90
return integral + copysign (1.0 , integral );
125
91
}
126
92
127
93
return integral ;
128
94
129
95
case PHP_ROUND_CEILING :
130
- if (value > 0.0 && fractional > 0.0 ) {
96
+ edge_case = php_round_get_zero_edge_case (integral , exponent , places );
97
+ if (value > 0.0 && value_abs > edge_case ) {
131
98
return integral + 1.0 ;
132
99
}
133
100
134
101
return integral ;
135
102
136
103
case PHP_ROUND_FLOOR :
137
- if (value < 0.0 && fractional > 0.0 ) {
104
+ edge_case = php_round_get_zero_edge_case (integral , exponent , places );
105
+ if (value < 0.0 && value_abs > edge_case ) {
138
106
return integral - 1.0 ;
139
107
}
140
108
@@ -144,18 +112,18 @@ static inline double php_round_helper(double value, int mode) {
144
112
return integral ;
145
113
146
114
case PHP_ROUND_AWAY_FROM_ZERO :
147
- if (fractional > 0.0 ) {
115
+ edge_case = php_round_get_zero_edge_case (integral , exponent , places );
116
+ if (value_abs > edge_case ) {
148
117
return integral + copysign (1.0 , integral );
149
118
}
150
119
151
120
return integral ;
152
121
153
122
case PHP_ROUND_HALF_EVEN :
154
- if (fractional > 0.5 ) {
123
+ edge_case = php_round_get_basic_edge_case (integral , exponent , places );
124
+ if (value_abs > edge_case ) {
155
125
return integral + copysign (1.0 , integral );
156
- }
157
-
158
- if (UNEXPECTED (fractional == 0.5 )) {
126
+ } else if (UNEXPECTED (value_abs == edge_case )) {
159
127
bool even = !fmod (integral , 2.0 );
160
128
161
129
/* If the integral part is not even we can make it even
@@ -169,11 +137,10 @@ static inline double php_round_helper(double value, int mode) {
169
137
return integral ;
170
138
171
139
case PHP_ROUND_HALF_ODD :
172
- if (fractional > 0.5 ) {
140
+ edge_case = php_round_get_basic_edge_case (integral , exponent , places );
141
+ if (value_abs > edge_case ) {
173
142
return integral + copysign (1.0 , integral );
174
- }
175
-
176
- if (UNEXPECTED (fractional == 0.5 )) {
143
+ } else if (UNEXPECTED (value_abs == edge_case )) {
177
144
bool even = !fmod (integral , 2.0 );
178
145
179
146
if (even ) {
@@ -196,63 +163,55 @@ static inline double php_round_helper(double value, int mode) {
196
163
* mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding
197
164
*/
198
165
PHPAPI double _php_math_round (double value , int places , int mode ) {
199
- double f1 , f2 ;
166
+ double exponent ;
200
167
double tmp_value ;
201
- int precision_places ;
168
+ int cpu_round_mode ;
202
169
203
170
if (!zend_finite (value ) || value == 0.0 ) {
204
171
return value ;
205
172
}
206
173
207
174
places = places < INT_MIN + 1 ? INT_MIN + 1 : places ;
208
- precision_places = 14 - php_intlog10abs (value );
209
175
210
- f1 = php_intpow10 (abs (places ));
176
+ exponent = php_intpow10 (abs (places ));
211
177
212
- /* If the decimal precision guaranteed by FP arithmetic is higher than
213
- the requested places BUT is small enough to make sure a non-zero value
214
- is returned, pre-round the result to the precision */
215
- if (precision_places > places && precision_places - 15 < places ) {
216
- int64_t use_precision = precision_places < INT_MIN + 1 ? INT_MIN + 1 : precision_places ;
217
-
218
- f2 = php_intpow10 (abs ((int )use_precision ));
219
- if (use_precision >= 0 ) {
220
- tmp_value = value * f2 ;
221
- } else {
222
- tmp_value = value / f2 ;
223
- }
224
- /* preround the result (tmp_value will always be something * 1e14,
225
- thus never larger than 1e15 here) */
226
- tmp_value = php_round_helper (tmp_value , mode );
227
-
228
- use_precision = places - precision_places ;
229
- use_precision = use_precision < INT_MIN + 1 ? INT_MIN + 1 : use_precision ;
230
- /* now correctly move the decimal point */
231
- f2 = php_intpow10 (abs ((int )use_precision ));
232
- /* because places < precision_places */
233
- tmp_value = tmp_value / f2 ;
178
+ /**
179
+ * When extracting the integer part, the result may be incorrect as a decimal
180
+ * number due to floating point errors.
181
+ * e.g.
182
+ * 0.285 * 10000000000 => 2849999999.9999995
183
+ * floor(0.285 * 10000000000) => 2849999999
184
+ *
185
+ * Therefore, change the CPU rounding mode to away from 0 only from
186
+ * fegetround to fesetround.
187
+ * e.g.
188
+ * 0.285 * 10000000000 => 2850000000.0
189
+ * floor(0.285 * 10000000000) => 2850000000
190
+ */
191
+ cpu_round_mode = fegetround ();
192
+ if (value >= 0.0 ) {
193
+ fesetround (FE_UPWARD );
194
+ tmp_value = floor (places > 0 ? value * exponent : value / exponent );
234
195
} else {
235
- /* adjust the value */
236
- if (places >= 0 ) {
237
- tmp_value = value * f1 ;
238
- } else {
239
- tmp_value = value / f1 ;
240
- }
241
- /* This value is beyond our precision, so rounding it is pointless */
242
- if (fabs (tmp_value ) >= 1e15 ) {
243
- return value ;
244
- }
196
+ fesetround (FE_DOWNWARD );
197
+ tmp_value = ceil (places > 0 ? value * exponent : value / exponent );
198
+ }
199
+ fesetround (cpu_round_mode );
200
+
201
+ /* This value is beyond our precision, so rounding it is pointless */
202
+ if (fabs (tmp_value ) >= 1e15 ) {
203
+ return value ;
245
204
}
246
205
247
206
/* round the temp value */
248
- tmp_value = php_round_helper (tmp_value , mode );
207
+ tmp_value = php_round_helper (tmp_value , value , exponent , places , mode );
249
208
250
209
/* see if it makes sense to use simple division to round the value */
251
210
if (abs (places ) < 23 ) {
252
211
if (places > 0 ) {
253
- tmp_value = tmp_value / f1 ;
212
+ tmp_value = tmp_value / exponent ;
254
213
} else {
255
- tmp_value = tmp_value * f1 ;
214
+ tmp_value = tmp_value * exponent ;
256
215
}
257
216
} else {
258
217
/* Simple division can't be used since that will cause wrong results.
@@ -272,7 +231,6 @@ PHPAPI double _php_math_round(double value, int places, int mode) {
272
231
tmp_value = value ;
273
232
}
274
233
}
275
-
276
234
return tmp_value ;
277
235
}
278
236
/* }}} */
0 commit comments