diff --git a/src/Symfony/Component/String/AbstractString.php b/src/Symfony/Component/String/AbstractString.php index 05f53fa712..9bbe348203 100644 --- a/src/Symfony/Component/String/AbstractString.php +++ b/src/Symfony/Component/String/AbstractString.php @@ -225,6 +225,16 @@ abstract class AbstractString implements \JsonSerializable return $this->slice(0, $i); } + /** + * @return int[] + */ + public function bytesAt(int $offset): array + { + $str = $this->slice($offset, 1); + + return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); + } + /** * @return static */ diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index 9ab070ff0b..bc0c7f8763 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -159,11 +159,14 @@ abstract class AbstractUnicodeString extends AbstractString return $str; } - public function codePoint(int $offset = 0): ?int + /** + * @return int[] + */ + public function codePointsAt(int $offset): array { - $str = $offset ? $this->slice($offset, 1) : $this; + $str = $this->slice($offset, 1); - return '' === $str->string ? null : mb_ord($str->string); + return '' === $str->string ? [] : array_map('mb_ord', preg_split('//u', $str->string, -1, PREG_SPLIT_NO_EMPTY)); } public function folded(bool $compat = true): parent diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php index 6c1bcc9e7f..d831940e16 100644 --- a/src/Symfony/Component/String/ByteString.php +++ b/src/Symfony/Component/String/ByteString.php @@ -43,11 +43,11 @@ class ByteString extends AbstractString return new static(substr($string, 0, $length)); } - public function byteCode(int $offset = 0): ?int + public function bytesAt(int $offset): array { - $str = $offset ? $this->slice($offset, 1) : $this; + $str = $this->string[$offset] ?? ''; - return '' === $str->string ? null : \ord($str->string); + return '' === $str ? [] : [\ord($str)]; } public function append(string ...$suffix): parent diff --git a/src/Symfony/Component/String/CodePointString.php b/src/Symfony/Component/String/CodePointString.php index 6aeac3d817..8a729bb9e1 100644 --- a/src/Symfony/Component/String/CodePointString.php +++ b/src/Symfony/Component/String/CodePointString.php @@ -75,6 +75,13 @@ class CodePointString extends AbstractUnicodeString return $chunks; } + public function codePointsAt(int $offset): array + { + $str = $offset ? $this->slice($offset, 1) : $this; + + return '' === $str->string ? [] : [mb_ord($str->string)]; + } + public function endsWith($suffix): bool { if ($suffix instanceof AbstractString) { diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index 298d0f4a37..e4fe9bdb9b 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -34,6 +34,27 @@ abstract class AbstractAsciiTestCase extends TestCase $this->assertTrue($instance->isEmpty()); } + /** + * @dataProvider provideBytesAt + */ + public function testBytesAt(array $expected, string $string, int $offset, int $form = null) + { + $instance = static::createFromString($string); + $instance = $form ? $instance->normalize($form) : $instance; + + $this->assertSame($expected, $instance->bytesAt($offset)); + } + + public static function provideBytesAt(): array + { + return [ + [[], '', 0], + [[], 'a', 1], + [[0x62], 'abc', 1], + [[0x63], 'abcde', -3], + ]; + } + /** * @dataProvider provideWrap */ diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php index 45bda349d6..20ef9cf975 100644 --- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php @@ -34,6 +34,40 @@ abstract class AbstractUnicodeTestCase extends AbstractAsciiTestCase ]; } + public static function provideBytesAt(): array + { + return array_merge( + parent::provideBytesAt(), + [ + [[0xC3, 0xA4], 'Späßchen', 2], + [[0xC3, 0x9F], 'Späßchen', -5], + ] + ); + } + + /** + * @dataProvider provideCodePointsAt + */ + public function testCodePointsAt(array $expected, string $string, int $offset, int $form = null) + { + $instance = static::createFromString($string); + $instance = $form ? $instance->normalize($form) : $instance; + + $this->assertSame($expected, $instance->codePointsAt($offset)); + } + + public static function provideCodePointsAt(): array + { + return [ + [[], '', 0], + [[], 'a', 1], + [[0x53], 'Späßchen', 0], + [[0xE4], 'Späßchen', 2], + [[0xDF], 'Späßchen', -5], + [[0x260E], '☢☎❄', 1], + ]; + } + public static function provideLength(): array { return [ diff --git a/src/Symfony/Component/String/Tests/ByteStringTest.php b/src/Symfony/Component/String/Tests/ByteStringTest.php index 25665f6a84..b7a47a562f 100644 --- a/src/Symfony/Component/String/Tests/ByteStringTest.php +++ b/src/Symfony/Component/String/Tests/ByteStringTest.php @@ -21,6 +21,19 @@ class ByteStringTest extends AbstractAsciiTestCase return new ByteString($string); } + public static function provideBytesAt(): array + { + return array_merge( + parent::provideBytesAt(), + [ + [[0xC3], 'Späßchen', 2], + [[0x61], "Spa\u{0308}ßchen", 2], + [[0xCC], "Spa\u{0308}ßchen", 3], + [[0xE0], 'नमस्ते', 6], + ] + ); + } + public static function provideLength(): array { return array_merge( diff --git a/src/Symfony/Component/String/Tests/CodePointStringTest.php b/src/Symfony/Component/String/Tests/CodePointStringTest.php index 9c61785195..cd51fa77b8 100644 --- a/src/Symfony/Component/String/Tests/CodePointStringTest.php +++ b/src/Symfony/Component/String/Tests/CodePointStringTest.php @@ -31,4 +31,28 @@ class CodePointStringTest extends AbstractUnicodeTestCase ] ); } + + public static function provideBytesAt(): array + { + return array_merge( + parent::provideBytesAt(), + [ + [[0x61], "Spa\u{0308}ßchen", 2], + [[0xCC, 0x88], "Spa\u{0308}ßchen", 3], + [[0xE0, 0xA5, 0x8D], 'नमस्ते', 3], + ] + ); + } + + public static function provideCodePointsAt(): array + { + return array_merge( + parent::provideCodePointsAt(), + [ + [[0x61], "Spa\u{0308}ßchen", 2], + [[0x0308], "Spa\u{0308}ßchen", 3], + [[0x094D], 'नमस्ते', 3], + ] + ); + } } diff --git a/src/Symfony/Component/String/Tests/UnicodeStringTest.php b/src/Symfony/Component/String/Tests/UnicodeStringTest.php index b535538e9a..5f3321d6da 100644 --- a/src/Symfony/Component/String/Tests/UnicodeStringTest.php +++ b/src/Symfony/Component/String/Tests/UnicodeStringTest.php @@ -90,6 +90,30 @@ class UnicodeStringTest extends AbstractUnicodeTestCase ); } + public static function provideBytesAt(): array + { + return array_merge( + parent::provideBytesAt(), + [ + [[0xC3, 0xA4], "Spa\u{0308}ßchen", 2], + [[0x61, 0xCC, 0x88], "Spa\u{0308}ßchen", 2, UnicodeString::NFD], + [[0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D], 'नमस्ते', 2], + ] + ); + } + + public static function provideCodePointsAt(): array + { + return array_merge( + parent::provideCodePointsAt(), + [ + [[0xE4], "Spa\u{0308}ßchen", 2], + [[0x61, 0x0308], "Spa\u{0308}ßchen", 2, UnicodeString::NFD], + [[0x0938, 0x094D], 'नमस्ते', 2], + ] + ); + } + public static function provideLower(): array { return array_merge(