Skip to content

Commit

Permalink
[Regex] Introduce matching() function
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Mar 5, 2021
1 parent f32efe4 commit c3e2cb9
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,13 @@ final class Loader
'Psl\Result\wrap',
'Psl\Regex\split',
'Psl\Regex\matches',
'Psl\Regex\matching',
'Psl\Regex\replace',
'Psl\Regex\replace_by',
'Psl\Regex\replace_every',
'Psl\Regex\Internal\get_prec_error',
'Psl\Regex\Internal\call_preg',
'Psl\Regex\Type\capture_groups',
'Psl\SecureRandom\bytes',
'Psl\SecureRandom\float',
'Psl\SecureRandom\int',
Expand Down
32 changes: 32 additions & 0 deletions src/Psl/Regex/Type/capture_groups.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Psl\Regex\Type;

use Psl\Type\TypeInterface;

use function Psl\Dict\from_keys;
use function Psl\Dict\unique;
use function Psl\Type\shape;
use function Psl\Type\string;

/**
* @param list<array-key> $groups
*
* @return TypeInterface<array<array-key, string>>
*
* @psalm-suppress MixedReturnTypeCoercion - Psalm uses track of the types: Trust the return type above.
*/
function capture_groups(array $groups): TypeInterface
{
return shape(
from_keys(
unique([0, ...$groups]),
/**
* @return TypeInterface<string>
*/
static fn() => string()
)
);
}
46 changes: 46 additions & 0 deletions src/Psl/Regex/matching.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Psl\Regex;

use Psl\Regex\Exception\RuntimeException;
use Psl\Type\Exception\CoercionException;
use Psl\Type\TypeInterface;

use function preg_match;

/**
* Determine if $subject matches the given $pattern and return the matches.
*
* @template T of array
*
* @param non-empty-string $pattern The pattern to match against.
* @param TypeInterface<T> $capture_groups What shape does the matching items have?
*
* @return T
*
* @throws Exception\RuntimeException If an internal error accord.
* @throws CoercionException If the capture groups don't match the pattern matches
* @throws Exception\InvalidPatternException If $pattern is invalid.
*/
function matching(string $subject, string $pattern, TypeInterface $capture_groups, int $offset = 0): array
{
$matching = Internal\call_preg(
'preg_match',
static function () use ($subject, $pattern, $offset): array {
$matching = [];
$matches = preg_match($pattern, $subject, $matching, 0, $offset);

if ($matches === 0) {
throw new RuntimeException(
'Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.'
);
}

return $matching;
}
);

return $capture_groups->coerce($matching);
}
103 changes: 103 additions & 0 deletions tests/Psl/Regex/MatchingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Regex;

use PHPUnit\Framework\TestCase;
use Psl\Regex;
use Psl\Type\TypeInterface;

use function Psl\Regex\Type\capture_groups;

final class MatchingTest extends TestCase
{
/**
* @dataProvider provideMatchingData
*/
public function testMatching(
array $expected,
string $subject,
string $pattern,
TypeInterface $shape,
int $offset = 0
): void {
static::assertSame($expected, Regex\matching($subject, $pattern, $shape, $offset));
}

/**
* @dataProvider provideNonMatchingData
*/
public function testNotMatching(string $subject, string $pattern, int $offset = 0)
{
$this->expectException(Regex\Exception\RuntimeException::class);
$this->expectErrorMessage('Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.');

Regex\matching($subject, $pattern, capture_groups([]), $offset);
}

public function testMatchingWithInvalidPattern(): void
{
$this->expectException(Regex\Exception\InvalidPatternException::class);
$this->expectExceptionMessage("No ending delimiter '/' found");

Regex\matching('hello', '/hello', capture_groups([]));
}

public function provideMatchingData(): iterable
{
yield [
[
0 => 'PHP',
1 => 'PHP',
],
'PHP is the web scripting language of choice.',
'/(php)/i',
capture_groups([0, 1])
];
yield [
[
0 => 'Hello world',
1 => 'Hello',
],
'Hello world is the web scripting language of choice.',
'/(hello) world/i',
capture_groups([0, 1])
];
yield [
[
0 => 'web',
1 => 'web',
],
'PHP is the web scripting language of choice.',
'/(\bweb\b)/i',
capture_groups([0, 1])
];
yield [
[
0 => 'PHP',
'language' => 'PHP',
],
'PHP is the web scripting language of choice.',
'/(?P<language>PHP)/',
capture_groups([0, 'language'])
];
yield [
[
0 => 'http://www.php.net',
1 => 'www.php.net'
],
'http://www.php.net/index.html',
'@^(?:http://)?([^/]+)@i',
capture_groups([1])
];
}

public function provideNonMatchingData(): iterable
{
yield ['PHP is the web scripting language of choice.', '/php/'];
yield ['PHP is the website scripting language of choice.', '/\bweb\b/i'];
yield ['php is the web scripting language of choice.', '/PHP/'];
yield ['hello', '/[^.]+\.[^.]+$/'];
}
}
20 changes: 20 additions & 0 deletions tests/Psl/Regex/Type/CaptureGroupsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Psl\Regex\Type;

use PHPUnit\Framework\TestCase;

final class CaptureGroupsTest extends TestCase
{

public function testItAlwaysAddsZeroCaptureResult(): void
{
$data = [0 => 'Hello', 1 => 'World'];
$shape = capture_groups([1]);
$actual = $shape->coerce($data);

static::assertSame($actual, $data);
}
}

0 comments on commit c3e2cb9

Please sign in to comment.