From 92292507f95a4cca4b0e54cb4eba1be730c8cf7a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:24:39 +0200 Subject: [PATCH 01/19] [7.0] Bump to PHP 8.2 minimum --- LazyString.php | 2 +- composer.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/LazyString.php b/LazyString.php index 3128ebb..0341bea 100644 --- a/LazyString.php +++ b/LazyString.php @@ -129,7 +129,7 @@ private static function getPrettyName(callable $callback): string } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); - if (str_contains($r->name, '{closure}') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + if (str_contains($r->name, '{closure}') || !$class = $r->getClosureCalledClass()) { return $r->name; } diff --git a/composer.json b/composer.json index 56c1368..26ce26d 100644 --- a/composer.json +++ b/composer.json @@ -16,18 +16,18 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0" }, "conflict": { "symfony/translation-contracts": "<2.5" From 09c2a386ca509a61348de71c40c844e42570a1d8 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 2 Jul 2023 23:52:21 +0200 Subject: [PATCH 02/19] [Components] Convert to native return types --- Slugger/AsciiSlugger.php | 5 +---- UnicodeString.php | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Slugger/AsciiSlugger.php b/Slugger/AsciiSlugger.php index 6e550c6..9f7eba9 100644 --- a/Slugger/AsciiSlugger.php +++ b/Slugger/AsciiSlugger.php @@ -74,10 +74,7 @@ public function __construct(string $defaultLocale = null, array|\Closure $symbol $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; } - /** - * @return void - */ - public function setLocale(string $locale) + public function setLocale(string $locale): void { $this->defaultLocale = $locale; } diff --git a/UnicodeString.php b/UnicodeString.php index 851e087..a888937 100644 --- a/UnicodeString.php +++ b/UnicodeString.php @@ -338,10 +338,7 @@ public function startsWith(string|iterable|AbstractString $prefix): bool return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); } - /** - * @return void - */ - public function __wakeup() + public function __wakeup(): void { if (!\is_string($this->string)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); From 95ff83d5a8a9f4b831cdd139e2ec10b931aa734f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:36:26 +0200 Subject: [PATCH 03/19] Add types to public and protected properties --- AbstractString.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AbstractString.php b/AbstractString.php index cc3a2e0..3f36eef 100644 --- a/AbstractString.php +++ b/AbstractString.php @@ -39,8 +39,8 @@ abstract class AbstractString implements \Stringable, \JsonSerializable public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; - protected $string = ''; - protected $ignoreCase = false; + protected string $string = ''; + protected ?bool $ignoreCase = false; abstract public function __construct(string $string = ''); From 2172dad98a2131a7a7a2ed01cfcea76c90a5d188 Mon Sep 17 00:00:00 2001 From: Bram Leeda Date: Fri, 20 Oct 2023 15:23:26 +0200 Subject: [PATCH 04/19] [String] New locale aware casing methods --- AbstractUnicodeString.php | 74 +++++++++++++++++++ CHANGELOG.md | 5 ++ Tests/AbstractUnicodeTestCase.php | 114 ++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+) diff --git a/AbstractUnicodeString.php b/AbstractUnicodeString.php index df7265f..af05329 100644 --- a/AbstractUnicodeString.php +++ b/AbstractUnicodeString.php @@ -220,6 +220,21 @@ public function lower(): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeLower(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Lower')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->lower(); + } + public function match(string $regexp, int $flags = 0, int $offset = 0): array { $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; @@ -363,6 +378,21 @@ public function title(bool $allWords = false): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeTitle(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Title')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->title(); + } + public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { @@ -450,6 +480,21 @@ public function upper(): static return $str; } + /** + * @param string $locale In the format language_region (e.g. tr_TR) + */ + public function localeUpper(string $locale): static + { + if (null !== $transliterator = $this->getLocaleTransliterator($locale, 'Upper')) { + $str = clone $this; + $str->string = $transliterator->transliterate($str->string); + + return $str; + } + + return $this->upper(); + } + public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; @@ -587,4 +632,33 @@ private function wcswidth(string $string): int return $width; } + + private function getLocaleTransliterator(string $locale, string $id): ?\Transliterator + { + $rule = $locale.'-'.$id; + if (\array_key_exists($rule, self::$transliterators)) { + return self::$transliterators[$rule]; + } + + if (null !== $transliterator = self::$transliterators[$rule] = \Transliterator::create($rule)) { + return $transliterator; + } + + // Try to find a parent locale (nl_BE -> nl) + if (false === $i = strpos($locale, '_')) { + return null; + } + + $parentRule = substr_replace($locale, '-'.$id, $i); + + // Parent locale was already cached, return and store as current locale + if (\array_key_exists($parentRule, self::$transliterators)) { + return self::$transliterators[$rule] = self::$transliterators[$parentRule]; + } + + // Create transliterator based on parent locale and cache the result on both initial and parent locale values + $transliterator = \Transliterator::create($parentRule); + + return self::$transliterators[$rule] = self::$transliterators[$parentRule] = $transliterator; + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a3b54..621cedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `localeLower()`, `localeUpper()`, `localeTitle()` methods to `AbstractUnicodeString` + 6.2 --- diff --git a/Tests/AbstractUnicodeTestCase.php b/Tests/AbstractUnicodeTestCase.php index 1ed16bc..17461fc 100644 --- a/Tests/AbstractUnicodeTestCase.php +++ b/Tests/AbstractUnicodeTestCase.php @@ -50,6 +50,48 @@ public function testAsciiClosureRule() $this->assertSame('Dieser Wert sollte grOEsser oder gleich', (string) $s->ascii([$rule])); } + /** + * @dataProvider provideLocaleLower + * + * @requires extension intl + */ + public function testLocaleLower(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeLower($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + + /** + * @dataProvider provideLocaleUpper + * + * @requires extension intl + */ + public function testLocaleUpper(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeUpper($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + + /** + * @dataProvider provideLocaleTitle + * + * @requires extension intl + */ + public function testLocaleTitle(string $locale, string $expected, string $origin) + { + $instance = static::createFromString($origin)->localeTitle($locale); + + $this->assertNotSame(static::createFromString($origin), $instance); + $this->assertEquals(static::createFromString($expected), $instance); + $this->assertSame($expected, (string) $instance); + } + public function provideCreateFromCodePoint(): array { return [ @@ -291,6 +333,78 @@ public static function provideLower(): array ); } + public static function provideLocaleLower(): array + { + return [ + // Lithuanian + // Introduce an explicit dot above when lowercasing capital I's and J's + // whenever there are more accents above. + // LATIN CAPITAL LETTER I WITH OGONEK -> LATIN SMALL LETTER I WITH OGONEK + ['lt', 'į', 'Į'], + // LATIN CAPITAL LETTER I WITH GRAVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE + ['lt', 'i̇̀', 'Ì'], + // LATIN CAPITAL LETTER I WITH ACUTE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING ACUTE ACCENT + ['lt', 'i̇́', 'Í'], + // LATIN CAPITAL LETTER I WITH TILDE -> LATIN SMALL LETTER I COMBINING DOT ABOVE COMBINING TILDE + ['lt', 'i̇̃', 'Ĩ'], + + // Turkish and Azeri + // When lowercasing, remove dot_above in the sequence I + dot_above, which will turn into 'i'. + // LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I + ['tr', 'i', 'İ'], + ['tr_TR', 'i', 'İ'], + ['az', 'i', 'İ'], + + // Default casing rules + // LATIN CAPITAL LETTER I WITH DOT ABOVE -> LATIN SMALL LETTER I COMBINING DOT ABOVE + ['en_US', 'i̇', 'İ'], + ['en', 'i̇', 'İ'], + ]; + } + + public static function provideLocaleUpper(): array + { + return [ + // Turkish and Azeri + // When uppercasing, i turns into a dotted capital I + // LATIN SMALL LETTER I -> LATIN CAPITAL LETTER I WITH DOT ABOVE + ['tr', 'İ', 'i'], + ['tr_TR', 'İ', 'i'], + ['az', 'İ', 'i'], + + // Greek + // Remove accents when uppercasing + // GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA + ['el', 'Α', 'ά'], + ['el_GR', 'Α', 'ά'], + + // Default casing rules + // GREEK SMALL LETTER ALPHA WITH TONOS -> GREEK CAPITAL LETTER ALPHA WITH TONOS + ['en_US', 'Ά', 'ά'], + ['en', 'Ά', 'ά'], + ]; + } + + public static function provideLocaleTitle(): array + { + return [ + // Greek + // Titlecasing words, should keep the accents on the first letter + ['el', 'Άδικος', 'άδικος'], + ['el_GR', 'Άδικος', 'άδικος'], + ['en', 'Άδικος', 'άδικος'], + + // Dutch + // Title casing should treat 'ij' as one character + ['nl_NL', 'IJssel', 'ijssel'], + ['nl_BE', 'IJssel', 'ijssel'], + ['nl', 'IJssel', 'ijssel'], + + // Default casing rules + ['en', 'Ijssel', 'ijssel'], + ]; + } + public static function provideUpper(): array { return array_merge( From 1cb3ad53b50464c7158268eee17b4e7fc742aec6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 30 Dec 2023 20:21:21 +0100 Subject: [PATCH 05/19] Leverage ReflectionFunction::isAnonymous() --- LazyString.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LazyString.php b/LazyString.php index 0341bea..4a303e1 100644 --- a/LazyString.php +++ b/LazyString.php @@ -129,7 +129,7 @@ private static function getPrettyName(callable $callback): string } elseif ($callback instanceof \Closure) { $r = new \ReflectionFunction($callback); - if (str_contains($r->name, '{closure}') || !$class = $r->getClosureCalledClass()) { + if ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) { return $r->name; } From 7a06dfa63d80e98eedc362c12fad3381a249cd63 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 29 Dec 2023 21:35:44 +0100 Subject: [PATCH 06/19] [String] Use CPP --- Resources/WcswidthDataGenerator.php | 8 +++----- Slugger/AsciiSlugger.php | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/WcswidthDataGenerator.php b/Resources/WcswidthDataGenerator.php index 6425ecc..dd6b923 100644 --- a/Resources/WcswidthDataGenerator.php +++ b/Resources/WcswidthDataGenerator.php @@ -21,13 +21,11 @@ */ final class WcswidthDataGenerator { - private string $outDir; private HttpClientInterface $client; - public function __construct(string $outDir) - { - $this->outDir = $outDir; - + public function __construct( + private string $outDir, + ) { $this->client = HttpClient::createForBaseUri('https://www.unicode.org/Public/UNIDATA/'); } diff --git a/Slugger/AsciiSlugger.php b/Slugger/AsciiSlugger.php index 9f7eba9..4f428da 100644 --- a/Slugger/AsciiSlugger.php +++ b/Slugger/AsciiSlugger.php @@ -55,7 +55,6 @@ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface 'zh' => 'Han-Latin', ]; - private ?string $defaultLocale; private \Closure|array $symbolsMap = [ 'en' => ['@' => 'at', '&' => 'and'], ]; @@ -68,9 +67,10 @@ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface */ private array $transliterators = []; - public function __construct(string $defaultLocale = null, array|\Closure $symbolsMap = null) - { - $this->defaultLocale = $defaultLocale; + public function __construct( + private ?string $defaultLocale = null, + array|\Closure $symbolsMap = null, + ) { $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; } From dd989887f0b906f5245888eab8fa0cc558382974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 16 Dec 2023 08:55:09 +0100 Subject: [PATCH 07/19] Move & adapt "emoji code" from Intl into its own component Transfert emoji data from Intl to emoji component Update main composer.json Fix phpunit config Update composer and README descriptions Fix LICENCE date Update src/Symfony/Component/Intl/CHANGELOG.md Co-authored-by: Oskar Stark Fix Changelog I feel that cool resolve some of the recent issues linked to the Profiler. Rename component Emoji + unlink from Intl (no shared resp/code) Isolated commit to move data Update Github worflows Use Emoji in String component Update src/Symfony/Component/Emoji/CHANGELOG.md Co-authored-by: Nicolas Grekas Update src/Symfony/Component/Emoji/README.md Co-authored-by: Nicolas Grekas Present the compress command in both README's Update src/Symfony/Component/Intl/CHANGELOG.md Co-authored-by: Nicolas Grekas Fix main composer.json Revert symfony/intl requires symfony/emoji Remove EmojiTransliteratorTrait Move emoji data Add "symfony/deprecation-contracts" to Intl Revert data test split Add symfony/emoji to String (dev) Fix String Test namespace Fix .gitattributes hides "bin/compress" script Please Psalm ? Compute quickCheck once Update LICENCE Add Intl conflict with string < 7.1 Fix Int changelog Fix composer.json CS Throw exception in Intl BC layer when symfony/emoji is not installed Test Intl & Emoji in the same job Remove useless check Remove useless check (without breaking things) --- Slugger/AsciiSlugger.php | 4 ++-- Tests/Slugger/AsciiSluggerTest.php | 2 +- composer.json | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Slugger/AsciiSlugger.php b/Slugger/AsciiSlugger.php index 4f428da..c937318 100644 --- a/Slugger/AsciiSlugger.php +++ b/Slugger/AsciiSlugger.php @@ -11,7 +11,7 @@ namespace Symfony\Component\String\Slugger; -use Symfony\Component\Intl\Transliterator\EmojiTransliterator; +use Symfony\Component\Emoji\EmojiTransliterator; use Symfony\Component\String\AbstractUnicodeString; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\Translation\LocaleAwareInterface; @@ -92,7 +92,7 @@ public function getLocale(): string public function withEmoji(bool|string $emoji = true): static { if (false !== $emoji && !class_exists(EmojiTransliterator::class)) { - throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/intl" package is not installed. Try running "composer require symfony/intl".', __METHOD__)); + throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/emoji" package is not installed. Try running "composer require symfony/emoji".', __METHOD__)); } $new = clone $this; diff --git a/Tests/Slugger/AsciiSluggerTest.php b/Tests/Slugger/AsciiSluggerTest.php index 3544367..ab4b799 100644 --- a/Tests/Slugger/AsciiSluggerTest.php +++ b/Tests/Slugger/AsciiSluggerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\String; +namespace Symfony\Component\String\Tests\Slugger; use PHPUnit\Framework\TestCase; use Symfony\Component\String\Slugger\AsciiSlugger; diff --git a/composer.json b/composer.json index 26ce26d..1959f5d 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,9 @@ }, "require-dev": { "symfony/error-handler": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.0", "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^6.4|^7.0" }, From 3d0a98879bbc7585aff5f9a5a539074328cc2ca0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 3 Feb 2024 20:41:36 +0100 Subject: [PATCH 08/19] bump version for symfony/emoji --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1959f5d..10d0ee6 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ }, "require-dev": { "symfony/error-handler": "^6.4|^7.0", - "symfony/emoji": "^7.0", + "symfony/emoji": "^7.1", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", From ef964850371aca5def9fc8a3059d9b1690bb3b6c Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 8 Mar 2024 13:47:07 +0100 Subject: [PATCH 09/19] [String] Leverage Randomizer::getBytesFromString() --- ByteString.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ByteString.php b/ByteString.php index 3ebe43c..e6b56ae 100644 --- a/ByteString.php +++ b/ByteString.php @@ -11,6 +11,7 @@ namespace Symfony\Component\String; +use Random\Randomizer; use Symfony\Component\String\Exception\ExceptionInterface; use Symfony\Component\String\Exception\InvalidArgumentException; use Symfony\Component\String\Exception\RuntimeException; @@ -55,6 +56,10 @@ public static function fromRandom(int $length = 16, ?string $alphabet = null): s throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); } + if (\PHP_VERSION_ID >= 80300) { + return new static((new Randomizer())->getBytesFromString($alphabet, $length)); + } + $ret = ''; while ($length > 0) { $urandomLength = (int) ceil(2 * $length * $bits / 8.0); From 29dc30bf66fe1a2b70c31543012bc108a81dc3c5 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 18 Mar 2024 20:27:13 +0100 Subject: [PATCH 10/19] chore: CS fixes --- Resources/bin/update-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/bin/update-data.php b/Resources/bin/update-data.php index c0bd1c1..2ba0cd8 100644 --- a/Resources/bin/update-data.php +++ b/Resources/bin/update-data.php @@ -18,7 +18,7 @@ error_reporting(\E_ALL); set_error_handler(static function (int $type, string $msg, string $file, int $line): void { - throw new \ErrorException($msg, 0, $type, $file, $line); + throw new ErrorException($msg, 0, $type, $file, $line); }); set_exception_handler(static function (Throwable $exception): void { From adfa98a608104d8f6b4d008d144523f8b79f0016 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 11/19] Prefix all sprintf() calls --- AbstractString.php | 12 ++++++------ AbstractUnicodeString.php | 4 ++-- ByteString.php | 4 ++-- LazyString.php | 4 ++-- Resources/WcswidthDataGenerator.php | 2 +- Slugger/AsciiSlugger.php | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AbstractString.php b/AbstractString.php index 253d2dc..d68f334 100644 --- a/AbstractString.php +++ b/AbstractString.php @@ -263,7 +263,7 @@ public function containsAny(string|iterable $needle): bool public function endsWith(string|iterable $suffix): bool { if (\is_string($suffix)) { - throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($suffix as $s) { @@ -312,7 +312,7 @@ public function ensureStart(string $prefix): static public function equalsTo(string|iterable $string): bool { if (\is_string($string)) { - throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($string as $s) { @@ -340,7 +340,7 @@ public function ignoreCase(): static public function indexOf(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { - throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = \PHP_INT_MAX; @@ -362,7 +362,7 @@ public function indexOf(string|iterable $needle, int $offset = 0): ?int public function indexOfLast(string|iterable $needle, int $offset = 0): ?int { if (\is_string($needle)) { - throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } $i = null; @@ -414,7 +414,7 @@ abstract public function prepend(string ...$prefix): static; public function repeat(int $multiplier): static { if (0 > $multiplier) { - throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier)); + throw new InvalidArgumentException(\sprintf('Multiplier must be positive, %d given.', $multiplier)); } $str = clone $this; @@ -481,7 +481,7 @@ public function split(string $delimiter, ?int $limit = null, ?int $flags = null) public function startsWith(string|iterable $prefix): bool { if (\is_string($prefix)) { - throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + throw new \TypeError(\sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); } foreach ($prefix as $prefix) { diff --git a/AbstractUnicodeString.php b/AbstractUnicodeString.php index f3acb2e..45649ff 100644 --- a/AbstractUnicodeString.php +++ b/AbstractUnicodeString.php @@ -124,7 +124,7 @@ public function ascii(array $rules = []): self } if (null === $transliterator) { - throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule)); + throw new InvalidArgumentException(\sprintf('Unknown transliteration rule "%s".', $rule)); } self::$transliterators['any-latin/bgn'] = $transliterator; @@ -139,7 +139,7 @@ public function ascii(array $rules = []): self $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { - throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); + throw new \LogicException(\sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); } return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); diff --git a/ByteString.php b/ByteString.php index e6b56ae..98bad02 100644 --- a/ByteString.php +++ b/ByteString.php @@ -46,7 +46,7 @@ public function __construct(string $string = '') public static function fromRandom(int $length = 16, ?string $alphabet = null): self { if ($length <= 0) { - throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); + throw new InvalidArgumentException(\sprintf('A strictly positive length is expected, "%d" given.', $length)); } $alphabet ??= self::ALPHABET_ALPHANUMERIC; @@ -441,7 +441,7 @@ public function toCodePointString(?string $fromEncoding = null): CodePointString } if (!$validEncoding) { - throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); + throw new InvalidArgumentException(\sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); } $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); diff --git a/LazyString.php b/LazyString.php index 8f2bbbf..b86d733 100644 --- a/LazyString.php +++ b/LazyString.php @@ -26,7 +26,7 @@ class LazyString implements \Stringable, \JsonSerializable public static function fromCallable(callable|array $callback, mixed ...$arguments): static { if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); + throw new \TypeError(\sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); } $lazyString = new static(); @@ -94,7 +94,7 @@ public function __toString(): string $r = new \ReflectionFunction($this->value); $callback = $r->getStaticVariables()['callback']; - $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); + $e = new \TypeError(\sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); } throw $e; diff --git a/Resources/WcswidthDataGenerator.php b/Resources/WcswidthDataGenerator.php index dd6b923..19e6e89 100644 --- a/Resources/WcswidthDataGenerator.php +++ b/Resources/WcswidthDataGenerator.php @@ -71,7 +71,7 @@ private function write(string $fileName, string $version, array $rawData): void $content = $this->getHeader($version).'return '.VarExporter::export($this->format($rawData)).";\n"; if (!file_put_contents($this->outDir.'/'.$fileName, $content)) { - throw new RuntimeException(sprintf('The "%s" file could not be written.', $fileName)); + throw new RuntimeException(\sprintf('The "%s" file could not be written.', $fileName)); } } diff --git a/Slugger/AsciiSlugger.php b/Slugger/AsciiSlugger.php index d254532..9d4edf1 100644 --- a/Slugger/AsciiSlugger.php +++ b/Slugger/AsciiSlugger.php @@ -92,7 +92,7 @@ public function getLocale(): string public function withEmoji(bool|string $emoji = true): static { if (false !== $emoji && !class_exists(EmojiTransliterator::class)) { - throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/emoji" package is not installed. Try running "composer require symfony/emoji".', __METHOD__)); + throw new \LogicException(\sprintf('You cannot use the "%s()" method as the "symfony/emoji" package is not installed. Try running "composer require symfony/emoji".', __METHOD__)); } $new = clone $this; From 531bcd7d236925c5e4ee52fb5b272f9277aef872 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 16 Jun 2024 17:17:26 +0200 Subject: [PATCH 12/19] chore: CS fixes --- Inflector/EnglishInflector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Inflector/EnglishInflector.php b/Inflector/EnglishInflector.php index 56f03b7..9c1b9cb 100644 --- a/Inflector/EnglishInflector.php +++ b/Inflector/EnglishInflector.php @@ -141,7 +141,7 @@ final class EnglishInflector implements InflectorInterface // shoes (shoe) ['se', 2, true, true, ['', 'e']], - // status (status) + // status (status) ['sutats', 6, true, true, 'status'], // tags (tag) From 25ad779fed5c907f820454d981f26b2f945a14ab Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Thu, 30 May 2024 14:00:21 +0200 Subject: [PATCH 13/19] [String] Add WORD_STRICT mode to truncate method --- AbstractString.php | 13 ++++++++-- CHANGELOG.md | 5 ++++ Tests/AbstractAsciiTestCase.php | 15 ++++++++++-- TruncateMode.php | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 TruncateMode.php diff --git a/AbstractString.php b/AbstractString.php index d68f334..176a7ae 100644 --- a/AbstractString.php +++ b/AbstractString.php @@ -605,7 +605,7 @@ public function trimSuffix($suffix): static return $str; } - public function truncate(int $length, string $ellipsis = '', bool $cut = true): static + public function truncate(int $length, string $ellipsis = '', bool|TruncateMode $cut = TruncateMode::Char): static { $stringLength = $this->length(); @@ -619,7 +619,8 @@ public function truncate(int $length, string $ellipsis = '', bool $cut = true): $ellipsisLength = 0; } - if (!$cut) { + $desiredLength = $length; + if (TruncateMode::WordAfter === $cut || TruncateMode::WordBefore === $cut || !$cut) { if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { return clone $this; } @@ -629,6 +630,14 @@ public function truncate(int $length, string $ellipsis = '', bool $cut = true): $str = $this->slice(0, $length - $ellipsisLength); + if (TruncateMode::WordBefore === $cut) { + if (0 === $ellipsisLength && $desiredLength === $this->indexOf([' ', "\r", "\n", "\t"], $length)) { + return $str; + } + + $str = $str->beforeLast([' ', "\r", "\n", "\t"]); + } + return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 621cedf..f1f7204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add `TruncateMode` enum to handle more truncate methods + 7.1 --- diff --git a/Tests/AbstractAsciiTestCase.php b/Tests/AbstractAsciiTestCase.php index f8b0509..3cbcd08 100644 --- a/Tests/AbstractAsciiTestCase.php +++ b/Tests/AbstractAsciiTestCase.php @@ -16,6 +16,7 @@ use Symfony\Component\String\ByteString; use Symfony\Component\String\CodePointString; use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\TruncateMode; use Symfony\Component\String\UnicodeString; abstract class AbstractAsciiTestCase extends TestCase @@ -1500,22 +1501,24 @@ public static function providePadStart() /** * @dataProvider provideTruncate */ - public function testTruncate(string $expected, string $origin, int $length, string $ellipsis, bool $cut = true) + public function testTruncate(string $expected, string $origin, int $length, string $ellipsis, bool|TruncateMode $cut = TruncateMode::Char) { $instance = static::createFromString($origin)->truncate($length, $ellipsis, $cut); $this->assertEquals(static::createFromString($expected), $instance); } - public static function provideTruncate() + public static function provideTruncate(): array { return [ ['', '', 3, ''], ['', 'foo', 0, '...'], ['foo', 'foo', 0, '...', false], + ['foo', 'foo', 0, '...', TruncateMode::WordAfter], ['fo', 'foobar', 2, ''], ['foobar', 'foobar', 10, ''], ['foobar', 'foobar', 10, '...', false], + ['foobar', 'foobar', 10, '...', TruncateMode::WordAfter], ['foo', 'foo', 3, '...'], ['fo', 'foobar', 2, '...'], ['...', 'foobar', 3, '...'], @@ -1524,6 +1527,14 @@ public static function provideTruncate() ['foobar...', 'foobar foo', 7, '...', false], ['foobar foo...', 'foobar foo a', 10, '...', false], ['foobar foo aar', 'foobar foo aar', 12, '...', false], + ['foobar...', 'foobar foo', 6, '...', TruncateMode::WordAfter], + ['foobar...', 'foobar foo', 7, '...', TruncateMode::WordAfter], + ['foobar foo...', 'foobar foo a', 10, '...', TruncateMode::WordAfter], + ['foobar foo aar', 'foobar foo aar', 12, '...', TruncateMode::WordAfter], + ['foobar foo', 'foobar foo aar', 10, '', TruncateMode::WordBefore], + ['foobar...', 'foobar foo aar', 10, '...', TruncateMode::WordBefore], + ['Lorem ipsum', 'Lorem ipsum dolor sit amet', 14, '', TruncateMode::WordBefore], + ['Lorem...', 'Lorem ipsum dolor sit amet', 10, '...', TruncateMode::WordBefore], ]; } diff --git a/TruncateMode.php b/TruncateMode.php new file mode 100644 index 0000000..12568cd --- /dev/null +++ b/TruncateMode.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +enum TruncateMode +{ + /** + * Will cut exactly at given length. + * + * Length: 14 + * Source: Lorem ipsum dolor sit amet + * Output: Lorem ipsum do + */ + case Char; + + /** + * Returns the string up to the last complete word containing the specified length. + * + * Length: 14 + * Source: Lorem ipsum dolor sit amet + * Output: Lorem ipsum + */ + case WordBefore; + + /** + * Returns the string up to the complete word after or at the given length. + * + * Length: 14 + * Source: Lorem ipsum dolor sit amet + * Output: Lorem ipsum dolor + */ + case WordAfter; +} From 10e1cbfff6d8f2c14f8f6236d5a1d9bca48797e8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 14/19] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0f57d86..166549d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,5 +2,4 @@ /Resources/WcswidthDataGenerator.php export-ignore /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore From f9fde996ab74dcc429d00161da2fad36a1f47d13 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 19 Jul 2024 14:33:38 +0200 Subject: [PATCH 15/19] fix truncating in WordBefore mode with length after last space --- AbstractString.php | 4 +++- Tests/AbstractAsciiTestCase.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/AbstractString.php b/AbstractString.php index 176a7ae..81636c4 100644 --- a/AbstractString.php +++ b/AbstractString.php @@ -620,11 +620,13 @@ public function truncate(int $length, string $ellipsis = '', bool|TruncateMode $ } $desiredLength = $length; - if (TruncateMode::WordAfter === $cut || TruncateMode::WordBefore === $cut || !$cut) { + if (TruncateMode::WordAfter === $cut || !$cut) { if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { return clone $this; } + $length += $ellipsisLength; + } elseif (TruncateMode::WordBefore === $cut && null !== $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { $length += $ellipsisLength; } diff --git a/Tests/AbstractAsciiTestCase.php b/Tests/AbstractAsciiTestCase.php index 0c04f30..196b55a 100644 --- a/Tests/AbstractAsciiTestCase.php +++ b/Tests/AbstractAsciiTestCase.php @@ -1534,14 +1534,42 @@ public static function provideTruncate(): array ['foobar...', 'foobar foo', 7, '...', false], ['foobar foo...', 'foobar foo a', 10, '...', false], ['foobar foo aar', 'foobar foo aar', 12, '...', false], + ['foobar', 'foobar foo', 6, '', TruncateMode::Char], + ['foobar', 'foobar foo', 6, '', TruncateMode::WordAfter], + ['foobar', 'foobar foo', 6, '', TruncateMode::WordBefore], + ['foo...', 'foobar foo', 6, '...', TruncateMode::Char], ['foobar...', 'foobar foo', 6, '...', TruncateMode::WordAfter], + ['foobar...', 'foobar foo', 6, '...', TruncateMode::WordBefore], + ['foobar ', 'foobar foo', 7, '', TruncateMode::Char], + ['foobar', 'foobar foo', 7, '', TruncateMode::WordAfter], + ['foobar', 'foobar foo', 7, '', TruncateMode::WordBefore], + ['foob...', 'foobar foo', 7, '...', TruncateMode::Char], ['foobar...', 'foobar foo', 7, '...', TruncateMode::WordAfter], + ['foobar...', 'foobar foo', 7, '...', TruncateMode::WordBefore], + ['foobar foo', 'foobar foo a', 10, '', TruncateMode::Char], + ['foobar foo', 'foobar foo a', 10, '', TruncateMode::WordAfter], + ['foobar foo', 'foobar foo a', 10, '', TruncateMode::WordBefore], + ['foobar...', 'foobar foo a', 10, '...', TruncateMode::Char], ['foobar foo...', 'foobar foo a', 10, '...', TruncateMode::WordAfter], + ['foobar...', 'foobar foo a', 10, '...', TruncateMode::WordBefore], + ['foobar foo a', 'foobar foo aar', 12, '', TruncateMode::Char], + ['foobar foo aar', 'foobar foo aar', 12, '', TruncateMode::WordAfter], + ['foobar foo', 'foobar foo aar', 12, '', TruncateMode::WordBefore], + ['foobar fo...', 'foobar foo aar', 12, '...', TruncateMode::Char], ['foobar foo aar', 'foobar foo aar', 12, '...', TruncateMode::WordAfter], + ['foobar...', 'foobar foo aar', 12, '...', TruncateMode::WordBefore], + ['foobar foo', 'foobar foo aar', 10, '', TruncateMode::Char], ['foobar foo', 'foobar foo aar', 10, '', TruncateMode::WordBefore], + ['foobar foo', 'foobar foo aar', 10, '', TruncateMode::WordAfter], + ['foobar...', 'foobar foo aar', 10, '...', TruncateMode::Char], ['foobar...', 'foobar foo aar', 10, '...', TruncateMode::WordBefore], + ['foobar foo...', 'foobar foo aar', 10, '...', TruncateMode::WordAfter], + ['Lorem ipsum do', 'Lorem ipsum dolor sit amet', 14, '', TruncateMode::Char], ['Lorem ipsum', 'Lorem ipsum dolor sit amet', 14, '', TruncateMode::WordBefore], + ['Lorem ipsum dolor', 'Lorem ipsum dolor sit amet', 14, '', TruncateMode::WordAfter], + ['Lorem i...', 'Lorem ipsum dolor sit amet', 10, '...', TruncateMode::Char], ['Lorem...', 'Lorem ipsum dolor sit amet', 10, '...', TruncateMode::WordBefore], + ['Lorem ipsum...', 'Lorem ipsum dolor sit amet', 10, '...', TruncateMode::WordAfter], ]; } From a97458d764c940e73b5debf56fe2054b9423eab4 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 29 Jul 2024 09:33:48 +0200 Subject: [PATCH 16/19] Remove useless code --- ByteString.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ByteString.php b/ByteString.php index 98bad02..5cbfd6d 100644 --- a/ByteString.php +++ b/ByteString.php @@ -340,7 +340,7 @@ public function reverse(): static public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; - $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); + $str->string = substr($this->string, $start, $length ?? \PHP_INT_MAX); return $str; } From f1f4d05da68b136a32593bf967cc716f94b2d95b Mon Sep 17 00:00:00 2001 From: Dennis Tobar Date: Tue, 10 Sep 2024 22:55:42 -0300 Subject: [PATCH 17/19] [String] Add Spanish inflector with some rules --- Inflector/SpanishInflector.php | 126 ++++++++++++++++++ Tests/Inflector/SpanishInflectorTest.php | 158 +++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 Inflector/SpanishInflector.php create mode 100644 Tests/Inflector/SpanishInflectorTest.php diff --git a/Inflector/SpanishInflector.php b/Inflector/SpanishInflector.php new file mode 100644 index 0000000..4b98cb6 --- /dev/null +++ b/Inflector/SpanishInflector.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +final class SpanishInflector implements InflectorInterface +{ + /** + * A list of all rules for pluralise. + * + * @see https://www.spanishdict.com/guide/spanish-plural-noun-forms + * @see https://www.rae.es/gram%C3%A1tica/morfolog%C3%ADa/la-formaci%C3%B3n-del-plural-plurales-en-s-y-plurales-en-es-reglas-generales + */ + // First entry: regex + // Second entry: replacement + private const PLURALIZE_REGEXP = [ + // Specials sí, no + ['/(sí|no)$/i', '\1es'], + + // Words ending with vowel must use -s (RAE 3.2a, 3.2c) + ['/(a|e|i|o|u|á|é|í|ó|ú)$/i', '\1s'], + + // Word ending in s or x and the previous letter is accented (RAE 3.2n) + ['/ás$/i', 'ases'], + ['/és$/i', 'eses'], + ['/ís$/i', 'ises'], + ['/ós$/i', 'oses'], + ['/ús$/i', 'uses'], + + // Words ending in -ión must changed to -iones + ['/ión$/i', '\1iones'], + + // Words ending in some consonants must use -es (RAE 3.2k) + ['/(l|r|n|d|j|s|x|ch|y)$/i', '\1es'], + + // Word ending in z, must changed to ces + ['/(z)$/i', 'ces'], + ]; + + /** + * A list of all rules for singularize. + */ + private const SINGULARIZE_REGEXP = [ + // Specials sí, no + ['/(sí|no)es$/i', '\1'], + + // Words ending in -ión must changed to -iones + ['/iones$/i', '\1ión'], + + // Word ending in z, must changed to ces + ['/ces$/i', 'z'], + + // Word ending in s or x and the previous letter is accented (RAE 3.2n) + ['/(\w)ases$/i', '\1ás'], + ['/eses$/i', 'és'], + ['/ises$/i', 'ís'], + ['/(\w{2,})oses$/i', '\1ós'], + ['/(\w)uses$/i', '\1ús'], + + // Words ending in some consonants and -es, must be the consonants + ['/(l|r|n|d|j|s|x|ch|y)e?s$/i', '\1'], + + // Words ended with vowel and s, must be vowel + ['/(a|e|i|o|u|á|é|ó|í|ú)s$/i', '\1'], + ]; + + private const UNINFLECTED_RULES = [ + // Words ending with pies (RAE 3.2n) + '/.*(piés)$/i', + ]; + + private const UNINFLECTED = '/^(lunes|martes|miércoles|jueves|viernes|análisis|torax|yo|pies)$/i'; + + public function singularize(string $plural): array + { + if ($this->isInflectedWord($plural)) { + return [$plural]; + } + + foreach (self::SINGULARIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $plural)) { + return [preg_replace($regexp, $replace, $plural)]; + } + } + + return [$plural]; + } + + public function pluralize(string $singular): array + { + if ($this->isInflectedWord($singular)) { + return [$singular]; + } + + foreach (self::PLURALIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $singular)) { + return [preg_replace($regexp, $replace, $singular)]; + } + } + + return [$singular.'s']; + } + + private function isInflectedWord(string $word): bool + { + foreach (self::UNINFLECTED_RULES as $rule) { + if (1 === preg_match($rule, $word)) { + return true; + } + } + + return 1 === preg_match(self::UNINFLECTED, $word); + } +} diff --git a/Tests/Inflector/SpanishInflectorTest.php b/Tests/Inflector/SpanishInflectorTest.php new file mode 100644 index 0000000..f0b8e42 --- /dev/null +++ b/Tests/Inflector/SpanishInflectorTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Tests\Inflector; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\String\Inflector\SpanishInflector; + +class SpanishInflectorTest extends TestCase +{ + public static function singularizeProvider(): array + { + return [ + // vowels (RAE 3.2a, 3.2c) + ['peras', 'pera'], + ['especies', 'especie'], + ['álcalis', 'álcali'], + ['códigos', 'código'], + ['espíritus', 'espíritu'], + + // accented (RAE 3.2a, 3.2c) + ['papás', 'papá'], + ['cafés', 'café'], + ['isrealís', 'isrealí'], + ['burós', 'buró'], + ['tisús', 'tisú'], + + // ending in -ión + ['aviones', 'avión'], + ['camiones', 'camión'], + + // ending in some letters (RAE 3.2k) + ['amores', 'amor'], + ['antifaces', 'antifaz'], + ['atriles', 'atril'], + ['fácsimiles', 'fácsimil'], + ['vides', 'vid'], + ['reyes', 'rey'], + ['relojes', 'reloj'], + ['faxes', 'fax'], + ['sándwiches', 'sándwich'], + ['cánones', 'cánon'], + + // (RAE 3.2n) + ['adioses', 'adiós'], + ['aguarrases', 'aguarrás'], + ['arneses', 'arnés'], + ['autobuses', 'autobús'], + ['kermeses', 'kermés'], + ['palmareses', 'palmarés'], + ['toses', 'tos'], + + // Special + ['síes', 'sí'], + ['noes', 'no'], + ]; + } + + public static function pluralizeProvider(): array + { + return [ + // vowels (RAE 3.2a, 3.2c) + ['pera', 'peras'], + ['especie', 'especies'], + ['álcali', 'álcalis'], + ['código', 'códigos'], + ['espíritu', 'espíritus'], + + // accented (RAE 3.2a, 3.2c) + ['papá', 'papás'], + ['café', 'cafés'], + ['isrealí', 'isrealís'], + ['buró', 'burós'], + ['tisú', 'tisús'], + + // ending in -ión + ['avión', 'aviones'], + ['camión', 'camiones'], + + // ending in some letters (RAE 3.2k) + ['amor', 'amores'], + ['antifaz', 'antifaces'], + ['atril', 'atriles'], + ['fácsimil', 'fácsimiles'], + ['vid', 'vides'], + ['rey', 'reyes'], + ['reloj', 'relojes'], + ['fax', 'faxes'], + ['sándwich', 'sándwiches'], + ['cánon', 'cánones'], + + // (RAE 3.2n) + ['adiós', 'adioses'], + ['aguarrás', 'aguarrases'], + ['arnés', 'arneses'], + ['autobús', 'autobuses'], + ['kermés', 'kermeses'], + ['palmarés', 'palmareses'], + ['tos', 'toses'], + + // Specials + ['sí', 'síes'], + ['no', 'noes'], + ]; + } + + public static function uninflectedProvider(): array + { + return [ + ['lunes'], + ['rodapiés'], + ['reposapiés'], + ['miércoles'], + ['pies'], + ]; + } + + /** + * @dataProvider singularizeProvider + */ + public function testSingularize(string $plural, $singular) + { + $this->assertSame( + \is_array($singular) ? $singular : [$singular], + (new SpanishInflector())->singularize($plural) + ); + } + + /** + * @dataProvider pluralizeProvider + */ + public function testPluralize(string $singular, $plural) + { + $this->assertSame( + \is_array($plural) ? $plural : [$plural], + (new SpanishInflector())->pluralize($singular) + ); + } + + /** + * @dataProvider uninflectedProvider + */ + public function testUninflected(string $word) + { + $this->assertSame( + \is_array($word) ? $word : [$word], + (new SpanishInflector())->pluralize($word) + ); + } +} From 8133473e9c048c97c698d6606efe2fe4ca629657 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 25 Sep 2024 13:30:45 +0200 Subject: [PATCH 18/19] [String] Add the `AbstractString::kebab()` method --- AbstractString.php | 5 +++++ CHANGELOG.md | 1 + Tests/AbstractAsciiTestCase.php | 29 +++++++++++++++++++++++++++++ Tests/AbstractUnicodeTestCase.php | 9 +++++++++ 4 files changed, 44 insertions(+) diff --git a/AbstractString.php b/AbstractString.php index 81636c4..500d7c3 100644 --- a/AbstractString.php +++ b/AbstractString.php @@ -433,6 +433,11 @@ abstract public function slice(int $start = 0, ?int $length = null): static; abstract public function snake(): static; + public function kebab(): static + { + return $this->snake()->replace('_', '-'); + } + abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static; /** diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f7204..ff505b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `TruncateMode` enum to handle more truncate methods + * Add the `AbstractString::kebab()` method 7.1 --- diff --git a/Tests/AbstractAsciiTestCase.php b/Tests/AbstractAsciiTestCase.php index e046255..ee4890f 100644 --- a/Tests/AbstractAsciiTestCase.php +++ b/Tests/AbstractAsciiTestCase.php @@ -1089,6 +1089,35 @@ public static function provideSnake() ]; } + /** + * @dataProvider provideKebab + */ + public function testKebab(string $expectedString, string $origin) + { + $instance = static::createFromString($origin)->kebab(); + + $this->assertEquals(static::createFromString($expectedString), $instance); + $this->assertNotSame($origin, $instance, 'Strings should be immutable'); + } + + public static function provideKebab(): array + { + return [ + ['', ''], + ['x-y', 'x_y'], + ['x-y', 'X_Y'], + ['xu-yo', 'xu_yo'], + ['symfony-is-great', 'symfonyIsGreat'], + ['symfony123-is-great', 'symfony123IsGreat'], + ['symfony123is-great', 'symfony123isGreat'], + ['symfony-is-great', 'Symfony is great'], + ['symfony-is-a-great-framework', 'symfonyIsAGreatFramework'], + ['symfony-is-great', 'symfonyIsGREAT'], + ['symfony-is-really-great', 'symfonyIsREALLYGreat'], + ['symfony', 'SYMFONY'], + ]; + } + /** * @dataProvider provideStartsWith */ diff --git a/Tests/AbstractUnicodeTestCase.php b/Tests/AbstractUnicodeTestCase.php index e838c44..bde19d7 100644 --- a/Tests/AbstractUnicodeTestCase.php +++ b/Tests/AbstractUnicodeTestCase.php @@ -665,6 +665,15 @@ public static function provideSnake() ); } + public static function provideKebab(): array + { + return [ + ...parent::provideKebab(), + ['symfony-ist-äußerst-cool', 'symfonyIstÄußerstCool'], + ['symfony-with-emojis', 'Symfony with 😃 emojis'], + ]; + } + public static function provideEqualsTo() { return array_merge( From 205580699b4d3e11f7b679faf2c0f57ffca6981c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 18 Oct 2024 16:04:52 +0200 Subject: [PATCH 19/19] Remove always true/false occurrences --- Tests/Inflector/SpanishInflectorTest.php | 2 +- UnicodeString.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Inflector/SpanishInflectorTest.php b/Tests/Inflector/SpanishInflectorTest.php index f0b8e42..b10509a 100644 --- a/Tests/Inflector/SpanishInflectorTest.php +++ b/Tests/Inflector/SpanishInflectorTest.php @@ -151,7 +151,7 @@ public function testPluralize(string $singular, $plural) public function testUninflected(string $word) { $this->assertSame( - \is_array($word) ? $word : [$word], + [$word], (new SpanishInflector())->pluralize($word) ); } diff --git a/UnicodeString.php b/UnicodeString.php index 4b16caf..b458de0 100644 --- a/UnicodeString.php +++ b/UnicodeString.php @@ -286,7 +286,7 @@ public function splice(string $replacement, int $start = 0, ?int $length = null) $str = clone $this; $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; - $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; + $length = $length ? \strlen(grapheme_substr($this->string, $start, $length)) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); if (normalizer_is_normalized($str->string)) {