Skip to content

Commit 5a3bc42

Browse files
authored
feat(cast): Add precision and rounding mode in FloatCast (#10086)
1 parent 444fc29 commit 5a3bc42

File tree

10 files changed

+246
-29
lines changed

10 files changed

+246
-29
lines changed

system/DataCaster/Cast/FloatCast.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace CodeIgniter\DataCaster\Cast;
1515

16+
use CodeIgniter\DataCaster\Exceptions\CastException;
17+
1618
/**
1719
* Class FloatCast
1820
*
@@ -30,6 +32,20 @@ public static function get(
3032
self::invalidTypeValueError($value);
3133
}
3234

33-
return (float) $value;
35+
$precision = isset($params[0]) ? (int) $params[0] : null;
36+
37+
if ($precision === null) {
38+
return (float) $value;
39+
}
40+
41+
$mode = match (strtolower($params[1] ?? 'up')) {
42+
'up' => PHP_ROUND_HALF_UP,
43+
'down' => PHP_ROUND_HALF_DOWN,
44+
'even' => PHP_ROUND_HALF_EVEN,
45+
'odd' => PHP_ROUND_HALF_ODD,
46+
default => throw CastException::forInvalidFloatRoundingMode($params[1]),
47+
};
48+
49+
return round((float) $value, $precision, $mode);
3450
}
3551
}

system/Entity/Cast/FloatCast.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,26 @@
1313

1414
namespace CodeIgniter\Entity\Cast;
1515

16+
use CodeIgniter\Entity\Exceptions\CastException;
17+
1618
class FloatCast extends BaseCast
1719
{
1820
public static function get($value, array $params = []): float
1921
{
20-
return (float) $value;
22+
$precision = isset($params[0]) ? (int) $params[0] : null;
23+
24+
if ($precision === null) {
25+
return (float) $value;
26+
}
27+
28+
$mode = match (strtolower($params[1] ?? 'up')) {
29+
'up' => PHP_ROUND_HALF_UP,
30+
'down' => PHP_ROUND_HALF_DOWN,
31+
'even' => PHP_ROUND_HALF_EVEN,
32+
'odd' => PHP_ROUND_HALF_ODD,
33+
default => throw CastException::forInvalidFloatRoundingMode($params[1]),
34+
};
35+
36+
return round((float) $value, $precision, $mode);
2137
}
2238
}

system/Entity/Exceptions/CastException.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,12 @@ public static function forInvalidEnumType(string $expectedClass, string $actualC
122122
{
123123
return new static(lang('Cast.enumInvalidType', [$actualClass, $expectedClass]));
124124
}
125+
126+
/**
127+
* Thrown when an invalid rounding mode is provided for float casting.
128+
*/
129+
public static function forInvalidFloatRoundingMode(string $mode): static
130+
{
131+
return new static(lang('Cast.invalidFloatRoundingMode', [$mode]));
132+
}
125133
}

system/Language/en/Cast.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@
1313

1414
// Cast language settings
1515
return [
16-
'baseCastMissing' => 'The "{0}" class must inherit the "CodeIgniter\Entity\Cast\BaseCast" class.',
17-
'enumInvalidCaseName' => 'Invalid case name "{0}" for enum "{1}".',
18-
'enumInvalidType' => 'Expected enum of type "{1}", but received "{0}".',
19-
'enumInvalidValue' => 'Invalid value "{1}" for enum "{0}".',
20-
'enumMissingClass' => 'Enum class must be specified for enum casting.',
21-
'enumNotEnum' => 'The "{0}" is not a valid enum class.',
22-
'invalidCastMethod' => 'The "{0}" is invalid cast method, valid methods are: ["get", "set"].',
23-
'invalidTimestamp' => 'Type casting "timestamp" expects a correct timestamp.',
24-
'jsonErrorCtrlChar' => 'Unexpected control character found.',
25-
'jsonErrorDepth' => 'Maximum stack depth exceeded.',
26-
'jsonErrorStateMismatch' => 'Underflow or the modes mismatch.',
27-
'jsonErrorSyntax' => 'Syntax error, malformed JSON.',
28-
'jsonErrorUnknown' => 'Unknown error.',
29-
'jsonErrorUtf8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.',
16+
'baseCastMissing' => 'The "{0}" class must inherit the "CodeIgniter\Entity\Cast\BaseCast" class.',
17+
'enumInvalidCaseName' => 'Invalid case name "{0}" for enum "{1}".',
18+
'enumInvalidType' => 'Expected enum of type "{1}", but received "{0}".',
19+
'enumInvalidValue' => 'Invalid value "{1}" for enum "{0}".',
20+
'enumMissingClass' => 'Enum class must be specified for enum casting.',
21+
'enumNotEnum' => 'The "{0}" is not a valid enum class.',
22+
'invalidCastMethod' => 'The "{0}" is invalid cast method, valid methods are: ["get", "set"].',
23+
'invalidTimestamp' => 'Type casting "timestamp" expects a correct timestamp.',
24+
'jsonErrorCtrlChar' => 'Unexpected control character found.',
25+
'jsonErrorDepth' => 'Maximum stack depth exceeded.',
26+
'jsonErrorStateMismatch' => 'Underflow or the modes mismatch.',
27+
'jsonErrorSyntax' => 'Syntax error, malformed JSON.',
28+
'jsonErrorUnknown' => 'Unknown error.',
29+
'jsonErrorUtf8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.',
30+
'invalidFloatRoundingMode' => 'Invalid rounding mode "{0}" for float casting.',
3031
];

tests/system/DataConverter/DataConverterTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,62 @@ public static function provideConvertDataFromDB(): iterable
197197
'temp' => 15.9,
198198
],
199199
],
200+
'float precise' => [
201+
[
202+
'id' => 'int',
203+
'temp' => 'float[2]',
204+
],
205+
[
206+
'id' => '1',
207+
'temp' => '15.98765',
208+
],
209+
[
210+
'id' => 1,
211+
'temp' => 15.99,
212+
],
213+
],
214+
'float precise-down' => [
215+
[
216+
'id' => 'int',
217+
'temp' => 'float[2,down]',
218+
],
219+
[
220+
'id' => '1',
221+
'temp' => '1.235',
222+
],
223+
[
224+
'id' => 1,
225+
'temp' => 1.23,
226+
],
227+
],
228+
'float precise-even' => [
229+
[
230+
'id' => 'int',
231+
'temp' => 'float[2,even]',
232+
],
233+
[
234+
'id' => '1',
235+
'temp' => '20.005',
236+
],
237+
[
238+
'id' => 1,
239+
'temp' => 20.00,
240+
],
241+
],
242+
'float precise-odd' => [
243+
[
244+
'id' => 'int',
245+
'temp' => 'float[2,odd]',
246+
],
247+
[
248+
'id' => '1',
249+
'temp' => '1.255',
250+
],
251+
[
252+
'id' => 1,
253+
'temp' => 1.25,
254+
],
255+
],
200256
'enum string-backed' => [
201257
[
202258
'id' => 'int',
@@ -984,4 +1040,20 @@ public static function provideEnumExceptions(): iterable
9841040
],
9851041
];
9861042
}
1043+
1044+
public function testInvalidFloatRoundingMode(): void
1045+
{
1046+
$this->expectException(CastException::class);
1047+
$this->expectExceptionMessage('Invalid rounding mode "wrong" for float casting.');
1048+
1049+
$converter = $this->createDataConverter([
1050+
'id' => 'int',
1051+
'temp' => 'float[2,wrong]',
1052+
]);
1053+
1054+
$converter->fromDataSource([
1055+
'id' => '123456',
1056+
'temp' => 123.456,
1057+
]);
1058+
}
9871059
}

tests/system/Entity/EntityTest.php

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,72 @@ public function testCastFloat(): void
419419
$this->assertEqualsWithDelta(3.6, $entity->second, PHP_FLOAT_EPSILON);
420420
}
421421

422+
public function testCastFloatWithPrecision(): void
423+
{
424+
$entity = $this->getCastEntity();
425+
426+
$entity->fourteenth = 3.1415926535;
427+
428+
$this->assertIsFloat($entity->fourteenth);
429+
$this->assertEqualsWithDelta(3.14, $entity->fourteenth, PHP_FLOAT_EPSILON);
430+
431+
$entity->fourteenth = '3.1415926535';
432+
433+
$this->assertIsFloat($entity->fourteenth);
434+
$this->assertEqualsWithDelta(3.14, $entity->fourteenth, PHP_FLOAT_EPSILON);
435+
}
436+
437+
public function testCastFloatWithPrecisionAndRoundingMode(): void
438+
{
439+
$entity = $this->getCastEntity();
440+
441+
$entity->fifteenth = 3.145;
442+
443+
$this->assertIsFloat($entity->fifteenth);
444+
$this->assertEqualsWithDelta(3.14, $entity->fifteenth, PHP_FLOAT_EPSILON);
445+
446+
$entity->fifteenth = '3.135';
447+
448+
$this->assertIsFloat($entity->fifteenth);
449+
$this->assertEqualsWithDelta(3.13, $entity->fifteenth, PHP_FLOAT_EPSILON);
450+
451+
$entity->sixteenth = 20.0005;
452+
453+
$this->assertIsFloat($entity->sixteenth);
454+
$this->assertEqualsWithDelta(20.000, $entity->sixteenth, PHP_FLOAT_EPSILON);
455+
456+
$entity->sixteenth = '20.0005';
457+
458+
$this->assertIsFloat($entity->sixteenth);
459+
$this->assertEqualsWithDelta(20.000, $entity->sixteenth, PHP_FLOAT_EPSILON);
460+
461+
$entity->seventeenth = 1.25;
462+
463+
$this->assertIsFloat($entity->seventeenth);
464+
$this->assertEqualsWithDelta(1.3, $entity->seventeenth, PHP_FLOAT_EPSILON);
465+
466+
$entity->seventeenth = '1.25';
467+
468+
$this->assertIsFloat($entity->seventeenth);
469+
$this->assertEqualsWithDelta(1.3, $entity->seventeenth, PHP_FLOAT_EPSILON);
470+
}
471+
472+
public function testCastFloatWithInvalidRoundingMode(): void
473+
{
474+
$this->expectException(CastException::class);
475+
$this->expectExceptionMessage('Invalid rounding mode "invalidMode" for float casting.');
476+
477+
$entity = new class () extends Entity {
478+
protected $casts = [
479+
'temp' => 'float[1,invalidMode]',
480+
];
481+
};
482+
483+
$entity->temp = '4.548';
484+
485+
$entity->temp; // @phpstan-ignore expr.resultUnused
486+
}
487+
422488
public function testCastDouble(): void
423489
{
424490
$entity = $this->getCastEntity();
@@ -1737,19 +1803,23 @@ private function getCastEntity($data = null): object
17371803

17381804
// 'bar' is db column, 'foo' is internal representation
17391805
protected $casts = [
1740-
'first' => 'integer',
1741-
'second' => 'float',
1742-
'third' => 'double',
1743-
'fourth' => 'string',
1744-
'fifth' => 'boolean',
1745-
'sixth' => 'object',
1746-
'seventh' => 'array',
1747-
'eighth' => 'datetime',
1748-
'ninth' => 'timestamp',
1749-
'tenth' => 'json',
1750-
'eleventh' => 'json-array',
1751-
'twelfth' => 'csv',
1752-
'thirteenth' => 'uri',
1806+
'first' => 'integer',
1807+
'second' => 'float',
1808+
'third' => 'double',
1809+
'fourth' => 'string',
1810+
'fifth' => 'boolean',
1811+
'sixth' => 'object',
1812+
'seventh' => 'array',
1813+
'eighth' => 'datetime',
1814+
'ninth' => 'timestamp',
1815+
'tenth' => 'json',
1816+
'eleventh' => 'json-array',
1817+
'twelfth' => 'csv',
1818+
'thirteenth' => 'uri',
1819+
'fourteenth' => 'float[2]',
1820+
'fifteenth' => 'float[2,down]',
1821+
'sixteenth' => 'float[3,even]',
1822+
'seventeenth' => 'float[1,odd]',
17531823
];
17541824

17551825
public function setSeventh(string $seventh): void

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ Validation
249249
Others
250250
======
251251

252+
- **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities.
253+
- Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided.
254+
252255
***************
253256
Message Changes
254257
***************

user_guide_src/source/models/entities.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ Add a question mark at the beginning of type to mark property as nullable, i.e.,
261261

262262
.. note:: **int-bool** can be used since v4.3.0.
263263
.. note:: **enum** can be used since v4.7.0.
264+
.. 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]**.
264265

265266
For example, if you had a User entity with an ``is_banned`` property, you can cast it as a boolean:
266267

user_guide_src/source/models/model.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,20 @@ of type to mark the field as nullable, i.e., ``?int``, ``?datetime``.
380380
|``enum`` | Enum | string/int type |
381381
+---------------+----------------+---------------------------+
382382

383+
float
384+
-----
385+
386+
Casting as ``float`` will convert the value to a float type in PHP.
387+
This is best used with database columns that are of a float or numeric type.
388+
389+
You can also pass arguments to the ``float`` type to specify the number
390+
of decimal places to round to as well as the rounding mode (up, down, even or odd).
391+
392+
.. literalinclude:: model/067.php
393+
394+
.. note:: Prior to v4.8.0 the ``float`` type did not support any parameters.
395+
It simply converted the value to a float type in PHP without rounding.
396+
383397
csv
384398
---
385399

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use CodeIgniter\Model;
6+
7+
class TransactionModel extends Model
8+
{
9+
// ...
10+
protected array $casts = [
11+
'id' => 'int',
12+
'currency' => 'string',
13+
'amount' => 'float[2,even]',
14+
];
15+
// ...
16+
}

0 commit comments

Comments
 (0)