Browse Source

Merge pull request #9432 from cakephp/issue-9424

Fix issues working with large float values.
José Lorenzo Rodríguez 9 years ago
parent
commit
b8749fce52

+ 1 - 1
src/Database/Type.php

@@ -37,7 +37,7 @@ class Type implements TypeInterface
         'boolean' => 'Cake\Database\Type\BoolType',
         'date' => 'Cake\Database\Type\DateType',
         'datetime' => 'Cake\Database\Type\DateTimeType',
-        'decimal' => 'Cake\Database\Type\FloatType',
+        'decimal' => 'Cake\Database\Type\DecimalType',
         'float' => 'Cake\Database\Type\FloatType',
         'integer' => 'Cake\Database\Type\IntegerType',
         'json' => 'Cake\Database\Type\JsonType',

+ 179 - 0
src/Database/Type/DecimalType.php

@@ -0,0 +1,179 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.4
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Type;
+
+use Cake\Database\Driver;
+use Cake\Database\Type;
+use Cake\Database\TypeInterface;
+use InvalidArgumentException;
+use PDO;
+use RuntimeException;
+
+/**
+ * Decimal type converter.
+ *
+ * Use to convert decimal data between PHP and the database types.
+ */
+class DecimalType extends Type implements TypeInterface
+{
+
+    /**
+     * Identifier name for this type
+     *
+     * @var string|null
+     */
+    protected $_name = null;
+
+    /**
+     * Constructor
+     *
+     * @param string|null $name The name identifying this type
+     */
+    public function __construct($name = null)
+    {
+        $this->_name = $name;
+    }
+
+    /**
+     * The class to use for representing number objects
+     *
+     * @var string
+     */
+    public static $numberClass = 'Cake\I18n\Number';
+
+    /**
+     * Whether numbers should be parsed using a locale aware parser
+     * when marshalling string inputs.
+     *
+     * @var bool
+     */
+    protected $_useLocaleParser = false;
+
+    /**
+     * Convert integer data into the database format.
+     *
+     * @param string|int|float $value The value to convert.
+     * @param \Cake\Database\Driver $driver The driver instance to convert with.
+     * @return string|null
+     * @throws \InvalidArgumentException
+     */
+    public function toDatabase($value, Driver $driver)
+    {
+        if ($value === null || $value === '') {
+            return null;
+        }
+        if (!is_scalar($value)) {
+            throw new InvalidArgumentException('Cannot convert value to a decimal.');
+        }
+        if (is_string($value) && is_numeric($value)) {
+            return $value;
+        }
+
+        return sprintf('%F', $value);
+    }
+
+    /**
+     * Convert float values to PHP integers
+     *
+     * @param null|string|resource $value The value to convert.
+     * @param \Cake\Database\Driver $driver The driver instance to convert with.
+     * @return resource
+     * @throws \Cake\Core\Exception\Exception
+     */
+    public function toPHP($value, Driver $driver)
+    {
+        if ($value === null) {
+            return null;
+        }
+
+        return (float)$value;
+    }
+
+    /**
+     * Get the correct PDO binding type for integer data.
+     *
+     * @param mixed $value The value being bound.
+     * @param \Cake\Database\Driver $driver The driver.
+     * @return int
+     */
+    public function toStatement($value, Driver $driver)
+    {
+        return PDO::PARAM_STR;
+    }
+
+    /**
+     * Marshalls request data into PHP floats.
+     *
+     * @param mixed $value The value to convert.
+     * @return mixed Converted value.
+     */
+    public function marshal($value)
+    {
+        if ($value === null || $value === '') {
+            return null;
+        }
+        if (is_numeric($value)) {
+            return (float)$value;
+        }
+        if (is_string($value) && $this->_useLocaleParser) {
+            return $this->_parseValue($value);
+        }
+        if (is_array($value)) {
+            return 1;
+        }
+
+        return $value;
+    }
+
+    /**
+     * Sets whether or not to parse numbers passed to the marshal() function
+     * by using a locale aware parser.
+     *
+     * @param bool $enable Whether or not to enable
+     * @return $this
+     */
+    public function useLocaleParser($enable = true)
+    {
+        if ($enable === false) {
+            $this->_useLocaleParser = $enable;
+
+            return $this;
+        }
+        if (static::$numberClass === 'Cake\I18n\Number' ||
+            is_subclass_of(static::$numberClass, 'Cake\I18n\Number')
+        ) {
+            $this->_useLocaleParser = $enable;
+
+            return $this;
+        }
+        throw new RuntimeException(
+            sprintf('Cannot use locale parsing with the %s class', static::$numberClass)
+        );
+    }
+
+    /**
+     * Converts a string into a float point after parseing it using the locale
+     * aware parser.
+     *
+     * @param string $value The value to parse and convert to an float.
+     * @return float
+     */
+    protected function _parseValue($value)
+    {
+        $class = static::$numberClass;
+
+        return $class::parseFloat($value);
+    }
+}

+ 1 - 4
src/Database/Type/FloatType.php

@@ -65,16 +65,13 @@ class FloatType extends Type implements TypeInterface
      *
      * @param string|resource $value The value to convert.
      * @param \Cake\Database\Driver $driver The driver instance to convert with.
-     * @return string|resource
+     * @return string|null
      */
     public function toDatabase($value, Driver $driver)
     {
         if ($value === null || $value === '') {
             return null;
         }
-        if (is_array($value)) {
-            return 1;
-        }
 
         return (float)$value;
     }

+ 39 - 0
tests/Fixture/DatatypesFixture.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.4
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+/**
+ * Fixture for testing decimal, float and bigint types
+ */
+class DatatypesFixture extends TestFixture
+{
+
+    /**
+     * @var array
+     */
+    public $fields = [
+        'id' => ['type' => 'biginteger'],
+        'cost' => ['type' => 'decimal', 'length' => 20, 'precision' => 0, 'null' => true],
+        'floaty' => ['type' => 'float', 'null' => true],
+        '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
+    ];
+
+    /**
+     * @var array
+     */
+    public $records = [];
+}

+ 1 - 1
tests/TestCase/Database/QueryTest.php

@@ -723,7 +723,7 @@ class QueryTest extends TestCase
                     'id' => '1something-crazy',
                     'created <' => new \DateTime('2013-01-01 12:00')
                 ],
-                ['created' => 'datetime', 'id' => 'float']
+                ['created' => 'datetime', 'id' => 'integer']
             )
             ->execute();
         $this->assertCount(1, $result);

+ 186 - 0
tests/TestCase/Database/Type/DecimalTypeTest.php

@@ -0,0 +1,186 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database\Type;
+
+use Cake\Database\Type;
+use Cake\Database\Type\DecimalType;
+use Cake\I18n\I18n;
+use Cake\TestSuite\TestCase;
+use \PDO;
+
+/**
+ * Test for the Decimal type.
+ */
+class DecimalTypeTest extends TestCase
+{
+
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $this->type = Type::build('decimal');
+        $this->driver = $this->getMockBuilder('Cake\Database\Driver')->getMock();
+        $this->locale = I18n::locale();
+        $this->numberClass = DecimalType::$numberClass;
+
+        I18n::locale($this->locale);
+    }
+
+    /**
+     * tearDown method
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+        I18n::locale($this->locale);
+        DecimalType::$numberClass = $this->numberClass;
+    }
+
+    /**
+     * Test toPHP
+     *
+     * @return void
+     */
+    public function testToPHP()
+    {
+        $this->assertNull($this->type->toPHP(null, $this->driver));
+
+        $result = $this->type->toPHP('some data', $this->driver);
+        $this->assertSame(0.0, $result);
+
+        $result = $this->type->toPHP('2', $this->driver);
+        $this->assertSame(2.0, $result);
+
+        $result = $this->type->toPHP('2 bears', $this->driver);
+        $this->assertSame(2.0, $result);
+
+        $result = $this->type->toPHP(['3', '4'], $this->driver);
+        $this->assertSame(1.0, $result);
+    }
+
+    /**
+     * Test converting to database format
+     *
+     * @return void
+     */
+    public function testToDatabase()
+    {
+        $result = $this->type->toDatabase('', $this->driver);
+        $this->assertNull($result);
+
+        $result = $this->type->toDatabase(null, $this->driver);
+        $this->assertNull($result);
+
+        $result = $this->type->toDatabase('some data', $this->driver);
+        $this->assertSame('0.000000', $result);
+
+        $result = $this->type->toDatabase(2, $this->driver);
+        $this->assertSame('2.000000', $result);
+
+        $result = $this->type->toDatabase(2.99, $this->driver);
+        $this->assertSame('2.990000', $result);
+
+        $result = $this->type->toDatabase('2.51', $this->driver);
+        $this->assertSame('2.51', $result);
+    }
+
+    /**
+     * Arrays are invalid.
+     *
+     * @expectedException InvalidArgumentException
+     * @return void
+     */
+    public function testToDatabaseInvalid()
+    {
+        $this->type->toDatabase(['3', '4'], $this->driver);
+    }
+
+    /**
+     * Test marshalling
+     *
+     * @return void
+     */
+    public function testMarshal()
+    {
+        $result = $this->type->marshal('some data', $this->driver);
+        $this->assertSame('some data', $result);
+
+        $result = $this->type->marshal('', $this->driver);
+        $this->assertNull($result);
+
+        $result = $this->type->marshal('2.51', $this->driver);
+        $this->assertSame(2.51, $result);
+
+        $result = $this->type->marshal('3.5 bears', $this->driver);
+        $this->assertSame('3.5 bears', $result);
+
+        $result = $this->type->marshal(['3', '4'], $this->driver);
+        $this->assertSame(1, $result);
+    }
+
+    /**
+     * Tests marshalling numbers using the locale aware parser
+     *
+     * @return void
+     */
+    public function testMarshalWithLocaleParsing()
+    {
+        I18n::locale('de_DE');
+        $this->type->useLocaleParser();
+        $expected = 1234.53;
+        $result = $this->type->marshal('1.234,53');
+        $this->assertEquals($expected, $result);
+
+        I18n::locale('en_US');
+        $this->type->useLocaleParser();
+        $expected = 1234;
+        $result = $this->type->marshal('1,234');
+        $this->assertEquals($expected, $result);
+
+        I18n::locale('pt_BR');
+        $this->type->useLocaleParser();
+        $expected = 5987123.231;
+        $result = $this->type->marshal('5.987.123,231');
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test that exceptions are raised on invalid parsers.
+     *
+     * @expectedException RuntimeException
+     * @return void
+     */
+    public function testUseLocaleParsingInvalid()
+    {
+        DecimalType::$numberClass = 'stdClass';
+        $this->type->useLocaleParser();
+    }
+
+    /**
+     * Test that the PDO binding type is correct.
+     *
+     * @return void
+     */
+    public function testToStatement()
+    {
+        $this->assertEquals(PDO::PARAM_STR, $this->type->toStatement('', $this->driver));
+    }
+}

+ 1 - 1
tests/TestCase/Database/Type/FloatTypeTest.php

@@ -99,7 +99,7 @@ class FloatTypeTest extends TestCase
         $this->assertSame(2.51, $result);
 
         $result = $this->type->toDatabase(['3', '4'], $this->driver);
-        $this->assertSame(1, $result);
+        $this->assertSame(1.0, $result);
     }
 
     /**

+ 1 - 1
tests/TestCase/Database/TypeTest.php

@@ -210,7 +210,7 @@ class TypeTest extends TestCase
         $this->assertSame(3.14159, $type->toPHP('3.14159', $driver));
         $this->assertSame(3.14159, $type->toPHP(3.14159, $driver));
         $this->assertSame(3.0, $type->toPHP(3, $driver));
-        $this->assertSame(1, $type->toPHP(['3', '4'], $driver));
+        $this->assertSame(1.0, $type->toPHP(['3', '4'], $driver));
     }
 
     /**

+ 23 - 0
tests/TestCase/ORM/QueryTest.php

@@ -42,6 +42,7 @@ class QueryTest extends TestCase
         'core.articles_tags',
         'core.authors',
         'core.comments',
+        'core.datatypes',
         'core.posts',
         'core.tags'
     ];
@@ -2913,6 +2914,28 @@ class QueryTest extends TestCase
         $this->assertEquals(2, $result->_matchingData['tags']->id);
     }
 
+
+    /**
+     * Tests that it is possible to find large numeric values.
+     *
+     * @return void
+     */
+    public function testSelectLargeNumbers()
+    {
+        $this->loadFixtures('Datatypes');
+
+        $big = 1234567890123456789.2;
+        $table = TableRegistry::get('Datatypes');
+        $entity = $table->newEntity([]);
+        $entity->cost = $big;
+        $table->save($entity);
+        $out = $table->find()->where([
+            'cost' => $big
+        ])->first();
+        $this->assertNotEmpty($out, 'Should get a record');
+        $this->assertSame(sprintf('%F', $big), sprintf('%F', $out->cost));
+    }
+
     /**
      * Tests that select() can be called with Table and Association
      * instance