diff --git a/composer.json b/composer.json index bfd95859c..dd5333aea 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "symfony/polyfill-intl-grapheme": "self.version", "symfony/polyfill-intl-icu": "self.version", "symfony/polyfill-intl-messageformatter": "self.version", + "symfony/polyfill-intl-listformatter": "self.version", "symfony/polyfill-intl-idn": "self.version", "symfony/polyfill-intl-normalizer": "self.version", "symfony/polyfill-mbstring": "self.version", @@ -62,6 +63,7 @@ "classmap": [ "src/Intl/Icu/Resources/stubs", "src/Intl/MessageFormatter/Resources/stubs", + "src/Intl/ListFormatter/Resources/stubs", "src/Intl/Normalizer/Resources/stubs", "src/Php85/Resources/stubs", "src/Php84/Resources/stubs", diff --git a/src/Intl/ListFormatter/IntlListFormatter.php b/src/Intl/ListFormatter/IntlListFormatter.php new file mode 100644 index 000000000..107c76f34 --- /dev/null +++ b/src/Intl/ListFormatter/IntlListFormatter.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\ListFormatter; + +/** + * A polyfill implementation of the IntlListFormatter class provided by the intl extension. + * + * @author Ayesh Karunaratne + * + * @internal + */ +class IntlListFormatter +{ + public const TYPE_AND = 0; + public const TYPE_OR = 1; + public const TYPE_UNITS = 2; + + public const WIDTH_WIDE = 0; + public const WIDTH_SHORT = 1; + public const WIDTH_NARROW = 2; + + /** + * @var string + */ + private $locale; + /** + * @var int + */ + private $type; + /** + * @var int + */ + private $width; + + protected static $listPatterns = [ + 'en' => [ + 'listPattern-type-standard' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, and {1}', + 2 => '{0} and {1}', + ], + 'listPattern-type-or' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, or {1}', + 2 => '{0} or {1}', + ], + 'listPattern-type-or-narrow' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, or {1}', + 2 => '{0} or {1}', + ], + 'listPattern-type-or-short' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, or {1}', + 2 => '{0} or {1}', + ], + 'listPattern-type-standard-narrow' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, {1}', + 2 => '{0}, {1}', + ], + 'listPattern-type-standard-short' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, & {1}', + 2 => '{0} & {1}', + ], + 'listPattern-type-unit' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, {1}', + 2 => '{0}, {1}', + ], + 'listPattern-type-unit-narrow' => [ + 'start' => '{0} {1}', + 'middle' => '{0} {1}', + 'end' => '{0} {1}', + 2 => '{0} {1}', + ], + 'listPattern-type-unit-short' => [ + 'start' => '{0}, {1}', + 'middle' => '{0}, {1}', + 'end' => '{0}, {1}', + 2 => '{0}, {1}', + ], + ], + ]; + + public function __construct( + string $locale, + int $type = self::TYPE_AND, + int $width = self::WIDTH_WIDE + ) { + $exceptionClass = PHP_VERSION_ID >= 80000 ? \ValueError::class : \InvalidArgumentException::class; + if ($locale !== 'en' && strpos($locale, 'en') !== 0) { + throw new $exceptionClass('Invalid locale, only "en" and "en-*" locales are supported'); + } + + if ($type !== self::TYPE_AND && $type !== self::TYPE_OR && $type !== self::TYPE_UNITS) { + throw new $exceptionClass('Argument #2 ($type) must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS.'); + } + + if ($width !== self::WIDTH_WIDE && $width !== self::WIDTH_SHORT && $width !== self::WIDTH_NARROW) { + throw new $exceptionClass('Argument #3 ($width) must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW.'); + } + + + $this->locale = 'en'; + $this->type = $type; + $this->width = $width; + } + + public function format(array $strings): string + { + $itemCount = count($strings); + + if ($itemCount === 0) { + return ''; + } + + $strings = array_values($strings); + + if ($itemCount === 1) { + return (string) $strings[0]; + } + + $pattern = $this->getListPattern(); + + switch ($this->type) { + case self::TYPE_AND: + $lookupKeyType = 'standard'; + break; + case self::TYPE_OR: + $lookupKeyType = 'or'; + break; + case self::TYPE_UNITS: + $lookupKeyType = 'unit'; + break; + } + + switch ($this->width) { + case self::WIDTH_WIDE: + $lookupKeyWidth = ''; + break; + case self::WIDTH_SHORT: + $lookupKeyWidth = '-short'; + break; + case self::WIDTH_NARROW: + $lookupKeyWidth = '-narrow'; + break; + } + + $pattern = $pattern['listPattern-type-' . $lookupKeyType . $lookupKeyWidth]; + + if ($itemCount === 2) { + return strtr($pattern[2], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]); + } + + if ($itemCount === 3) { + $start = strtr($pattern['start'], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]); + return strtr($pattern['end'], ['{0}' => $start, '{1}' => (string) $strings[2]]); + } + + $result = strtr($pattern['start'], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]); + + for ($i = 2; $i < $itemCount - 1; $i++) { + $result = strtr($pattern["middle"], [ + "{0}" => $result, + "{1}" => $strings[$i], + ]); + } + + return strtr($pattern["end"], [ + "{0}" => $result, + "{1}" => $strings[$itemCount - 1], + ]); + } + + protected function getListPattern(): array { + return self::$listPatterns[$this->locale]; + } + + public function getErrorCode() + { + return 0; + } + + public function getErrorMessage() + { + return ''; + } +} diff --git a/src/Intl/ListFormatter/LICENSE b/src/Intl/ListFormatter/LICENSE new file mode 100644 index 000000000..7536caeae --- /dev/null +++ b/src/Intl/ListFormatter/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Intl/ListFormatter/README.md b/src/Intl/ListFormatter/README.md new file mode 100644 index 000000000..b00533c7e --- /dev/null +++ b/src/Intl/ListFormatter/README.md @@ -0,0 +1,14 @@ +Symfony Polyfill / Intl: ListFormatter +========================================= + +This component provides a fallback implementation for the +[`ListFormatter`](https://php.net/ListFormatter) class provided +by the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/src/Intl/ListFormatter/Resources/stubs/IntlException.php b/src/Intl/ListFormatter/Resources/stubs/IntlException.php new file mode 100644 index 000000000..1ac6c2948 --- /dev/null +++ b/src/Intl/ListFormatter/Resources/stubs/IntlException.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class IntlException extends Exception +{ +} diff --git a/src/Intl/ListFormatter/Resources/stubs/IntlListFormatter.php b/src/Intl/ListFormatter/Resources/stubs/IntlListFormatter.php new file mode 100644 index 000000000..c9f8fc328 --- /dev/null +++ b/src/Intl/ListFormatter/Resources/stubs/IntlListFormatter.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +final class IntlListFormatter extends Symfony\Polyfill\Intl\ListFormatter\IntlListFormatter +{ +} diff --git a/src/Intl/ListFormatter/composer.json b/src/Intl/ListFormatter/composer.json new file mode 100644 index 000000000..6d21e36dc --- /dev/null +++ b/src/Intl/ListFormatter/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/polyfill-intl-listformatter", + "type": "library", + "description": "Symfony polyfill for intl's ListFormatter class and related functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "listformatter"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\ListFormatter\\": "" }, + "classmap": [ "Resources/stubs" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/tests/Intl/IntlListFormatter/IntlListFormatterTest.php b/tests/Intl/IntlListFormatter/IntlListFormatterTest.php new file mode 100644 index 000000000..1c95d0969 --- /dev/null +++ b/tests/Intl/IntlListFormatter/IntlListFormatterTest.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Tests\Intl\ListFormatter; + +use IntlListFormatter; +use PHPUnit\Framework\TestCase; + +/** + * @author Ayesh Karunaratne + * + * @group class-polyfill + */ +class IntlListFormatterTest extends TestCase +{ + public function testUnsupportedLocales() + { + new IntlListFormatter('en'); + new IntlListFormatter('en-US'); + new IntlListFormatter('en_US'); + new IntlListFormatter('en-LK'); + + if (PHP_VERSION_ID >= 80000) { + $this->expectException(\ValueError::class); + } + else { + $this->expectException(\InvalidArgumentException::class); + } + + new IntlListFormatter('ja'); + } + + public function testUnsupportedType() + { + if (PHP_VERSION_ID >= 80000) { + $this->expectException(\ValueError::class); + } + else { + $this->expectException(\InvalidArgumentException::class); + } + $this->expectExceptionMessage('must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS'); + new IntlListFormatter('en', 42); + } + + public function testUnsupportedWidth() + { + if (PHP_VERSION_ID >= 80000) { + $this->expectException(\ValueError::class); + } + else { + $this->expectException(\InvalidArgumentException::class); + } + $this->expectExceptionMessage('must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW'); + new IntlListFormatter('en', IntlListFormatter::TYPE_AND, 42); + } + + /** + * @dataProvider formattingLists + */ + public function testFormatting(int $type, int $wide, array $strings, string $expected) + { + $formatter = new IntlListFormatter('en', $type, $wide); + self::assertSame($expected, $formatter->format($strings)); + } + + public function formattingLists() { + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, [], '']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, [1], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['1'], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, and strawberry']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, and orange']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, and 16']; + + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, [], '']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, [1], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['1'], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, & strawberry']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, & orange']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, & 16']; + + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, [], '']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, [1], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['1'], '1']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange']; + yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16']; + + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, [], '']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, [1], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['1'], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, or strawberry']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16']; + + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, [], '']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, [1], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['1'], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, or strawberry']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16']; + + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, [], '']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, [1], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['1'], '1']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange']; + yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16']; + + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, [], '']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, [1], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['1'], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16']; + + + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, [], '']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, [1], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['1'], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16']; + + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, [], '']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, [1], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['1'], '1']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry'], 'apple banana strawberry']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple banana strawberry orange']; + yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple banana strawberry orange 16']; + } +}