From 8f0e97f804ef93e547d4bb4c75965c452d4e13d2 Mon Sep 17 00:00:00 2001 From: Branislav Belohorec Date: Thu, 29 Jan 2026 09:49:15 +0100 Subject: [PATCH 1/3] refs #83719 Add `StringHelper` and its test suite for string utility methods --- src/Helper/StringHelper.php | 64 +++++++ tests/Helper/StringHelperTest.php | 274 ++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 src/Helper/StringHelper.php create mode 100644 tests/Helper/StringHelperTest.php diff --git a/src/Helper/StringHelper.php b/src/Helper/StringHelper.php new file mode 100644 index 0000000..f413a88 --- /dev/null +++ b/src/Helper/StringHelper.php @@ -0,0 +1,64 @@ + $maxLength) { + return mb_substr($string, 0, $maxLength) . self::SHORTHAND_SUFFIX; + } + + return $string; + } + + public static function extractFirstLetter(string $string): string + { + return (new UnicodeString(trim(preg_replace('/[^\p{L}]+/u', '', $string)))) + ->slice(0, 1) + ->ascii() + ->lower() + ->toString(); + } +} diff --git a/tests/Helper/StringHelperTest.php b/tests/Helper/StringHelperTest.php new file mode 100644 index 0000000..88b8383 --- /dev/null +++ b/tests/Helper/StringHelperTest.php @@ -0,0 +1,274 @@ +assertSame('a', StringHelper::extractFirstLetter('Anderson')); + $this->assertSame('b', StringHelper::extractFirstLetter('Brown')); + $this->assertSame('s', StringHelper::extractFirstLetter('Smith')); + $this->assertSame('j', StringHelper::extractFirstLetter('Johnson')); + } + + #[DataProvider('normalStringDataProvider')] + public function testExtractFirstLetterWithNormalStrings(string $input, string $expected): void + { + $result = StringHelper::extractFirstLetter($input); + $this->assertSame($expected, $result); + } + + public function testExtractFirstLetterUnicodeHandling(): void + { + // Test Slovak character handling (á → a, č → c, etc.) + $this->assertSame('c', StringHelper::extractFirstLetter('Čapek')); + $this->assertSame('s', StringHelper::extractFirstLetter('Štefan')); + $this->assertSame('z', StringHelper::extractFirstLetter('Žitný')); + $this->assertSame('n', StringHelper::extractFirstLetter('Ňahaj')); + $this->assertSame('l', StringHelper::extractFirstLetter('Ľuboš')); + $this->assertSame('d', StringHelper::extractFirstLetter('Ďuriš')); + $this->assertSame('t', StringHelper::extractFirstLetter('Ťapák')); + } + + #[DataProvider('unicodeStringDataProvider')] + public function testExtractFirstLetterWithUnicodeCharacters(string $input, string $expected): void + { + $result = StringHelper::extractFirstLetter($input); + $this->assertSame($expected, $result); + } + + public function testExtractFirstLetterEdgeCases(): void + { + // Test empty strings, whitespace, single characters + $this->assertSame('', StringHelper::extractFirstLetter('')); + $this->assertSame('a', StringHelper::extractFirstLetter('a')); + $this->assertSame('z', StringHelper::extractFirstLetter('Z')); + $this->assertSame('s', StringHelper::extractFirstLetter(' Smith')); // Whitespace trimmed + $this->assertSame('', StringHelper::extractFirstLetter(' ')); // Only whitespace + } + + #[DataProvider('edgeCaseDataProvider')] + public function testExtractFirstLetterWithEdgeCases(string $input, string $expected): void + { + $result = StringHelper::extractFirstLetter($input); + $this->assertSame($expected, $result); + } + + public function testExtractFirstLetterSpecialCharacters(): void + { + // Test numbers, punctuation, symbols + $this->assertSame('s', StringHelper::extractFirstLetter('1Smith')); + $this->assertSame('n', StringHelper::extractFirstLetter('2nd Street')); + $this->assertSame('', StringHelper::extractFirstLetter('!@#$%')); // Special chars get normalized + $this->assertSame('p', StringHelper::extractFirstLetter('(parentheses)')); + $this->assertSame('d', StringHelper::extractFirstLetter('-dash')); + } + + public function testExtractFirstLetterCaseHandling(): void + { + // Test uppercase/lowercase normalization + $this->assertSame('a', StringHelper::extractFirstLetter('A')); + $this->assertSame('z', StringHelper::extractFirstLetter('Z')); + $this->assertSame('a', StringHelper::extractFirstLetter('a')); + $this->assertSame('z', StringHelper::extractFirstLetter('z')); + $this->assertSame('m', StringHelper::extractFirstLetter('MixedCase')); + $this->assertSame('l', StringHelper::extractFirstLetter('lowerCase')); + } + + public function testExtractFirstLetterSlovakNameScenarios(): void + { + // Test realistic Slovak name scenarios + $slovakSurnames = [ + 'Novák' => 'n', + 'Kováč' => 'k', + 'Dvořák' => 'd', + 'Horváth' => 'h', + 'Varga' => 'v', + 'Tóth' => 't', + 'Nagy' => 'n', + 'Takács' => 't', + 'Molnár' => 'm', + 'Szabó' => 's', + ]; + + foreach ($slovakSurnames as $surname => $expectedInitial) { + $result = StringHelper::extractFirstLetter($surname); + $this->assertSame($expectedInitial, $result); + } + } + + public function testExtractFirstLetterAccentedNames(): void + { + // Test accented names: "Ľubomír" → "l", "Žofia" → "z", "Čapek" → "c" + $accentedNames = [ + 'Ľubomír' => 'l', + 'Žofia' => 'z', + 'Čapek' => 'c', + 'Šimon' => 's', + 'Ťažký' => 't', + 'Ňuňa' => 'n', + 'Ďaleko' => 'd', + 'Ráž' => 'r', + 'Áno' => 'a', + 'Éva' => 'e', + 'Ívan' => 'i', + 'Óla' => 'o', + 'Úrsula' => 'u', + 'Ýves' => 'y', + ]; + + foreach ($accentedNames as $name => $expectedInitial) { + $result = StringHelper::extractFirstLetter($name); + $this->assertSame($expectedInitial, $result); + } + } + + public function testExtractFirstLetterCompoundNames(): void + { + // Test compound names: "Van Der Berg" → "v", "De Silva" → "d" + $this->assertSame('v', StringHelper::extractFirstLetter('Van Der Berg')); + $this->assertSame('d', StringHelper::extractFirstLetter('De Silva')); + $this->assertSame('v', StringHelper::extractFirstLetter('von Habsburg')); + $this->assertSame('d', StringHelper::extractFirstLetter('da Vinci')); + $this->assertSame('l', StringHelper::extractFirstLetter('La Fontaine')); + } + + public function testExtractFirstLetterNamesWithPrefixes(): void + { + // Test names with prefixes: "Mc Donald" → "m", "O'Connor" → "o" + $this->assertSame('m', StringHelper::extractFirstLetter('McDonald')); + $this->assertSame('m', StringHelper::extractFirstLetter('MacLeod')); + $this->assertSame('o', StringHelper::extractFirstLetter("O'Connor")); + $this->assertSame('o', StringHelper::extractFirstLetter("O'Brien")); + $this->assertSame('d', StringHelper::extractFirstLetter("D'Angelo")); + } + + public function testExtractFirstLetterConsistencyForAlphabetFiltering(): void + { + // Ensure consistent lowercase output for alphabet filtering + $testNames = ['Anderson', 'BROWN', 'clark', 'Davis', 'EVANS']; + $expectedInitials = ['a', 'b', 'c', 'd', 'e']; + + foreach ($testNames as $index => $name) { + $result = StringHelper::extractFirstLetter($name); + $this->assertSame($expectedInitials[$index], $result); + + // Verify it's lowercase + $this->assertSame(strtolower($result), $result); + + // Verify it's suitable for alphabet filtering + $this->assertTrue(ctype_alpha($result) || '' === $result); + $this->assertTrue(mb_strlen($result) <= 1); + } + } + + public function testExtractFirstLetterPersonTextsCompatibility(): void + { + // Test that the method handles the same character sets used in PersonTexts + $personNames = [ + 'John Doe' => 'j', + 'Jane Smith' => 'j', + 'Peter Novák' => 'p', + 'Maria Kováč' => 'm', + 'Disabled Person' => 'd', + 'Spectator Official' => 's', + 'News Editor' => 'n', + 'Sports Manager' => 's', + ]; + + foreach ($personNames as $name => $expectedInitial) { + $result = StringHelper::extractFirstLetter($name); + $this->assertSame($expectedInitial, $result); + } + } + + public function testExtractFirstLetterNormalizationConsistency(): void + { + // Test that the same input always produces the same output + $testInputs = ['Novák', 'SMITH', 'čapek', 'Žitný']; + + foreach ($testInputs as $input) { + $result1 = StringHelper::extractFirstLetter($input); + $result2 = StringHelper::extractFirstLetter($input); + $result3 = StringHelper::extractFirstLetter($input); + + $this->assertSame($result1, $result2); + $this->assertSame($result2, $result3); + } + } + + public static function normalStringDataProvider(): array + { + return [ + 'Simple name' => ['Smith', 's'], + 'Uppercase name' => ['JOHNSON', 'j'], + 'Mixed case name' => ['Anderson', 'a'], + 'Single character' => ['A', 'a'], + 'Long name' => ['Schwarzenegger', 's'], + 'Common surname' => ['Brown', 'b'], + 'International name' => ['García', 'g'], + 'Eastern European' => ['Petrov', 'p'], + 'Nordic name' => ['Andersson', 'a'], + 'Celtic name' => ['Murphy', 'm'], + ]; + } + + public static function unicodeStringDataProvider(): array + { + return [ + // Slovak characters + 'Č character' => ['Čapek', 'c'], + 'Š character' => ['Štefan', 's'], + 'Ž character' => ['Žitný', 'z'], + 'Ň character' => ['Ňahaj', 'n'], + 'Ľ character' => ['Ľuboš', 'l'], + 'Ď character' => ['Ďuriš', 'd'], + 'Ť character' => ['Ťapák', 't'], + 'Ŕ character' => ['Ŕíša', 'r'], + + // Accented vowels + 'Á character' => ['Álvarez', 'a'], + 'É character' => ['Éva', 'e'], + 'Í character' => ['Ívan', 'i'], + 'Ó character' => ['Óla', 'o'], + 'Ú character' => ['Úrsula', 'u'], + 'Ý character' => ['Ýves', 'y'], + + // International characters + 'German Ö' => ['Ömer', 'o'], + 'German Ü' => ['Üwe', 'u'], + 'German Ä' => ['Äpfel', 'a'], + 'French Ç' => ['Çelik', 'c'], + 'Spanish Ñ' => ['Ñoño', 'n'], + 'Polish Ł' => ['Łukasz', 'l'], + ]; + } + + public static function edgeCaseDataProvider(): array + { + return [ + 'Empty string' => ['', ''], + 'Single space' => [' ', ''], + 'Multiple spaces' => [' ', ''], + 'Leading space' => [' Smith', 's'], + 'Tab character' => ["\t", ''], + 'Newline character' => ["\n", ''], + 'Mixed whitespace' => [" \t\n ", ''], + 'Single letter' => ['a', 'a'], + 'Single uppercase' => ['Z', 'z'], + 'Number first' => ['1Smith', 's'], + 'Special char first' => ['!Important', 'i'], + 'Hyphen first' => ['-dash', 'd'], + 'Underscore first' => ['_underscore', 'u'], + 'Parenthesis first' => ['(test)', 't'], + ]; + } +} From bf2baa9158f316e64e239c2c77ed45d597d64aa5 Mon Sep 17 00:00:00 2001 From: Branislav Belohorec Date: Thu, 29 Jan 2026 11:24:33 +0100 Subject: [PATCH 2/3] refs #83719 Add psalm suppress and Empty string --- src/Helper/StringHelper.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Helper/StringHelper.php b/src/Helper/StringHelper.php index f413a88..eaab8ad 100644 --- a/src/Helper/StringHelper.php +++ b/src/Helper/StringHelper.php @@ -8,6 +8,7 @@ final class StringHelper { + private const string EMPTY_STRING = ''; private const string SHORTHAND_SUFFIX = '...'; public static function isNotEmpty(string $string): bool @@ -27,7 +28,7 @@ public static function isNotSame(string $string1, string $string2): bool public static function isEmpty(string $string): bool { - return '' === $string; + return self::EMPTY_STRING === $string; } public static function cutString( @@ -55,7 +56,8 @@ public static function shorthandString( public static function extractFirstLetter(string $string): string { - return (new UnicodeString(trim(preg_replace('/[^\p{L}]+/u', '', $string)))) + /** @psalm-suppress PossiblyNullArgument */ + return (new UnicodeString(trim(preg_replace('/[^\p{L}]+/u', self::EMPTY_STRING, $string)))) ->slice(0, 1) ->ascii() ->lower() From e19083c1c0fa3e1c628b3f5fe4fbe74b3913c210 Mon Sep 17 00:00:00 2001 From: Branislav Belohorec Date: Thu, 29 Jan 2026 11:58:26 +0100 Subject: [PATCH 3/3] refs #83719 Change property to annotation in test --- tests/Helper/StringHelperTest.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/Helper/StringHelperTest.php b/tests/Helper/StringHelperTest.php index 88b8383..db253c8 100644 --- a/tests/Helper/StringHelperTest.php +++ b/tests/Helper/StringHelperTest.php @@ -5,7 +5,6 @@ namespace AnzuSystems\CommonBundle\Tests\Helper; use AnzuSystems\CommonBundle\Helper\StringHelper; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class StringHelperTest extends TestCase @@ -19,7 +18,9 @@ public function testExtractFirstLetterNormalCases(): void $this->assertSame('j', StringHelper::extractFirstLetter('Johnson')); } - #[DataProvider('normalStringDataProvider')] + /** + * @dataProvider normalStringDataProvider + */ public function testExtractFirstLetterWithNormalStrings(string $input, string $expected): void { $result = StringHelper::extractFirstLetter($input); @@ -38,7 +39,9 @@ public function testExtractFirstLetterUnicodeHandling(): void $this->assertSame('t', StringHelper::extractFirstLetter('Ťapák')); } - #[DataProvider('unicodeStringDataProvider')] + /** + * @dataProvider unicodeStringDataProvider + */ public function testExtractFirstLetterWithUnicodeCharacters(string $input, string $expected): void { $result = StringHelper::extractFirstLetter($input); @@ -55,7 +58,9 @@ public function testExtractFirstLetterEdgeCases(): void $this->assertSame('', StringHelper::extractFirstLetter(' ')); // Only whitespace } - #[DataProvider('edgeCaseDataProvider')] + /** + * @dataProvider edgeCaseDataProvider + */ public function testExtractFirstLetterWithEdgeCases(string $input, string $expected): void { $result = StringHelper::extractFirstLetter($input);