Skip to content

Commit

Permalink
added Cast
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Aug 7, 2024
1 parent 736c567 commit a846fab
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 5 deletions.
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)<br>
[Callback](https://doc.nette.org/utils/callback) - PHP callbacks<br>
[Cast](https://doc.nette.org/utils/cast) - Safe lossless type casting<br>
[Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …<br>
[Finder](https://doc.nette.org/utils/finder) - finds files and directories<br>
[Floats](https://doc.nette.org/utils/floats) - floating point numbers<br>
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/Arrays.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}


Expand Down
195 changes: 195 additions & 0 deletions src/Utils/Cast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Utils;

use Nette;
use TypeError;


/**
* Provides safe, lossless type casting. Throws TypeError if conversion would result
* in data loss. Supports bool, int, float, string, and array types.
*/
final class Cast
{
use Nette\StaticClass;

/**
* Safely converts a value to a specified type. Supported types: bool, int, float, string, array.
* @throws TypeError if the value cannot be converted
*/
public static function to(mixed $value, string $type): mixed
{
return match ($type) {
'bool' => 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;
}
}
4 changes: 1 addition & 3 deletions src/Utils/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/Strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ private static function pos(string $haystack, string $needle, int $nth = 1): ?in
}
}

return Helpers::falseToNull($pos);
return Cast::falseToNull($pos);
}


Expand Down
17 changes: 17 additions & 0 deletions tests/Utils/Cast.falseToNull().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

use Nette\Utils\Cast;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::same(1, Cast::falseToNull(1));
Assert::same(0, Cast::falseToNull(0));
Assert::same(null, Cast::falseToNull(null));
Assert::same(true, Cast::falseToNull(true));
Assert::same(null, Cast::falseToNull(false));
Assert::same([], Cast::falseToNull([]));
135 changes: 135 additions & 0 deletions tests/Utils/Cast.methods.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

use Nette\Utils\Cast;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


// bool
Assert::false(Cast::toBool(null));
Assert::true(Cast::toBool(true));
Assert::true(Cast::toBool(1));
Assert::true(Cast::toBool(2));
Assert::true(Cast::toBool(0.1));
Assert::true(Cast::toBool('1'));
Assert::true(Cast::toBool('0.0'));
Assert::false(Cast::toBool(false));
Assert::false(Cast::toBool(0));
Assert::false(Cast::toBool(0.0));
Assert::false(Cast::toBool(''));
Assert::false(Cast::toBool('0'));
Assert::exception(
fn() => 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));
Loading

0 comments on commit a846fab

Please sign in to comment.