Skip to content

Commit 0518c84

Browse files
committed
[Intl] Add PHP 8.5 IntlListFormatter to ICU polyfill
Adds a new polyfill to `symfony/polyfill-intl-icu` that provides the functionality of the new `IntlListFormatter` to PHP 7.2 and later. - [ICU listPatterns](https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-misc-full/main/en/listPatterns.json) - [php-src commit](php/php-src@3f7545245) - [PHP.Watch: IntlListFormatter](https://php.watch/versions/8.5/IntlListFormatter) Closes GH-530.
1 parent 41ace00 commit 0518c84

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"symfony/polyfill-intl-grapheme": "self.version",
3939
"symfony/polyfill-intl-icu": "self.version",
4040
"symfony/polyfill-intl-messageformatter": "self.version",
41+
"symfony/polyfill-intl-listformatter": "self.version",
4142
"symfony/polyfill-intl-idn": "self.version",
4243
"symfony/polyfill-intl-normalizer": "self.version",
4344
"symfony/polyfill-mbstring": "self.version",
@@ -62,6 +63,7 @@
6263
"classmap": [
6364
"src/Intl/Icu/Resources/stubs",
6465
"src/Intl/MessageFormatter/Resources/stubs",
66+
"src/Intl/ListFormatter/Resources/stubs",
6567
"src/Intl/Normalizer/Resources/stubs",
6668
"src/Php85/Resources/stubs",
6769
"src/Php84/Resources/stubs",

src/Intl/Icu/IntlListFormatter.php

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Polyfill\Intl\Icu;
13+
14+
/**
15+
* A polyfill implementation of the IntlListFormatter class provided by the intl extension.
16+
*
17+
* @author Ayesh Karunaratne <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
class IntlListFormatter
22+
{
23+
public const TYPE_AND = 0;
24+
public const TYPE_OR = 1;
25+
public const TYPE_UNITS = 2;
26+
27+
public const WIDTH_WIDE = 0;
28+
public const WIDTH_SHORT = 1;
29+
public const WIDTH_NARROW = 2;
30+
31+
/**
32+
* @var string
33+
*/
34+
private $locale;
35+
/**
36+
* @var int
37+
*/
38+
private $type;
39+
/**
40+
* @var int
41+
*/
42+
private $width;
43+
44+
private const LIST_PATTERNS = [
45+
'en' => [
46+
'listPattern-type-standard' => [
47+
'start' => '{0}, {1}',
48+
'middle' => '{0}, {1}',
49+
'end' => '{0}, and {1}',
50+
2 => '{0} and {1}',
51+
],
52+
'listPattern-type-or' => [
53+
'start' => '{0}, {1}',
54+
'middle' => '{0}, {1}',
55+
'end' => '{0}, or {1}',
56+
2 => '{0} or {1}',
57+
],
58+
'listPattern-type-or-narrow' => [
59+
'start' => '{0}, {1}',
60+
'middle' => '{0}, {1}',
61+
'end' => '{0}, or {1}',
62+
2 => '{0} or {1}',
63+
],
64+
'listPattern-type-or-short' => [
65+
'start' => '{0}, {1}',
66+
'middle' => '{0}, {1}',
67+
'end' => '{0}, or {1}',
68+
2 => '{0} or {1}',
69+
],
70+
'listPattern-type-standard-narrow' => [
71+
'start' => '{0}, {1}',
72+
'middle' => '{0}, {1}',
73+
'end' => '{0}, {1}',
74+
2 => '{0}, {1}',
75+
],
76+
'listPattern-type-standard-short' => [
77+
'start' => '{0}, {1}',
78+
'middle' => '{0}, {1}',
79+
'end' => '{0}, & {1}',
80+
2 => '{0} & {1}',
81+
],
82+
'listPattern-type-unit' => [
83+
'start' => '{0}, {1}',
84+
'middle' => '{0}, {1}',
85+
'end' => '{0}, {1}',
86+
2 => '{0}, {1}',
87+
],
88+
'listPattern-type-unit-narrow' => [
89+
'start' => '{0} {1}',
90+
'middle' => '{0} {1}',
91+
'end' => '{0} {1}',
92+
2 => '{0} {1}',
93+
],
94+
'listPattern-type-unit-short' => [
95+
'start' => '{0}, {1}',
96+
'middle' => '{0}, {1}',
97+
'end' => '{0}, {1}',
98+
2 => '{0}, {1}',
99+
],
100+
],
101+
];
102+
103+
public function __construct(
104+
string $locale,
105+
int $type = self::TYPE_AND,
106+
int $width = self::WIDTH_WIDE
107+
) {
108+
$exceptionClass = PHP_VERSION_ID >= 80000 ? \ValueError::class : \InvalidArgumentException::class;
109+
if ($locale !== 'en' && strpos($locale, 'en') !== 0) {
110+
throw new $exceptionClass('Invalid locale, only "en" and "en-*" locales are supported');
111+
}
112+
113+
if ($type !== self::TYPE_AND && $type !== self::TYPE_OR && $type !== self::TYPE_UNITS) {
114+
throw new $exceptionClass('Argument #2 ($type) must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS.');
115+
}
116+
117+
if ($width !== self::WIDTH_WIDE && $width !== self::WIDTH_SHORT && $width !== self::WIDTH_NARROW) {
118+
throw new $exceptionClass('Argument #3 ($width) must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW.');
119+
}
120+
121+
122+
$this->locale = 'en';
123+
$this->type = $type;
124+
$this->width = $width;
125+
}
126+
127+
public function format(array $strings): string
128+
{
129+
$itemCount = count($strings);
130+
131+
if ($itemCount === 0) {
132+
return '';
133+
}
134+
135+
$strings = array_values($strings);
136+
137+
if ($itemCount === 1) {
138+
return (string) $strings[0];
139+
}
140+
141+
$pattern = $this->getListPattern();
142+
143+
switch ($this->type) {
144+
case self::TYPE_AND:
145+
$lookupKeyType = 'standard';
146+
break;
147+
case self::TYPE_OR:
148+
$lookupKeyType = 'or';
149+
break;
150+
case self::TYPE_UNITS:
151+
$lookupKeyType = 'unit';
152+
break;
153+
}
154+
155+
switch ($this->width) {
156+
case self::WIDTH_WIDE:
157+
$lookupKeyWidth = '';
158+
break;
159+
case self::WIDTH_SHORT:
160+
$lookupKeyWidth = '-short';
161+
break;
162+
case self::WIDTH_NARROW:
163+
$lookupKeyWidth = '-narrow';
164+
break;
165+
}
166+
167+
$pattern = $pattern['listPattern-type-' . $lookupKeyType . $lookupKeyWidth];
168+
169+
if ($itemCount === 2) {
170+
return strtr($pattern[2], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]);
171+
}
172+
173+
if ($itemCount === 3) {
174+
$start = strtr($pattern['start'], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]);
175+
return strtr($pattern['end'], ['{0}' => $start, '{1}' => (string) $strings[2]]);
176+
}
177+
178+
$result = strtr($pattern['start'], ['{0}' => (string) $strings[0], '{1}' => (string) $strings[1]]);
179+
180+
for ($i = 2; $i < $itemCount - 1; $i++) {
181+
$result = strtr($pattern["middle"], [
182+
"{0}" => $result,
183+
"{1}" => $strings[$i],
184+
]);
185+
}
186+
187+
return strtr($pattern["end"], [
188+
"{0}" => $result,
189+
"{1}" => $strings[$itemCount - 1],
190+
]);
191+
}
192+
193+
private function getListPattern(): array {
194+
return self::LIST_PATTERNS[$this->locale];
195+
}
196+
197+
public function getErrorCode(): int
198+
{
199+
return 0;
200+
}
201+
202+
public function getErrorMessage(): string
203+
{
204+
return '';
205+
}
206+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
final class IntlListFormatter extends Symfony\Polyfill\Intl\Icu\IntlListFormatter
13+
{
14+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Polyfill\Tests\Intl\ListFormatter;
13+
14+
use IntlListFormatter;
15+
use PHPUnit\Framework\TestCase;
16+
17+
/**
18+
* @author Ayesh Karunaratne <[email protected]>
19+
*
20+
* @group class-polyfill
21+
*/
22+
class IntlListFormatterTest extends TestCase
23+
{
24+
public function testUnsupportedLocales()
25+
{
26+
new IntlListFormatter('en');
27+
new IntlListFormatter('en-US');
28+
new IntlListFormatter('en_US');
29+
new IntlListFormatter('en-LK');
30+
31+
if (PHP_VERSION_ID >= 80000) {
32+
$this->expectException(\ValueError::class);
33+
}
34+
else {
35+
$this->expectException(\InvalidArgumentException::class);
36+
}
37+
38+
new IntlListFormatter('ja');
39+
}
40+
41+
public function testUnsupportedType()
42+
{
43+
if (PHP_VERSION_ID >= 80000) {
44+
$this->expectException(\ValueError::class);
45+
}
46+
else {
47+
$this->expectException(\InvalidArgumentException::class);
48+
}
49+
$this->expectExceptionMessage('must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS');
50+
new IntlListFormatter('en', 42);
51+
}
52+
53+
public function testUnsupportedWidth()
54+
{
55+
if (PHP_VERSION_ID >= 80000) {
56+
$this->expectException(\ValueError::class);
57+
}
58+
else {
59+
$this->expectException(\InvalidArgumentException::class);
60+
}
61+
$this->expectExceptionMessage('must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW');
62+
new IntlListFormatter('en', IntlListFormatter::TYPE_AND, 42);
63+
}
64+
65+
/**
66+
* @dataProvider formattingLists
67+
*/
68+
public function testFormatting(int $type, int $wide, array $strings, string $expected)
69+
{
70+
$formatter = new IntlListFormatter('en', $type, $wide);
71+
self::assertSame($expected, $formatter->format($strings));
72+
}
73+
74+
public function formattingLists() {
75+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, [], ''];
76+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, [1], '1'];
77+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['1'], '1'];
78+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
79+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
80+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, and strawberry'];
81+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, and orange'];
82+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, and 16'];
83+
84+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, [], ''];
85+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, [1], '1'];
86+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['1'], '1'];
87+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
88+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
89+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, & strawberry'];
90+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, & orange'];
91+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, & 16'];
92+
93+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, [], ''];
94+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, [1], '1'];
95+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['1'], '1'];
96+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
97+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
98+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry'];
99+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange'];
100+
yield [IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16'];
101+
102+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, [], ''];
103+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, [1], '1'];
104+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['1'], '1'];
105+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
106+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
107+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, or strawberry'];
108+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange'];
109+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16'];
110+
111+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, [], ''];
112+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, [1], '1'];
113+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['1'], '1'];
114+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
115+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
116+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, or strawberry'];
117+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange'];
118+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16'];
119+
120+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, [], ''];
121+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, [1], '1'];
122+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['1'], '1'];
123+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
124+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
125+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, or orange'];
126+
yield [IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, or 16'];
127+
128+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, [], ''];
129+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, [1], '1'];
130+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['1'], '1'];
131+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
132+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple'], 'apple'];
133+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry'];
134+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange'];
135+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16'];
136+
137+
138+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, [], ''];
139+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, [1], '1'];
140+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['1'], '1'];
141+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
142+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple'], 'apple'];
143+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry'], 'apple, banana, strawberry'];
144+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange'], 'apple, banana, strawberry, orange'];
145+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple, banana, strawberry, orange, 16'];
146+
147+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, [], ''];
148+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, [1], '1'];
149+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['1'], '1'];
150+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
151+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple'], 'apple'];
152+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry'], 'apple banana strawberry'];
153+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange'], 'apple banana strawberry orange'];
154+
yield [IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW, ['apple', 'banana', 'strawberry', 'orange', 16], 'apple banana strawberry orange 16'];
155+
}
156+
}

0 commit comments

Comments
 (0)