Index: trunk/extensions/ParserFunctions/Expr.php |
— | — | @@ -46,6 +46,9 @@ |
47 | 47 | define( 'EXPR_POW', 35 ); |
48 | 48 | define( 'EXPR_PI', 36 ); |
49 | 49 | |
| 50 | +// Tolerance for comparison and integer conversions |
| 51 | +define( 'EXPR_TOLERANCE', 1e-10 ); |
| 52 | + |
50 | 53 | class ExprError extends Exception { |
51 | 54 | public function __construct($msg, $parameter = ''){ |
52 | 55 | wfLoadExtensionMessages( 'ParserFunctions' ); |
— | — | @@ -153,24 +156,39 @@ |
154 | 157 | ); |
155 | 158 | |
156 | 159 | /** |
157 | | - * Checks if $expr is an integer within one part in 10^10. |
158 | | - * If so, recast as integer and return, else return $expr unchanged. |
159 | | - * |
160 | | - * Explicit checking for round-off errors should eliminate |
161 | | - * many bugs associated with floating point conversion to integers. |
| 160 | + * Tests whether the fractional difference between two numbers |
| 161 | + * is within EXPR_TOLERANCE of each other. |
162 | 162 | */ |
163 | | - function checkInteger( $expr ) { |
164 | | - $intval = (int)$expr; |
165 | | - |
166 | | - if( $expr >= 0 ) { |
167 | | - if( $expr - $intval > 0.9999999999 ) { $intval += 1; } |
| 163 | + function toleranceComparison( $a, $b ) { |
| 164 | + if( $b == 0 || $a == 0 ) { |
| 165 | + if( $a == $b ) { |
| 166 | + return 0; |
| 167 | + } elseif( $a > $b ) { |
| 168 | + return 1; |
| 169 | + } else { |
| 170 | + return -1; |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + $c = (( $a / $b ) - ( $b / $a )) / 2.0; |
| 175 | + if( abs( $c ) < EXPR_TOLERANCE ) { |
| 176 | + return 0; |
| 177 | + } elseif( $c > 0 ) { |
| 178 | + return 1; |
168 | 179 | } else { |
169 | | - if( $expr - $intval < -0.9999999999 ) { $intval -= 1; } |
| 180 | + return -1; |
170 | 181 | } |
| 182 | + } |
171 | 183 | |
172 | | - if( abs( $intval - $expr ) < 0.0000000001 ) { |
| 184 | + /** |
| 185 | + * Checks if $expr is an integer within EXPR_TOLERANCE |
| 186 | + * If so, recast as integer and return, else return $expr unchanged. |
| 187 | + */ |
| 188 | + function checkInteger( $expr ) { |
| 189 | + $intval = round($expr); |
| 190 | + if( toleranceComparison( $expr, $intval ) == 0 ) { |
173 | 191 | return $intval; |
174 | | - } else { |
| 192 | + } else { |
175 | 193 | return $expr; |
176 | 194 | } |
177 | 195 | } |
— | — | @@ -453,7 +471,7 @@ |
454 | 472 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
455 | 473 | $right = array_pop( $stack ); |
456 | 474 | $left = array_pop( $stack ); |
457 | | - $stack[] = ( $this->checkInteger( $left ) == $this->checkInteger( $right ) ) ? 1 : 0; |
| 475 | + $stack[] = ( $this->toleranceComparison( $left, $right ) == 0 ) ? 1 : 0; |
458 | 476 | break; |
459 | 477 | case EXPR_NOT: |
460 | 478 | if ( count( $stack ) < 1 ) throw new ExprError('missing_operand', $this->names[$op]); |
— | — | @@ -464,37 +482,37 @@ |
465 | 483 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
466 | 484 | $digits = intval( array_pop( $stack ) ); |
467 | 485 | $value = array_pop( $stack ); |
468 | | - $stack[] = round( $this->checkInteger( $value ), $digits ); |
| 486 | + $stack[] = round( $value, $digits ); |
469 | 487 | break; |
470 | 488 | case EXPR_LESS: |
471 | 489 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
472 | 490 | $right = array_pop( $stack ); |
473 | 491 | $left = array_pop( $stack ); |
474 | | - $stack[] = ( $this->checkInteger( $left ) < $this->checkInteger( $right ) ) ? 1 : 0; |
| 492 | + $stack[] = ( $this->toleranceComparison( $left, $right ) < 0 ) ? 1 : 0; |
475 | 493 | break; |
476 | 494 | case EXPR_GREATER: |
477 | 495 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
478 | 496 | $right = array_pop( $stack ); |
479 | 497 | $left = array_pop( $stack ); |
480 | | - $stack[] = ( $this->checkInteger( $left ) > $this->checkInteger( $right ) ) ? 1 : 0; |
| 498 | + $stack[] = ( $this->toleranceComparison( $left, $right ) > 0 ) ? 1 : 0; |
481 | 499 | break; |
482 | 500 | case EXPR_LESSEQ: |
483 | 501 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
484 | 502 | $right = array_pop( $stack ); |
485 | 503 | $left = array_pop( $stack ); |
486 | | - $stack[] = ( $this->checkInteger( $left ) <= $this->checkInteger( $right ) ) ? 1 : 0; |
| 504 | + $stack[] = ( $this->toleranceComparison( $left, $right ) <= 0 ) ? 1 : 0; |
487 | 505 | break; |
488 | 506 | case EXPR_GREATEREQ: |
489 | 507 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
490 | 508 | $right = array_pop( $stack ); |
491 | 509 | $left = array_pop( $stack ); |
492 | | - $stack[] = ( $this->checkInteger( $left ) >= $this->checkInteger( $right ) ) ? 1 : 0; |
| 510 | + $stack[] = ( $this->toleranceComparison( $left, $right ) >= 0 ) ? 1 : 0; |
493 | 511 | break; |
494 | 512 | case EXPR_NOTEQ: |
495 | 513 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |
496 | 514 | $right = array_pop( $stack ); |
497 | 515 | $left = array_pop( $stack ); |
498 | | - $stack[] = ( $this->checkInteger( $left ) != $this->checkInteger( $right ) ) ? 1 : 0; |
| 516 | + $stack[] = ( $this->toleranceComparison( $left, $right ) != 0 ) ? 1 : 0; |
499 | 517 | break; |
500 | 518 | case EXPR_EXPONENT: |
501 | 519 | if ( count( $stack ) < 2 ) throw new ExprError('missing_operand', $this->names[$op]); |