Daniel Werner has submitted this change and it was merged.

Change subject: (bug #55511) Introducing DecimalValue
......................................................................


(bug #55511) Introducing DecimalValue

DecimalValue represent an arbitrary precision decimal number.

Change-Id: I964734c711f9949be9389a97953520ec7f562ca6
---
M DataValuesCommon/DataValuesCommon.classes.php
A DataValuesCommon/src/DataValues/DecimalValue.php
A DataValuesCommon/tests/DataValues/DecimalValueTest.php
3 files changed, 515 insertions(+), 0 deletions(-)

Approvals:
  Daniel Werner: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/DataValuesCommon/DataValuesCommon.classes.php 
b/DataValuesCommon/DataValuesCommon.classes.php
index e5f3b8e..277c744 100644
--- a/DataValuesCommon/DataValuesCommon.classes.php
+++ b/DataValuesCommon/DataValuesCommon.classes.php
@@ -54,6 +54,7 @@
        'DataValues\LatLongValue' => 'src/DataValues/LatLongValue.php',
        'DataValues\MonolingualTextValue' => 
'src/DataValues/MonolingualTextValue.php',
        'DataValues\MultilingualTextValue' => 
'src/DataValues/MultilingualTextValue.php',
+       'DataValues\DecimalValue' => 'src/DataValues/DecimalValue.php',
        'DataValues\QuantityValue' => 'src/DataValues/QuantityValue.php',
        'DataValues\TimeValue' => 'src/DataValues/TimeValue.php',
 );
diff --git a/DataValuesCommon/src/DataValues/DecimalValue.php 
b/DataValuesCommon/src/DataValues/DecimalValue.php
new file mode 100644
index 0000000..29a6d93
--- /dev/null
+++ b/DataValuesCommon/src/DataValues/DecimalValue.php
@@ -0,0 +1,312 @@
+<?php
+
+namespace DataValues;
+
+/**
+ * Class representing a decimal number with (nearly) arbitrary precision.
+ *
+ * For simple numeric values use @see NumberValue.
+ *
+ * The decimal 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.
+ * - the decimal point must not be the last character nor the fist character 
after the sign.
+ * - no leading zeros, except one directly before the decimal point
+ * - zero is always positive.
+ *
+ * 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, in the format described in the class
+        * level documentation of @see DecimalValue, matching @see 
QUANTITY_VALUE_PATTERN.
+        *
+        * @since 0.1
+        *
+        * @var string
+        */
+       protected $value;
+
+       /**
+        * Regular expression for matching decimal strings that conform to the 
format
+        * described in the class level documentation of @see DecimalValue.
+        */
+       const QUANTITY_VALUE_PATTERN = '/^[-+]([1-9][0-9]*|[0-9])(\.[0-9]+)?$/';
+
+       /**
+        * Constructs a new DecimalValue object, representing the given value.
+        *
+        * @param string|int|float $value If given as a string, the value must 
match
+        *                         QUANTITY_VALUE_PATTERN.
+        */
+       public function __construct( $value ) {
+               if ( is_int( $value ) || is_float( $value ) ) {
+                       $value = self::convertToDecimal( $value );
+               }
+
+               self::assertNumberString( $value, '$value' );
+
+               // make "negative" zero positive
+               $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. The resulting string 
conforms to the
+        * rules described in the class level documentation of @see 
DecimalValue and matches
+        * @see DecimalValue::QUANTITY_VALUE_PATTERN.
+        *
+        * @param int|float $number
+        *
+        * @return string
+        * @throws \InvalidArgumentException
+        */
+       protected static function convertToDecimal( $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.
+        *
+        * @since 0.1
+        *
+        * @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;
+               }
+
+               // 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 ) );
+       }
+
+       /**
+        * @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 value as a decimal string, using the format described in 
the class level
+        * documentation of @see DecimalValue and matching @see 
DecimalValue::QUANTITY_VALUE_PATTERN.
+        * In particular, the string always starts with a sign (either '+' or 
'-')
+        * and has no leading zeros (except immediately before the decimal 
point). The decimal point is
+        * optional, but must not be the last character. Trailing zeros are 
significant.
+        *
+        * @see DataValue::getValue
+        *
+        * @since 0.1
+        *
+        * @return string
+        */
+       public function getValue() {
+               return $this->value;
+       }
+
+       /**
+        * Returns the sign of the amount (+ or -).
+        *
+        * @since 0.1
+        *
+        * @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() );
+       }
+
+       /**
+        * @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 );
+       }
+
+       /**
+        * @since 0.1
+        *
+        * @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..a13e22b
--- /dev/null
+++ b/DataValuesCommon/tests/DataValues/DecimalValueTest.php
@@ -0,0 +1,202 @@
+<?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;
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/88771
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I964734c711f9949be9389a97953520ec7f562ca6
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/extensions/DataValues
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Addshore <[email protected]>
Gerrit-Reviewer: Aude <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Daniel Werner <[email protected]>
Gerrit-Reviewer: Henning Snater <[email protected]>
Gerrit-Reviewer: Jeroen De Dauw <[email protected]>
Gerrit-Reviewer: Tobias Gritschacher <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to