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 fc12ead commit 20bd93c
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 3 deletions.
194 changes: 194 additions & 0 deletions src/Utils/Cast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?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;


/**
* Converts variables in a similar way to implicit casting in PHP in strict types mode.
*/
final class Cast
{
use Nette\StaticClass;

/**
* 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'."),
};
}


/**
* 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);
}


/**
* 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.'),
};
}


/**
* 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.'),
};
}


/**
* 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.'),
};
}


/**
* 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.'),
};
}


/**
* Ensures the value is an array. Wraps the value in an array if it is not already one.
*/
public static function toArray(mixed $value): array
{
return match (true) {
is_array($value) => $value,
$value === null => [],
default => [$value],
};
}


/**
* 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);
}


/**
* 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);
}


/**
* 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);
}


/**
* 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);
}


/**
* Converts a value to an array 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.
*/
/** @deprecated use Cast::falseToNull() */
public static function falseToNull(mixed $value): mixed
{
return $value === false ? null : $value;
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));
30 changes: 30 additions & 0 deletions tests/Utils/Cast.to.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

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

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


// to
Assert::same(false, Cast::to(null, 'bool'));
Assert::same(0, Cast::to(null, 'int'));
Assert::same(0.0, Cast::to(null, 'float'));
Assert::same('', Cast::to(null, 'string'));
Assert::same([], Cast::to(null, 'array'));
Assert::exception(
fn() => 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

0 comments on commit 20bd93c

Please sign in to comment.