Skip to content

Commit

Permalink
[Type] add positive-int type (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelpetri authored Apr 7, 2021
1 parent eee8390 commit 963a0bb
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/component/type.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
- [num](./../../src/Psl/Type/num.php#L10)
- [object](./../../src/Psl/Type/object.php#L14)
- [optional](./../../src/Psl/Type/optional.php#L14)
- [positive_int](./../../src/Psl/Type/positive_int.php#L10)
- [resource](./../../src/Psl/Type/resource.php#L12)
- [scalar](./../../src/Psl/Type/scalar.php#L10)
- [shape](./../../src/Psl/Type/shape.php#L15)
Expand Down
2 changes: 2 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ final class Loader
'Psl\Type\null',
'Psl\Type\nullable',
'Psl\Type\optional',
'Psl\Type\positive_int',
'Psl\Type\num',
'Psl\Type\object',
'Psl\Type\resource',
Expand Down Expand Up @@ -595,6 +596,7 @@ final class Loader
'Psl\Type\Internal\NullType',
'Psl\Type\Internal\NullableType',
'Psl\Type\Internal\OptionalType',
'Psl\Type\Internal\PositiveIntType',
'Psl\Type\Internal\NumType',
'Psl\Type\Internal\ObjectType',
'Psl\Type\Internal\ResourceType',
Expand Down
99 changes: 99 additions & 0 deletions src/Psl/Type/Internal/PositiveIntType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use Psl\Str;
use Psl\Type;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;

use function is_float;
use function is_int;
use function is_object;
use function is_string;

/**
* @extends Type\Type<positive-int>
*
* @internal
*/
final class PositiveIntType extends Type\Type
{
/**
* @param mixed $value
*
* @psalm-assert-if-true positive-int $value
*/
public function matches($value): bool
{
return is_int($value) && $value > 0;
}

/**
* @param mixed $value
*
* @throws CoercionException
*
* @return positive-int
*/
public function coerce($value): int
{
if (is_int($value) && $value > 0) {
return $value;
}

if (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
$str = (string)$value;
$int = Str\to_int($str);
if (null !== $int && $int > 0) {
return $int;
}

$trimmed = Str\trim_left($str, '0');
$int = Str\to_int($trimmed);
if (null !== $int && $int > 0) {
return $int;
}

// Exceptional case "000" -(trim)-> "", but we treat it as 0
if ('' === $trimmed && '' !== $str) {
CoercionException::withValue($value, $this->toString(), $this->getTrace());
}
}

if (is_float($value)) {
$integer_value = (int) $value;
$reconstructed = (float) $integer_value;
if ($reconstructed === $value && $integer_value > 0) {
return $integer_value;
}
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/**
* @param mixed $value
*
* @psalm-assert positive-int $value
*
* @throws AssertException
*
* @return positive-int
*/
public function assert($value): int
{
if (is_int($value) && $value > 0) {
return $value;
}

throw AssertException::withValue($value, $this->toString(), $this->getTrace());
}

public function toString(): string
{
return 'positive-int';
}
}
13 changes: 13 additions & 0 deletions src/Psl/Type/positive_int.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Psl\Type;

/**
* @return TypeInterface<positive-int>
*/
function positive_int(): TypeInterface
{
return new Internal\PositiveIntType();
}
69 changes: 69 additions & 0 deletions tests/Psl/Type/PositiveIntTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Type;

use Psl\Math;
use Psl\Type;

/**
* @extends TypeTest<positive-int>
*/
final class PositiveIntTypeTest extends TypeTest
{
public function getType(): Type\TypeInterface
{
return Type\positive_int();
}

public function getValidCoercions(): iterable
{
yield [123, 123];
yield ['123', 123];
yield [$this->stringable('123'), 123];
yield [$this->stringable((string) Math\INT16_MAX), Math\INT16_MAX];
yield [$this->stringable((string) Math\INT64_MAX), Math\INT64_MAX];
yield [(string) Math\INT64_MAX, Math\INT64_MAX];
yield [Math\INT64_MAX, Math\INT64_MAX];
yield ['7', 7];
yield ['07', 7];
yield ['007', 7];
yield [1.0, 1];
}

public function getInvalidCoercions(): iterable
{

yield [0];
yield ['0'];
yield ['-321'];
yield [-321];
yield ['000'];
yield [1.23];
yield ['1.23'];
yield ['1e123'];
yield ['-1e123'];
yield [''];
yield [[]];
yield [[123]];
yield [null];
yield [false];
yield [$this->stringable('1.23')];
yield [$this->stringable('-007')];
yield [$this->stringable('-321')];
yield ['-007'];
yield ['9223372036854775808'];
yield [$this->stringable('9223372036854775808')];
yield ['-9223372036854775809'];
yield [$this->stringable('-9223372036854775809')];
yield ['0xFF'];
yield ['-0xFF'];
yield [''];
}

public function getToStringExamples(): iterable
{
yield [$this->getType(), 'positive-int'];
}
}

0 comments on commit 963a0bb

Please sign in to comment.