Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(str): add range function #385

Merged
merged 1 commit into from
Dec 22, 2022
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
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