Daniel Kinzler has uploaded a new change for review. https://gerrit.wikimedia.org/r/88771
Change subject: (bug #55511) Introducing DecimalValue ...................................................................... (bug #55511) Introducing DecimalValue DecimalValue represent an arbitrary precision decimal number. Change-Id: I964734c711f9949be9389a97953520ec7f562ca6 --- A DataValuesCommon/src/DataValues/DecimalValue.php A DataValuesCommon/tests/DataValues/DecimalValueTest.php 2 files changed, 943 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DataValues refs/changes/71/88771/1 diff --git a/DataValuesCommon/src/DataValues/DecimalValue.php b/DataValuesCommon/src/DataValues/DecimalValue.php new file mode 100644 index 0000000..9e81424 --- /dev/null +++ b/DataValuesCommon/src/DataValues/DecimalValue.php @@ -0,0 +1,542 @@ +<?php + +namespace DataValues; + +/** + * Class representing a decimal number with (nearly) arbitrary precision. + * + * For simple numeric values use @see NumberValue. + * + * The value notation for the value follows ISO 31-0, with some additional restrictions: + * - the decimal separator is '.' (period). Comma is not used anywhere. + * - no spacing or other separators are included for groups of digits. + * - the first character in the string always gives the sign, either plus (+) or minus (-). + * - scientific (exponential) notation is not used. + * - there no trailing zeros, except directly after the decimal point + * - there no leading zeros, except directly before the decimal point + * + * These rules are enforced by @see QUANTITY_VALUE_PATTERN + * + * @since 0.1 + * + * @licence GNU GPL v2+ + * @author Daniel Kinzler + */ +class DecimalValue extends DataValueObject { + + /** + * The $value as a decimal string. See the class level documentation for details. + * + * @see QUANTITY_VALUE_PATTERN + * + * @since 0.1 + * + * @var string + */ + protected $value; + + /** + * Regular expression for matching valid numbers. + */ + const QUANTITY_VALUE_PATTERN = '/^[-+]([1-9][0-9]*|[0-9])(\.[0-9]+)?$/'; + + /** + * Constructs a new DecimalValue object, representing the given value. + * + * @since 0.1 + * + * @param string|int|float $value If given as a string, the value must match + * QUANTITY_VALUE_PATTERN. + * @throws IllegalValueException + */ + public function __construct( $value ) { + if ( is_int( $value ) || is_float( $value ) ) { + $value = self::decimal( $value ); + } + + self::assertNumberString( $value, '$value' ); + + // round "negative" zero + $value = preg_replace( '/^-(0+(\.0+)?)$/', '+\1', $value ); + + $this->value = $value; + } + + /** + * Checks that the given value is a number string. + * + * @param string $number The value to check + * @param string $name The name to use in error messages + * + * @throws IllegalValueException + */ + protected static function assertNumberString( $number, $name ) { + if ( !is_string( $number ) ) { + throw new IllegalValueException( $name . ' must be a numeric string' ); + } + + if ( !preg_match( self::QUANTITY_VALUE_PATTERN, $number ) ) { + throw new IllegalValueException( $name . ' must match the pattern for numeric values: bad value: `' . $number . '`' ); + } + + if ( strlen( $number ) > 127 ) { + throw new IllegalValueException( $name . ' must be at most 127 characters long.' ); + } + } + + /** + * Converts the given number to decimal notation + * + * @param int|float $number + * + * @return string + * @throws \InvalidArgumentException + */ + protected static function decimal( $number ) { + if ( !is_int( $number ) && !is_float( $number ) ) { + throw new \InvalidArgumentException( '$number must be an int or float' ); + } + + if ( $number === NAN || abs( $number ) === INF ) { + throw new \InvalidArgumentException( '$number must not be NAN or INF' ); + } + + if ( is_int( $number ) ) { + $decimal = strval( abs( $number ) ); + } else { + $decimal = trim( number_format( abs( $number ), 100, '.', '' ), 0 ); + + if ( $decimal[0] === '.' ) { + $decimal = '0' . $decimal; + } + + $last = strlen($decimal)-1; + + if ( $decimal[$last] === '.' ) { + $decimal = $decimal . '0'; + } + } + + $decimal = ( ( $number >= 0.0 ) ? '+' : '-' ) . $decimal; + + self::assertNumberString( $decimal, '$number' ); + return $decimal; + } + + /** + * Compares this DecimalValue to another DecimalValue. + * + * @param DecimalValue $that + * + * @throws \LogicException + * @return int +1 if $this > $that, 0 if $this == $that, -1 if $this < $that + */ + public function compare( DecimalValue $that ) { + if ( $this === $that ) { + return 0; + } + + $a = $this->getValue(); + $b = $that->getValue(); + + if ( $a === $b ) { + return 0; + } + + if ( $a[0] === '+' && $b[0] === '-' ) { + return 1; + } + + if ( $a[0] === '-' && $b[0] === '+' ) { + return -1; + } + + //FIXME: handle "minus null"! + + // compare the integer parts + $aIntDigits = strpos( $a, '.' ); + $bIntDigits = strpos( $b, '.' ); + $aInt = ltrim( substr( $a, 1, ( $aIntDigits ? $aIntDigits : strlen( $a ) ) -1 ), '0' ); + $bInt = ltrim( substr( $b, 1, ( $bIntDigits ? $bIntDigits : strlen( $b ) ) -1 ), '0' ); + + $sense = $a[0] === '+' ? 1 : -1; + + // per precondition, there are no leading zeros, so the longer nummber is greater + if ( strlen( $aInt ) > strlen( $bInt ) ) { + return $sense; + } + + if ( strlen( $aInt ) < strlen( $bInt ) ) { + return -$sense; + } + + // if both have equal length, compare alphanumerically + if ( $aInt > $bInt ) { + return $sense; + } + + if ( $aInt < $bInt ) { + return -$sense; + } + + // compare fractional parts + $aFract = rtrim( substr( $a, $aIntDigits +1 ), '0' ); + $bFract = rtrim( substr( $b, $bIntDigits +1 ), '0' ); + + // the fractional part is left-aligned, so just check alphanumeric ordering + $cmp = strcmp( $aFract, $bFract ); + return ( $cmp > 0 ? 1 : ( $cmp < 0 ? -1 : 0 ) ); + } + + /** + * Returns the given value string, with any insignificant digits removed or zeroed. + * Rounding is applied using the "round half away from zero" rule (that is, +0.5 is + * rounded to +1 and -0.5 is rounded to -1). + * + * @since 0.1 + * + * @param string $value + * @param int $significantDigits + * + * @return string + */ + protected static function roundDecimal( $value, $significantDigits ) { + // whether the last character is already part of the integer part of the decimal value + $inIntPart = ( strpos( $value, '.' ) === false ); + + $five = ord( '5' ); + $length = strlen( $value ); + + $rounded = ''; + + // iterator over characters from back to front + for ( $i = $length -1; $i > 0 && $i > $significantDigits; $i-- ) { + $ch = $value[$i]; + + if ( $ch === '.' ) { + $inIntPart = true; + $next = '.'; + } else { + if ( $inIntPart ) { + // in the integer part, zero out insignificant digits + $next = '0'; + } else { + // in the fractional part, strip insignificant digits + $next = ''; + } + + if ( ord( $ch ) >= $five ) { + // strip and apply rounding. + $remaining = substr( $value, 0, $i ); + + if ( $remaining[ strlen( $remaining ) -1 ] === '.' ) { + $remaining = rtrim( $remaining, '.' ); + $inIntPart = true; + } + + // rounding may add digits, adjust $i for that + $value = self::bumpDecimal( $remaining ); + $i = strlen( $value ); + } + } + + $rounded = $next . $rounded; + } + + $rounded = substr( $value, 0, $i +1 ) . $rounded; + + // strip trailing decimal point + $rounded = rtrim( $rounded, '.' ); + return $rounded; + } + + /** + * Increment the least significant digit by one if it is less than 9, and + * set it to zero and continue to the next more significant digit if it is 9. + * Exception: bump( 0 ) == 1; + * + * E.g.: bump( 0.2 ) == 0.3, bump( -0.09 ) == -0.10, bump( 9.99 ) == 10.00 + * + * This is the inverse of @see slump() + * + * @since 0.1 + * + * @param string $value + * + * @return string + */ + protected static function bumpDecimal( $value ) { + if ( $value === '+0' ) { + return '+1'; + } + + $bumped = ''; + + for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { + $ch = $value[$i]; + + if ( $ch === '.' ) { + $bumped = '.' . $bumped; + continue; + } elseif ( $ch === '9' ) { + $bumped = '0' . $bumped; + continue; + } elseif ( $ch === '+' || $ch === '-' ) { + $bumped = $ch . '1' . $bumped; + break; + } else { + $bumped = chr( ord( $ch ) + 1 ) . $bumped; + break; + } + } + + $bumped = substr( $value, 0, $i ) . $bumped; + return $bumped; + } + + /** + * Decrement the least significant digit by one if it is more than 0, and + * set it to 9 and continue to the next more significant digit if it is 0. + * Exception: slump( 0 ) == -1; + * + * E.g.: slump( 0.2 ) == 0.1, slump( -0.10 ) == -0.01, slump( 0.0 ) == -1.0 + * + * This is the inverse of @see bump() + * + * @since 0.1 + * + * @param string $value + * + * @return string + */ + protected static function slumpDecimal( $value ) { + self::assertNumberString( $value, '$value' ); + + if ( $value === '+0' ) { + return '-1'; + } + + // a "percise zero" will become negative + if ( preg_match( '/^\+0\.(0*)0$/', $value, $m ) ) { + return '-0.' . $m[1] . '1'; + } + + $slumped = ''; + + for ( $i = strlen( $value ) -1; $i >= 0; $i-- ) { + $ch = substr( $value, $i, 1 ); + + if ( $ch === '.' ) { + $slumped = '.' . $slumped; + continue; + } elseif ( $ch === '0' ) { + $slumped = '9' . $slumped; + continue; + } elseif ( $ch === '+' || $ch === '-' ) { + $slumped = '0'; + break; + } else { + $slumped = chr( ord( $ch ) - 1 ) . $slumped; + break; + } + } + + // preserve prefix + $slumped = substr( $value, 0, $i ) . $slumped; + + // strip leading zeros + $slumped = preg_replace( '/^([-+])(0+)([0-9](\.|$))/', '\1\3', $slumped ); + + if ( $slumped === '-0' ) { + $slumped = '+0'; + } + + return $slumped; + } + + /** + * @see Serializable::serialize + * + * @since 0.1 + * + * @return string + */ + public function serialize() { + return serialize( $this->value ); + } + + /** + * @see Serializable::unserialize + * + * @since 0.1 + * + * @param string $data + * + * @return DecimalValue + */ + public function unserialize( $data ) { + $value = unserialize( $data ); + $this->__construct( $value ); + } + + /** + * @see DataValue::getType + * + * @since 0.1 + * + * @return string + */ + public static function getType() { + return 'decimal'; + } + + /** + * @see DataValue::getSortKey + * + * @since 0.1 + * + * @return float + */ + public function getSortKey() { + return $this->getValueFloat(); + } + + /** + * Returns the quantity object. + * @see DataValue::getValue + * + * @since 0.1 + * + * @return string + */ + public function getValue() { + return $this->value; + } + + /** + * Returns the sign of the amount (+ or -). + * + * @return string "+" or "-". + */ + public function getSign() { + return substr( $this->value, 0, 1 ); + } + + /** + * Returns the value held by this object, as a float. + * Equivalent to floatval( $this->getvalue() ). + * + * @since 0.1 + * + * @return float + */ + public function getValueFloat() { + return floatval( $this->getValue() ); + } + + /** + * Returns the value held by this object, as a string, + * with any insignificant digits removed or zeroed. Rounding is applied + * using the "round half away from zero" rule (that is, +0.5 is rounded to +1 and + * -0.5 is rounded to -1). + * + * @since 0.1 + * + * @param int $digits The significant number of sigits (not counting the sign, + * but counting the decimal point ) + * + * @return string + */ + public function getValueRounded( $digits ) { + return self::roundDecimal( $this->value, $digits ); + } + + /** + * Returns the value held by this object, as a string, + * with the least significant digit incremented, e.g.: + * 2.01 becomes 2.02, -2.10 becomes -2.11, -2.9 becomes -3, + * and 10 becomes 11. + * + * @since 0.1 + * + * @return string + */ + public function getValueBumped() { + return self::bumpDecimal( $this->value ); + } + + /** + * Returns the value held by this object, as a string, + * with the least significant digit decremented, e.g.: + * 2.02 becomes 2.01, -2.10 becomes -2.09, -3 becomes -2, + * and 20 becomes 19. + * + * @since 0.1 + * + * @return string + */ + public function getValueSlumped() { + return self::slumpDecimal( $this->value ); + } + + /** + * Returns the product of this DecimalValue and the $factor parameter. + * + * @param DecimalValue $factor + * + * @return DataValue + */ + public function product( DecimalValue $factor ) { + //TODO: use bcmath if available + $product = $this->getValueFloat() * $factor->getValueFloat(); + + return new DecimalValue( $product ); + } + + /** + * Returns the sum of this DecimalValue and the $offset parameter. + * + * @param DecimalValue $offset + * + * @return DataValue + */ + public function sum( DecimalValue $offset ) { + //TODO: use bcmath if available + $product = $this->getValueFloat() + $offset->getValueFloat(); + + return new DecimalValue( $product ); + } + + /** + * @see DataValue::getArrayValue + * + * @since 0.1 + * + * @return string + */ + public function getArrayValue() { + return $this->value; + } + + /** + * Constructs a new instance of the DataValue from the provided data. + * This can round-trip with @see getArrayValue + * + * @since 0.1 + * + * @param string $data + * + * @return DecimalValue + * @throws IllegalValueException + */ + public static function newFromArray( $data ) { + return new static( $data ); + } + + /** + * @return string + */ + public function __toString() { + return $this->value; + } +} diff --git a/DataValuesCommon/tests/DataValues/DecimalValueTest.php b/DataValuesCommon/tests/DataValues/DecimalValueTest.php new file mode 100644 index 0000000..2009258 --- /dev/null +++ b/DataValuesCommon/tests/DataValues/DecimalValueTest.php @@ -0,0 +1,401 @@ +<?php + +namespace DataValues\Tests; + +use DataValues\DecimalValue; + +/** + * @covers DataValues\DecimalValue + * + * @since 0.1 + * + * @ingroup DataValue + * + * @group DataValue + * @group DataValueExtensions + * + * @licence GNU GPL v2+ + * + * @author Daniel Kinzler + */ +class DecimalValueTest extends DataValueTest { + + /** + * @see DataValueTest::getClass + * + * @since 0.1 + * + * @return string + */ + public function getClass() { + return 'DataValues\DecimalValue'; + } + + public function validConstructorArgumentsProvider() { + $argLists = array(); + + $argLists[] = array( 42 ); + $argLists[] = array( -42 ); + $argLists[] = array( '-42' ); + $argLists[] = array( 4.2 ); + $argLists[] = array( -4.2 ); + $argLists[] = array( '+4.2' ); + $argLists[] = array( 0 ); + $argLists[] = array( 0.2 ); + $argLists[] = array( '-0.42' ); + $argLists[] = array( '-0.0' ); + $argLists[] = array( '-0' ); + $argLists[] = array( '+0.0' ); + $argLists[] = array( '+0' ); + + return $argLists; + } + + public function invalidConstructorArgumentsProvider() { + $argLists = array(); + + $argLists[] = array(); + + + $argLists[] = array( 'foo' ); + $argLists[] = array( '' ); + $argLists[] = array( '4.2' ); + $argLists[] = array( '++4.2' ); + $argLists[] = array( '--4.2' ); + $argLists[] = array( '-+4.2' ); + $argLists[] = array( '+-4.2' ); + $argLists[] = array( '-.42' ); + $argLists[] = array( '+.42' ); + $argLists[] = array( '.42' ); + $argLists[] = array( '.0' ); + $argLists[] = array( '-00' ); + $argLists[] = array( '+01.2' ); + $argLists[] = array( 'x2' ); + $argLists[] = array( '2x' ); + $argLists[] = array( '+0100' ); + $argLists[] = array( false ); + $argLists[] = array( true ); + $argLists[] = array( null ); + $argLists[] = array( '0x20' ); + + return $argLists; + } + + /** + * @dataProvider compareProvider + * + * @since 0.1 + */ + public function testCompare( DecimalValue $a, DecimalValue $b, $expected ) { + $actual = $a->compare( $b ); + $this->assertSame( $expected, $actual ); + + $actual = $b->compare( $a ); + $this->assertSame( -$expected, $actual ); + } + + public function compareProvider() { + return array( + 'zero/equal' => array( new DecimalValue( 0 ), new DecimalValue( 0 ), 0 ), + 'zero-signs/equal' => array( new DecimalValue( '+0' ), new DecimalValue( '-0' ), 0 ), + 'zero-digits/equal' => array( new DecimalValue( '+0' ), new DecimalValue( '+0.000' ), 0 ), + 'digits/equal' => array( new DecimalValue( '+2.2' ), new DecimalValue( '+2.2000' ), 0 ), + 'conversion/equal' => array( new DecimalValue( 2.5 ), new DecimalValue( '+2.50' ), 0 ), + 'negative/equal' => array( new DecimalValue( '-1.33' ), new DecimalValue( '-1.33' ), 0 ), + + 'simple/smaller' => array( new DecimalValue( '+1' ), new DecimalValue( '+2' ), -1 ), + 'simple/greater' => array( new DecimalValue( '+2' ), new DecimalValue( '+1' ), +1 ), + 'negative/greater' => array( new DecimalValue( '-1' ), new DecimalValue( '-2' ), +1 ), + 'negative/smaller' => array( new DecimalValue( '-2' ), new DecimalValue( '-1' ), -1 ), + + 'digits/greater' => array( new DecimalValue( '+11' ), new DecimalValue( '+8' ), +1 ), + 'digits-sub/greater' => array( new DecimalValue( '+11' ), new DecimalValue( '+8.0' ), +1 ), + 'negative-digits/greater' => array( new DecimalValue( '-11' ), new DecimalValue( '-80' ), +1 ), + 'small/greater' => array( new DecimalValue( '+0.050' ), new DecimalValue( '+0.005' ), +1 ), + + 'signs/greater' => array( new DecimalValue( '+1' ), new DecimalValue( '-8' ), +1 ), + 'signs/less' => array( new DecimalValue( '-8' ), new DecimalValue( '+1' ), -1 ), + ); + } + + /** + * @dataProvider getSignProvider + * + * @since 0.1 + */ + public function testGetSign( DecimalValue $value, $expected ) { + $actual = $value->getSign(); + $this->assertSame( $expected, $actual ); + } + + public function getSignProvider() { + return array( + 'zero is positive' => array( new DecimalValue( 0 ), '+' ), + 'zero is always positive' => array( new DecimalValue( '-0' ), '+' ), + 'zero is ALWAYS positive' => array( new DecimalValue( '-0.00' ), '+' ), + '+1 is positive' => array( new DecimalValue( '+1' ), '+' ), + '-1 is negative' => array( new DecimalValue( '-1' ), '-' ), + '+0.01 is positive' => array( new DecimalValue( '+0.01' ), '+' ), + '-0.01 is negative' => array( new DecimalValue( '-0.01' ), '-' ), + ); + } + + /** + * @dataProvider getValueProvider + * + * @since 0.1 + */ + public function testGetValue( DecimalValue $value, $expected ) { + $actual = $value->getValue(); + $this->assertSame( $expected, $actual ); + } + + public function getValueProvider() { + $argLists = array(); + + $argLists[] = array( new DecimalValue( 42 ), '+42' ); + $argLists[] = array( new DecimalValue( -42 ), '-42' ); + $argLists[] = array( new DecimalValue( '-42' ), '-42' ); + $argLists[] = array( new DecimalValue( 4.5 ), '+4.5' ); + $argLists[] = array( new DecimalValue( -4.5 ), '-4.5' ); + $argLists[] = array( new DecimalValue( '+4.2' ), '+4.2' ); + $argLists[] = array( new DecimalValue( 0 ), '+0' ); + $argLists[] = array( new DecimalValue( 0.5 ), '+0.5' ); + $argLists[] = array( new DecimalValue( '-0.42' ), '-0.42' ); + $argLists[] = array( new DecimalValue( '-0.0' ), '+0.0' ); + $argLists[] = array( new DecimalValue( '-0' ), '+0' ); + $argLists[] = array( new DecimalValue( '+0.0' ), '+0.0' ); + $argLists[] = array( new DecimalValue( '+0' ), '+0' ); + + return $argLists; + } + + /** + * @dataProvider getValueFloatProvider + * + * @since 0.1 + */ + public function testGetValueFloat( DecimalValue $value, $expected ) { + $actual = $value->getValueFloat(); + $this->assertSame( $expected, $actual ); + } + + public function getValueFloatProvider() { + $argLists = array(); + + $argLists[] = array( new DecimalValue( 42 ), 42.0 ); + $argLists[] = array( new DecimalValue( -42 ), -42.0 ); + $argLists[] = array( new DecimalValue( '-42' ), -42.0 ); + $argLists[] = array( new DecimalValue( 4.5 ), 4.5 ); + $argLists[] = array( new DecimalValue( -4.5 ), -4.5 ); + $argLists[] = array( new DecimalValue( '+4.2' ), 4.2 ); + $argLists[] = array( new DecimalValue( 0 ), 0.0 ); + $argLists[] = array( new DecimalValue( 0.5 ), 0.5 ); + $argLists[] = array( new DecimalValue( '-0.42' ), -0.42 ); + $argLists[] = array( new DecimalValue( '-0.0' ), 0.0 ); + $argLists[] = array( new DecimalValue( '-0' ), 0.0 ); + $argLists[] = array( new DecimalValue( '+0.0' ), 0.0 ); + $argLists[] = array( new DecimalValue( '+0' ), 0.0 ); + + return $argLists; + } + + /** + * @dataProvider getGetValueBumpedProvider + * + * @since 0.1 + */ + public function testGetValueBumped( DecimalValue $value, $expected ) { + $actual = $value->getValueBumped(); + $this->assertSame( $expected, $actual ); + } + + public function getGetValueBumpedProvider() { + return array( + array( new DecimalValue( '+0' ), '+1' ), + array( new DecimalValue( '-0' ), '+1' ), + array( new DecimalValue( '+0.0' ), '+0.1' ), + array( new DecimalValue( '-0.0' ), '+0.1' ), + array( new DecimalValue( '+1' ), '+2' ), + array( new DecimalValue( '-1' ), '-2' ), + array( new DecimalValue( '+10' ), '+11' ), + array( new DecimalValue( '-10' ), '-11' ), + array( new DecimalValue( '+9' ), '+10' ), + array( new DecimalValue( '-9' ), '-10' ), + array( new DecimalValue( '+0.01' ), '+0.02' ), + array( new DecimalValue( '-0.01' ), '-0.02' ), + array( new DecimalValue( '+0.09' ), '+0.10' ), + array( new DecimalValue( '-0.09' ), '-0.10' ), + array( new DecimalValue( '+0.9' ), '+1.0' ), + array( new DecimalValue( '-0.9' ), '-1.0' ), + ); + } + + /** + * @dataProvider getGetValueSlumpedProvider + * + * @since 0.1 + */ + public function testGetValueSlumped( DecimalValue $value, $expected ) { + $actual = $value->getValueSlumped(); + $this->assertSame( $expected, $actual ); + } + + public function getGetValueSlumpedProvider() { + return array( + array( new DecimalValue( '+0' ), '-1' ), + array( new DecimalValue( '-0' ), '-1' ), + array( new DecimalValue( '+0.0' ), '-0.1' ), + array( new DecimalValue( '-0.0' ), '-0.1' ), + array( new DecimalValue( '+0.00' ), '-0.01' ), + array( new DecimalValue( '-0.00' ), '-0.01' ), + array( new DecimalValue( '+1' ), '+0' ), + array( new DecimalValue( '-1' ), '+0' ), + array( new DecimalValue( '+1.0' ), '+0.9' ), + array( new DecimalValue( '-1.0' ), '-0.9' ), + array( new DecimalValue( '+0.1' ), '+0.0' ), + array( new DecimalValue( '-0.1' ), '-0.0' ), + array( new DecimalValue( '+0.01' ), '+0.00' ), + array( new DecimalValue( '-0.01' ), '-0.00' ), + array( new DecimalValue( '+12' ), '+11' ), + array( new DecimalValue( '-12' ), '-11' ), + array( new DecimalValue( '+10' ), '+9' ), + array( new DecimalValue( '-10' ), '-9' ), + array( new DecimalValue( '+0.02' ), '+0.01' ), + array( new DecimalValue( '-0.02' ), '-0.01' ), + array( new DecimalValue( '+0.10' ), '+0.09' ), + array( new DecimalValue( '-0.10' ), '-0.09' ), + ); + } + + /** + * @dataProvider productProvider + */ + public function testProduct( DecimalValue $a, DecimalValue $b, $value ) { + $actual = $a->product( $b ); + $this->assertEquals( $value, $actual->getValue() ); + + $actual = $b->product( $a ); + $this->assertEquals( $value, $actual->getValue() ); + } + + public function productProvider() { + return array( + array( new DecimalValue( '+0' ), new DecimalValue( '+0' ), '+0' ), + array( new DecimalValue( '+0' ), new DecimalValue( '+1' ), '+0' ), + array( new DecimalValue( '+0' ), new DecimalValue( '+2' ), '+0' ), + + array( new DecimalValue( '+1' ), new DecimalValue( '+0' ), '+0' ), + array( new DecimalValue( '+1' ), new DecimalValue( '+1' ), '+1' ), + array( new DecimalValue( '+1' ), new DecimalValue( '+2' ), '+2' ), + + array( new DecimalValue( '+2' ), new DecimalValue( '+0' ), '+0' ), + array( new DecimalValue( '+2' ), new DecimalValue( '+1' ), '+2' ), + array( new DecimalValue( '+2' ), new DecimalValue( '+2' ), '+4' ), + + array( new DecimalValue( '+0.5' ), new DecimalValue( '+0' ), '+0' ), + array( new DecimalValue( '+0.5' ), new DecimalValue( '+1' ), '+0.5' ), + array( new DecimalValue( '+0.5' ), new DecimalValue( '+2' ), '+1' ), + ); + } + + /** + * @dataProvider sumProvider + */ + public function testSum( DecimalValue $a, DecimalValue $b, $value ) { + $actual = $a->sum( $b ); + $this->assertEquals( $value, $actual->getValue() ); + + $actual = $b->sum( $a ); + $this->assertEquals( $value, $actual->getValue() ); + } + + public function sumProvider() { + return array( + array( new DecimalValue( '+0' ), new DecimalValue( '+0' ), '+0' ), + array( new DecimalValue( '+0' ), new DecimalValue( '+1' ), '+1' ), + array( new DecimalValue( '+0' ), new DecimalValue( '+2' ), '+2' ), + + array( new DecimalValue( '+2' ), new DecimalValue( '+0' ), '+2' ), + array( new DecimalValue( '+2' ), new DecimalValue( '+1' ), '+3' ), + array( new DecimalValue( '+2' ), new DecimalValue( '+2' ), '+4' ), + + array( new DecimalValue( '+0.5' ), new DecimalValue( '+0' ), '+0.5' ), + array( new DecimalValue( '+0.5' ), new DecimalValue( '+0.5' ), '+1.0' ), + array( new DecimalValue( '+0.5' ), new DecimalValue( '+2' ), '+2.5' ), + ); + } + + /** + * @dataProvider getValueRoundedProvider + * + * @since 0.1 + */ + public function testGetValueRounded( DecimalValue $value, $digits, $expected ) { + $actual = $value->getValueRounded( $digits ); + $this->assertSame( $expected, $actual ); + } + + public function getValueRoundedProvider() { + $argLists = array(); + + //NOTE: Rounding is applied using the "round half away from zero" logic. + + $argLists[] = array( new DecimalValue( '+0' ), 1, '+0' ); + $argLists[] = array( new DecimalValue( '+0' ), 2, '+0' ); + $argLists[] = array( new DecimalValue( '+0.0' ), 1, '+0' ); + $argLists[] = array( new DecimalValue( '+0.0' ), 2, '+0' ); + $argLists[] = array( new DecimalValue( '+0.0' ), 3, '+0.0' ); + + $argLists[] = array( new DecimalValue( '-2' ), 1, '-2' ); + $argLists[] = array( new DecimalValue( '-2' ), 2, '-2' ); + + $argLists[] = array( new DecimalValue( '+23' ), 1, '+20' ); + $argLists[] = array( new DecimalValue( '+23' ), 2, '+23' ); + $argLists[] = array( new DecimalValue( '+23' ), 3, '+23' ); + + $argLists[] = array( new DecimalValue( '-234' ), 1, '-200' ); + $argLists[] = array( new DecimalValue( '-234' ), 2, '-230' ); + $argLists[] = array( new DecimalValue( '-234' ), 3, '-234' ); + + $argLists[] = array( new DecimalValue( '-2.0' ), 1, '-2' ); + $argLists[] = array( new DecimalValue( '-2.0' ), 2, '-2' ); // edge case, may change + $argLists[] = array( new DecimalValue( '-2.0' ), 3, '-2.0' ); + $argLists[] = array( new DecimalValue( '-2.0' ), 4, '-2.0' ); // edge case, may change + + $argLists[] = array( new DecimalValue( '-2.000' ), 1, '-2' ); + $argLists[] = array( new DecimalValue( '-2.000' ), 2, '-2' ); + $argLists[] = array( new DecimalValue( '-2.000' ), 3, '-2.0' ); + $argLists[] = array( new DecimalValue( '-2.000' ), 4, '-2.00' ); + + $argLists[] = array( new DecimalValue( '+2.5' ), 1, '+3' ); // rounded up + $argLists[] = array( new DecimalValue( '+2.5' ), 2, '+3' ); + $argLists[] = array( new DecimalValue( '+2.5' ), 3, '+2.5' ); + $argLists[] = array( new DecimalValue( '+2.5' ), 4, '+2.5' ); + + $argLists[] = array( new DecimalValue( '+2.05' ), 1, '+2' ); + $argLists[] = array( new DecimalValue( '+2.05' ), 2, '+2' ); + $argLists[] = array( new DecimalValue( '+2.05' ), 3, '+2.1' ); // rounded up + $argLists[] = array( new DecimalValue( '+2.05' ), 4, '+2.05' ); + + $argLists[] = array( new DecimalValue( '-23.05' ), 1, '-20' ); + $argLists[] = array( new DecimalValue( '-23.05' ), 2, '-23' ); + $argLists[] = array( new DecimalValue( '-23.05' ), 3, '-23' ); // edge case, may change + $argLists[] = array( new DecimalValue( '-23.05' ), 4, '-23.1' ); // rounded down + $argLists[] = array( new DecimalValue( '-23.05' ), 5, '-23.05' ); + + $argLists[] = array( new DecimalValue( '+9.33' ), 1, '+9' ); // no rounding + $argLists[] = array( new DecimalValue( '+9.87' ), 1, '+10' ); // rounding ripples up + $argLists[] = array( new DecimalValue( '+9.87' ), 3, '+9.9' ); // rounding ripples up + $argLists[] = array( new DecimalValue( '+99' ), 1, '+100' ); // rounding ripples up + $argLists[] = array( new DecimalValue( '+99' ), 2, '+99' ); // rounding ripples up + + $argLists[] = array( new DecimalValue( '-9.33' ), 1, '-9' ); // no rounding + $argLists[] = array( new DecimalValue( '-9.87' ), 1, '-10' ); // rounding ripples down + $argLists[] = array( new DecimalValue( '-9.87' ), 3, '-9.9' ); // rounding ripples down + $argLists[] = array( new DecimalValue( '-99' ), 1, '-100' ); // rounding ripples down + $argLists[] = array( new DecimalValue( '-99' ), 2, '-99' ); // rounding ripples down + + return $argLists; + } +} -- To view, visit https://gerrit.wikimedia.org/r/88771 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I964734c711f9949be9389a97953520ec7f562ca6 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/DataValues Gerrit-Branch: master Gerrit-Owner: Daniel Kinzler <daniel.kinz...@wikimedia.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits