‪TYPO3CMS  ‪main
DateFormatter.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
21 
27 {
36  public function ‪format(mixed $date, string|int $format, string|‪Locale $locale): string
37  {
38  $locale = (string)$locale;
39  // Use fallback locale if 'C' is provided.
40  if ($locale === 'C') {
41  $locale = 'en-US';
42  }
43  if (is_int($format) || ‪MathUtility::canBeInterpretedAsInteger($format)) {
44  $dateFormatter = new \IntlDateFormatter($locale, (int)$format, (int)$format);
45  } else {
46  $dateFormatter = match (strtoupper($format)) {
47  'FULL' => new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL),
48  'FULLDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::NONE),
49  'FULLTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::FULL),
50  'LONG' => new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::LONG),
51  'LONGDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::LONG, \IntlDateFormatter::NONE),
52  'LONGTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::LONG),
53  'MEDIUM' => new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM),
54  'MEDIUMDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE),
55  'MEDIUMTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM),
56  'SHORT' => new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT),
57  'SHORTDATE' => new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE),
58  'SHORTTIME' => new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT),
59  // Use a custom pattern
60  default => new \IntlDateFormatter($locale, \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, null, null, $format),
61  };
62  }
63  return $dateFormatter->format($date) ?: '';
64  }
65 
75  public function ‪strftime(string $format, int|string|\DateTimeInterface|null $timestamp, string|‪Locale|null $locale = null, $useUtcTimeZone = false): string
76  {
77  if (!$timestamp instanceof \DateTimeInterface) {
78  $timestamp = ‪MathUtility::canBeInterpretedAsInteger($timestamp) ? '@' . $timestamp : (string)$timestamp;
79  try {
80  $timestamp = new \DateTime($timestamp);
81  } catch (\‪Exception $e) {
82  throw new \InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 1679091446, $e);
83  }
84  $timestamp->setTimezone(new \DateTimeZone($useUtcTimeZone ? 'UTC' : date_default_timezone_get()));
85  }
86 
87  if (empty($locale)) {
88  // get current locale
89  $locale = (string)setlocale(LC_TIME, '0');
90  } else {
91  $locale = (string)$locale;
92  }
93  // Use fallback locale if 'C' is provided.
94  if ($locale === 'C') {
95  $locale = 'en-US';
96  }
97  // remove trailing part not supported by ext-intl locale
98  $locale = preg_replace('/[^\w-].*$/', '', $locale);
99 
100  $intl_formats = [
101  '%a' => 'EEE', // An abbreviated textual representation of the day Sun through Sat
102  '%A' => 'EEEE', // A full textual representation of the day Sunday through Saturday
103  '%b' => 'MMM', // Abbreviated month name, based on the locale Jan through Dec
104  '%B' => 'MMMM', // Full month name, based on the locale January through December
105  '%h' => 'MMM', // Abbreviated month name, based on the locale (an alias of %b) Jan through Dec
106  ];
107 
108  $intl_formatter = function (\DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale): string {
109  $tz = $timestamp->getTimezone();
110  $date_type = \IntlDateFormatter::FULL;
111  $time_type = \IntlDateFormatter::FULL;
112  $pattern = '';
113 
114  switch ($format) {
115  // %c = Preferred date and time stamp based on locale
116  // Example: Tue Feb 5 00:45:10 2009 for February 5, 2009 at 12:45:10 AM
117  case '%c':
118  $date_type = \IntlDateFormatter::LONG;
119  $time_type = \IntlDateFormatter::SHORT;
120  break;
121 
122  // %x = Preferred date representation based on locale, without the time
123  // Example: 02/05/09 for February 5, 2009
124  case '%x':
125  $date_type = \IntlDateFormatter::SHORT;
126  $time_type = \IntlDateFormatter::NONE;
127  break;
128 
129  // Localized time format
130  case '%X':
131  $date_type = \IntlDateFormatter::NONE;
132  $time_type = \IntlDateFormatter::MEDIUM;
133  break;
134 
135  default:
136  $pattern = $intl_formats[$format];
137  }
138 
139  // In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and
140  // the 4th October was followed by the 15th October.
141  // ICU (including IntlDateFormattter) interprets and formats dates based on this cutover.
142  // Posix (including strftime) and timelib (including DateTimeImmutable) instead use
143  // a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever.
144  // This leads to the same instants in time, as expressed in Unix time, having different representations
145  // in formatted strings.
146  // To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past.
147  $calendar = \IntlGregorianCalendar::createInstance();
148  $calendar->setGregorianChange(PHP_INT_MIN);
149 
150  return (new \IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp) ?: '';
151  };
152 
153  // Same order as https://www.php.net/manual/en/function.strftime.php
154  $translation_table = [
155  // Day
156  '%a' => $intl_formatter,
157  '%A' => $intl_formatter,
158  '%d' => 'd',
159  '%e' => function (\DateTimeInterface $timestamp, string $_): string {
160  return sprintf('% 2u', $timestamp->format('j'));
161  },
162  '%j' => function (\DateTimeInterface $timestamp, string $_): string {
163  // Day number in year, 001 to 366
164  return sprintf('%03d', (int)($timestamp->format('z')) + 1);
165  },
166  '%u' => 'N',
167  '%w' => 'w',
168 
169  // Week
170  '%U' => function (\DateTimeInterface $timestamp, string $_): string {
171  // Number of weeks between date and first Sunday of year
172  $day = new \DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y')));
173  return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7);
174  },
175  '%V' => 'W',
176  '%W' => function (\DateTimeInterface $timestamp, string $_): string {
177  // Number of weeks between date and first Monday of year
178  $day = new \DateTime(sprintf('%d-01 Monday', $timestamp->format('Y')));
179  return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7);
180  },
181 
182  // Month
183  '%b' => $intl_formatter,
184  '%B' => $intl_formatter,
185  '%h' => $intl_formatter,
186  '%m' => 'm',
187 
188  // Year
189  '%C' => function (\DateTimeInterface $timestamp, string $_): string {
190  // Century (-1): 19 for 20th century
191  return (string)floor($timestamp->format('Y') / 100);
192  },
193  '%g' => function (\DateTimeInterface $timestamp, string $_): string {
194  return substr($timestamp->format('o'), -2);
195  },
196  '%G' => 'o',
197  '%y' => 'y',
198  '%Y' => 'Y',
199 
200  // Time
201  '%H' => 'H',
202  '%k' => function (\DateTimeInterface $timestamp, string $_): string {
203  return sprintf('% 2u', $timestamp->format('G'));
204  },
205  '%I' => 'h',
206  '%l' => function (\DateTimeInterface $timestamp, string $_): string {
207  return sprintf('% 2u', $timestamp->format('g'));
208  },
209  '%M' => 'i',
210  '%p' => 'A', // AM PM (this is reversed on purpose!)
211  '%P' => 'a', // am pm
212  '%r' => 'h:i:s A', // %I:%M:%S %p
213  '%R' => 'H:i', // %H:%M
214  '%S' => 's',
215  '%T' => 'H:i:s', // %H:%M:%S
216  '%X' => $intl_formatter, // Preferred time representation based on locale, without the date
217 
218  // Timezone
219  '%z' => 'O',
220  '%Z' => 'T',
221 
222  // Time and Date Stamps
223  '%c' => $intl_formatter,
224  '%D' => 'm/d/Y',
225  '%F' => 'Y-m-d',
226  '%s' => 'U',
227  '%x' => $intl_formatter,
228  ];
229 
230  $out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) {
231  $prefix = $match[1];
232  $char = $match[2];
233  $pattern = '%' . $char;
234  if ($pattern == '%n') {
235  return "\n";
236  }
237  if ($pattern == '%t') {
238  return "\t";
239  }
240 
241  if (!isset($translation_table[$pattern])) {
242  throw new \InvalidArgumentException(sprintf('Format "%s" is unknown in time format', $pattern), 1679091475);
243  }
244 
245  $replace = $translation_table[$pattern];
246 
247  if (is_string($replace)) {
248  $result = $timestamp->format($replace);
249  } else {
250  $result = $replace($timestamp, $pattern);
251  }
252 
253  return match ($prefix) {
254  '_' => preg_replace('/\G0(?=.)/', ' ', $result),
255  '#', '-' => preg_replace('/^0+(?=.)/', '', $result),
256  default => $result,
257  };
258  }, $format);
259  return str_replace('%%', '%', $out);
260  }
261 }
‪TYPO3\CMS\Core\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\Localization\DateFormatter\format
‪string format(mixed $date, string|int $format, string|Locale $locale)
Definition: DateFormatter.php:36
‪TYPO3\CMS\Core\Localization
Definition: CacheWarmer.php:18
‪TYPO3\CMS\Core\Localization\DateFormatter
Definition: DateFormatter.php:27
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Localization\Locale
Definition: Locale.php:30
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Localization\DateFormatter\strftime
‪strftime(string $format, int|string|\DateTimeInterface|null $timestamp, string|Locale|null $locale=null, $useUtcTimeZone=false)
Definition: DateFormatter.php:75