diff --git a/.github/workflows/security-analysis.yml b/.github/workflows/security-analysis.yml index f83d1c872..3e4a23df3 100644 --- a/.github/workflows/security-analysis.yml +++ b/.github/workflows/security-analysis.yml @@ -4,7 +4,7 @@ on: ["pull_request", "push"] jobs: security-analysis: - name: "security anaylsis" + name: "security analysis" runs-on: "ubuntu-latest" steps: - name: "checkout" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 857c9c2de..f3ac2ad03 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -4,7 +4,7 @@ on: ["pull_request", "push"] jobs: static-analysis: - name: "static anaylsis" + name: "static analysis" runs-on: "ubuntu-latest" steps: - name: "checkout" diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 99247d888..eb12b57cb 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -325,6 +325,20 @@ final class Loader 'Psl\Password\hash', 'Psl\Password\needs_rehash', 'Psl\Password\verify', + 'Psl\Str\Grapheme\contains', + 'Psl\Str\Grapheme\contains_ci', + 'Psl\Str\Grapheme\ends_with', + 'Psl\Str\Grapheme\ends_with_ci', + 'Psl\Str\Grapheme\length', + 'Psl\Str\Grapheme\search', + 'Psl\Str\Grapheme\search_ci', + 'Psl\Str\Grapheme\search_last', + 'Psl\Str\Grapheme\search_last_ci', + 'Psl\Str\Grapheme\slice', + 'Psl\Str\Grapheme\starts_with', + 'Psl\Str\Grapheme\starts_with_ci', + 'Psl\Str\Grapheme\strip_prefix', + 'Psl\Str\Grapheme\strip_suffix', ]; public const INTERFACES = [ diff --git a/src/Psl/Str/Grapheme/contains.php b/src/Psl/Str/Grapheme/contains.php new file mode 100644 index 000000000..daf207d14 --- /dev/null +++ b/src/Psl/Str/Grapheme/contains.php @@ -0,0 +1,30 @@ + $total_length) { + return false; + } + + /** @psalm-suppress MissingThrowsDocblock */ + $position = search_last($string, $suffix); + if (null === $position) { + return false; + } + + return $position + $suffix_length === $total_length; +} diff --git a/src/Psl/Str/Grapheme/ends_with_ci.php b/src/Psl/Str/Grapheme/ends_with_ci.php new file mode 100644 index 000000000..9779e2ed2 --- /dev/null +++ b/src/Psl/Str/Grapheme/ends_with_ci.php @@ -0,0 +1,33 @@ + $total_length) { + return false; + } + + /** @psalm-suppress MissingThrowsDocblock */ + $position = search_last_ci($string, $suffix); + if (null === $position) { + return false; + } + + return $position + $suffix_length === $total_length; +} diff --git a/src/Psl/Str/Grapheme/length.php b/src/Psl/Str/Grapheme/length.php new file mode 100644 index 000000000..ee823dddc --- /dev/null +++ b/src/Psl/Str/Grapheme/length.php @@ -0,0 +1,17 @@ += -$haystack_length && $offset <= $haystack_length, 'Offset is out-of-bounds.'); + + return false === ($pos = grapheme_strrpos($haystack, $needle, $offset)) ? + null : + $pos; +} diff --git a/src/Psl/Str/Grapheme/search_last_ci.php b/src/Psl/Str/Grapheme/search_last_ci.php new file mode 100644 index 000000000..d1295e474 --- /dev/null +++ b/src/Psl/Str/Grapheme/search_last_ci.php @@ -0,0 +1,35 @@ += -$haystack_length && $offset <= $haystack_length, 'Offset is out-of-bounds.'); + + return false === ($pos = grapheme_strripos($haystack, $needle, $offset)) ? + null : + $pos; +} diff --git a/src/Psl/Str/Grapheme/slice.php b/src/Psl/Str/Grapheme/slice.php new file mode 100644 index 000000000..380594582 --- /dev/null +++ b/src/Psl/Str/Grapheme/slice.php @@ -0,0 +1,39 @@ += 0, 'Expected a non-negative length.'); + $string_length = length($string); + $offset = Psl\Internal\validate_offset($offset, $string_length); + + if (0 === $offset && (null === $length || $string_length <= $length)) { + return $string; + } + + if (null === $length) { + return (string) grapheme_substr($string, $offset); + } + + return (string) grapheme_substr($string, $offset, $length); +} diff --git a/src/Psl/Str/Grapheme/starts_with.php b/src/Psl/Str/Grapheme/starts_with.php new file mode 100644 index 000000000..f1cfd09ad --- /dev/null +++ b/src/Psl/Str/Grapheme/starts_with.php @@ -0,0 +1,18 @@ +expectException(Exception\InvariantViolationException::class); + + Grapheme\slice('Hello', 0, -1); + } + + public function testSliceThrowsForOutOfBoundOffset(): void + { + $this->expectException(Exception\InvariantViolationException::class); + + Grapheme\slice('Hello', 10); + } + + public function testSliceThrowsForNegativeOutOfBoundOffset(): void + { + $this->expectException(Exception\InvariantViolationException::class); + + Grapheme\slice('hello', -6); + } +} diff --git a/tests/Psl/Str/Grapheme/StartsWithCiTest.php b/tests/Psl/Str/Grapheme/StartsWithCiTest.php new file mode 100644 index 000000000..3703a6be1 --- /dev/null +++ b/tests/Psl/Str/Grapheme/StartsWithCiTest.php @@ -0,0 +1,41 @@ +