diff --git a/Number.php b/Number.php index 4451640..3c0ca50 100644 --- a/Number.php +++ b/Number.php @@ -15,4 +15,4 @@ return 1; } -define( 'DATAVALUES_NUMBER_VERSION', '0.9.1' ); +define( 'DATAVALUES_NUMBER_VERSION', '0.10.0' ); diff --git a/README.md b/README.md index 50eb9ff..cc7b637 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,16 @@ the [Wikidata project](https://www.wikidata.org/). ## Release notes +### 0.10.0 (2018-04-10) + +* Changed the float to string conversion algorithm for `DecimalValue`, `QuantityValue`, and + `UnboundedQuantityValue`. Instead of a hundred mostly irrelevant decimal places it now uses PHP's + "serialize_precision" default of 17 significant digits. +* Drop compatibility with data-values/interfaces 0.1 and data-values/common 0.2 + ### 0.9.1 (2017-08-09) -* Allow use with ~0.4.0 of DataValues/Common +* Allow use with data-values/common 0.4 ### 0.9.0 (2017-08-09) @@ -103,7 +110,7 @@ the [Wikidata project](https://www.wikidata.org/). #### Other changes * Fixed `DecimalValue` and `QuantityValue` allowing values with a newline at the end. * `DecimalValue` strings are trimmed now, allowing any number of leading and trailing whitespace. -* Added explicit compatibility with DataValues Common 0.2 and 0.3. +* Added explicit compatibility with data-values/common 0.2 and 0.3. ### 0.6.0 (2015-09-09) diff --git a/src/DataValues/DecimalValue.php b/src/DataValues/DecimalValue.php index 1863eb4..50c84d6 100644 --- a/src/DataValues/DecimalValue.php +++ b/src/DataValues/DecimalValue.php @@ -91,29 +91,31 @@ private function convertToDecimal( $number ) { throw new InvalidArgumentException( '$number must not be NAN or INF.' ); } - $decimal = strval( abs( $number ) ); - $decimal = preg_replace_callback( - '/(\d*)\.(\d*)E([-+]\d+)/i', - function ( $matches ) { - list( , $before, $after, $exponent ) = $matches; - - // Fill with as many zeros as necessary, and move the decimal point - if ( $exponent < 0 ) { - $before = str_repeat( '0', -$exponent - strlen( $before ) + 1 ) . $before; - $before = substr_replace( $before, '.', $exponent, 0 ); - } else { - $after .= str_repeat( '0', $exponent - strlen( $after ) ); - $after = substr_replace( $after, '.', $exponent, 0 ); - } - - // Remove not needed ".0" or just "." from the end - return $before . rtrim( rtrim( $after, '0' ), '.' ); - }, - $decimal, - 1 - ); - - return ( $number < 0 ? '-' : '+' ) . $decimal; + /** + * The 16 digits after the decimal point are derived from PHP's "serialize_precision" + * default of 17 significant digits (including 1 digit before the decimal point). This + * ensures a full float-string-float roundtrip. + * @see http://php.net/manual/en/ini.core.php#ini.serialize-precision + */ + $decimal = sprintf( '%.16e', abs( $number ) ); + list( $base, $exponent ) = explode( 'e', $decimal ); + list( $before, $after ) = explode( '.', $base ); + + // Fill with as many zeros as necessary, and move the decimal point + if ( $exponent < 0 ) { + $before = str_repeat( '0', -$exponent - strlen( $before ) + 1 ) . $before; + $before = substr_replace( $before, '.', $exponent, 0 ); + } else { + $pad = $exponent - strlen( $after ); + if ( $pad > 0 ) { + $after .= str_repeat( '0', $pad ); + } + // Always add the decimal point back, even if the exponent is 0 + $after = substr_replace( $after, '.', $exponent, 0 ); + } + + // Remove not needed ".0" or just "." from the end + return ( $number < 0 ? '-' : '+' ) . $before . rtrim( rtrim( $after, '0' ), '.' ); } /** diff --git a/tests/DataValues/DecimalValueTest.php b/tests/DataValues/DecimalValueTest.php index 918546f..1c1cd96 100644 --- a/tests/DataValues/DecimalValueTest.php +++ b/tests/DataValues/DecimalValueTest.php @@ -98,7 +98,10 @@ public function testTrailingNewlineRobustness() { * @dataProvider provideFloats */ public function testFloatInputs( $float, $expectedPrefix ) { + $originalLocale = setlocale( LC_NUMERIC, '0' ); + setlocale( LC_NUMERIC, 'de_DE.utf8' ); $value = DecimalValue::newFromArray( $float ); + setlocale( LC_NUMERIC, $originalLocale ); $this->assertStringStartsWith( $expectedPrefix, $value->getValue(), 'getValue' ); } @@ -179,38 +182,61 @@ public function getSignProvider() { /** * @dataProvider getValueProvider */ - public function testGetValue( DecimalValue $value, $expected ) { - $actual = $value->getValue(); + public function testGetValue( $value, $expected ) { + $precision = ini_set( 'serialize_precision', '2' ); + $actual = ( new DecimalValue( $value ) )->getValue(); + ini_set( 'serialize_precision', $precision ); + $this->assertSame( $expected, $actual ); } public function getValueProvider() { $argLists = []; - $argLists[] = [ new DecimalValue( 42 ), '+42' ]; - $argLists[] = [ new DecimalValue( -42 ), '-42' ]; - $argLists[] = [ new DecimalValue( -42.0 ), '-42' ]; - $argLists[] = [ new DecimalValue( '-42' ), '-42' ]; - $argLists[] = [ new DecimalValue( 4.5 ), '+4.5' ]; - $argLists[] = [ new DecimalValue( -4.5 ), '-4.5' ]; - $argLists[] = [ new DecimalValue( '+4.2' ), '+4.2' ]; - $argLists[] = [ new DecimalValue( 0 ), '+0' ]; - $argLists[] = [ new DecimalValue( 0.0 ), '+0' ]; - $argLists[] = [ new DecimalValue( 1.0 ), '+1' ]; - $argLists[] = [ new DecimalValue( 0.5 ), '+0.5' ]; - $argLists[] = [ new DecimalValue( '-0.42' ), '-0.42' ]; - $argLists[] = [ new DecimalValue( '-0.0' ), '+0.0' ]; - $argLists[] = [ new DecimalValue( '-0' ), '+0' ]; - $argLists[] = [ new DecimalValue( '+0.0' ), '+0.0' ]; - $argLists[] = [ new DecimalValue( '+0' ), '+0' ]; - $argLists[] = [ new DecimalValue( 2147483649 ), '+2147483649' ]; - $argLists[] = [ new DecimalValue( 1000000000000000 ), '+1000000000000000' ]; + $argLists[] = [ 42, '+42' ]; + $argLists[] = [ -42, '-42' ]; + $argLists[] = [ -42.0, '-42' ]; + $argLists[] = [ '-42', '-42' ]; + $argLists[] = [ 4.5, '+4.5' ]; + $argLists[] = [ -4.5, '-4.5' ]; + $argLists[] = [ '+4.2', '+4.2' ]; + $argLists[] = [ 0, '+0' ]; + $argLists[] = [ 0.0, '+0' ]; + $argLists[] = [ 1.0, '+1' ]; + $argLists[] = [ 0.5, '+0.5' ]; + $argLists[] = [ '-0.42', '-0.42' ]; + $argLists[] = [ '-0.0', '+0.0' ]; + $argLists[] = [ '-0', '+0' ]; + $argLists[] = [ '+0.0', '+0.0' ]; + $argLists[] = [ '+0', '+0' ]; + $argLists[] = [ 2147483649, '+2147483649' ]; + $argLists[] = [ 1000000000000000, '+1000000000000000' ]; + $argLists[] = [ + 1 + 1e-12 / 3, + '+1.0000000000003333' + ]; + $argLists[] = [ + 1 + 1e-13 / 3, + '+1.0000000000000333' + ]; + $argLists[] = [ + 1 + 1e-14 / 3, + '+1.0000000000000033' + ]; + $argLists[] = [ + 1 + 1e-15 / 3, + '+1.0000000000000004' + ]; + $argLists[] = [ + 1 + 1e-16 / 3, + '+1' + ]; $argLists[] = [ - new DecimalValue( 1 + 1e-12 / 3 ), - '+1.0000000000003' + 1 - 1e-16, + '+0.99999999999999989' ]; $argLists[] = [ - new DecimalValue( 1 + 1e-14 / 3 ), + 1 - 1e-17, '+1' ];