From 981769f1337dc5d3a9ba446e4bd76114474fd388 Mon Sep 17 00:00:00 2001 From: Saif Eddin Gmati <29315886+azjezz@users.noreply.github.com> Date: Thu, 22 Dec 2022 11:19:35 +0100 Subject: [PATCH] feat(str): add range function (#385) Signed-off-by: azjezz --- config/.phpcs.xml | 3 ++ docs/component/str-byte.md | 1 + docs/component/str-grapheme.md | 1 + docs/component/str.md | 1 + src/Psl/Internal/Loader.php | 3 ++ src/Psl/Str/Byte/range.php | 64 +++++++++++++++++++++++++ src/Psl/Str/Grapheme/range.php | 65 ++++++++++++++++++++++++++ src/Psl/Str/Grapheme/slice.php | 2 +- src/Psl/Str/range.php | 63 +++++++++++++++++++++++++ tests/unit/Str/Byte/RangeTest.php | 59 +++++++++++++++++++++++ tests/unit/Str/Grapheme/RangeTest.php | 67 +++++++++++++++++++++++++++ tests/unit/Str/RangeTest.php | 67 +++++++++++++++++++++++++++ 12 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 src/Psl/Str/Byte/range.php create mode 100644 src/Psl/Str/Grapheme/range.php create mode 100644 src/Psl/Str/range.php create mode 100644 tests/unit/Str/Byte/RangeTest.php create mode 100644 tests/unit/Str/Grapheme/RangeTest.php create mode 100644 tests/unit/Str/RangeTest.php diff --git a/config/.phpcs.xml b/config/.phpcs.xml index a1f6b340..a1c70fb1 100644 --- a/config/.phpcs.xml +++ b/config/.phpcs.xml @@ -7,6 +7,7 @@ ../config ../tests + */src/Psl/File/(WriteMode|LockType).php */src/Psl/Network/SocketScheme.php */src/Psl/Str/Encoding.php @@ -16,6 +17,8 @@ */src/Psl/OS/OperatingSystemFamily.php */src/Psl/Password/Algorithm.php */src/Psl/Shell/ErrorOutputBehavior.php + + */tests/unit/Str/Grapheme/RangeTest.php diff --git a/docs/component/str-byte.md b/docs/component/str-byte.md index 18a09ceb..33656271 100644 --- a/docs/component/str-byte.md +++ b/docs/component/str-byte.md @@ -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) diff --git a/docs/component/str-grapheme.md b/docs/component/str-grapheme.md index 881ca15d..3ff113d0 100644 --- a/docs/component/str-grapheme.md +++ b/docs/component/str-grapheme.md @@ -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) diff --git a/docs/component/str.md b/docs/component/str.md index 7fac0256..45b24198 100644 --- a/docs/component/str.md +++ b/docs/component/str.md @@ -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) diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 26e497dc..7b8a2462 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -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', @@ -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', @@ -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', diff --git a/src/Psl/Str/Byte/range.php b/src/Psl/Str/Byte/range.php new file mode 100644 index 00000000..b7fedd87 --- /dev/null +++ b/src/Psl/Str/Byte/range.php @@ -0,0 +1,64 @@ + $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); +} diff --git a/src/Psl/Str/Grapheme/range.php b/src/Psl/Str/Grapheme/range.php new file mode 100644 index 00000000..e723eb26 --- /dev/null +++ b/src/Psl/Str/Grapheme/range.php @@ -0,0 +1,65 @@ + $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); +} diff --git a/src/Psl/Str/Grapheme/slice.php b/src/Psl/Str/Grapheme/slice.php index be1a4373..dac48252 100644 --- a/src/Psl/Str/Grapheme/slice.php +++ b/src/Psl/Str/Grapheme/slice.php @@ -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 { diff --git a/src/Psl/Str/range.php b/src/Psl/Str/range.php new file mode 100644 index 00000000..65c856cc --- /dev/null +++ b/src/Psl/Str/range.php @@ -0,0 +1,63 @@ + $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); +} diff --git a/tests/unit/Str/Byte/RangeTest.php b/tests/unit/Str/Byte/RangeTest.php new file mode 100644 index 00000000..f1c3f9b8 --- /dev/null +++ b/tests/unit/Str/Byte/RangeTest.php @@ -0,0 +1,59 @@ + $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}> + */ + 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)); + } +} diff --git a/tests/unit/Str/Grapheme/RangeTest.php b/tests/unit/Str/Grapheme/RangeTest.php new file mode 100644 index 00000000..67cc21c7 --- /dev/null +++ b/tests/unit/Str/Grapheme/RangeTest.php @@ -0,0 +1,67 @@ + $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}> + */ + 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)); + } +} diff --git a/tests/unit/Str/RangeTest.php b/tests/unit/Str/RangeTest.php new file mode 100644 index 00000000..409d3ab1 --- /dev/null +++ b/tests/unit/Str/RangeTest.php @@ -0,0 +1,67 @@ + $range + * + * @dataProvider provideData + */ + public function testRange(string $expected, string $string, Range\RangeInterface $range): void + { + static::assertSame($expected, Str\range($string, $range)); + } + + /** + * @return list<{0: string, 1: string, 2: Range\RangeInterface}> + */ + 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̡̙̬͎̿́̐̅̕͢', 'he̡̙̬͎̿́̐̅̕͢l͕̮͕͈̜͐̈́̇̕͠ļ͚͉̗̘̽͑̿͑̚o̼̰̼͕̞̍̄̎̿̊,̻̰̻̘́̎͒̋͘͟ ̧̬̝͈̬̿͌̿̑̕ẉ̣̟͉̮͆̊̃͐̈́ờ̢̫͎͖̹͊́͐r̨̮͓͓̣̅̋͐͐͆ḻ̩̦͚̯͑̌̓̅͒d͇̯͔̼͍͛̾͛͡͝', Range\between(0, 11, true)], + ]; + } + + public function testRangeThrowsForOutOfBoundOffset(): void + { + $this->expectException(Exception\OutOfBoundsException::class); + + Str\range('Hello', Range\from(10)); + } + + public function testRangeThrowsForNegativeOutOfBoundOffset(): void + { + $this->expectException(Exception\OutOfBoundsException::class); + + Str\range('Hello', Range\from(-6)); + } +}