1db926724SAndreas Gohr<?php 2db926724SAndreas Gohr namespace PHP81_BC; 3db926724SAndreas Gohr 4db926724SAndreas Gohr use DateTime; 5db926724SAndreas Gohr use DateTimeZone; 6*4739030dSAndreas Gohr use DateTimeInterface; 7db926724SAndreas Gohr use Exception; 8db926724SAndreas Gohr use InvalidArgumentException; 9*4739030dSAndreas Gohr use Locale; 10*4739030dSAndreas Gohr use PHP81_BC\strftime\DateLocaleFormatter; 11*4739030dSAndreas Gohr use PHP81_BC\strftime\IntlLocaleFormatter; 12db926724SAndreas Gohr 13db926724SAndreas Gohr /** 14db926724SAndreas Gohr * Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible) 15db926724SAndreas Gohr * This provides a cross-platform alternative to strftime() for when it will be removed from PHP. 16db926724SAndreas Gohr * Note that output can be slightly different between libc sprintf and this function as it is using ICU. 17db926724SAndreas Gohr * 18db926724SAndreas Gohr * Usage: 19db926724SAndreas Gohr * use function \PHP81_BC\strftime; 20db926724SAndreas Gohr * echo strftime('%A %e %B %Y %X', new \DateTime('2021-09-28 00:00:00'), 'fr_FR'); 21db926724SAndreas Gohr * 22db926724SAndreas Gohr * Original use: 23db926724SAndreas Gohr * \setlocale(LC_TIME, 'fr_FR.UTF-8'); 24db926724SAndreas Gohr * echo \strftime('%A %e %B %Y %X', strtotime('2021-09-28 00:00:00')); 25db926724SAndreas Gohr * 26db926724SAndreas Gohr * @param string $format Date format 27db926724SAndreas Gohr * @param integer|string|DateTime $timestamp Timestamp 28*4739030dSAndreas Gohr * @param string|null $locale locale 29db926724SAndreas Gohr * @return string 30*4739030dSAndreas Gohr * @throws InvalidArgumentException 31db926724SAndreas Gohr * @author BohwaZ <https://bohwaz.net/> 32db926724SAndreas Gohr */ 33db926724SAndreas Gohr function strftime (string $format, $timestamp = null, ?string $locale = null) : string { 34db926724SAndreas Gohr if (!($timestamp instanceof DateTimeInterface)) { 35db926724SAndreas Gohr $timestamp = is_int($timestamp) ? '@' . $timestamp : (string) $timestamp; 36db926724SAndreas Gohr 37db926724SAndreas Gohr try { 38db926724SAndreas Gohr $timestamp = new DateTime($timestamp); 39db926724SAndreas Gohr } catch (Exception $e) { 40db926724SAndreas Gohr throw new InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 0, $e); 41db926724SAndreas Gohr } 42db926724SAndreas Gohr 43db926724SAndreas Gohr $timestamp->setTimezone(new DateTimeZone(date_default_timezone_get())); 44*4739030dSAndreas Gohr } 45db926724SAndreas Gohr 46db926724SAndreas Gohr if (class_exists('\\IntlDateFormatter') && !isset($_SERVER['STRFTIME_NO_INTL'])) { 47*4739030dSAndreas Gohr $locale = Locale::canonicalize($locale ?? (Locale::getDefault() ?? setlocale(LC_TIME, '0'))); 48*4739030dSAndreas Gohr $locale_formatter = new IntlLocaleFormatter($locale); 49db926724SAndreas Gohr } else { 50*4739030dSAndreas Gohr $locale_formatter = new DateLocaleFormatter($locale); 51db926724SAndreas Gohr } 52db926724SAndreas Gohr 53db926724SAndreas Gohr // Same order as https://www.php.net/manual/en/function.strftime.php 54db926724SAndreas Gohr $translation_table = [ 55db926724SAndreas Gohr // Day 56db926724SAndreas Gohr '%a' => $locale_formatter, 57db926724SAndreas Gohr '%A' => $locale_formatter, 58db926724SAndreas Gohr '%d' => 'd', 59db926724SAndreas Gohr '%e' => function ($timestamp) { 60db926724SAndreas Gohr return sprintf('% 2u', $timestamp->format('j')); 61db926724SAndreas Gohr }, 62db926724SAndreas Gohr '%j' => function ($timestamp) { 63db926724SAndreas Gohr // Day number in year, 001 to 366 64db926724SAndreas Gohr return sprintf('%03d', $timestamp->format('z')+1); 65db926724SAndreas Gohr }, 66db926724SAndreas Gohr '%u' => 'N', 67db926724SAndreas Gohr '%w' => 'w', 68db926724SAndreas Gohr 69db926724SAndreas Gohr // Week 70db926724SAndreas Gohr '%U' => function ($timestamp) { 71db926724SAndreas Gohr // Number of weeks between date and first Sunday of year 72db926724SAndreas Gohr $day = new DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y'))); 73db926724SAndreas Gohr return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); 74db926724SAndreas Gohr }, 75db926724SAndreas Gohr '%V' => 'W', 76db926724SAndreas Gohr '%W' => function ($timestamp) { 77db926724SAndreas Gohr // Number of weeks between date and first Monday of year 78db926724SAndreas Gohr $day = new DateTime(sprintf('%d-01 Monday', $timestamp->format('Y'))); 79db926724SAndreas Gohr return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); 80db926724SAndreas Gohr }, 81db926724SAndreas Gohr 82db926724SAndreas Gohr // Month 83db926724SAndreas Gohr '%b' => $locale_formatter, 84db926724SAndreas Gohr '%B' => $locale_formatter, 85db926724SAndreas Gohr '%h' => $locale_formatter, 86db926724SAndreas Gohr '%m' => 'm', 87db926724SAndreas Gohr 88db926724SAndreas Gohr // Year 89db926724SAndreas Gohr '%C' => function ($timestamp) { 90db926724SAndreas Gohr // Century (-1): 19 for 20th century 91db926724SAndreas Gohr return floor($timestamp->format('Y') / 100); 92db926724SAndreas Gohr }, 93db926724SAndreas Gohr '%g' => function ($timestamp) { 94db926724SAndreas Gohr return substr($timestamp->format('o'), -2); 95db926724SAndreas Gohr }, 96db926724SAndreas Gohr '%G' => 'o', 97db926724SAndreas Gohr '%y' => 'y', 98db926724SAndreas Gohr '%Y' => 'Y', 99db926724SAndreas Gohr 100db926724SAndreas Gohr // Time 101db926724SAndreas Gohr '%H' => 'H', 102db926724SAndreas Gohr '%k' => function ($timestamp) { 103db926724SAndreas Gohr return sprintf('% 2u', $timestamp->format('G')); 104db926724SAndreas Gohr }, 105db926724SAndreas Gohr '%I' => 'h', 106db926724SAndreas Gohr '%l' => function ($timestamp) { 107db926724SAndreas Gohr return sprintf('% 2u', $timestamp->format('g')); 108db926724SAndreas Gohr }, 109db926724SAndreas Gohr '%M' => 'i', 110db926724SAndreas Gohr '%p' => 'A', // AM PM (this is reversed on purpose!) 111db926724SAndreas Gohr '%P' => 'a', // am pm 112db926724SAndreas Gohr '%r' => 'h:i:s A', // %I:%M:%S %p 113db926724SAndreas Gohr '%R' => 'H:i', // %H:%M 114db926724SAndreas Gohr '%S' => 's', 115db926724SAndreas Gohr '%T' => 'H:i:s', // %H:%M:%S 116db926724SAndreas Gohr '%X' => $locale_formatter, // Preferred time representation based on locale, without the date 117db926724SAndreas Gohr 118db926724SAndreas Gohr // Timezone 119db926724SAndreas Gohr '%z' => 'O', 120db926724SAndreas Gohr '%Z' => 'T', 121db926724SAndreas Gohr 122db926724SAndreas Gohr // Time and Date Stamps 123db926724SAndreas Gohr '%c' => $locale_formatter, 124db926724SAndreas Gohr '%D' => 'm/d/Y', 125db926724SAndreas Gohr '%F' => 'Y-m-d', 126db926724SAndreas Gohr '%s' => 'U', 127db926724SAndreas Gohr '%x' => $locale_formatter, 128db926724SAndreas Gohr ]; 129db926724SAndreas Gohr 130db926724SAndreas Gohr $out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) { 131db926724SAndreas Gohr $prefix = $match[1]; 132db926724SAndreas Gohr $char = $match[2]; 133db926724SAndreas Gohr $pattern = '%'.$char; 134db926724SAndreas Gohr if ($pattern == '%n') { 135db926724SAndreas Gohr return "\n"; 136db926724SAndreas Gohr } elseif ($pattern == '%t') { 137db926724SAndreas Gohr return "\t"; 138db926724SAndreas Gohr } 139db926724SAndreas Gohr 140db926724SAndreas Gohr if (!isset($translation_table[$pattern])) { 141db926724SAndreas Gohr throw new InvalidArgumentException(sprintf('Format "%s" is unknown in time format', $pattern)); 142db926724SAndreas Gohr } 143db926724SAndreas Gohr 144db926724SAndreas Gohr $replace = $translation_table[$pattern]; 145db926724SAndreas Gohr 146db926724SAndreas Gohr if (is_string($replace)) { 147db926724SAndreas Gohr $result = $timestamp->format($replace); 148db926724SAndreas Gohr } else { 149db926724SAndreas Gohr $result = $replace($timestamp, $pattern); 150db926724SAndreas Gohr } 151db926724SAndreas Gohr 152db926724SAndreas Gohr switch ($prefix) { 153db926724SAndreas Gohr case '_': 154db926724SAndreas Gohr // replace leading zeros with spaces but keep last char if also zero 155db926724SAndreas Gohr return preg_replace('/\G0(?=.)/', ' ', $result); 156db926724SAndreas Gohr case '#': 157db926724SAndreas Gohr case '-': 158db926724SAndreas Gohr // remove leading zeros but keep last char if also zero 159*4739030dSAndreas Gohr return preg_replace('/^[0\s]+(?=.)/', '', $result); 160db926724SAndreas Gohr } 161db926724SAndreas Gohr 162db926724SAndreas Gohr return $result; 163db926724SAndreas Gohr }, $format); 164db926724SAndreas Gohr 165db926724SAndreas Gohr $out = str_replace('%%', '%', $out); 166db926724SAndreas Gohr return $out; 167db926724SAndreas Gohr } 168