Skip to content

Commit b95a718

Browse files
authored
fix: implement dayPeriod and ensure hour12 functions properly (#45)
1 parent 4255dfd commit b95a718

10 files changed

Lines changed: 207 additions & 27 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ Additional options include:
325325
"am", "noon", "n" etc. Values include: `narrow`, `short`, or `long`.
326326
* `fractionalSecondDigits`: The number of digits used to represent fractions of
327327
a second (any additional digits are truncated). Values may be: `0`, `1`, `2`,
328-
or `3`.
328+
or `3`. **This property is not yet implemented.**
329329
* `hour12`: If `true`, `hourCycle` will be `h12`, if `false`, `hourCycle` will
330330
be `h23`. This property overrides any value set by `hourCycle`.
331331
* `hourCycle`: The hour cycle to use. Values include: `h11`, `h12`, `h23`, and

src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,17 @@ private function setOption(string $skeletonOption, Type\DateTimeFormatOptions $o
144144

145145
break;
146146
case 'b': // am, pm, noon, midnight
147+
if ($length === 5) {
148+
$options->dayPeriod = IntlDateTimeFormatOptions::PERIOD_NARROW;
149+
} else {
150+
$options->dayPeriod = IntlDateTimeFormatOptions::PERIOD_SHORT;
151+
}
152+
153+
break;
147154
case 'B': // flexible day periods
148-
throw new Exception\InvalidSkeletonOption(
149-
'"b/B" (period) patterns are not supported, use "a" instead',
150-
);
155+
$options->dayPeriod = IntlDateTimeFormatOptions::PERIOD_LONG;
151156

157+
break;
152158
// Hour
153159
case 'h':
154160
$options->hourCycle = IntlDateTimeFormatOptions::HOUR_H12;

src/Intl/DateTimeFormat.php

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ class DateTimeFormat implements DateTimeFormatInterface
111111
DateTimeFormatOptions::PERIOD_LONG => 'EEEE',
112112
];
113113

114+
private const SYMBOLS_DAY_PERIOD = [
115+
DateTimeFormatOptions::PERIOD_NARROW => 'bbbbb',
116+
DateTimeFormatOptions::PERIOD_SHORT => 'b',
117+
DateTimeFormatOptions::PERIOD_LONG => 'B',
118+
];
119+
114120
private const SYMBOLS_HOUR = [
115121
DateTimeFormatOptions::HOUR_H12 => [
116122
DateTimeFormatOptions::WIDTH_NUMERIC => 'h',
@@ -233,6 +239,16 @@ public function getSkeleton(): string
233239
return $this->skeleton;
234240
}
235241

242+
/**
243+
* Returns the locale constructed from the date/time format options
244+
*
245+
* @internal
246+
*/
247+
public function getEvaluatedLocale(): string
248+
{
249+
return $this->localeName;
250+
}
251+
236252
/**
237253
* @throws PhpIntlException
238254
* @throws UnableToFormatDateTimeException
@@ -339,23 +355,24 @@ private function combineLocaleWithOptions(LocaleInterface $locale, DateTimeForma
339355

340356
private function withHourCycleFallback(LocaleInterface $locale, DateTimeFormatOptions $options): LocaleInterface
341357
{
342-
if ($options->hourCycle !== null) {
343-
return $locale->withHourCycle($options->hourCycle);
358+
// The `hour12` property overrides the `hourCycle` property.
359+
if ($options->hour12 !== null) {
360+
$options->hourCycle = $options->hour12 ? 'h12' : 'h23';
344361
}
345362

346-
// The `hour12` property overrides the `hourCycle` property, in case
347-
// both are present.
348-
if ($options->hour12 !== null) {
349-
return $locale->withHourCycle($options->hour12 ? 'h12' : 'h23');
363+
if ($options->hourCycle !== null) {
364+
return $locale->withHourCycle($options->hourCycle);
350365
}
351366

352367
if ($locale->hourCycle() !== null) {
368+
$options->hourCycle = $locale->hourCycle();
369+
353370
return $locale;
354371
}
355372

356-
// If neither `hourCycle` nor `hour12` are set, we will use PHP's
357-
// IntlDateFormatter class to determine the default hour cycle for
358-
// the locale.
373+
// If neither `hourCycle` nor `hour12` are set, and we can't find the
374+
// hour cycle on the locale, we will use PHP's IntlDateFormatter class
375+
// to determine the default hour cycle for the locale.
359376
$dateFormatter = new PhpIntlDateFormatter(
360377
$locale->toString(),
361378
PhpIntlDateFormatter::FULL,
@@ -383,6 +400,7 @@ private function buildSkeleton(DateTimeFormatOptions $options, LocaleInterface $
383400
$pattern .= self::SYMBOLS_MINUTE[$options->minute] ?? '';
384401
$pattern .= self::SYMBOLS_SECOND[$options->second] ?? '';
385402
$pattern .= self::SYMBOLS_TIME_ZONE[$options->timeZoneName] ?? '';
403+
$pattern .= self::SYMBOLS_DAY_PERIOD[$options->dayPeriod] ?? '';
386404

387405
if ($pattern === '') {
388406
// Use the "short" style as the default.

src/Intl/NumberFormat.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ public function getSkeleton(): string
190190
return $this->skeleton;
191191
}
192192

193+
/**
194+
* Returns the locale constructed from the date/time format options
195+
*
196+
* @internal
197+
*/
198+
public function getEvaluatedLocale(): string
199+
{
200+
return $this->localeName;
201+
}
202+
193203
/**
194204
* @param int | float $number
195205
*

tests/Icu/MessageFormat/Parser/DateTimeSkeletonParserTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public function dateTimeSkeletonProvider(): array
6666
['cccccc'],
6767
['KK'],
6868
['k'],
69+
['hb'],
70+
['hB'],
71+
['hbbbbb'],
6972
];
7073
}
7174
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dayPeriod": "narrow",
3+
"hourCycle": "h12",
4+
"hour": "numeric"
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dayPeriod": "short",
3+
"hourCycle": "h12",
4+
"hour": "numeric"
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dayPeriod": "long",
3+
"hourCycle": "h12",
4+
"hour": "numeric"
5+
}

tests/Intl/DateTimeFormatTest.php

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,60 @@ public function testFormatThrowsExceptionWhenDateStyleOrTimeStyleMixedWithStyleP
105105

106106
public function testHour12OverridesHourCycle(): void
107107
{
108-
$enLocale = new Locale('en');
108+
$enLocale = new Locale('en-u-hc-h11');
109+
$enLocale = $enLocale->withHourCycle('h23');
110+
109111
$formatOptions = new DateTimeFormatOptions([
110-
'dateStyle' => 'full',
111-
'timeStyle' => 'full',
112+
'hour' => '2-digit',
112113
// Specify both hourCycle and hour12 to show that hour12 overrides hourCycle.
113-
'hourCycle' => 'h23',
114114
'hour12' => true,
115+
'hourCycle' => 'h24',
116+
'minute' => '2-digit',
117+
'second' => '2-digit',
115118
'timeZone' => 'America/Denver',
116119
]);
117120

118121
$enFormatter = new DateTimeFormat($enLocale, $formatOptions);
119122
$date = new DateTimeImmutable('@' . self::TS);
120123

121-
$this->assertSame(
122-
'Monday, June 15, 2020 at 10:48:20 PM Mountain Daylight Time',
123-
$enFormatter->format($date),
124-
);
124+
$this->assertSame('10:48:20 PM', $enFormatter->format($date));
125+
126+
$this->assertSame('hhmmss', $enFormatter->getSkeleton());
127+
$this->assertSame('en-u-hc-h12', $enFormatter->getEvaluatedLocale());
128+
}
129+
130+
public function testEvaluatedLocaleWithNoOptions(): void
131+
{
132+
$locale = new Locale('en-US');
133+
$formatter = new DateTimeFormat($locale);
134+
135+
// We automatically look up the locale's preferred hour cycle and add it.
136+
$this->assertSame('en-US-u-hc-h12', $formatter->getEvaluatedLocale());
137+
}
138+
139+
public function testEvaluatedLocaleWithOptionsUsingHour12(): void
140+
{
141+
$locale = new Locale('en-US');
142+
$formatter = new DateTimeFormat($locale, new DateTimeFormatOptions([
143+
'hour12' => false,
144+
'hourCycle' => 'h12',
145+
'calendar' => 'islamic',
146+
'numberingSystem' => 'thai',
147+
]));
148+
149+
$this->assertSame('en-US-u-ca-islamic-nu-thai-hc-h23', $formatter->getEvaluatedLocale());
150+
}
151+
152+
public function testEvaluatedLocaleWithOptionsUsingHourCycle(): void
153+
{
154+
$locale = new Locale('en-US');
155+
$formatter = new DateTimeFormat($locale, new DateTimeFormatOptions([
156+
'hourCycle' => 'h24',
157+
'calendar' => 'coptic',
158+
'numberingSystem' => 'mathmono',
159+
]));
160+
161+
$this->assertSame('en-US-u-ca-coptic-nu-mathmono-hc-h24', $formatter->getEvaluatedLocale());
125162
}
126163

127164
public function testUseHourCycleFromLocale(): void
@@ -130,18 +167,19 @@ public function testUseHourCycleFromLocale(): void
130167
$enLocale = $enLocale->withHourCycle('h23');
131168

132169
$formatOptions = new DateTimeFormatOptions([
133-
'dateStyle' => 'full',
134-
'timeStyle' => 'full',
170+
'hour' => '2-digit',
171+
'minute' => '2-digit',
172+
'second' => '2-digit',
135173
'timeZone' => 'America/Denver',
136174
]);
137175

138176
$enFormatter = new DateTimeFormat($enLocale, $formatOptions);
139177
$date = new DateTimeImmutable('@' . self::TS);
140178

141-
$this->assertSame(
142-
'Monday, June 15, 2020 at 10:48:20 PM Mountain Daylight Time',
143-
$enFormatter->format($date),
144-
);
179+
$this->assertSame('22:48:20', $enFormatter->format($date));
180+
181+
$this->assertSame('HHmmss', $enFormatter->getSkeleton());
182+
$this->assertSame('en-u-hc-h23', $enFormatter->getEvaluatedLocale());
145183
}
146184

147185
/**
@@ -489,6 +527,78 @@ public function formatProvider(): array
489527
'en' => 'Monday, June 15, 2020 at 10:48:20 PM Mountain Daylight Time',
490528
'skeleton' => 'EEEEMMMMdyhmmssazzzz',
491529
],
530+
[
531+
'options' => [
532+
'dayPeriod' => 'long',
533+
'hour' => 'numeric',
534+
'minute' => 'numeric',
535+
'second' => 'numeric',
536+
'timeZone' => 'America/Chicago',
537+
],
538+
'ko' => '밤 11:48:20',
539+
'en' => '11:48:20 at night',
540+
'skeleton' => 'hmsB',
541+
],
542+
[
543+
'options' => [
544+
'dayPeriod' => 'short',
545+
'hour' => 'numeric',
546+
'minute' => 'numeric',
547+
'second' => 'numeric',
548+
'timeZone' => 'America/Chicago',
549+
],
550+
'ko' => '오후 11:48:20',
551+
'en' => '11:48:20 PM',
552+
'skeleton' => 'hmsb',
553+
],
554+
[
555+
'options' => [
556+
'dayPeriod' => 'narrow',
557+
'hour' => 'numeric',
558+
'minute' => 'numeric',
559+
'second' => 'numeric',
560+
'timeZone' => 'America/Chicago',
561+
],
562+
'ko' => 'PM 11:48:20',
563+
'en' => '11:48:20 p',
564+
'skeleton' => 'hmsbbbbb',
565+
],
566+
[
567+
'options' => [
568+
'dayPeriod' => 'long',
569+
'hour' => 'numeric',
570+
'minute' => 'numeric',
571+
'second' => 'numeric',
572+
'timeZone' => 'Asia/Kolkata',
573+
],
574+
'ko' => '오전 10:18:20',
575+
'en' => '10:18:20 in the morning',
576+
'skeleton' => 'hmsB',
577+
],
578+
[
579+
'options' => [
580+
'dayPeriod' => 'short',
581+
'hour' => 'numeric',
582+
'minute' => 'numeric',
583+
'second' => 'numeric',
584+
'timeZone' => 'Asia/Kolkata',
585+
],
586+
'ko' => '오전 10:18:20',
587+
'en' => '10:18:20 AM',
588+
'skeleton' => 'hmsb',
589+
],
590+
[
591+
'options' => [
592+
'dayPeriod' => 'narrow',
593+
'hour' => 'numeric',
594+
'minute' => 'numeric',
595+
'second' => 'numeric',
596+
'timeZone' => 'Asia/Kolkata',
597+
],
598+
'ko' => 'AM 10:18:20',
599+
'en' => '10:18:20 a',
600+
'skeleton' => 'hmsbbbbb',
601+
],
492602
];
493603
}
494604

tests/Intl/NumberFormatTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ public function testThrowsExceptionForIllegalArgumentError(): void
103103
}
104104
}
105105

106+
public function testEvaluatedLocaleWithNoOptions(): void
107+
{
108+
$locale = new Locale('en-US');
109+
$formatter = new NumberFormat($locale);
110+
111+
$this->assertSame('en-US', $formatter->getEvaluatedLocale());
112+
}
113+
114+
public function testEvaluatedLocaleWithOptions(): void
115+
{
116+
$locale = new Locale('en-US');
117+
$formatter = new NumberFormat($locale, new NumberFormatOptions([
118+
'numberingSystem' => 'arab',
119+
]));
120+
121+
$this->assertSame('en-US-u-nu-arab', $formatter->getEvaluatedLocale());
122+
}
123+
106124
/**
107125
* @param int | float $number
108126
*

0 commit comments

Comments
 (0)