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