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