diff --git a/src/Symfony/Component/String/CHANGELOG.md b/src/Symfony/Component/String/CHANGELOG.md
index 094072510d..28b9c62541 100644
--- a/src/Symfony/Component/String/CHANGELOG.md
+++ b/src/Symfony/Component/String/CHANGELOG.md
@@ -1,7 +1,7 @@
CHANGELOG
=========
-4.4.0
+5.0.0
-----
- * added the component
+ * added the component as experimental
diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
new file mode 100644
index 0000000000..baff67eaaf
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
@@ -0,0 +1,732 @@
+assertSame('Symfony is a PHP framework!', (string) $bytes);
+ $this->assertSame(27, $bytes->length());
+ $this->assertFalse($bytes->isEmpty());
+ }
+
+ public function testCreateFromEmptyString()
+ {
+ $instance = static::createFromString('');
+
+ $this->assertSame('', (string) $instance);
+ $this->assertSame(0, $instance->length());
+ $this->assertTrue($instance->isEmpty());
+ }
+
+ /**
+ * @dataProvider provideLength
+ */
+ public function testLength(int $length, string $string)
+ {
+ $instance = static::createFromString($string);
+
+ $this->assertSame($length, $instance->length());
+ }
+
+ public static function provideLength(): array
+ {
+ return [
+ [1, 'a'],
+ [2, 'is'],
+ [3, 'PHP'],
+ [4, 'Java'],
+ [7, 'Symfony'],
+ [10, 'pineapples'],
+ [22, 'Symfony is super cool!'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideIndexOf
+ */
+ public function testIndexOf(?int $result, string $string, string $needle, int $offset)
+ {
+ $instance = static::createFromString($string);
+
+ $this->assertSame($result, $instance->indexOf($needle, $offset));
+ }
+
+ public static function provideIndexOf(): array
+ {
+ return [
+ [null, 'abc', '', 0],
+ [null, 'ABC', '', 0],
+ [null, 'abc', 'd', 0],
+ [null, 'abc', 'a', 3],
+ [null, 'ABC', 'c', 0],
+ [null, 'ABC', 'c', 2],
+ [null, 'abc', 'a', -1],
+ [null, '123abc', 'B', -3],
+ [null, '123abc', 'b', 6],
+ [0, 'abc', 'a', 0],
+ [1, 'abc', 'b', 1],
+ [2, 'abc', 'c', 1],
+ [4, '123abc', 'b', -3],
+ ];
+ }
+
+ /**
+ * @dataProvider provideIndexOfIgnoreCase
+ */
+ public function testIndexOfIgnoreCase(?int $result, string $string, string $needle, int $offset)
+ {
+ $instance = static::createFromString($string);
+
+ $this->assertSame($result, $instance->ignoreCase()->indexOf($needle, $offset));
+ }
+
+ public static function provideIndexOfIgnoreCase(): array
+ {
+ return [
+ [null, 'ABC', '', 0],
+ [null, 'ABC', '', 0],
+ [null, 'abc', 'a', 3],
+ [null, 'abc', 'A', 3],
+ [null, 'abc', 'a', -1],
+ [null, 'abc', 'A', -1],
+ [null, '123abc', 'B', 6],
+ [0, 'ABC', 'a', 0],
+ [0, 'ABC', 'A', 0],
+ [1, 'ABC', 'b', 0],
+ [1, 'ABC', 'b', 1],
+ [2, 'ABC', 'c', 0],
+ [2, 'ABC', 'c', 2],
+ [4, '123abc', 'B', -3],
+ ];
+ }
+
+ /**
+ * @dataProvider provideIndexOfLast
+ */
+ public function testIndexOfLast(?int $result, string $string, string $needle, int $offset)
+ {
+ $instance = static::createFromString($string);
+
+ $this->assertSame($result, $instance->indexOfLast($needle, $offset));
+ }
+
+ public static function provideIndexOfLast(): array
+ {
+ return [
+ [null, 'abc', '', 0],
+ [null, 'abc', '', -2],
+ [null, 'elegant', 'z', -1],
+ [5, 'DEJAAAA', 'A', -2],
+ [74, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'i', 0],
+ [19, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'i', -40],
+ [6, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'ipsum', 0],
+ [57, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'amet', 0],
+ [57, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'amet', -10],
+ [22, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'amet', -30],
+ ];
+ }
+
+ /**
+ * @dataProvider provideIndexOfLastIgnoreCase
+ */
+ public function testIndexOfLastIgnoreCase(?int $result, string $string, string $needle, int $offset)
+ {
+ $instance = static::createFromString($string);
+
+ $this->assertSame($result, $instance->ignoreCase()->indexOfLast($needle, $offset));
+ }
+
+ public static function provideIndexOfLastIgnoreCase(): array
+ {
+ return [
+ [null, 'abc', '', 0],
+ [null, 'abc', '', -2],
+ [null, 'elegant', 'z', -1],
+ [1, 'abc', 'b', 0],
+ [1, 'abc', 'b', -1],
+ [2, 'abcdefgh', 'c', -1],
+ [2, 'abcdefgh', 'C', -1],
+ [5, 'dejaaaa', 'A', -2],
+ [5, 'DEJAAAA', 'a', -2],
+ [74, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'I', 0],
+ [19, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'I', -40],
+ [6, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'IPSUM', 0],
+ [57, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'AmeT', 0],
+ [57, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'aMEt', -10],
+ [22, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, amet sagittis felis.', 'AMET', -30],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSplit
+ */
+ public function testSplit(string $string, string $delimiter, array $chunks, ?int $limit)
+ {
+ $this->assertEquals($chunks, static::createFromString($string)->split($delimiter, $limit));
+ }
+
+ public static function provideSplit(): array
+ {
+ return [
+ [
+ 'hello world',
+ ' ',
+ [
+ static::createFromString('hello'),
+ static::createFromString('world'),
+ ],
+ null,
+ ],
+ [
+ 'radar',
+ 'd',
+ [
+ static::createFromString('ra'),
+ static::createFromString('ar'),
+ ],
+ 2,
+ ],
+ [
+ 'foo,bar,baz,qux,kix',
+ ',',
+ [
+ static::createFromString('foo'),
+ static::createFromString('bar'),
+ static::createFromString('baz'),
+ static::createFromString('qux'),
+ static::createFromString('kix'),
+ ],
+ null,
+ ],
+ [
+ 'foo,bar,baz,qux,kix',
+ ',',
+ [
+ static::createFromString('foo,bar,baz,qux,kix'),
+ ],
+ 1,
+ ],
+ [
+ 'foo,bar,baz,qux,kix',
+ ',',
+ [
+ static::createFromString('foo'),
+ static::createFromString('bar'),
+ static::createFromString('baz,qux,kix'),
+ ],
+ 3,
+ ],
+ [
+ 'Quisque viverra tincidunt elit. Vestibulum convallis dui nec lacis suscipit cursus.',
+ 'is',
+ [
+ static::createFromString('Qu'),
+ static::createFromString('que viverra tincidunt elit. Vestibulum convall'),
+ static::createFromString(' dui nec lac'),
+ static::createFromString(' suscipit cursus.'),
+ ],
+ null,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInvalidChunkLength
+ */
+ public function testInvalidChunkLength(int $length)
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('foo|bar|baz')->chunk($length);
+ }
+
+ public static function provideInvalidChunkLength(): array
+ {
+ return [
+ [-2],
+ [-1],
+ [0],
+ ];
+ }
+
+ /**
+ * @dataProvider provideChunk
+ */
+ public function testChunk(string $string, array $chunks, int $length)
+ {
+ $this->assertEquals($chunks, static::createFromString($string)->chunk($length));
+ }
+
+ public static function provideChunk()
+ {
+ return [
+ [
+ '',
+ [],
+ 1,
+ ],
+ [
+ 'hello',
+ [
+ static::createFromString('h'),
+ static::createFromString('e'),
+ static::createFromString('l'),
+ static::createFromString('l'),
+ static::createFromString('o'),
+ ],
+ 1,
+ ],
+ [
+ 'hello you!',
+ [
+ static::createFromString('h'),
+ static::createFromString('e'),
+ static::createFromString('l'),
+ static::createFromString('l'),
+ static::createFromString('o'),
+ static::createFromString(' '),
+ static::createFromString('y'),
+ static::createFromString('o'),
+ static::createFromString('u'),
+ static::createFromString('!'),
+ ],
+ 1,
+ ],
+ [
+ 'hell',
+ [
+ static::createFromString('h'),
+ static::createFromString('e'),
+ static::createFromString('l'),
+ static::createFromString('l'),
+ ],
+ 1,
+ ],
+ [
+ 'hell',
+ [
+ static::createFromString('he'),
+ static::createFromString('ll'),
+ ],
+ 2,
+ ],
+ [
+ str_repeat('-', 65537),
+ [
+ static::createFromString(str_repeat('-', 65536)),
+ static::createFromString('-'),
+ ],
+ 65536,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideLower
+ */
+ public function testLower(string $expected, string $origin)
+ {
+ $instance = static::createFromString($origin)->lower();
+
+ $this->assertNotSame(static::createFromString($origin), $instance);
+ $this->assertEquals(static::createFromString($expected), $instance);
+ $this->assertSame($expected, (string) $instance);
+ }
+
+ public static function provideLower()
+ {
+ return [
+ ['hello world', 'hello world'],
+ ['hello world', 'HELLO WORLD'],
+ ['hello world', 'Hello World'],
+ ['symfony', 'symfony'],
+ ['symfony', 'Symfony'],
+ ['symfony', 'sYmFOny'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideUpper
+ */
+ public function testUpper(string $expected, string $origin)
+ {
+ $instance = static::createFromString($origin)->upper();
+
+ $this->assertNotSame(static::createFromString($origin), $instance);
+ $this->assertEquals(static::createFromString($expected), $instance);
+ $this->assertSame($expected, (string) $instance);
+ }
+
+ public static function provideUpper()
+ {
+ return [
+ ['HELLO WORLD', 'hello world'],
+ ['HELLO WORLD', 'HELLO WORLD'],
+ ['HELLO WORLD', 'Hello World'],
+ ['SYMFONY', 'symfony'],
+ ['SYMFONY', 'Symfony'],
+ ['SYMFONY', 'sYmFOny'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTitle
+ */
+ public function testTitle(string $expected, string $origin, bool $allWords)
+ {
+ $this->assertEquals(
+ static::createFromString($expected),
+ static::createFromString($origin)->title($allWords)
+ );
+ }
+
+ public static function provideTitle()
+ {
+ return [
+ ['Hello world', 'hello world', false],
+ ['Hello World', 'hello world', true],
+ ['HELLO WORLD', 'HELLO WORLD', false],
+ ['HELLO WORLD', 'HELLO WORLD', true],
+ ['HELLO wORLD', 'hELLO wORLD', false],
+ ['HELLO WORLD', 'hELLO wORLD', true],
+ ['Symfony', 'symfony', false],
+ ['Symfony', 'Symfony', false],
+ ['SYmFOny', 'sYmFOny', false],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSlice
+ */
+ public function testSlice(string $expected, string $origin, int $start, int $length = null)
+ {
+ $this->assertEquals(
+ static::createFromString($expected),
+ static::createFromString($origin)->slice($start, $length)
+ );
+ }
+
+ public static function provideSlice()
+ {
+ return [
+ ['Symfony', 'Symfony is awesome', 0, 7],
+ [' ', 'Symfony is awesome', 7, 1],
+ ['is', 'Symfony is awesome', 8, 2],
+ [' ', 'Symfony is awesome', 10, 1],
+ ['awesome', 'Symfony is awesome', 11, 7],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAppend
+ */
+ public function testAppend(string $expected, array $suffixes)
+ {
+ $instance = static::createFromString('');
+ foreach ($suffixes as $suffix) {
+ $instance = $instance->append($suffix);
+ }
+
+ $this->assertEquals($expected, $instance);
+
+ $instance = static::createFromString('')->append(...$suffixes);
+
+ $this->assertEquals(static::createFromString($expected), $instance);
+ }
+
+ public static function provideAppend()
+ {
+ return [
+ [
+ 'Symfony',
+ ['Sym', 'fony'],
+ ],
+ [
+ 'Hello World!',
+ ['Hel', 'lo', ' ', 'World', '!'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideAppend
+ */
+ public function testPrepend(string $expected, array $prefixes)
+ {
+ $instance = static::createFromString('');
+ foreach (array_reverse($prefixes) as $suffix) {
+ $instance = $instance->prepend($suffix);
+ }
+
+ $this->assertEquals(static::createFromString($expected), $instance);
+
+ $instance = static::createFromString('')->prepend(...$prefixes);
+
+ $this->assertEquals(static::createFromString($expected), $instance);
+ }
+
+ /**
+ * @dataProvider provideTrim
+ */
+ public function testTrim(string $expected, string $origin, ?string $chars)
+ {
+ $result = static::createFromString($origin);
+ $result = null !== $chars ? $result->trim($chars) : $result->trim();
+
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideTrim()
+ {
+ return [
+ [
+ "Symfony IS GREAT\t!!!",
+ " Symfony IS GREAT\t!!!\n",
+ null,
+ ],
+ [
+ "Symfony IS GREAT\t!!!\n",
+ " Symfony IS GREAT\t!!!\n",
+ ' ',
+ ],
+ [
+ " Symfony IS GREAT\t!!!",
+ " Symfony IS GREAT\t!!!\n",
+ "\n",
+ ],
+ [
+ "Symfony IS GREAT\t",
+ " Symfony IS GREAT\t!!!\n",
+ " \n!",
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTrimStart
+ */
+ public function testTrimStart(string $expected, string $origin, ?string $chars)
+ {
+ $result = static::createFromString($origin);
+ $result = null !== $chars ? $result->trimStart($chars) : $result->trimStart();
+
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideTrimStart()
+ {
+ return [
+ [
+ "Symfony is a PHP framework\n",
+ "\n\tSymfony is a PHP framework\n",
+ null,
+ ],
+ [
+ "\tSymfony is a PHP framework\n",
+ "\n\tSymfony is a PHP framework\n",
+ "\n",
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTrimEnd
+ */
+ public function testTrimEnd(string $expected, string $origin, ?string $chars)
+ {
+ $result = static::createFromString($origin);
+ $result = null !== $chars ? $result->trimEnd($chars) : $result->trimEnd();
+
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideTrimEnd()
+ {
+ return [
+ [
+ "\n\tSymfony is a PHP framework",
+ "\n\tSymfony is a PHP framework \n",
+ null,
+ ],
+ [
+ "\n\tSymfony is a PHP framework ",
+ "\n\tSymfony is a PHP framework \n",
+ "\n",
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBeforeAfter
+ */
+ public function testBeforeAfter(string $expected, string $needle, string $origin, bool $before)
+ {
+ $result = static::createFromString($origin);
+ $result = $before ? $result->before($needle, false) : $result->after($needle, true);
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideBeforeAfter()
+ {
+ return [
+ ['', '', 'hello world', true],
+ ['', '', 'hello world', false],
+ ['', 'w', 'hello World', true],
+ ['', 'w', 'hello World', false],
+ ['hello ', 'w', 'hello world', true],
+ ['world', 'w', 'hello world', false],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBeforeAfterIgnoreCase
+ */
+ public function testBeforeAfterIgnoreCase(string $expected, string $needle, string $origin, bool $before)
+ {
+ $result = static::createFromString($origin)->ignoreCase();
+ $result = $before ? $result->before($needle, false) : $result->after($needle, true);
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideBeforeAfterIgnoreCase()
+ {
+ return [
+ ['', '', 'hello world', true],
+ ['', '', 'hello world', false],
+ ['', 'foo', 'hello world', true],
+ ['', 'foo', 'hello world', false],
+ ['hello ', 'w', 'hello world', true],
+ ['world', 'w', 'hello world', false],
+ ['hello ', 'W', 'hello world', true],
+ ['world', 'W', 'hello world', false],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBeforeAfterLast
+ */
+ public function testBeforeAfterLast(string $expected, string $needle, string $origin, bool $before)
+ {
+ $result = static::createFromString($origin);
+ $result = $before ? $result->beforeLast($needle, false) : $result->afterLast($needle, true);
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideBeforeAfterLast()
+ {
+ return [
+ ['', '', 'hello world', true],
+ ['', '', 'hello world', false],
+ ['', 'L', 'hello world', true],
+ ['', 'L', 'hello world', false],
+ ['hello wor', 'l', 'hello world', true],
+ ['ld', 'l', 'hello world', false],
+ ['hello w', 'o', 'hello world', true],
+ ['orld', 'o', 'hello world', false],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBeforeAfterLastIgnoreCase
+ */
+ public function testBeforeAfterLastIgnoreCase(string $expected, string $needle, string $origin, bool $before)
+ {
+ $result = static::createFromString($origin)->ignoreCase();
+ $result = $before ? $result->beforeLast($needle, false) : $result->afterLast($needle, true);
+ $this->assertEquals(static::createFromString($expected), $result);
+ }
+
+ public static function provideBeforeAfterLastIgnoreCase()
+ {
+ return [
+ ['', '', 'hello world', true],
+ ['', '', 'hello world', false],
+ ['', 'FOO', 'hello world', true],
+ ['', 'FOO', 'hello world', false],
+ ['hello wor', 'l', 'hello world', true],
+ ['ld', 'l', 'hello world', false],
+ ['hello wor', 'L', 'hello world', true],
+ ['ld', 'L', 'hello world', false],
+ ['hello w', 'O', 'hello world', true],
+ ['orld', 'O', 'hello world', false],
+ ];
+ }
+
+ /**
+ * @dataProvider provideFolded
+ */
+ public function testFolded(string $expected, string $origin)
+ {
+ $this->assertEquals(
+ static::createFromString($expected),
+ static::createFromString($origin)->folded()
+ );
+ }
+
+ public static function provideFolded()
+ {
+ return [
+ ['hello', 'HELlo'],
+ ['world', 'worLd'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideReplace
+ */
+ public function testReplace(string $expectedString, int $expectedCount, string $origin, string $from, string $to)
+ {
+ $origin = static::createFromString($origin);
+ $result = $origin->replace($from, $to);
+
+ $this->assertEquals(static::createFromString($expectedString), $result);
+ }
+
+ public static function provideReplace()
+ {
+ return [
+ ['hello world', 0, 'hello world', '', ''],
+ ['hello world', 0, 'hello world', '', '_'],
+ ['helloworld', 1, 'hello world', ' ', ''],
+ ['hello_world', 1, 'hello world', ' ', '_'],
+ ['hemmo wormd', 3, 'hello world', 'l', 'm'],
+ ['hello world', 0, 'hello world', 'L', 'm'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideReplaceIgnoreCase
+ */
+ public function testReplaceIgnoreCase(string $expectedString, int $expectedCount, string $origin, string $from, string $to)
+ {
+ $origin = static::createFromString($origin);
+ $result = $origin->ignoreCase()->replace($from, $to);
+
+ $this->assertEquals(static::createFromString($expectedString), $result);
+ }
+
+ public static function provideReplaceIgnoreCase()
+ {
+ return [
+ ['hello world', 0, 'hello world', '', ''],
+ ['hello world', 0, 'hello world', '', '_'],
+ ['helloworld', 1, 'hello world', ' ', ''],
+ ['hello_world', 1, 'hello world', ' ', '_'],
+ ['hemmo wormd', 3, 'hello world', 'l', 'm'],
+ ['heMMo worMd', 3, 'hello world', 'L', 'M'],
+ ];
+ }
+}
diff --git a/src/Symfony/Component/String/Tests/AbstractUtf8TestCase.php b/src/Symfony/Component/String/Tests/AbstractUtf8TestCase.php
new file mode 100644
index 0000000000..d2216d5a56
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/AbstractUtf8TestCase.php
@@ -0,0 +1,425 @@
+expectException(InvalidArgumentException::class);
+
+ static::createFromString("\xE9");
+ }
+
+ public function provideCreateFromCodePoint(): array
+ {
+ return [
+ ['', []],
+ ['*', [42]],
+ ['AZ', [65, 90]],
+ ['€', [8364]],
+ ['€', [0x20ac]],
+ ['Ʃ', [425]],
+ ['Ʃ', [0x1a9]],
+ ['☢☎❄', [0x2622, 0x260E, 0x2744]],
+ ];
+ }
+
+ public static function provideLength(): array
+ {
+ return [
+ [1, 'a'],
+ [1, 'ß'],
+ [2, 'is'],
+ [3, 'PHP'],
+ [3, '한국어'],
+ [4, 'Java'],
+ [7, 'Symfony'],
+ [10, 'pineapples'],
+ [22, 'Symfony is super cool!'],
+ ];
+ }
+
+ public static function provideIndexOf(): array
+ {
+ return array_merge(
+ parent::provideIndexOf(),
+ [
+ [1, '한국어', '국', 0],
+ [1, '한국어', '국', 1],
+ [null, '한국어', '국', 2],
+ [8, 'der Straße nach Paris', 'ß', 4],
+ ]
+ );
+ }
+
+ public static function provideIndexOfIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideIndexOfIgnoreCase(),
+ [
+ [3, 'DÉJÀ', 'À', 0],
+ [3, 'DÉJÀ', 'à', 0],
+ [1, 'DÉJÀ', 'É', 1],
+ [1, 'DÉJÀ', 'é', 1],
+ [1, 'aςσb', 'ΣΣ', 0],
+ [16, 'der Straße nach Paris', 'Paris', 0],
+ [8, 'der Straße nach Paris', 'ß', 4],
+ ]
+ );
+ }
+
+ public static function provideIndexOfLast(): array
+ {
+ return array_merge(
+ parent::provideIndexOfLast(),
+ [
+ [null, '한국어', '', 0],
+ [1, '한국어', '국', 0],
+ [5, '한국어어어어국국', '어', 0],
+ // see https://bugs.php.net/bug.php?id=74264
+ [15, 'abcdéf12é45abcdéf', 'é', 0],
+ [8, 'abcdéf12é45abcdéf', 'é', -4],
+ ]
+ );
+ }
+
+ public static function provideIndexOfLastIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideIndexOfLastIgnoreCase(),
+ [
+ [null, '한국어', '', 0],
+ [3, 'DÉJÀ', 'à', 0],
+ [3, 'DÉJÀ', 'À', 0],
+ [6, 'DÉJÀÀÀÀ', 'à', 0],
+ [6, 'DÉJÀÀÀÀ', 'à', 3],
+ [5, 'DÉJÀÀÀÀ', 'àà', 0],
+ [2, 'DÉJÀÀÀÀ', 'jà', 0],
+ [2, 'DÉJÀÀÀÀ', 'jà', -5],
+ [6, 'DÉJÀÀÀÀ!', 'à', -2],
+ // see https://bugs.php.net/bug.php?id=74264
+ [5, 'DÉJÀÀÀÀ', 'à', -2],
+ [15, 'abcdéf12é45abcdéf', 'é', 0],
+ [8, 'abcdéf12é45abcdéf', 'é', -4],
+ [1, 'aςσb', 'ΣΣ', 0],
+ ]
+ );
+ }
+
+ public static function provideSplit(): array
+ {
+ return array_merge(
+ parent::provideSplit(),
+ [
+ [
+ '會|意|文|字|/|会|意|文|字',
+ '|',
+ [
+ static::createFromString('會'),
+ static::createFromString('意'),
+ static::createFromString('文'),
+ static::createFromString('字'),
+ static::createFromString('/'),
+ static::createFromString('会'),
+ static::createFromString('意'),
+ static::createFromString('文'),
+ static::createFromString('字'),
+ ],
+ null,
+ ],
+ [
+ '會|意|文|字|/|会|意|文|字',
+ '|',
+ [
+ static::createFromString('會'),
+ static::createFromString('意'),
+ static::createFromString('文'),
+ static::createFromString('字'),
+ static::createFromString('/|会|意|文|字'),
+ ],
+ 5,
+ ],
+ ]
+ );
+ }
+
+ public static function provideChunk(): array
+ {
+ return array_merge(
+ parent::provideChunk(),
+ [
+ [
+ 'déjà',
+ [
+ static::createFromString('d'),
+ static::createFromString('é'),
+ static::createFromString('j'),
+ static::createFromString('à'),
+ ],
+ 1,
+ ],
+ [
+ 'déjà',
+ [
+ static::createFromString('dé'),
+ static::createFromString('jà'),
+ ],
+ 2,
+ ],
+ ]
+ );
+ }
+
+ public function testTrimWithInvalidUtf8CharList()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->trim("\xE9");
+ }
+
+ public function testTrimStartWithInvalidUtf8CharList()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->trimStart("\xE9");
+ }
+
+ public function testTrimEndWithInvalidUtf8CharList()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->trimEnd("\xE9");
+ }
+
+ public static function provideLower(): array
+ {
+ return array_merge(
+ parent::provideLower(),
+ [
+ // French
+ ['garçon', 'garçon'],
+ ['garçon', 'GARÇON'],
+ ["œuvre d'art", "Œuvre d'Art"],
+
+ // Spanish
+ ['el niño', 'El Niño'],
+
+ // Romanian
+ ['împărat', 'Împărat'],
+
+ // Random symbols
+ ['déjà σσς i̇iıi', 'DÉJÀ Σσς İIıi'],
+ ]
+ );
+ }
+
+ public static function provideUpper(): array
+ {
+ return array_merge(
+ parent::provideUpper(),
+ [
+ // French
+ ['GARÇON', 'garçon'],
+ ['GARÇON', 'GARÇON'],
+ ["ŒUVRE D'ART", "Œuvre d'Art"],
+
+ // Spanish
+ ['EL NIÑO', 'El Niño'],
+
+ // Romanian
+ ['ÎMPĂRAT', 'Împărat'],
+
+ // Random symbols
+ ['DÉJÀ ΣΣΣ İIII', 'Déjà Σσς İIıi'],
+ ]
+ );
+ }
+
+ public static function provideTitle(): array
+ {
+ return array_merge(
+ parent::provideTitle(),
+ [
+ ['Deja', 'deja', false],
+ ['Σσς', 'σσς', false],
+ ['DEJa', 'dEJa', false],
+ ['ΣσΣ', 'σσΣ', false],
+ ['Deja Σσς DEJa ΣσΣ', 'deja σσς dEJa σσΣ', true],
+ ]
+ );
+ }
+
+ public static function provideSlice(): array
+ {
+ return array_merge(
+ parent::provideSlice(),
+ [
+ ['jà', 'déjà', 2, null],
+ ['jà', 'déjà', 2, null],
+ ['jà', 'déjà', -2, null],
+ ['jà', 'déjà', -2, 3],
+ ['', 'déjà', -1, 0],
+ ['', 'déjà', 1, -4],
+ ['j', 'déjà', -2, -1],
+ ['', 'déjà', -2, -2],
+ ['', 'déjà', 5, 0],
+ ['', 'déjà', -5, 0],
+ ]
+ );
+ }
+
+ public static function provideAppend(): array
+ {
+ return array_merge(
+ parent::provideAppend(),
+ [
+ [
+ 'Déjà Σσς',
+ ['Déjà', ' ', 'Σσς'],
+ ],
+ [
+ 'Déjà Σσς İIıi',
+ ['Déjà', ' Σσς', ' İIıi'],
+ ],
+ ]
+ );
+ }
+
+ public function testAppendInvalidUtf8String()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->append("\xE9");
+ }
+
+ public static function providePrepend(): array
+ {
+ return array_merge(
+ parent::providePrepend(),
+ [
+ [
+ 'Σσς Déjà',
+ ['Déjà', 'Σσς '],
+ ],
+ [
+ 'İIıi Σσς Déjà',
+ ['Déjà', 'Σσς ', 'İIıi '],
+ ],
+ ]
+ );
+ }
+
+ public function testPrependInvalidUtf8String()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->prepend("\xE9");
+ }
+
+ public static function provideBeforeAfter(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfter(),
+ [
+ ['jàdéjà', 'jà', 'déjàdéjà', false],
+ ['dé', 'jà', 'déjàdéjà', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterIgnoreCase(),
+ [
+ ['jàdéjà', 'JÀ', 'déjàdéjà', false],
+ ['dé', 'jÀ', 'déjàdéjà', true],
+ ['éjàdéjà', 'é', 'déjàdéjà', false],
+ ['d', 'é', 'déjàdéjà', true],
+ ['', 'Ç', 'déjàdéjà', false],
+ ['', 'Ç', 'déjàdéjà', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterLast(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterLast(),
+ [
+ ['', 'Ç', 'déjàdéjà', false],
+ ['', 'Ç', 'déjàdéjà', true],
+ ['éjà', 'é', 'déjàdéjà', false],
+ ['déjàd', 'é', 'déjàdéjà', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterLastIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterLastIgnoreCase(),
+ [
+ ['', 'Ç', 'déjàdéjà', false],
+ ['éjà', 'é', 'déjàdéjà', false],
+ ['éjà', 'É', 'déjàdéjà', false],
+ ]
+ );
+ }
+
+ public static function provideToFoldedCase(): array
+ {
+ return array_merge(
+ parent::provideToFoldedCase(),
+ [
+ ['déjà', 'DéjÀ'],
+ ['σσσ', 'Σσς'],
+ ['iıi̇i', 'Iıİi'],
+ ]
+ );
+ }
+
+ public static function provideReplace(): array
+ {
+ return array_merge(
+ parent::provideReplace(),
+ [
+ ['ΣσΣ', 1, 'Σσς', 'ς', 'Σ'],
+ ['漢字はユニコード', 0, '漢字はユニコード', 'foo', 'bar'],
+ ['漢字ーユニコード', 1, '漢字はユニコード', 'は', 'ー'],
+ ['This is a jamais-vu situation!', 1, 'This is a déjà-vu situation!', 'déjà', 'jamais'],
+ ]
+ );
+ }
+
+ public static function provideReplaceIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideReplaceIgnoreCase(),
+ [
+ // σ and ς are lowercase variants for Σ
+ ['ΣΣΣ', 3, 'σσσ', 'σ', 'Σ'],
+ ['ΣΣΣ', 3, 'σσσ', 'ς', 'Σ'],
+ ['Σσ', 1, 'σσσ', 'σσ', 'Σ'],
+ ['漢字はユニコード', 0, '漢字はユニコード', 'foo', 'bar'],
+ ['漢字ーユニコード', 1, '漢字はユニコード', 'は', 'ー'],
+ ['This is a jamais-vu situation!', 1, 'This is a déjà-vu situation!', 'DÉjÀ', 'jamais'],
+ ]
+ );
+ }
+
+ public function testReplaceWithInvalidUtf8Pattern()
+ {
+ $this->assertEquals('Symfony', static::createFromString('Symfony')->replace("\xE9", 'p'));
+ }
+
+ public function testReplaceWithInvalidUtf8PatternReplacement()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ static::createFromString('Symfony')->replace('f', "\xE9");
+ }
+}
diff --git a/src/Symfony/Component/String/Tests/BinaryStringTest.php b/src/Symfony/Component/String/Tests/BinaryStringTest.php
new file mode 100644
index 0000000000..3605b5db2c
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/BinaryStringTest.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\String\Tests;
+
+use Symfony\Component\String\AbstractString;
+use Symfony\Component\String\BinaryString;
+
+class BinaryStringTest extends AbstractAsciiTestCase
+{
+ protected static function createFromString(string $string): AbstractString
+ {
+ return new BinaryString($string);
+ }
+}
diff --git a/src/Symfony/Component/String/Tests/GraphemeStringTest.php b/src/Symfony/Component/String/Tests/GraphemeStringTest.php
new file mode 100644
index 0000000000..18775de077
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/GraphemeStringTest.php
@@ -0,0 +1,203 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\String\Tests;
+
+use Symfony\Component\String\AbstractString;
+use Symfony\Component\String\GraphemeString;
+
+class GraphemeStringTest extends AbstractUtf8TestCase
+{
+ protected static function createFromString(string $string): AbstractString
+ {
+ return new GraphemeString($string);
+ }
+
+ public static function provideLength(): array
+ {
+ return array_merge(
+ parent::provideLength(),
+ [
+ // 5 letters + 3 combining marks
+ [5, 'अनुच्छेद'],
+ ]
+ );
+ }
+
+ public static function provideSplit(): array
+ {
+ return array_merge(
+ parent::provideSplit(),
+ [
+ [
+ 'अ.नु.च्.छे.द',
+ '.',
+ [
+ static::createFromString('अ'),
+ static::createFromString('नु'),
+ static::createFromString('च्'),
+ static::createFromString('छे'),
+ static::createFromString('द'),
+ ],
+ null,
+ ],
+ ]
+ );
+ }
+
+ public static function provideChunk(): array
+ {
+ return array_merge(
+ parent::provideChunk(),
+ [
+ [
+ 'अनुच्छेद',
+ [
+ static::createFromString('अ'),
+ static::createFromString('नु'),
+ static::createFromString('च्'),
+ static::createFromString('छे'),
+ static::createFromString('द'),
+ ],
+ 1,
+ ],
+ ]
+ );
+ }
+
+ public static function provideLower(): array
+ {
+ return array_merge(
+ parent::provideLower(),
+ [
+ // Hindi
+ ['अनुच्छेद', 'अनुच्छेद'],
+ ]
+ );
+ }
+
+ public static function provideUpper(): array
+ {
+ return array_merge(
+ parent::provideUpper(),
+ [
+ // Hindi
+ ['अनुच्छेद', 'अनुच्छेद'],
+ ]
+ );
+ }
+
+ public static function provideAppend(): array
+ {
+ return array_merge(
+ parent::provideAppend(),
+ [
+ [
+ 'तद्भव देशज',
+ ['तद्भव', ' ', 'देशज'],
+ ],
+ [
+ 'तद्भव देशज विदेशी',
+ ['तद्भव', ' देशज', ' विदेशी'],
+ ],
+ ]
+ );
+ }
+
+ public static function providePrepend(): array
+ {
+ return array_merge(
+ parent::providePrepend(),
+ [
+ [
+ 'देशज तद्भव',
+ ['तद्भव', 'देशज '],
+ ],
+ [
+ 'विदेशी देशज तद्भव',
+ ['तद्भव', 'देशज ', 'विदेशी '],
+ ],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfter(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfter(),
+ [
+ ['द foo अनुच्छेद', 'द', 'अनुच्छेद foo अनुच्छेद', false],
+ ['अनुच्छे', 'द', 'अनुच्छेद foo अनुच्छेद', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterIgnoreCase(),
+ [
+ ['', 'छेछे', 'दछेच्नुअ', false],
+ ['', 'छेछे', 'दछेच्नुअ', true],
+ ['छेच्नुअ', 'छे', 'दछेच्नुअ', false],
+ ['द', 'छे', 'दछेच्नुअ', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterLast(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterLast(),
+ [
+ ['', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', false],
+ ['', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', true],
+ ['-दछेच्नु', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', false],
+ ['दछेच्नुअ-दछेच्नु-अद', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', true],
+ ]
+ );
+ }
+
+ public static function provideBeforeAfterLastIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideBeforeAfterLastIgnoreCase(),
+ [
+ ['', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', false],
+ ['', 'छेछे', 'दछेच्नुअ-दछेच्नु-अदछेच्नु', true],
+ ['-दछेच्नु', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', false],
+ ['दछेच्नुअ-दछेच्नु-अद', '-द', 'दछेच्नुअ-दछेच्नु-अद-दछेच्नु', true],
+ ]
+ );
+ }
+
+ public static function provideReplace(): array
+ {
+ return array_merge(
+ parent::provideReplace(),
+ [
+ ['Das Innenministerium', 1, 'Das Außenministerium', 'Auß', 'Inn'],
+ ['दछेच्नुद-दछेच्नु-ददछेच्नु', 2, 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 'अ', 'द'],
+ ]
+ );
+ }
+
+ public static function provideReplaceIgnoreCase(): array
+ {
+ return array_merge(
+ parent::provideReplaceIgnoreCase(),
+ [
+ ['Das Aussenministerium', 1, 'Das Außenministerium', 'auß', 'Auss'],
+ ['दछेच्नुद-दछेच्नु-ददछेच्नु', 2, 'दछेच्नुअ-दछेच्नु-अदछेच्नु', 'अ', 'द'],
+ ]
+ );
+ }
+}
diff --git a/src/Symfony/Component/String/Tests/Utf8StringTest.php b/src/Symfony/Component/String/Tests/Utf8StringTest.php
new file mode 100644
index 0000000000..b5381b44cf
--- /dev/null
+++ b/src/Symfony/Component/String/Tests/Utf8StringTest.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\String\Tests;
+
+use Symfony\Component\String\AbstractString;
+use Symfony\Component\String\Utf8String;
+
+class Utf8StringTest extends AbstractUtf8TestCase
+{
+ protected static function createFromString(string $string): AbstractString
+ {
+ return new Utf8String($string);
+ }
+
+ public static function provideLength(): array
+ {
+ return array_merge(
+ parent::provideLength(),
+ [
+ // 8 instead of 5 if it were processed as a grapheme cluster
+ [8, 'अनुच्छेद'],
+ ]
+ );
+ }
+}