Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Please see the [upgrade.md](upgrade.md) file.
- **Construct enum by name or value**: `wrap()`, `from()`, `tryFrom()`, `fromName()`, `tryFromName()`, `fromValue()`, `tryFromValue()` methods
- **Enums Inspection**: `isPure()`, `isBacked()`, `has()`, `hasName()`, `hasValue()` methods
- **Enums Equality**: `is()`, `isNot()`, `in()`, `notIn()` methods
- **Comparison**: `Comparable` contract with default `compare()` implementations by value (backed enums) or by name (pure enums)
- **Names**: methods to have a list of case names (`names()`, `namesByValue()`)
- **Values**: methods to have a list of case values (`values()`, `valuesByName()`)
- **Serialization**: get an unique identifier from instance or instance from identifier (`serialize()`, `unserialize()`)
Expand Down Expand Up @@ -91,6 +92,7 @@ The package works with cases written in UPPER_CASE, snake_case and PascalCase.
- [From](#from-fromName)
- [Enums Inspection](#inspection)
- [Enums Equality](#equality)
- [Comparison](#comparison)
- [Names](#names)
- [Values](#values)
- [Serialization](#serialization)
Expand Down Expand Up @@ -356,6 +358,60 @@ StringBackedEnum::PENDING->in(['P', 'D']); // true
StringBackedEnum::PENDING->notIn(['A','D']); // true
```

### Comparison
The `Comparable` contract and the `ComparesByValue` / `ComparesByName` traits are a minimal default implementation to compare and sort enum cases by value (`BackedEnum`) or by name (pure enums).
They are not included in `EnumHelper`: if you need a different ordering (e.g. a custom priority) implement `compare()` by yourself or define your own contract.
If you implement `Comparable` without the trait, `compare()` parameters must be typed as `Comparable` (PHP doesn't allow narrowing them to your enum): check the actual type inside the method.

`compare()` returns `-1`, `0` or `1`: int values are compared numerically, string values with `strcmp()` (so `'10'` comes before `'9'`).
Comparing cases of different enums throws an `InvalidArgumentException`.
```php
use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\Traits\ComparesByValue;

enum StringBackedEnum: string implements Comparable
{
use EnumHelper;
use ComparesByValue;

case PENDING = 'P';
case ACCEPTED = 'A';
case DISCARDED = 'D';
case NO_RESPONSE = 'N';
}

IntBackedEnum::compare(IntBackedEnum::PENDING, IntBackedEnum::ACCEPTED); // -1
StringBackedEnum::compare(StringBackedEnum::PENDING, StringBackedEnum::ACCEPTED); // 1
StringBackedEnum::compare(StringBackedEnum::PENDING, StringBackedEnum::PENDING); // 0

$cases = StringBackedEnum::cases();
usort($cases, StringBackedEnum::compare(...)); // [ACCEPTED, DISCARDED, NO_RESPONSE, PENDING]
```

For pure enums use `ComparesByName`: case names are compared with `strcmp()` (case-sensitive, so `'Z'` comes before `'a'`).
```php
use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\Traits\ComparesByName;

enum PureEnum implements Comparable
{
use EnumHelper;
use ComparesByName;

case PENDING;
case ACCEPTED;
case DISCARDED;
case NO_RESPONSE;
}

PureEnum::compare(PureEnum::ACCEPTED, PureEnum::PENDING); // -1
PureEnum::compare(PureEnum::PENDING, PureEnum::DISCARDED); // 1
PureEnum::compare(PureEnum::PENDING, PureEnum::PENDING); // 0

$cases = PureEnum::cases();
usort($cases, PureEnum::compare(...)); // [ACCEPTED, DISCARDED, NO_RESPONSE, PENDING]
```



### Names
Expand Down
13 changes: 13 additions & 0 deletions src/Contracts/Comparable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Datomatic\EnumHelper\Contracts;

interface Comparable
{
/**
* Compare two cases: returns a negative int if $a < $b, 0 if equal, a positive int if $a > $b.
*/
public static function compare(Comparable $a, Comparable $b): int;
}
27 changes: 27 additions & 0 deletions src/Traits/ComparesByName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Datomatic\EnumHelper\Traits;

use Datomatic\EnumHelper\Contracts\Comparable;
use InvalidArgumentException;

/**
* @phpstan-require-implements \UnitEnum
*/
trait ComparesByName
{
/**
* Compare two cases by name with strcmp (case-sensitive, byte order).
* Returns -1, 0 or 1, so it can be used as usort() callback.
*/
public static function compare(Comparable $a, Comparable $b): int
{
if (! $a instanceof static || ! $b instanceof static) {
throw new InvalidArgumentException(sprintf('Cannot compare %s with %s using %s::compare()', $a::class, $b::class, static::class));
}

return strcmp($a->name, $b->name) <=> 0;
}
}
40 changes: 40 additions & 0 deletions src/Traits/ComparesByValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Datomatic\EnumHelper\Traits;

use BackedEnum;
use Datomatic\EnumHelper\Contracts\Comparable;
use InvalidArgumentException;

/**
* @phpstan-require-implements \BackedEnum
*/
trait ComparesByValue
{
/**
* Compare two cases by value (int values numerically, string values with strcmp).
* Returns -1, 0 or 1, so it can be used as usort() callback.
*/
public static function compare(Comparable $a, Comparable $b): int
{
if (! $a instanceof static || ! $b instanceof static) {
throw new InvalidArgumentException(sprintf('Cannot compare %s with %s using %s::compare()', $a::class, $b::class, static::class));
}

return self::compareBackedValues($a, $b);
}

/**
* Typed as BackedEnum (not static) so static analysis doesn't depend on the backing type of the using enum.
*/
private static function compareBackedValues(BackedEnum $a, BackedEnum $b): int
{
if (is_string($a->value) && is_string($b->value)) {
return strcmp($a->value, $b->value) <=> 0;
}

return $a->value <=> $b->value;
}
}
33 changes: 33 additions & 0 deletions tests/ComparesByNameTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

use Datomatic\EnumHelper\Tests\Support\Enums\IntBackedEnum;
use Datomatic\EnumHelper\Tests\Support\Enums\PureEnum;

it('can compare enum cases by name', function ($a, $b, $result) {
expect($a::compare($a, $b))->toBe($result);
})->with([
[PureEnum::ACCEPTED, PureEnum::PENDING, -1],
[PureEnum::PENDING, PureEnum::DISCARDED, 1],
[PureEnum::PENDING, PureEnum::PENDING, 0],
]);

it('can sort enum cases using compare as usort callback', function () {
$cases = PureEnum::cases();
usort($cases, PureEnum::compare(...));

expect($cases)->toBe([
PureEnum::ACCEPTED,
PureEnum::DISCARDED,
PureEnum::NO_RESPONSE,
PureEnum::PENDING,
]);
});

it('throws an exception comparing cases of different enums', function ($a, $b) {
PureEnum::compare($a, $b);
})->with([
[PureEnum::PENDING, IntBackedEnum::PENDING],
[IntBackedEnum::PENDING, PureEnum::PENDING],
])->throws(InvalidArgumentException::class);
40 changes: 40 additions & 0 deletions tests/ComparesByValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

use Datomatic\EnumHelper\Tests\Support\Enums\IntBackedEnum;
use Datomatic\EnumHelper\Tests\Support\Enums\NumericStringBackedEnum;
use Datomatic\EnumHelper\Tests\Support\Enums\StringBackedEnum;

it('can compare enum cases by value', function ($a, $b, $result) {
expect($a::compare($a, $b))->toBe($result);
})->with([
[IntBackedEnum::PENDING, IntBackedEnum::ACCEPTED, -1],
[IntBackedEnum::NO_RESPONSE, IntBackedEnum::ACCEPTED, 1],
[IntBackedEnum::ACCEPTED, IntBackedEnum::ACCEPTED, 0],
[StringBackedEnum::ACCEPTED, StringBackedEnum::PENDING, -1],
[StringBackedEnum::PENDING, StringBackedEnum::DISCARDED, 1],
[StringBackedEnum::PENDING, StringBackedEnum::PENDING, 0],
[NumericStringBackedEnum::TEN, NumericStringBackedEnum::NINE, -1],
[NumericStringBackedEnum::NINE, NumericStringBackedEnum::TEN, 1],
]);

it('can sort enum cases using compare as usort callback', function () {
$cases = StringBackedEnum::cases();
usort($cases, StringBackedEnum::compare(...));

expect($cases)->toBe([
StringBackedEnum::ACCEPTED,
StringBackedEnum::DISCARDED,
StringBackedEnum::NO_RESPONSE,
StringBackedEnum::PENDING,
]);
});

it('throws an exception comparing cases of different enums', function ($a, $b) {
StringBackedEnum::compare($a, $b);
})->with([
[StringBackedEnum::PENDING, IntBackedEnum::PENDING],
[IntBackedEnum::PENDING, StringBackedEnum::PENDING],
[NumericStringBackedEnum::NINE, NumericStringBackedEnum::TEN],
])->throws(InvalidArgumentException::class);
5 changes: 4 additions & 1 deletion tests/Support/Enums/IntBackedEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Datomatic\EnumHelper\Tests\Support\Enums;

use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\EnumHelper;
use Datomatic\EnumHelper\Traits\ComparesByValue;
use Datomatic\EnumHelper\Traits\EnumDescription;
use Datomatic\EnumHelper\Traits\EnumLabel;
use Datomatic\EnumHelper\Traits\EnumSerialization;
Expand All @@ -17,8 +19,9 @@
* @method static string NO_RESPONSE()
* @method static string NoResponse()
*/
enum IntBackedEnum: int
enum IntBackedEnum: int implements Comparable
{
use ComparesByValue;
use EnumDescription;
use EnumHelper;
use EnumLabel;
Expand Down
17 changes: 17 additions & 0 deletions tests/Support/Enums/NumericStringBackedEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Datomatic\EnumHelper\Tests\Support\Enums;

use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\Traits\ComparesByValue;

enum NumericStringBackedEnum: string implements Comparable
{
use ComparesByValue;

case NINE = '9';

case TEN = '10';
}
5 changes: 4 additions & 1 deletion tests/Support/Enums/PureEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Datomatic\EnumHelper\Tests\Support\Enums;

use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\EnumHelper;
use Datomatic\EnumHelper\Traits\ComparesByName;
use Datomatic\EnumHelper\Traits\EnumDescription;
use Datomatic\EnumHelper\Traits\EnumLabel;
use Datomatic\EnumHelper\Traits\EnumSerialization;
Expand All @@ -18,8 +20,9 @@
* @method static string NoResponse()
* @method static string INVALID()
*/
enum PureEnum
enum PureEnum implements Comparable
{
use ComparesByName;
use EnumDescription;
use EnumHelper;
use EnumLabel;
Expand Down
5 changes: 4 additions & 1 deletion tests/Support/Enums/StringBackedEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Datomatic\EnumHelper\Tests\Support\Enums;

use Datomatic\EnumHelper\Contracts\Comparable;
use Datomatic\EnumHelper\EnumHelper;
use Datomatic\EnumHelper\Traits\ComparesByValue;
use Datomatic\EnumHelper\Traits\EnumDescription;
use Datomatic\EnumHelper\Traits\EnumLabel;
use Datomatic\EnumHelper\Traits\EnumSerialization;
Expand All @@ -18,8 +20,9 @@
* @method static string NoResponse()
* @method static string INVALID()
*/
enum StringBackedEnum: string
enum StringBackedEnum: string implements Comparable
{
use ComparesByValue;
use EnumDescription;
use EnumHelper;
use EnumLabel;
Expand Down
Loading