forked from cakephp/cakephp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Methods for converting mixed values to specific data types
This PR introduces a set of methods for converting mixed values to string/int/bool data types, ensuring a type-safe approach to data manipulation. This utility is useful for safely narrowing down the data types. cakephp#17177 (comment) https://en.wikipedia.org/wiki/IEEE_754 https://www.h-schmidt.net/FloatConverter/IEEE754.html https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
- Loading branch information
1 parent
4a1556a
commit a3270c4
Showing
2 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org) | ||
* Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org) | ||
* @since 5.0.1 | ||
* @license https://opensource.org/licenses/mit-license.php MIT License | ||
*/ | ||
|
||
namespace Cake\Utility; | ||
|
||
use JsonException; | ||
use Stringable; | ||
|
||
/** | ||
* Methods for converting mixed values to specific data types, ensuring a type-safe approach to data manipulation. | ||
* This utility is useful for safely narrowing down the data types. | ||
*/ | ||
class Filter | ||
{ | ||
/** | ||
* Converts the given value to a string. | ||
* | ||
* This method attempts to convert the given value to a string. | ||
* If the value is already a string, it returns the value as it is. | ||
* If the conversion is not possible, it returns NULL. | ||
* | ||
* @param mixed $value The value to be converted. | ||
* @return ?string Returns the string representation of the value, or null if the value is not a string. | ||
*/ | ||
public static function toString(mixed $value): ?string | ||
{ | ||
if (is_string($value)) { | ||
return $value; | ||
} elseif (is_int($value)) { | ||
return (string)$value; | ||
} elseif (is_bool($value)) { | ||
return $value ? '1' : '0'; | ||
} elseif (is_float($value)) { | ||
if (is_nan($value) || is_infinite($value)) { | ||
return null; | ||
} | ||
try { | ||
$return = json_encode($value, JSON_THROW_ON_ERROR); | ||
} catch (JsonException) { | ||
$return = null; | ||
} | ||
|
||
if ($return === null || str_contains($return, 'e')) { | ||
$return = rtrim(sprintf('%.' . (PHP_FLOAT_DIG + 3) . 'F', $value), '.0'); | ||
} | ||
|
||
return $return; | ||
} elseif ($value instanceof Stringable) { | ||
return (string)$value; | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Converts a value to an integer. | ||
* | ||
* This method attempts to convert the given value to an integer. | ||
* If the conversion is successful, it returns the value as an integer. | ||
* If the conversion fails, it returns NULL. | ||
* | ||
* String values are trimmed using trim(). | ||
* | ||
* @param mixed $value The value to be converted to an integer. | ||
* @return int|null Returns the converted integer value or null if the conversion fails. | ||
*/ | ||
public static function toInt(mixed $value): ?int | ||
{ | ||
if (is_int($value)) { | ||
return $value; | ||
} elseif (is_string($value)) { | ||
$value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); | ||
|
||
return $value === PHP_INT_MIN ? null : $value; | ||
} elseif (is_float($value)) { | ||
/** | ||
* @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER | ||
* 9007199254740991 = 2^53-1 = the maximum safe integer that can be represented without losing precision. | ||
* Beyond this numerical limit, the equality (int)9007199254740993.0 === 9007199254740992 returns true. | ||
*/ | ||
if ($value >= -9007199254740991 && $value <= 9007199254740991) { | ||
return (int)$value; | ||
} | ||
|
||
return null; | ||
} elseif (is_bool($value)) { | ||
return (int)$value; | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Converts a value to boolean. | ||
* | ||
* 1 | '1' | 1.0 | true - values returns as true | ||
* 0 | '0' | 0.0 | false - values returns as false | ||
* Other values returns as null. | ||
* | ||
* @param mixed $value The value to convert to boolean. | ||
* @return bool|null Returns true if the value is truthy, false if it's falsy, or NULL otherwise. | ||
*/ | ||
public static function toBool(mixed $value): ?bool | ||
{ | ||
if ($value === '1' || $value === 1 || $value === 1.0 || $value === true) { | ||
return true; | ||
} elseif ($value === '0' || $value === 0 || $value === 0.0 || $value === false) { | ||
return false; | ||
} else { | ||
return null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org) | ||
* Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org) | ||
* @link https://cakephp.org CakePHP(tm) Project | ||
* @since 5.0.1 | ||
* @license https://opensource.org/licenses/mit-license.php MIT License | ||
*/ | ||
|
||
namespace Cake\Test\TestCase\Utility; | ||
|
||
use Cake\I18n\Time; | ||
use Cake\Utility\Filter; | ||
use PHPUnit\Framework\TestCase; | ||
use stdClass; | ||
|
||
class FilterTest extends TestCase | ||
{ | ||
/** | ||
* @dataProvider toStringProvider | ||
*/ | ||
public function testToString(mixed $rawValue, ?string $expected): void | ||
{ | ||
$this->assertSame($expected, Filter::toString($rawValue)); | ||
} | ||
|
||
/** | ||
* @return array The array of test cases. | ||
*/ | ||
public static function toStringProvider(): array | ||
{ | ||
return [ | ||
// input like string | ||
'(string) empty' => ['', ''], | ||
'(string) space' => [' ', ' '], | ||
'(string) dash' => ['-', '-'], | ||
'(string) zero' => ['0', '0'], | ||
'(string) number' => ['55', '55'], | ||
'(string) partially2 number' => ['5x', '5x'], | ||
// input like int | ||
'(int) number' => [55, '55'], | ||
'(int) negative number' => [-5, '-5'], | ||
'(int) PHP_INT_MAX + 2' => [9223372036854775809, '9223372036854775808'], //is float: see IEEE 754 | ||
'(int) PHP_INT_MAX + 1' => [9223372036854775808, '9223372036854775808'], //is float: see IEEE 754 | ||
'(int) PHP_INT_MAX + 0' => [9223372036854775807, '9223372036854775807'], | ||
'(int) PHP_INT_MAX - 1' => [9223372036854775806, '9223372036854775806'], | ||
'(int) PHP_INT_MIN + 1' => [-9223372036854775807, '-9223372036854775807'], | ||
'(int) PHP_INT_MIN + 0' => [-9223372036854775808, '-9223372036854775808'], | ||
'(int) PHP_INT_MIN - 1' => [-9223372036854775809, '-9223372036854775808'], //is float: see IEEE 754 | ||
'(int) PHP_INT_MIN - 2' => [-9223372036854775810, '-9223372036854775808'], //is float: see IEEE 754 | ||
// input like float | ||
'(float) zero' => [0.0, '0'], | ||
'(float) positive' => [5.5, '5.5'], | ||
'(float) round' => [5.0, '5'], | ||
'(float) negative' => [-5.5, '-5.5'], | ||
'(float) round negative' => [-5.0, '-5'], | ||
'(float) small' => [0.000000000003, '0.000000000003'], | ||
'(float) small2' => [64321.0000003, '64321.0000003'], | ||
'(float) fractions' => [-9223372036778.2233, '-9223372036778.223'], //is float: see IEEE 754 | ||
'(float) NaN' => [acos(8), null], | ||
'(float) INF' => [INF, null], | ||
'(float) -INF' => [-INF, null], | ||
// boolean input types | ||
'(bool) true' => [true, '1'], | ||
'(bool) false' => [false, '0'], | ||
// other input types | ||
'(other) null' => [null, null], | ||
'(other) empty-array' => [[], null], | ||
'(other) int-array' => [[5], null], | ||
'(other) string-array' => [['5'], null], | ||
'(other) simple object' => [new stdClass(), null], | ||
'(other) Stringable object' => [new Time('10:10:10'), '10:10 AM'], | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider toIntProvider | ||
*/ | ||
public function testToInt(mixed $rawValue, null|int $expected): void | ||
{ | ||
$this->assertSame($expected, Filter::toInt($rawValue)); | ||
} | ||
|
||
/** | ||
* @return array The array of test cases. | ||
*/ | ||
public static function toIntProvider(): array | ||
{ | ||
return [ | ||
// string input types | ||
'(string) empty' => ['', null], | ||
'(string) space' => [' ', null], | ||
'(string) null' => ['null', null], | ||
'(string) dash' => ['-', null], | ||
'(string) ctz' => ['čťž', null], | ||
'(string) hex' => ['0x539', null], | ||
'(string) binary' => ['0b10100111001', null], | ||
'(string) scientific e' => ['1.2e+2', null], | ||
'(string) scientific E' => ['1.2E+2', null], | ||
'(string) octal old' => ['0123', null], | ||
'(string) octal new' => ['0o123', null], | ||
'(string) decimal php74' => ['1_234_567', null], | ||
'(string) zero' => ['0', 0], | ||
'(string) number' => ['55', 55], | ||
'(string) number_space_before' => [' 55', 55], | ||
'(string) number_space_after' => ['55 ', 55], | ||
'(string) negative number' => ['-5', -5], | ||
'(string) float round' => ['5.0', null], | ||
'(string) float round negative' => ['-5.0', null], | ||
'(string) float real' => ['5.1', null], | ||
'(string) float round slovak' => ['5,0', null], | ||
'(string) money' => ['5 €', null], | ||
'(string) PHP_INT_MAX + 1' => ['9223372036854775808', null], | ||
'(string) PHP_INT_MAX + 0' => ['9223372036854775807', 9223372036854775807], | ||
'(string) PHP_INT_MAX - 1' => ['9223372036854775806', 9223372036854775806], | ||
'(string) PHP_INT_MIN + 1' => ['-9223372036854775807', -9223372036854775807], | ||
'(string) PHP_INT_MIN + 0' => ['-9223372036854775808', null], | ||
'(string) PHP_INT_MIN - 1' => ['-9223372036854775809', null], | ||
'(string) string' => ['f', null], | ||
'(string) partially1 number' => ['5 5', null], | ||
'(string) partially2 number' => ['5x', null], | ||
'(string) partially3 number' => ['x4', null], | ||
'(string) double dot' => ['5.1.0', null], | ||
// int input types | ||
'(int) number' => [55, 55], | ||
'(int) negative number' => [-5, -5], | ||
'(int) PHP_INT_MAX + 1' => [9223372036854775808, null], | ||
'(int) PHP_INT_MAX + 0' => [9223372036854775807, 9223372036854775807], | ||
'(int) PHP_INT_MAX - 1' => [9223372036854775806, 9223372036854775806], | ||
'(int) PHP_INT_MIN + 1' => [-9223372036854775807, -9223372036854775807], | ||
// PHP_INT_MIN is float -> PHP inconsistency https://bugs.php.net/bug.php?id=53934 | ||
'(int) PHP_INT_MIN + 0' => [-9223372036854775808, null], | ||
'(int) PHP_INT_MIN - 1' => [-9223372036854775809, null], | ||
// float input types | ||
'(float) zero' => [0.0, 0], | ||
'(float) positive' => [5.5, 5], | ||
'(float) round' => [5.0, 5], | ||
'(float) negative' => [-5.5, -5], | ||
'(float) round negative' => [-5.0, -5], | ||
'(float) PHP_INT_MAX + 1' => [9223372036854775808.0, null], | ||
'(float) PHP_INT_MAX + 0' => [9223372036854775807.0, null], | ||
'(float) PHP_INT_MAX - 1' => [9223372036854775806.0, null], | ||
'(float) PHP_INT_MIN + 1' => [-9223372036854775807.0, null], | ||
'(float) PHP_INT_MIN + 0' => [-9223372036854775808.0, null], | ||
'(float) PHP_INT_MIN - 1' => [-9223372036854775809.0, null], | ||
'(float) 2^53 + 2' => [9007199254740994.0, null], | ||
'(float) 2^53 + 1' => [9007199254740993.0, null], | ||
'(float) 2^53 + 0' => [9007199254740992.0, null], | ||
'(float) 2^53 - 1' => [9007199254740991.0, 9007199254740991], | ||
'(float) 2^53 - 2' => [9007199254740990.0, 9007199254740990], | ||
'(float) -(2^53) + 2' => [-9007199254740990.0, -9007199254740990], | ||
'(float) -(2^53) + 1' => [-9007199254740991.0, -9007199254740991], | ||
'(float) -(2^53) + 0' => [-9007199254740992.0, null], | ||
'(float) -(2^53) - 1' => [-9007199254740992.0, null], | ||
'(float) -(2^53) - 2' => [-9007199254740994.0, null], | ||
'(float) NaN' => [acos(8), null], | ||
'(float) INF' => [INF, null], | ||
'(float) -INF' => [-INF, null], | ||
// boolean input types | ||
'(bool) true' => [true, 1], | ||
'(bool) false' => [false, 0], | ||
// other input types | ||
'(other) null' => [null, null], | ||
'(other) empty-array' => [[], null], | ||
'(other) int-array' => [[5], null], | ||
'(other) string-array' => [['5'], null], | ||
'(other) simple object' => [new stdClass(), null], | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider toBoolProvider | ||
*/ | ||
public function testToBool(mixed $rawValue, ?bool $expected): void | ||
{ | ||
$this->assertSame($expected, Filter::toBool($rawValue)); | ||
} | ||
|
||
/** | ||
* @return array The array of test cases. | ||
*/ | ||
public static function toBoolProvider(): array | ||
{ | ||
return [ | ||
// string input types | ||
'(string) empty string' => ['', null], | ||
'(string) space' => [' ', null], | ||
'(string) some word' => ['abc', null], | ||
'(string) double 0' => ['00', null], | ||
'(string) single 0' => ['0', false], | ||
'(string) false' => ['false', null], | ||
'(string) double 1' => ['11', null], | ||
'(string) single 1' => ['1', true], | ||
'(string) true-string' => ['true', null], | ||
// int input types | ||
'(int) 0' => [0, false], | ||
'(int) 1' => [1, true], | ||
'(int) -1' => [-1, null], | ||
'(int) 55' => [55, null], | ||
'(int) negative number' => [-5, null], | ||
// float input types | ||
'(float) positive' => [5.5, null], | ||
'(float) round' => [5.0, null], | ||
'(float) 0.0' => [0.0, false], | ||
'(float) 1.0' => [1.0, true], | ||
'(float) NaN' => [acos(8), null], | ||
'(float) INF' => [INF, null], | ||
'(float) -INF' => [-INF, null], | ||
// boolean input types | ||
'(bool) true' => [true, true], | ||
'(bool) false' => [false, false], | ||
// other input types | ||
'(other) null' => [null, null], | ||
'(other) empty-array' => [[], null], | ||
'(other) int-array' => [[5], null], | ||
'(other) string-array' => [['5'], null], | ||
'(other) simple object' => [new stdClass(), null], | ||
]; | ||
} | ||
} |