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