diff --git a/system/DataCaster/Cast/FloatCast.php b/system/DataCaster/Cast/FloatCast.php index d2173826265a..858ff7ce7aac 100644 --- a/system/DataCaster/Cast/FloatCast.php +++ b/system/DataCaster/Cast/FloatCast.php @@ -13,6 +13,8 @@ namespace CodeIgniter\DataCaster\Cast; +use CodeIgniter\DataCaster\Exceptions\CastException; + /** * Class FloatCast * @@ -30,6 +32,20 @@ public static function get( self::invalidTypeValueError($value); } - return (float) $value; + $precision = isset($params[0]) ? (int) $params[0] : null; + + if ($precision === null) { + return (float) $value; + } + + $mode = match (strtolower($params[1] ?? 'up')) { + 'up' => PHP_ROUND_HALF_UP, + 'down' => PHP_ROUND_HALF_DOWN, + 'even' => PHP_ROUND_HALF_EVEN, + 'odd' => PHP_ROUND_HALF_ODD, + default => throw CastException::forInvalidFloatRoundingMode($params[1]), + }; + + return round((float) $value, $precision, $mode); } } diff --git a/system/Entity/Cast/FloatCast.php b/system/Entity/Cast/FloatCast.php index 1a767c0953f0..e2fc12b9ec45 100644 --- a/system/Entity/Cast/FloatCast.php +++ b/system/Entity/Cast/FloatCast.php @@ -13,10 +13,26 @@ namespace CodeIgniter\Entity\Cast; +use CodeIgniter\DataCaster\Exceptions\CastException; + class FloatCast extends BaseCast { public static function get($value, array $params = []): float { - return (float) $value; + $precision = isset($params[0]) ? (int) $params[0] : null; + + if ($precision === null) { + return (float) $value; + } + + $mode = match (strtolower($params[1] ?? 'up')) { + 'up' => PHP_ROUND_HALF_UP, + 'down' => PHP_ROUND_HALF_DOWN, + 'even' => PHP_ROUND_HALF_EVEN, + 'odd' => PHP_ROUND_HALF_ODD, + default => throw CastException::forInvalidFloatRoundingMode($params[1]), + }; + + return round((float) $value, $precision, $mode); } } diff --git a/system/Entity/Exceptions/CastException.php b/system/Entity/Exceptions/CastException.php index 033f1ced478e..252eb0c42df2 100644 --- a/system/Entity/Exceptions/CastException.php +++ b/system/Entity/Exceptions/CastException.php @@ -122,4 +122,12 @@ public static function forInvalidEnumType(string $expectedClass, string $actualC { return new static(lang('Cast.enumInvalidType', [$actualClass, $expectedClass])); } + + /** + * Thrown when an invalid rounding mode is provided for float casting. + */ + public static function forInvalidFloatRoundingMode(string $mode): static + { + return new static(lang('Cast.invalidFloatRoundingMode', [$mode])); + } } diff --git a/system/Language/en/Cast.php b/system/Language/en/Cast.php index 63d9fba01b7c..d4762634ca81 100644 --- a/system/Language/en/Cast.php +++ b/system/Language/en/Cast.php @@ -13,18 +13,19 @@ // Cast language settings return [ - 'baseCastMissing' => 'The "{0}" class must inherit the "CodeIgniter\Entity\Cast\BaseCast" class.', - 'enumInvalidCaseName' => 'Invalid case name "{0}" for enum "{1}".', - 'enumInvalidType' => 'Expected enum of type "{1}", but received "{0}".', - 'enumInvalidValue' => 'Invalid value "{1}" for enum "{0}".', - 'enumMissingClass' => 'Enum class must be specified for enum casting.', - 'enumNotEnum' => 'The "{0}" is not a valid enum class.', - 'invalidCastMethod' => 'The "{0}" is invalid cast method, valid methods are: ["get", "set"].', - 'invalidTimestamp' => 'Type casting "timestamp" expects a correct timestamp.', - 'jsonErrorCtrlChar' => 'Unexpected control character found.', - 'jsonErrorDepth' => 'Maximum stack depth exceeded.', - 'jsonErrorStateMismatch' => 'Underflow or the modes mismatch.', - 'jsonErrorSyntax' => 'Syntax error, malformed JSON.', - 'jsonErrorUnknown' => 'Unknown error.', - 'jsonErrorUtf8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.', + 'baseCastMissing' => 'The "{0}" class must inherit the "CodeIgniter\Entity\Cast\BaseCast" class.', + 'enumInvalidCaseName' => 'Invalid case name "{0}" for enum "{1}".', + 'enumInvalidType' => 'Expected enum of type "{1}", but received "{0}".', + 'enumInvalidValue' => 'Invalid value "{1}" for enum "{0}".', + 'enumMissingClass' => 'Enum class must be specified for enum casting.', + 'enumNotEnum' => 'The "{0}" is not a valid enum class.', + 'invalidCastMethod' => 'The "{0}" is invalid cast method, valid methods are: ["get", "set"].', + 'invalidTimestamp' => 'Type casting "timestamp" expects a correct timestamp.', + 'jsonErrorCtrlChar' => 'Unexpected control character found.', + 'jsonErrorDepth' => 'Maximum stack depth exceeded.', + 'jsonErrorStateMismatch' => 'Underflow or the modes mismatch.', + 'jsonErrorSyntax' => 'Syntax error, malformed JSON.', + 'jsonErrorUnknown' => 'Unknown error.', + 'jsonErrorUtf8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.', + 'invalidFloatRoundingMode' => 'Invalid rounding mode "{0}" for float casting.', ]; diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 220e101ba762..9e4bcc49c25b 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -197,6 +197,62 @@ public static function provideConvertDataFromDB(): iterable 'temp' => 15.9, ], ], + 'float precise' => [ + [ + 'id' => 'int', + 'temp' => 'float[2]', + ], + [ + 'id' => '1', + 'temp' => '15.98765', + ], + [ + 'id' => 1, + 'temp' => 15.99, + ], + ], + 'float precise-down' => [ + [ + 'id' => 'int', + 'temp' => 'float[2,down]', + ], + [ + 'id' => '1', + 'temp' => '1.235', + ], + [ + 'id' => 1, + 'temp' => 1.23, + ], + ], + 'float precise-even' => [ + [ + 'id' => 'int', + 'temp' => 'float[2,even]', + ], + [ + 'id' => '1', + 'temp' => '20.005', + ], + [ + 'id' => 1, + 'temp' => 20.00, + ], + ], + 'float precise-odd' => [ + [ + 'id' => 'int', + 'temp' => 'float[2,odd]', + ], + [ + 'id' => '1', + 'temp' => '1.255', + ], + [ + 'id' => 1, + 'temp' => 1.25, + ], + ], 'enum string-backed' => [ [ 'id' => 'int', diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 7c19d9d09b89..2d5ed2671f1a 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -419,6 +419,36 @@ public function testCastFloat(): void $this->assertEqualsWithDelta(3.6, $entity->second, PHP_FLOAT_EPSILON); } + public function testCastFloatWithPrecision(): void + { + $entity = $this->getCastEntity(); + + $entity->fourteenth = 3.1415926535; + + $this->assertIsFloat($entity->fourteenth); + $this->assertEqualsWithDelta(3.14, $entity->fourteenth, PHP_FLOAT_EPSILON); + + $entity->fourteenth = '3.1415926535'; + + $this->assertIsFloat($entity->fourteenth); + $this->assertEqualsWithDelta(3.14, $entity->fourteenth, PHP_FLOAT_EPSILON); + } + + public function testCastFloatWithPrecisionAndRoundingMode(): void + { + $entity = $this->getCastEntity(); + + $entity->fifteenth = 3.145; + + $this->assertIsFloat($entity->fifteenth); + $this->assertEqualsWithDelta(3.14, $entity->fifteenth, PHP_FLOAT_EPSILON); + + $entity->fifteenth = '3.135'; + + $this->assertIsFloat($entity->fifteenth); + $this->assertEqualsWithDelta(3.13, $entity->fifteenth, PHP_FLOAT_EPSILON); + } + public function testCastDouble(): void { $entity = $this->getCastEntity(); @@ -1750,6 +1780,8 @@ private function getCastEntity($data = null): object 'eleventh' => 'json-array', 'twelfth' => 'csv', 'thirteenth' => 'uri', + 'fourteenth' => 'float[2]', + 'fifteenth' => 'float[2,down]', ]; public function setSeventh(string $seventh): void diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index e026fdadaa11..99c0adfc8355 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -249,6 +249,9 @@ Validation Others ====== +- **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities. +- Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided. + *************** Message Changes *************** diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 56c17114138e..f31c2981ba87 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -261,6 +261,7 @@ Add a question mark at the beginning of type to mark property as nullable, i.e., .. note:: **int-bool** can be used since v4.3.0. .. note:: **enum** can be used since v4.7.0. +.. note:: Since v4.8.0, you can also pass parameters to **float** and **double** types to specify the number of decimal places and rounding mode, i.e., **float[2,even]**. For example, if you had a User entity with an ``is_banned`` property, you can cast it as a boolean: diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index beab1d17c5c7..800ae304f117 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -380,6 +380,20 @@ of type to mark the field as nullable, i.e., ``?int``, ``?datetime``. |``enum`` | Enum | string/int type | +---------------+----------------+---------------------------+ +float +----- + +Casting as ``float`` will convert the value to a float type in PHP. +This is best used with database columns that are of a float or numeric type. + +You can also pass arguments to the ``float`` type to specify the number +of decimal places to round to as well as the rounding mode (up, down, even or odd). + +.. literalinclude:: model/067.php + +.. note:: Prior to v4.8.0 the ``float`` type did not support any parameters. + It simply converted the value to a float type in PHP without rounding. + csv --- diff --git a/user_guide_src/source/models/model/067.php b/user_guide_src/source/models/model/067.php new file mode 100644 index 000000000000..128fe1313956 --- /dev/null +++ b/user_guide_src/source/models/model/067.php @@ -0,0 +1,16 @@ + 'int', + 'currency' => 'string', + 'amount' => 'float[2,even]', + ]; + // ... +}