Skip to content

Commit

Permalink
Merge branch 'next' into pm/add-from-items
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee authored Sep 3, 2024
2 parents 5a7d144 + c10ae65 commit 6f182b9
Show file tree
Hide file tree
Showing 20 changed files with 843 additions and 133 deletions.
198 changes: 101 additions & 97 deletions composer.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions config/infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
},
"DecrementInteger": {
"ignore": [
"Psl\\DataStructure\\PriorityQueue::peek"
"Psl\\DataStructure\\PriorityQueue::peek",
"Psl\\DateTime\\TemporalConvenienceMethodsTrait::since"
]
},
"FunctionCallRemoval": {
Expand All @@ -42,7 +43,8 @@
},
"IncrementInteger": {
"ignore": [
"Psl\\DataStructure\\PriorityQueue::peek"
"Psl\\DataStructure\\PriorityQueue::peek",
"Psl\\DateTime\\TemporalConvenienceMethodsTrait::since"
]
},
"LogicalNot": {
Expand All @@ -58,6 +60,7 @@
},
"Throw_": {
"ignore": [
"Psl\\DateTime\\DateTime::__construct",
"Psl\\File\\ReadHandle::__construct",
"Psl\\File\\WriteHandle::__construct",
"Psl\\File\\ReadWriteHandle::__construct",
Expand Down
9 changes: 4 additions & 5 deletions src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,15 +398,15 @@ public function plusMonths(int $months): static
return $this;
}

if (0 > $months) {
if ($months < 1) {
return $this->minusMonths(-$months);
}

$plus_years = Math\div($months, MONTHS_PER_YEAR);
$months_left = $months - ($plus_years * MONTHS_PER_YEAR);
$target_month = $this->getMonth() + $months_left;

if ($target_month > 12) {
if ($target_month > MONTHS_PER_YEAR) {
$plus_years++;
$target_month = $target_month - MONTHS_PER_YEAR;
}
Expand Down Expand Up @@ -438,7 +438,7 @@ public function minusMonths(int $months): static
return $this;
}

if (0 > $months) {
if ($months < 1) {
return $this->plusMonths(-$months);
}

Expand Down Expand Up @@ -616,10 +616,9 @@ public function toString(null|DateStyle $date_style = null, null|TimeStyle $time
$timestamp = $this->getTimestamp();

/**
* @psalm-suppress InvalidOperand
* @psalm-suppress ImpureMethodCall
*/
return Internal\create_intl_date_formatter($date_style, $time_style, null, $timezone ?? $this->getTimezone(), $locale)
->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
->format($timestamp->getSeconds());
}
}
29 changes: 13 additions & 16 deletions src/Psl/DateTime/Duration.php
Original file line number Diff line number Diff line change
Expand Up @@ -674,27 +674,24 @@ public function toString(int $max_decimals = 3): string
$sec_sign = $this->seconds < 0 || $this->nanoseconds < 0 ? '-' : '';
$sec = Math\abs($this->seconds);

/** @var list<array{string, string}> $values */
$values = [
[((string) $this->hours), 'hour(s)'],
[((string) $this->minutes), 'minute(s)'],
[$sec_sign . ((string) $sec) . $decimal_part, 'second(s)'],
];

// $end is the sizeof($values), use static value for better performance.
$end = 3;
while ($end > 0 && $values[$end - 1][0] === '0') {
--$end;
$containsHours = $this->hours !== 0;
$containsMinutes = $this->minutes !== 0;
$concatenatedSeconds = $sec_sign . ((string) $sec) . $decimal_part;
$containsSeconds = $concatenatedSeconds !== '0';

/** @var list<string> $output */
$output = [];
if ($containsHours) {
$output[] = ((string) $this->hours) . ' hour(s)';
}

$start = 0;
while ($start < $end && $values[$start][0] === '0') {
++$start;
if ($containsMinutes || ($containsHours && $containsSeconds)) {
$output[] = ((string) $this->minutes) . ' minute(s)';
}

$output = [];
for ($i = $start; $i < $end; ++$i) {
$output[] = $values[$i][0] . ' ' . $values[$i][1];
if ($containsSeconds) {
$output[] = $concatenatedSeconds . ' second(s)';
}

return ([] === $output) ? '0 second(s)' : Str\join($output, ', ');
Expand Down
1 change: 1 addition & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ final class Loader
'Psl\\Type\\is_nan' => 'Psl/Type/is_nan.php',
'Psl\\Type\\literal_scalar' => 'Psl/Type/literal_scalar.php',
'Psl\\Type\\backed_enum' => 'Psl/Type/backed_enum.php',
'Psl\\Type\\backed_enum_value' => 'Psl/Type/backed_enum_value.php',
'Psl\\Type\\unit_enum' => 'Psl/Type/unit_enum.php',
'Psl\\Type\\converted' => 'Psl/Type/converted.php',
'Psl\\Json\\encode' => 'Psl/Json/encode.php',
Expand Down
134 changes: 134 additions & 0 deletions src/Psl/Type/Internal/BackedEnumValueType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use BackedEnum;
use Psl\Exception\InvariantViolationException;
use Psl\Exception\RuntimeException;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;
use Psl\Type\Type;
use ReflectionEnum;
use ReflectionException;
use ReflectionNamedType;

use function is_a;
use function is_int;
use function is_string;
use function Psl\invariant;
use function Psl\Type\int;
use function Psl\Type\string;

/**
* @template T of BackedEnum
*
* @extends Type<value-of<T>>
*
* @internal
*/
final readonly class BackedEnumValueType extends Type
{
private bool $isStringBacked;

/**
* @psalm-mutation-free
*
* @param class-string<T> $enum
*
* @throws RuntimeException If reflection fails.
* @throws InvariantViolationException If the given value is not class-string<BackedEnum>.
*/
public function __construct(
private string $enum
) {
$this->isStringBacked = $this->hasStringBackingType($this->enum);
}

/**
* @param class-string<T> $enum
*
* @throws RuntimeException If reflection fails.
* @throws InvariantViolationException If the given value is not class-string<BackedEnum>.
*/
private function hasStringBackingType(string $enum): bool
{
invariant(is_a($enum, BackedEnum::class, true), 'A BackedEnum class-string is required');

// If the enum has any cases, detect its type by inspecting the first case found
$case = $enum::cases()[0] ?? null;
if ($case !== null) {
return is_string($case->value);
}

// Fallback to reflection to detect the backing type:
try {
$reflection = new ReflectionEnum($enum);
$type = $reflection->getBackingType();
invariant($type instanceof ReflectionNamedType, 'Unexpected type');
return $type->getName() === 'string';
} catch (ReflectionException $e) {
throw new RuntimeException('Failed to reflect an enum class-string', 0, $e);
}
}

/**
* @psalm-assert-if-true value-of<T> $value
*/
public function matches(mixed $value): bool
{
return match ($this->isStringBacked) {
true => is_string($value) && $this->enum::tryFrom($value) !== null,
false => is_int($value) && $this->enum::tryFrom($value) !== null,
};
}

/**
* @throws CoercionException
*
* @return value-of<T>
*
* @psalm-suppress MismatchingDocblockReturnType,DocblockTypeContradiction
* Psalm has issues with value-of<T> when used with an enum
*/
public function coerce(mixed $value): string|int
{
try {
$case = $this->isStringBacked
? string()->coerce($value)
: int()->coerce($value);

if ($this->matches($case)) {
return $case;
}
} catch (CoercionException) {
}

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

/**
* @throws AssertException
*
* @return value-of<T>
*
* @psalm-assert value-of<T> $value
*
* @psalm-suppress MismatchingDocblockReturnType
* Psalm has issues with value-of<T> when used with an enum
*/
public function assert(mixed $value): string|int
{
if ($this->matches($value)) {
return $value;
}

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

public function toString(): string
{
return 'value-of<' . $this->enum . '>';
}
}
19 changes: 18 additions & 1 deletion src/Psl/Type/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,24 @@ Provides a type that can parse backed-enums.
Can coerce from:

* `string` when `T` is a string-backed enum.
* `int` when `T` is a string-backed enum.
* `int` when `T` is an integer-backed enum.

---

#### [backed_enum_value](backed_enum_value.php)

```hack
@pure
@template T of BackedEnum
Type\backed_enum_value(class-string<T> $enum): TypeInterface<value-of<T>>
```

Provides a type that can verify a value matches a backed enum value.

Can coerce from:

* `string|int` when `T` is a string-backed enum.
* `int|numeric-string` when `T` is an integer-backed enum.

---

Expand Down
26 changes: 26 additions & 0 deletions src/Psl/Type/backed_enum_value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Psl\Type;

use BackedEnum;
use Psl\Exception\InvariantViolationException;
use Psl\Exception\RuntimeException;

/**
* @psalm-pure
*
* @template T of BackedEnum
*
* @param class-string<T> $enum
*
* @throws RuntimeException If reflection fails.
* @throws InvariantViolationException If the given value is not class-string<BackedEnum>.
*
* @return TypeInterface<value-of<T>>
*/
function backed_enum_value(string $enum): TypeInterface
{
return new Internal\BackedEnumValueType($enum);
}
9 changes: 9 additions & 0 deletions tests/fixture/IntegerEnumWithNoCases.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Fixture;

enum IntegerEnumWithNoCases: int
{
}
9 changes: 9 additions & 0 deletions tests/fixture/StringEnumWithNoCases.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Fixture;

enum StringEnumWithNoCases: string
{
}
10 changes: 10 additions & 0 deletions tests/unit/Collection/MutableSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ public function testFromItems(): void
static::assertSame(['a' => 'a', 'b' => 'b', 'c' => 'c'], $set->toArray());
}

public function testFromArrayKeysConstructor()
{
$set = MutableSet::fromArrayKeys(['foo' => 1, 'bar' => 1, 'baz' => 1]);

static::assertCount(3, $set);
static::assertTrue($set->contains('foo'));
static::assertTrue($set->contains('bar'));
static::assertTrue($set->contains('baz'));
}

/**
* @template T of array-key
*
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/Collection/SetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ protected function createFromList(array $items): Set
{
return Set::fromArray($items);
}

public function testFromArrayKeysConstructor()
{
$set = Set::fromArrayKeys(['foo' => 1, 'bar' => 1, 'baz' => 1]);

static::assertCount(3, $set);
static::assertTrue($set->contains('foo'));
static::assertTrue($set->contains('bar'));
static::assertTrue($set->contains('baz'));
}
}
Loading

0 comments on commit 6f182b9

Please sign in to comment.