diff --git a/readme.md b/readme.md index 0038f534..fa08f9bc 100644 --- a/readme.md +++ b/readme.md @@ -14,6 +14,7 @@ In package nette/utils you will find a set of useful classes for everyday use: ✅ [Arrays](https://doc.nette.org/utils/arrays)
✅ [Callback](https://doc.nette.org/utils/callback) - PHP callbacks
+✅ [Cast](https://doc.nette.org/utils/cast) - Safe lossless type casting
✅ [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …
✅ [Finder](https://doc.nette.org/utils/finder) - finds files and directories
✅ [Floats](https://doc.nette.org/utils/floats) - floating point numbers
diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 00a4a8cd..b256a8ee 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -98,7 +98,7 @@ public static function mergeTree(array $array1, array $array2): array */ public static function getKeyOffset(array $array, string|int $key): ?int { - return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true)); + return Cast::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true)); } diff --git a/src/Utils/Cast.php b/src/Utils/Cast.php new file mode 100644 index 00000000..ef943ab0 --- /dev/null +++ b/src/Utils/Cast.php @@ -0,0 +1,195 @@ + self::toBool($value), + 'int' => self::toInt($value), + 'float' => self::toFloat($value), + 'string' => self::toString($value), + 'array' => self::toArray($value), + default => throw new TypeError("Unsupported type '$type'."), + }; + } + + + /** + * Safely converts a value to a specified type or returns null if the value is null. + * Supported types: bool, int, float, string, array. + * @throws TypeError if the value cannot be converted + */ + public static function toOrNull(mixed $value, string $type): mixed + { + return $value === null ? null : self::to($value, $type); + } + + + /** + * Safely converts a value to a boolean. + * @throws TypeError if the value cannot be converted + */ + public static function toBool(mixed $value): bool + { + return match (true) { + is_bool($value) => $value, + is_int($value) => $value !== 0, + is_float($value) => $value !== 0.0, + is_string($value) => $value !== '' && $value !== '0', + $value === null => false, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to bool.'), + }; + } + + + /** + * Safely converts a value to an integer. + * @throws TypeError if the value cannot be converted + */ + public static function toInt(mixed $value): int + { + return match (true) { + is_bool($value) => (int) $value, + is_int($value) => $value, + is_float($value) => $value === (float) ($tmp = (int) $value) + ? $tmp + : throw new TypeError('Cannot cast ' . self::toString($value) . ' to int.'), + is_string($value) => preg_match('~^-?\d+(\.0*)?$~D', $value) + ? (int) $value + : throw new TypeError("Cannot cast '$value' to int."), + $value === null => 0, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to int.'), + }; + } + + + /** + * Safely converts a value to a float. + * @throws TypeError if the value cannot be converted + */ + public static function toFloat(mixed $value): float + { + return match (true) { + is_bool($value) => $value ? 1.0 : 0.0, + is_int($value) => (float) $value, + is_float($value) => $value, + is_string($value) => preg_match('~^-?\d+(\.\d*)?$~D', $value) + ? (float) $value + : throw new TypeError("Cannot cast '$value' to float."), + $value === null => 0.0, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to float.'), + }; + } + + + /** + * Safely converts a value to a string. + * @throws TypeError if the value cannot be converted + */ + public static function toString(mixed $value): string + { + return match (true) { + is_bool($value) => $value ? '1' : '0', + is_int($value) => (string) $value, + is_float($value) => str_contains($tmp = (string) $value, '.') ? $tmp : $tmp . '.0', + is_string($value) => $value, + $value === null => '', + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to string.'), + }; + } + + + /** + * Wraps the value in an array if it is not already one or returns empty array if the value is null. + */ + public static function toArray(mixed $value): array + { + return match (true) { + is_array($value) => $value, + $value === null => [], + default => [$value], + }; + } + + + /** + * Safely converts a value to a boolean or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toBoolOrNull(mixed $value): ?bool + { + return $value === null ? null : self::toBool($value); + } + + + /** + * Safely converts a value to an integer or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toIntOrNull(mixed $value): ?int + { + return $value === null ? null : self::toInt($value); + } + + + /** + * Safely converts a value to a float or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toFloatOrNull(mixed $value): ?float + { + return $value === null ? null : self::toFloat($value); + } + + + /** + * Safely converts a value to a string or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toStringOrNull(mixed $value): ?string + { + return $value === null ? null : self::toString($value); + } + + + /** + * Wraps the value in an array if it is not already one or returns null if the value is null. + */ + public static function toArrayOrNull(mixed $value): ?array + { + return $value === null ? null : self::toArray($value); + } + + + /** + * Converts false to null, does not change other values. + */ + public static function falseToNull(mixed $value): mixed + { + return $value === false ? null : $value; + } +} diff --git a/src/Utils/Helpers.php b/src/Utils/Helpers.php index b3586c16..bd04094d 100644 --- a/src/Utils/Helpers.php +++ b/src/Utils/Helpers.php @@ -43,9 +43,7 @@ public static function getLastError(): string } - /** - * Converts false to null, does not change other values. - */ + /** use Cast::falseToNull() */ public static function falseToNull(mixed $value): mixed { return $value === false ? null : $value; diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index c0735659..6324cb4d 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -521,7 +521,7 @@ private static function pos(string $haystack, string $needle, int $nth = 1): ?in } } - return Helpers::falseToNull($pos); + return Cast::falseToNull($pos); } diff --git a/tests/Utils/Cast.falseToNull().phpt b/tests/Utils/Cast.falseToNull().phpt new file mode 100644 index 00000000..9a8e6f9c --- /dev/null +++ b/tests/Utils/Cast.falseToNull().phpt @@ -0,0 +1,17 @@ + Cast::toBool([]), + TypeError::class, + 'Cannot cast array to bool.', +); + + +// int +Assert::same(0, Cast::toInt(null)); +Assert::same(0, Cast::toInt(false)); +Assert::same(1, Cast::toInt(true)); +Assert::same(0, Cast::toInt(0)); +Assert::same(1, Cast::toInt(1)); +Assert::exception( + fn() => Cast::toInt(PHP_INT_MAX + 1), + TypeError::class, + 'Cannot cast 9.2233720368548E+18 to int.', +); +Assert::same(0, Cast::toInt(0.0)); +Assert::same(1, Cast::toInt(1.0)); +Assert::exception( + fn() => Cast::toInt(0.1), + TypeError::class, + 'Cannot cast 0.1 to int.', +); +Assert::exception( + fn() => Cast::toInt(''), + TypeError::class, + "Cannot cast '' to int.", +); +Assert::same(0, Cast::toInt('0')); +Assert::same(1, Cast::toInt('1')); +Assert::same(-1, Cast::toInt('-1.')); +Assert::same(1, Cast::toInt('1.0000')); +Assert::exception( + fn() => Cast::toInt('0.1'), + TypeError::class, + "Cannot cast '0.1' to int.", +); +Assert::exception( + fn() => Cast::toInt([]), + TypeError::class, + 'Cannot cast array to int.', +); + + +// float +Assert::same(0.0, Cast::toFloat(null)); +Assert::same(0.0, Cast::toFloat(false)); +Assert::same(1.0, Cast::toFloat(true)); +Assert::same(0.0, Cast::toFloat(0)); +Assert::same(1.0, Cast::toFloat(1)); +Assert::same(0.0, Cast::toFloat(0.0)); +Assert::same(1.0, Cast::toFloat(1.0)); +Assert::same(0.1, Cast::toFloat(0.1)); +Assert::exception( + fn() => Cast::toFloat(''), + TypeError::class, + "Cannot cast '' to float.", +); +Assert::same(0.0, Cast::toFloat('0')); +Assert::same(1.0, Cast::toFloat('1')); +Assert::same(-1.0, Cast::toFloat('-1.')); +Assert::same(1.0, Cast::toFloat('1.0')); +Assert::same(0.1, Cast::toFloat('0.1')); +Assert::exception( + fn() => Cast::toFloat([]), + TypeError::class, + 'Cannot cast array to float.', +); + + +// string +Assert::same('', Cast::toString(null)); +Assert::same('0', Cast::toString(false)); // differs from PHP strict casting +Assert::same('1', Cast::toString(true)); +Assert::same('0', Cast::toString(0)); +Assert::same('1', Cast::toString(1)); +Assert::same('0.0', Cast::toString(0.0)); // differs from PHP strict casting +Assert::same('1.0', Cast::toString(1.0)); // differs from PHP strict casting +Assert::same('-0.1', Cast::toString(-0.1)); +Assert::same('9.2233720368548E+18', Cast::toString(PHP_INT_MAX + 1)); +Assert::same('', Cast::toString('')); +Assert::same('x', Cast::toString('x')); +Assert::exception( + fn() => Cast::toString([]), + TypeError::class, + 'Cannot cast array to string.', +); + + +// array +Assert::same([], Cast::toArray(null)); +Assert::same([false], Cast::toArray(false)); +Assert::same([true], Cast::toArray(true)); +Assert::same([0], Cast::toArray(0)); +Assert::same([0.0], Cast::toArray(0.0)); +Assert::same([1], Cast::toArray([1])); +Assert::equal([new stdClass], Cast::toArray(new stdClass)); // differs from PHP strict casting + + +// OrNull +Assert::true(Cast::toBoolOrNull(true)); +Assert::null(Cast::toBoolOrNull(null)); +Assert::same(0, Cast::toIntOrNull(0)); +Assert::null(Cast::toIntOrNull(null)); +Assert::same(0.0, Cast::toFloatOrNull(0)); +Assert::null(Cast::toFloatOrNull(null)); +Assert::same('0', Cast::toStringOrNull(0)); +Assert::null(Cast::toStringOrNull(null)); +Assert::same([], Cast::toArrayOrNull([])); +Assert::null(Cast::toArrayOrNull(null)); diff --git a/tests/Utils/Cast.to.phpt b/tests/Utils/Cast.to.phpt new file mode 100644 index 00000000..d9a23f1c --- /dev/null +++ b/tests/Utils/Cast.to.phpt @@ -0,0 +1,30 @@ + Cast::to(null, 'unknown'), + TypeError::class, + "Unsupported type 'unknown'.", +); + + +// toOrNull +Assert::null(Cast::toOrNull(null, 'bool')); +Assert::null(Cast::toOrNull(null, 'int')); +Assert::null(Cast::toOrNull(null, 'float')); +Assert::null(Cast::toOrNull(null, 'string')); +Assert::null(Cast::toOrNull(null, 'array')); +Assert::null(Cast::toOrNull(null, 'unknown')); // implementation imperfection