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