Skip to content

Commit

Permalink
feat(str): add range function (#385)
Browse files Browse the repository at this point in the history
Signed-off-by: azjezz <azjezz@protonmail.com>
  • Loading branch information
azjezz committed Dec 22, 2022
1 parent 443f942 commit 981769f
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 1 deletion.
3 changes: 3 additions & 0 deletions config/.phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<file>../config</file>
<file>../tests</file>

<!-- enums -->
<exclude-pattern>*/src/Psl/File/(WriteMode|LockType).php</exclude-pattern>
<exclude-pattern>*/src/Psl/Network/SocketScheme.php</exclude-pattern>
<exclude-pattern>*/src/Psl/Str/Encoding.php</exclude-pattern>
Expand All @@ -16,6 +17,8 @@
<exclude-pattern>*/src/Psl/OS/OperatingSystemFamily.php</exclude-pattern>
<exclude-pattern>*/src/Psl/Password/Algorithm.php</exclude-pattern>
<exclude-pattern>*/src/Psl/Shell/ErrorOutputBehavior.php</exclude-pattern>
<!-- file includes a really long string made up of grapheme cluster -->
<exclude-pattern>*/tests/unit/Str/Grapheme/RangeTest.php</exclude-pattern>

<arg name="basepath" value="."/>
<arg name="colors"/>
Expand Down
1 change: 1 addition & 0 deletions docs/component/str-byte.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [ord](./../../src/Psl/Str/Byte/ord.php#L12)
- [pad_left](./../../src/Psl/Str/Byte/pad_left.php#L24)
- [pad_right](./../../src/Psl/Str/Byte/pad_right.php#L24)
- [range](./../../src/Psl/Str/Byte/range.php#L42)
- [replace](./../../src/Psl/Str/Byte/replace.php#L15)
- [replace_ci](./../../src/Psl/Str/Byte/replace_ci.php#L15)
- [replace_every](./../../src/Psl/Str/Byte/replace_every.php#L17)
Expand Down
1 change: 1 addition & 0 deletions docs/component/str-grapheme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [ends_with](./../../src/Psl/Str/Grapheme/ends_with.php#L16)
- [ends_with_ci](./../../src/Psl/Str/Grapheme/ends_with_ci.php#L16)
- [length](./../../src/Psl/Str/Grapheme/length.php#L18)
- [range](./../../src/Psl/Str/Grapheme/range.php#L43)
- [reverse](./../../src/Psl/Str/Grapheme/reverse.php#L16)
- [search](./../../src/Psl/Str/Grapheme/search.php#L27)
- [search_ci](./../../src/Psl/Str/Grapheme/search_ci.php#L27)
Expand Down
1 change: 1 addition & 0 deletions docs/component/str.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- [ord](./../../src/Psl/Str/ord.php#L22)
- [pad_left](./../../src/Psl/Str/pad_left.php#L34)
- [pad_right](./../../src/Psl/Str/pad_right.php#L34)
- [range](./../../src/Psl/Str/range.php#L41)
- [repeat](./../../src/Psl/Str/repeat.php#L26)
- [replace](./../../src/Psl/Str/replace.php#L15)
- [replace_ci](./../../src/Psl/Str/replace_ci.php#L16)
Expand Down
3 changes: 3 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ final class Loader
'Psl\\Str\\Byte\\search_last_ci' => 'Psl/Str/Byte/search_last_ci.php',
'Psl\\Str\\Byte\\shuffle' => 'Psl/Str/Byte/shuffle.php',
'Psl\\Str\\Byte\\slice' => 'Psl/Str/Byte/slice.php',
'Psl\\Str\\Byte\\range' => 'Psl/Str/Byte/range.php',
'Psl\\Str\\Byte\\splice' => 'Psl/Str/Byte/splice.php',
'Psl\\Str\\Byte\\split' => 'Psl/Str/Byte/split.php',
'Psl\\Str\\Byte\\starts_with' => 'Psl/Str/Byte/starts_with.php',
Expand Down Expand Up @@ -275,6 +276,7 @@ final class Loader
'Psl\\Str\\search_last' => 'Psl/Str/search_last.php',
'Psl\\Str\\search_last_ci' => 'Psl/Str/search_last_ci.php',
'Psl\\Str\\slice' => 'Psl/Str/slice.php',
'Psl\\Str\\range' => 'Psl/Str/range.php',
'Psl\\Str\\splice' => 'Psl/Str/splice.php',
'Psl\\Str\\split' => 'Psl/Str/split.php',
'Psl\\Str\\starts_with' => 'Psl/Str/starts_with.php',
Expand Down Expand Up @@ -366,6 +368,7 @@ final class Loader
'Psl\\Str\\Grapheme\\search_last' => 'Psl/Str/Grapheme/search_last.php',
'Psl\\Str\\Grapheme\\search_last_ci' => 'Psl/Str/Grapheme/search_last_ci.php',
'Psl\\Str\\Grapheme\\slice' => 'Psl/Str/Grapheme/slice.php',
'Psl\\Str\\Grapheme\\range' => 'Psl/Str/Grapheme/range.php',
'Psl\\Str\\Grapheme\\starts_with' => 'Psl/Str/Grapheme/starts_with.php',
'Psl\\Str\\Grapheme\\starts_with_ci' => 'Psl/Str/Grapheme/starts_with_ci.php',
'Psl\\Str\\Grapheme\\strip_prefix' => 'Psl/Str/Grapheme/strip_prefix.php',
Expand Down
64 changes: 64 additions & 0 deletions src/Psl/Str/Byte/range.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Psl\Str\Byte;

use Psl\Range\LowerBoundRangeInterface;
use Psl\Range\RangeInterface;
use Psl\Range\UpperBoundRangeInterface;
use Psl\Str\Exception;

/**
* Slice a string using a range.
*
* If the range doesn't have an upper range, the slice will contain the
* rest of the string. If the upper-bound is equal to the lower-bound,
* then an empty string will be returned.
*
* Example:
*
* ```php
* use Psl\Range;
* use Psl\Str\Byte;
*
* $string = 'Hello, World!';
*
* Byte\range($string, Range\between(0, 3, upper_inclusive: true)); // 'Hell'
* Byte\range($string, Range\between(0, 3, upper_inclusive: false)); // 'Hel'
* Byte\range($string, Range\from(3)); // 'lo, World!'
* Byte\range($string, Range\to(3, true)); // 'Hell'
* Byte\range($string, Range\to(3, false)); // 'Hel'
* Byte\range($string, Range\full()); // 'Hello, World!'
* Byte\range($string, Range\between(7, 5, true)); // 'World'
* ```
*
* @param RangeInterface<int> $range
*
* @throws Exception\OutOfBoundsException If the $offset is out-of-bounds.
*
* @pure
*/
function range(string $string, RangeInterface $range): string
{
$offset = 0;
$length = null;
if ($range instanceof LowerBoundRangeInterface) {
/** @var int<0, max> $offset */
$offset = $range->getLowerBound();
}

if ($range instanceof UpperBoundRangeInterface) {
/**
* @psalm-suppress InvalidOperand
*
* @var int<0, max> $length
*/
$length = $range->getUpperBound() - $offset;
if ($range->isUpperInclusive()) {
$length += 1;
}
}

return slice($string, $offset, $length);
}
65 changes: 65 additions & 0 deletions src/Psl/Str/Grapheme/range.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Psl\Str\Grapheme;

use Psl\Range\LowerBoundRangeInterface;
use Psl\Range\RangeInterface;
use Psl\Range\UpperBoundRangeInterface;
use Psl\Str\Exception;

/**
* Slice a string using a range.
*
* If the range doesn't have an upper range, the slice will contain the
* rest of the string. If the upper-bound is equal to the lower-bound,
* then an empty string will be returned.
*
* Example:
*
* ```php
* use Psl\Range;
* use Psl\Str;
*
* $string = 'Hello, World!';
*
* Str\range($string, Range\between(0, 3, upper_inclusive: true)); // 'Hell'
* Str\range($string, Range\between(0, 3, upper_inclusive: false)); // 'Hel'
* Str\range($string, Range\from(3)); // 'lo, World!'
* Str\range($string, Range\to(3, true)); // 'Hell'
* Str\range($string, Range\to(3, false)); // 'Hel'
* Str\range($string, Range\full()); // 'Hello, World!'
* Str\range($string, Range\between(7, 5, true)); // 'World'
* ```
*
* @param RangeInterface<int> $range
*
* @throws Exception\OutOfBoundsException If the $offset is out-of-bounds.
* @throws Exception\InvalidArgumentException If $string is not made of grapheme clusters.
*
* @pure
*/
function range(string $string, RangeInterface $range): string
{
$offset = 0;
$length = null;
if ($range instanceof LowerBoundRangeInterface) {
/** @var int<0, max> $offset */
$offset = $range->getLowerBound();
}

if ($range instanceof UpperBoundRangeInterface) {
/**
* @psalm-suppress InvalidOperand
*
* @var int<0, max> $length
*/
$length = $range->getUpperBound() - $offset;
if ($range->isUpperInclusive()) {
$length += 1;
}
}

return slice($string, $offset, $length);
}
2 changes: 1 addition & 1 deletion src/Psl/Str/Grapheme/slice.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @pure
*
* @throws Str\Exception\OutOfBoundsException If $offset is out-of-bounds.
* @throws Str\Exception\InvalidArgumentException If $haystack is not made of grapheme clusters.
* @throws Str\Exception\InvalidArgumentException If $string is not made of grapheme clusters.
*/
function slice(string $string, int $offset, ?int $length = null): string
{
Expand Down
63 changes: 63 additions & 0 deletions src/Psl/Str/range.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Psl\Str;

use Psl\Range\LowerBoundRangeInterface;
use Psl\Range\RangeInterface;
use Psl\Range\UpperBoundRangeInterface;

/**
* Slice a string using a range.
*
* If the range doesn't have an upper range, the slice will contain the
* rest of the string. If the upper-bound is equal to the lower-bound,
* then an empty string will be returned.
*
* Example:
*
* ```php
* use Psl\Range;
* use Psl\Str;
*
* $string = 'Hello, World!';
*
* Str\range($string, Range\between(0, 3, upper_inclusive: true)); // 'Hell'
* Str\range($string, Range\between(0, 3, upper_inclusive: false)); // 'Hel'
* Str\range($string, Range\from(3)); // 'lo, World!'
* Str\range($string, Range\to(3, true)); // 'Hell'
* Str\range($string, Range\to(3, false)); // 'Hel'
* Str\range($string, Range\full()); // 'Hello, World!'
* Str\range($string, Range\between(7, 5, true)); // 'World'
* ```
*
* @param RangeInterface<int> $range
*
* @throws Exception\OutOfBoundsException If the $offset is out-of-bounds.
*
* @pure
*/
function range(string $string, RangeInterface $range, Encoding $encoding = Encoding::UTF_8): string
{
$offset = 0;
$length = null;
if ($range instanceof LowerBoundRangeInterface) {
/** @var int<0, max> $offset */
$offset = $range->getLowerBound();
}

if ($range instanceof UpperBoundRangeInterface) {
/**
* @psalm-suppress InvalidOperand
*
* @var int<0, max> $length
*/
$length = $range->getUpperBound() - $offset;
if ($range->isUpperInclusive()) {
$length += 1;
}
}

return slice($string, $offset, $length, $encoding);
}
59 changes: 59 additions & 0 deletions tests/unit/Str/Byte/RangeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Unit\Str\Byte;

use PHPUnit\Framework\TestCase;
use Psl\Range;
use Psl\Str\Byte;
use Psl\Str\Exception;

final class RangeTest extends TestCase
{
/**
* @param Range\RangeInterface<int> $range
*
* @dataProvider provideData
*/
public function testRange(string $expected, string $string, Range\RangeInterface $range): void
{
static::assertSame($expected, Byte\range($string, $range));
}

/**
* @return list<{0: string, 1: string, 2: Range\RangeInterface<int>}>
*/
public function provideData(): array
{
return [
['', '', Range\between(0, 5, upper_inclusive: true)],
['Hello,', 'Hello, World!', Range\between(0, 5, upper_inclusive: true)],
['Hello', 'Hello, World!', Range\between(0, 5, upper_inclusive: false)],
['Hello, World!', 'Hello, World!', Range\from(0)],
['World!', 'Hello, World!', Range\between(7, 12, upper_inclusive: true)],
['World', 'Hello, World!', Range\between(7, 12, upper_inclusive: false)],
['destiny', 'People linked by destiny will always find each other.', Range\between(17, 23, upper_inclusive: true)],
['destiny', 'People linked by destiny will always find each other.', Range\between(17, 24, upper_inclusive: false)],
['hel', 'hello world', Range\to(3, inclusive: false)],
['', 'lo world', Range\between(3, 3)],
['', 'foo', Range\between(3, 3)],
['', 'foo', Range\between(3, 12)],
['foo', 'foo', Range\full()],
];
}

public function testRangeThrowsForOutOfBoundOffset(): void
{
$this->expectException(Exception\OutOfBoundsException::class);

Byte\range('Hello', Range\from(10));
}

public function testRangeThrowsForNegativeOutOfBoundOffset(): void
{
$this->expectException(Exception\OutOfBoundsException::class);

Byte\range('Hello', Range\from(-6));
}
}
67 changes: 67 additions & 0 deletions tests/unit/Str/Grapheme/RangeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Unit\Str\Grapheme;

use PHPUnit\Framework\TestCase;
use Psl\Range;
use Psl\Str\Exception;
use Psl\Str\Grapheme;

final class RangeTest extends TestCase
{
/**
* @param Range\RangeInterface<int> $range
*
* @dataProvider provideData
*/
public function testRange(string $expected, string $string, Range\RangeInterface $range): void
{
static::assertSame($expected, Grapheme\range($string, $range));
}

/**
* @return list<{0: string, 1: string, 2: Range\RangeInterface<int>}>
*/
public function provideData(): array
{
return [
['', '', Range\between(0, 5, upper_inclusive: true)],
['Hello,', 'Hello, World!', Range\between(0, 5, upper_inclusive: true)],
['Hello', 'Hello, World!', Range\between(0, 5, upper_inclusive: false)],
['Hello, World!', 'Hello, World!', Range\from(0)],
['World!', 'Hello, World!', Range\between(7, 12, upper_inclusive: true)],
['World', 'Hello, World!', Range\between(7, 12, upper_inclusive: false)],
['سيف', 'مرحبا سيف', Range\between(6, 9, upper_inclusive: true)],
['اهلا', 'اهلا بكم', Range\between(0, 3, upper_inclusive: true)],
['اهلا', 'اهلا بكم', Range\between(0, 4, upper_inclusive: false)],
['destiny', 'People linked by destiny will always find each other.', Range\between(17, 23, upper_inclusive: true)],
['destiny', 'People linked by destiny will always find each other.', Range\between(17, 24, upper_inclusive: false)],
['lö ', 'héllö wôrld', Range\between(3, 5, upper_inclusive: true)],
['lö ', 'héllö wôrld', Range\between(3, 6, upper_inclusive: false)],
['lö wôrld', 'héllö wôrld', Range\from(3)],
['héll', 'héllö wôrld', Range\to(3, inclusive: true)],
['hél', 'héllö wôrld', Range\to(3, inclusive: false)],
['', 'lö wôrld', Range\between(3, 3)],
['', 'fôo', Range\between(3, 3)],
['', 'fôo', Range\between(3, 12)],
['fôo', 'fôo', Range\full()],
['he̡̙̬͎̿́̐̅̕͢l͕̮͕͈̜͐̈́̇̕͠ļ͚͉̗̘̽͑̿͑̚o̼̰̼͕̞̍̄̎̿̊,̻̰̻̘́̎͒̋͘͟ ̧̬̝͈̬̿͌̿̑̕ẉ̣̟͉̮͆̊̃͐̈́ờ̢̫͎͖̹͊́͐r̨̮͓͓̣̅̋͐͐͆ḻ̩̦͚̯͑̌̓̅͒d͇̯͔̼͍͛̾͛͡͝', 'he̡̙̬͎̿́̐̅̕͢l͕̮͕͈̜͐̈́̇̕͠ļ͚͉̗̘̽͑̿͑̚o̼̰̼͕̞̍̄̎̿̊,̻̰̻̘́̎͒̋͘͟ ̧̬̝͈̬̿͌̿̑̕ẉ̣̟͉̮͆̊̃͐̈́ờ̢̫͎͖̹͊́͐r̨̮͓͓̣̅̋͐͐͆ḻ̩̦͚̯͑̌̓̅͒d͇̯͔̼͍͛̾͛͡͝', Range\between(0, 11, true)],
];
}

public function testRangeThrowsForOutOfBoundOffset(): void
{
$this->expectException(Exception\OutOfBoundsException::class);

Grapheme\range('Hello', Range\from(10));
}

public function testRangeThrowsForNegativeOutOfBoundOffset(): void
{
$this->expectException(Exception\OutOfBoundsException::class);

Grapheme\range('Hello', Range\from(-6));
}
}
Loading

0 comments on commit 981769f

Please sign in to comment.