diff --git a/README.md b/README.md index c5485051..370956e5 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Polyfills are provided for: - the `str_increment` and `str_decrement` functions introduced in PHP 8.3; - the `Date*Exception/Error` classes introduced in PHP 8.3; - the `SQLite3Exception` class introduced in PHP 8.3; +- the `mb_ucfirst` and `mb_lcfirst` functions introduced in PHP 8.4; It is strongly recommended to upgrade your PHP version and/or install the missing extensions whenever possible. This polyfill should be used only when there is no diff --git a/src/Mbstring/Mbstring.php b/src/Mbstring/Mbstring.php index 2e0b9694..475a5015 100644 --- a/src/Mbstring/Mbstring.php +++ b/src/Mbstring/Mbstring.php @@ -48,6 +48,8 @@ * - mb_strstr - Finds first occurrence of a string within another * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences + * - mb_ucfirst - Make a string's first character uppercase + * - mb_lcfirst - Make a string's first character lowercase * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) @@ -871,6 +873,51 @@ public static function mb_str_pad(string $string, int $length, string $pad_strin } } + public static function mb_ucfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } + + try { + $validEncoding = @self::mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, MB_CASE_TITLE, $encoding); + + return $firstChar . mb_substr($string, 1, null, $encoding); + } + + public static function mb_lcfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = self::mb_internal_encoding(); + } + + try { + $validEncoding = @self::mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, MB_CASE_LOWER, $encoding); + + return $firstChar . mb_substr($string, 1, null, $encoding); + } + private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { diff --git a/src/Mbstring/bootstrap.php b/src/Mbstring/bootstrap.php index ecf1a035..6e4b5fce 100644 --- a/src/Mbstring/bootstrap.php +++ b/src/Mbstring/bootstrap.php @@ -136,6 +136,14 @@ function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstrin function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } } +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + if (extension_loaded('mbstring')) { return; } diff --git a/src/Mbstring/bootstrap80.php b/src/Mbstring/bootstrap80.php index 2f9fb5b4..ec2ae427 100644 --- a/src/Mbstring/bootstrap80.php +++ b/src/Mbstring/bootstrap80.php @@ -132,6 +132,14 @@ function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = nul function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } } +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } +} + if (extension_loaded('mbstring')) { return; } diff --git a/src/Php84/Php84.php b/src/Php84/Php84.php index dc8eea84..c8a9cf16 100644 --- a/src/Php84/Php84.php +++ b/src/Php84/Php84.php @@ -18,4 +18,49 @@ */ final class Php84 { + public static function mb_ucfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, MB_CASE_TITLE, $encoding); + + return $firstChar . mb_substr($string, 1, null, $encoding); + } + + public static function mb_lcfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, MB_CASE_LOWER, $encoding); + + return $firstChar . mb_substr($string, 1, null, $encoding); + } } diff --git a/src/Php84/README.md b/src/Php84/README.md index af10869c..77d249be 100644 --- a/src/Php84/README.md +++ b/src/Php84/README.md @@ -3,6 +3,8 @@ Symfony Polyfill / Php84 This component provides features added to PHP 8.4 core: +- [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst) + More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). diff --git a/src/Php84/bootstrap.php b/src/Php84/bootstrap.php index a0371b29..f73ba3d4 100644 --- a/src/Php84/bootstrap.php +++ b/src/Php84/bootstrap.php @@ -14,3 +14,12 @@ if (\PHP_VERSION_ID >= 80400) { return; } + + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst($string, ?string $encoding = null): string { return p\Php84::mb_ucfirst($string, $encoding); } +} + +if (!function_exists('mb_lcfirst')) { + function mb_lcfirst($string, ?string $encoding = null): string { return p\Php84::mb_lcfirst($string, $encoding); } +} diff --git a/tests/Mbstring/MbstringTest.php b/tests/Mbstring/MbstringTest.php index 2499bed1..be371960 100644 --- a/tests/Mbstring/MbstringTest.php +++ b/tests/Mbstring/MbstringTest.php @@ -657,6 +657,20 @@ public function testMbStrPadInvalidArguments(string $expectedError, string $stri mb_str_pad($string, $length, $padString, $padType, $encoding); } + /** + * @dataProvider ucFirstDataProvider + */ + public function testMbUcFirst(string $string, string $expected): void { + $this->assertSame($expected, mb_ucfirst($string)); + } + + /** + * @dataProvider lcFirstDataProvider + */ + public function testMbLcFirst(string $string, string $expected): void { + $this->assertSame($expected, mb_lcfirst($string)); + } + public static function paddingStringProvider(): iterable { // Simple ASCII strings @@ -727,4 +741,43 @@ public static function mbStrPadInvalidArgumentsProvider(): iterable yield ['mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', '▶▶', 6, ' ', 123456]; yield ['mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "unexisting" given', '▶▶', 6, ' ', \STR_PAD_BOTH, 'unexisting']; } + + public static function ucFirstDataProvider(): array { + return [ + ['', ''], + ['test', 'Test'], + ['TEST', 'TEST'], + ['TesT', 'TesT'], + ['ab', 'Ab'], + ['ABS', 'ABS'], + ['đắt quá!', 'Đắt quá!'], + ['აბგ', 'აბგ'], + ['lj', 'Lj'], + ["\u{01CA}", "\u{01CB}"], + ["\u{01CA}\u{01CA}", "\u{01CB}\u{01CA}"], + ["łámał", "Łámał"], + // Full case-mapping and case-folding that changes the length of the string only supported + // in PHP > 7.3. + ["ßst", PHP_VERSION_ID < 70300 ? "ßst" : "Ssst"], + ]; + } + + public static function lcFirstDataProvider(): array { + return [ + ['', ''], + ['test', 'test'], + ['Test', 'test'], + ['tEST', 'tEST'], + ['Ab', 'ab'], + ['ABS', 'aBS'], + ['Đắt quá!', 'đắt quá!'], + ['აბგ', 'აბგ'], + ['Lj', PHP_VERSION_ID < 70200 ? 'Lj' : 'lj'], + ["\u{01CB}", PHP_VERSION_ID < 70200 ? "\u{01CB}" : "\u{01CC}"], + ["\u{01CA}", "\u{01CC}"], + ["\u{01CA}\u{01CA}", "\u{01CC}\u{01CA}"], + ["\u{212A}\u{01CA}", "\u{006b}\u{01CA}"], + ["ß", "ß"], + ]; + } } diff --git a/tests/Php84/Php84Test.php b/tests/Php84/Php84Test.php index 43dd6e7b..c66f402d 100644 --- a/tests/Php84/Php84Test.php +++ b/tests/Php84/Php84Test.php @@ -11,8 +11,61 @@ namespace Symfony\Polyfill\Tests\Php84; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class Php84Test extends TestCase { + /** + * @dataProvider ucFirstDataProvider + */ + public function testMbUcFirst(string $string, string $expected): void { + $this->assertSame($expected, mb_ucfirst($string)); + } + + /** + * @dataProvider lcFirstDataProvider + */ + public function testMbLcFirst(string $string, string $expected): void { + $this->assertSame($expected, mb_lcfirst($string)); + } + + public static function ucFirstDataProvider(): array { + return [ + ['', ''], + ['test', 'Test'], + ['TEST', 'TEST'], + ['TesT', 'TesT'], + ['ab', 'Ab'], + ['ABS', 'ABS'], + ['đắt quá!', 'Đắt quá!'], + ['აბგ', 'აბგ'], + ['lj', 'Lj'], + ["\u{01CA}", "\u{01CB}"], + ["\u{01CA}\u{01CA}", "\u{01CB}\u{01CA}"], + ["łámał", "Łámał"], + // Full case-mapping and case-folding that changes the length of the string only supported + // in PHP > 7.3. + ["ßst", PHP_VERSION_ID < 70300 ? "ßst" : "Ssst"], + ]; + } + + public static function lcFirstDataProvider(): array { + return [ + ['', ''], + ['test', 'test'], + ['Test', 'test'], + ['tEST', 'tEST'], + ['Ab', 'ab'], + ['ABS', 'aBS'], + ['Đắt quá!', 'đắt quá!'], + ['აბგ', 'აბგ'], + ['Lj', PHP_VERSION_ID < 70200 ? 'Lj' : 'lj'], + ["\u{01CB}", PHP_VERSION_ID < 70200 ? "\u{01CB}" : "\u{01CC}"], + ["\u{01CA}", "\u{01CC}"], + ["\u{01CA}\u{01CA}", "\u{01CC}\u{01CA}"], + ["\u{212A}\u{01CA}", "\u{006b}\u{01CA}"], + ["ß", "ß"], + ]; + } }