See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Core date and time related code. 19 * 20 * @package core 21 * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 * @author Petr Skoda <petr.skoda@totaralms.com> 24 */ 25 26 /** 27 * Core date and time related code. 28 * 29 * @since Moodle 2.9 30 * @package core 31 * @copyright 2015 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 * @author Petr Skoda <petr.skoda@totaralms.com> 34 */ 35 class core_date { 36 /** @var array list of recommended zones */ 37 protected static $goodzones = null; 38 39 /** @var array list of BC zones supported by PHP */ 40 protected static $bczones = null; 41 42 /** @var array mapping of timezones not supported by PHP */ 43 protected static $badzones = null; 44 45 /** @var string the default PHP timezone right after config.php */ 46 protected static $defaultphptimezone = null; 47 48 /** 49 * Returns a localised list of timezones. 50 * @param string $currentvalue 51 * @param bool $include99 should the server timezone info be included? 52 * @return array 53 */ 54 public static function get_list_of_timezones($currentvalue = null, $include99 = false) { 55 self::init_zones(); 56 57 // Localise first. 58 $timezones = array(); 59 foreach (self::$goodzones as $tzkey => $ignored) { 60 $timezones[$tzkey] = self::get_localised_timezone($tzkey); 61 } 62 core_collator::asort($timezones); 63 64 // Add '99' if requested. 65 if ($include99 or $currentvalue == 99) { 66 $timezones['99'] = self::get_localised_timezone('99'); 67 } 68 69 if (!isset($currentvalue) or isset($timezones[$currentvalue])) { 70 return $timezones; 71 } 72 73 if (is_numeric($currentvalue)) { 74 // UTC offset. 75 if ($currentvalue == 0) { 76 $a = 'UTC'; 77 } else { 78 $modifier = ($currentvalue > 0) ? '+' : ''; 79 $a = 'UTC' . $modifier . number_format($currentvalue, 1); 80 } 81 $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $a); 82 } else { 83 // Some string we don't recognise. 84 $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $currentvalue); 85 } 86 87 return $timezones; 88 } 89 90 /** 91 * Returns localised timezone name. 92 * @param string $tz 93 * @return string 94 */ 95 public static function get_localised_timezone($tz) { 96 if ($tz == 99) { 97 $tz = self::get_server_timezone(); 98 $tz = self::get_localised_timezone($tz); 99 return get_string('timezoneserver', 'core_admin', $tz); 100 } 101 102 if (get_string_manager()->string_exists(strtolower($tz), 'core_timezones')) { 103 $tz = get_string(strtolower($tz), 'core_timezones'); 104 } else if ($tz === 'GMT' or $tz === 'Etc/GMT' or $tz === 'Etc/UTC') { 105 $tz = 'UTC'; 106 } else if (preg_match('|^Etc/GMT([+-])([0-9]+)$|', $tz, $matches)) { 107 $sign = $matches[1] === '+' ? '-' : '+'; 108 $tz = 'UTC' . $sign . $matches[2]; 109 } 110 111 return $tz; 112 } 113 114 /** 115 * Normalise the timezone name. If timezone not supported 116 * this method falls back to server timezone (if valid) 117 * or default PHP timezone. 118 * 119 * @param int|string|float|DateTimeZone $tz 120 * @return string timezone compatible with PHP 121 */ 122 public static function normalise_timezone($tz) { 123 global $CFG; 124 125 if ($tz instanceof DateTimeZone) { 126 return $tz->getName(); 127 } 128 129 self::init_zones(); 130 $tz = (string)$tz; 131 132 if (isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) { 133 return $tz; 134 } 135 136 $fixed = false; 137 if (isset(self::$badzones[$tz])) { 138 // Convert to known zone. 139 $tz = self::$badzones[$tz]; 140 $fixed = true; 141 } else if (is_numeric($tz)) { 142 // Half hour numeric offsets were already tested, try rounding to integers here. 143 $roundedtz = (string)(int)$tz; 144 if (isset(self::$badzones[$roundedtz])) { 145 $tz = self::$badzones[$roundedtz]; 146 $fixed = true; 147 } 148 } 149 150 if ($fixed and isset(self::$goodzones[$tz]) or isset(self::$bczones[$tz])) { 151 return $tz; 152 } 153 154 // Is server timezone usable? 155 if (isset($CFG->timezone) and !is_numeric($CFG->timezone)) { 156 $result = @timezone_open($CFG->timezone); // Hide notices if invalid. 157 if ($result !== false) { 158 return $result->getName(); 159 } 160 } 161 162 // Bad luck, use the php.ini default or value set in config.php. 163 return self::get_default_php_timezone(); 164 } 165 166 /** 167 * Returns server timezone. 168 * @return string normalised timezone name compatible with PHP 169 **/ 170 public static function get_server_timezone() { 171 global $CFG; 172 173 if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') { 174 return self::get_default_php_timezone(); 175 } 176 177 return self::normalise_timezone($CFG->timezone); 178 } 179 180 /** 181 * Returns server timezone. 182 * @return DateTimeZone 183 **/ 184 public static function get_server_timezone_object() { 185 $tz = self::get_server_timezone(); 186 return new DateTimeZone($tz); 187 } 188 189 /** 190 * Set PHP default timezone to $CFG->timezone. 191 */ 192 public static function set_default_server_timezone() { 193 global $CFG; 194 195 if (!isset($CFG->timezone) or $CFG->timezone == 99 or $CFG->timezone === '') { 196 date_default_timezone_set(self::get_default_php_timezone()); 197 return; 198 } 199 200 $current = date_default_timezone_get(); 201 if ($current === $CFG->timezone) { 202 // Nothing to do. 203 return; 204 } 205 206 if (!isset(self::$goodzones)) { 207 // For better performance try do do this without full tz init, 208 // because this is called from lib/setup.php file on each page. 209 $result = @timezone_open($CFG->timezone); // Ignore error if setting invalid. 210 if ($result !== false) { 211 date_default_timezone_set($result->getName()); 212 return; 213 } 214 } 215 216 // Slow way is the last option. 217 date_default_timezone_set(self::get_server_timezone()); 218 } 219 220 /** 221 * Returns user timezone. 222 * 223 * Ideally the parameter should be a real user record, 224 * unfortunately the legacy code is using 99 for both server 225 * and default value. 226 * 227 * Example of using legacy API: 228 * // Date for other user via legacy API. 229 * $datestr = userdate($time, core_date::get_user_timezone($user)); 230 * 231 * The coding style rules in Moodle are moronic, 232 * why cannot the parameter names have underscores in them? 233 * 234 * @param mixed $userorforcedtz user object or legacy forced timezone string or tz object 235 * @return string normalised timezone name compatible with PHP 236 */ 237 public static function get_user_timezone($userorforcedtz = null) { 238 global $USER, $CFG; 239 240 if ($userorforcedtz instanceof DateTimeZone) { 241 return $userorforcedtz->getName(); 242 } 243 244 if (isset($userorforcedtz) and !is_object($userorforcedtz) and $userorforcedtz != 99) { 245 // Legacy code is forcing timezone in legacy API. 246 return self::normalise_timezone($userorforcedtz); 247 } 248 249 if (isset($CFG->forcetimezone) and $CFG->forcetimezone != 99) { 250 // Override any user timezone. 251 return self::normalise_timezone($CFG->forcetimezone); 252 } 253 254 if ($userorforcedtz === null) { 255 $tz = isset($USER->timezone) ? $USER->timezone : 99; 256 257 } else if (is_object($userorforcedtz)) { 258 $tz = isset($userorforcedtz->timezone) ? $userorforcedtz->timezone : 99; 259 260 } else { 261 if ($userorforcedtz == 99) { 262 $tz = isset($USER->timezone) ? $USER->timezone : 99; 263 } else { 264 $tz = $userorforcedtz; 265 } 266 } 267 268 if ($tz == 99) { 269 return self::get_server_timezone(); 270 } 271 272 return self::normalise_timezone($tz); 273 } 274 275 /** 276 * Return user timezone object. 277 * 278 * @param mixed $userorforcedtz 279 * @return DateTimeZone 280 */ 281 public static function get_user_timezone_object($userorforcedtz = null) { 282 $tz = self::get_user_timezone($userorforcedtz); 283 return new DateTimeZone($tz); 284 } 285 286 /** 287 * Return default timezone set in php.ini or config.php. 288 * @return string normalised timezone compatible with PHP 289 */ 290 public static function get_default_php_timezone() { 291 if (!isset(self::$defaultphptimezone)) { 292 // This should not happen. 293 self::store_default_php_timezone(); 294 } 295 296 return self::$defaultphptimezone; 297 } 298 299 /** 300 * To be called from lib/setup.php only! 301 */ 302 public static function store_default_php_timezone() { 303 if ((defined('PHPUNIT_TEST') and PHPUNIT_TEST) 304 or defined('BEHAT_SITE_RUNNING') or defined('BEHAT_TEST') or defined('BEHAT_UTIL')) { 305 // We want all test sites to be consistent by default. 306 self::$defaultphptimezone = 'Australia/Perth'; 307 return; 308 } 309 if (!isset(self::$defaultphptimezone)) { 310 self::$defaultphptimezone = date_default_timezone_get(); 311 } 312 } 313 314 /** 315 * Do not use directly - use $this->setTimezone('xx', $tz) instead in your test case. 316 * @param string $tz valid timezone name 317 */ 318 public static function phpunit_override_default_php_timezone($tz) { 319 if (!defined('PHPUNIT_TEST')) { 320 throw new coding_exception('core_date::phpunit_override_default_php_timezone() must be used only from unit tests'); 321 } 322 $result = timezone_open($tz ?? ''); // This triggers error if $tz invalid. 323 if ($result !== false) { 324 self::$defaultphptimezone = $tz; 325 } else { 326 self::$defaultphptimezone = 'Australia/Perth'; 327 } 328 } 329 330 /** 331 * To be called from phpunit reset only, after restoring $CFG. 332 */ 333 public static function phpunit_reset() { 334 global $CFG; 335 if (!defined('PHPUNIT_TEST')) { 336 throw new coding_exception('core_date::phpunit_reset() must be used only from unit tests'); 337 } 338 self::store_default_php_timezone(); 339 date_default_timezone_set($CFG->timezone); 340 } 341 342 /** 343 * Initialise timezone arrays, call before use. 344 */ 345 protected static function init_zones() { 346 if (isset(self::$goodzones)) { 347 return; 348 } 349 350 $zones = DateTimeZone::listIdentifiers(); 351 self::$goodzones = array_fill_keys($zones, true); 352 353 $zones = DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC); 354 self::$bczones = array(); 355 foreach ($zones as $zone) { 356 if (isset(self::$goodzones[$zone])) { 357 continue; 358 } 359 self::$bczones[$zone] = true; 360 } 361 362 self::$badzones = array( 363 // Windows time zones. 364 'Dateline Standard Time' => 'Etc/GMT+12', 365 'Hawaiian Standard Time' => 'Pacific/Honolulu', 366 'Alaskan Standard Time' => 'America/Anchorage', 367 'Pacific Standard Time (Mexico)' => 'America/Tijuana', 368 'Pacific Standard Time' => 'America/Los_Angeles', 369 'US Mountain Standard Time' => 'America/Phoenix', 370 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', 371 'Mountain Standard Time' => 'America/Denver', 372 'Central America Standard Time' => 'America/Guatemala', 373 'Central Standard Time' => 'America/Chicago', 374 'Central Standard Time (Mexico)' => 'America/Mexico_City', 375 'Canada Central Standard Time' => 'America/Regina', 376 'SA Pacific Standard Time' => 'America/Bogota', 377 'S.A. Pacific Standard Time' => 'America/Bogota', 378 'Eastern Standard Time' => 'America/New_York', 379 'US Eastern Standard Time' => 'America/Indiana/Indianapolis', 380 'U.S. Eastern Standard Time' => 'America/Indiana/Indianapolis', 381 'Venezuela Standard Time' => 'America/Caracas', 382 'Paraguay Standard Time' => 'America/Asuncion', 383 'Atlantic Standard Time' => 'America/Halifax', 384 'Central Brazilian Standard Time' => 'America/Cuiaba', 385 'SA Western Standard Time' => 'America/La_Paz', 386 'S.A. Western Standard Time' => 'America/La_Paz', 387 'Pacific SA Standard Time' => 'America/Santiago', 388 'Pacific S.A. Standard Time' => 'America/Santiago', 389 'Newfoundland Standard Time' => 'America/St_Johns', 390 'Newfoundland and Labrador Standard Time' => 'America/St_Johns', 391 'E. South America Standard Time' => 'America/Sao_Paulo', 392 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', 393 'SA Eastern Standard Time' => 'America/Cayenne', 394 'S.A. Eastern Standard Time' => 'America/Cayenne', 395 'Greenland Standard Time' => 'America/Godthab', 396 'Montevideo Standard Time' => 'America/Montevideo', 397 'Bahia Standard Time' => 'America/Bahia', 398 'Azores Standard Time' => 'Atlantic/Azores', 399 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', 400 'Morocco Standard Time' => 'Africa/Casablanca', 401 'GMT Standard Time' => 'Europe/London', 402 'Greenwich Standard Time' => 'Atlantic/Reykjavik', 403 'W. Europe Standard Time' => 'Europe/Berlin', 404 'Central Europe Standard Time' => 'Europe/Budapest', 405 'Romance Standard Time' => 'Europe/Paris', 406 'Central European Standard Time' => 'Europe/Warsaw', 407 'W. Central Africa Standard Time' => 'Africa/Lagos', 408 'Namibia Standard Time' => 'Africa/Windhoek', 409 'Jordan Standard Time' => 'Asia/Amman', 410 'GTB Standard Time' => 'Europe/Bucharest', 411 'Middle East Standard Time' => 'Asia/Beirut', 412 'Egypt Standard Time' => 'Africa/Cairo', 413 'Syria Standard Time' => 'Asia/Damascus', 414 'South Africa Standard Time' => 'Africa/Johannesburg', 415 'FLE Standard Time' => 'Europe/Kiev', 416 'Turkey Standard Time' => 'Europe/Istanbul', 417 'Israel Standard Time' => 'Asia/Jerusalem', 418 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', 419 'Libya Standard Time' => 'Africa/Tripoli', 420 'Arabic Standard Time' => 'Asia/Baghdad', 421 'Arab Standard Time' => 'Asia/Riyadh', 422 'Belarus Standard Time' => 'Europe/Minsk', 423 'Russian Standard Time' => 'Europe/Moscow', 424 'E. Africa Standard Time' => 'Africa/Nairobi', 425 'Iran Standard Time' => 'Asia/Tehran', 426 'Arabian Standard Time' => 'Asia/Dubai', 427 'Azerbaijan Standard Time' => 'Asia/Baku', 428 'Russia Time Zone 3' => 'Europe/Samara', 429 'Mauritius Standard Time' => 'Indian/Mauritius', 430 'Georgian Standard Time' => 'Asia/Tbilisi', 431 'Caucasus Standard Time' => 'Asia/Yerevan', 432 'Afghanistan Standard Time' => 'Asia/Kabul', 433 'West Asia Standard Time' => 'Asia/Tashkent', 434 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', 435 'Pakistan Standard Time' => 'Asia/Karachi', 436 'India Standard Time' => 'Asia/Kolkata', // PHP and Windows differ in spelling. 437 'Sri Lanka Standard Time' => 'Asia/Colombo', 438 'Nepal Standard Time' => 'Asia/Kathmandu', 439 'Central Asia Standard Time' => 'Asia/Almaty', 440 'Bangladesh Standard Time' => 'Asia/Dhaka', 441 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 442 'Myanmar Standard Time' => 'Asia/Yangon', 443 'SE Asia Standard Time' => 'Asia/Bangkok', 444 'S.E. Asia Standard Time' => 'Asia/Bangkok', 445 'North Asia Standard Time' => 'Asia/Krasnoyarsk', 446 'China Standard Time' => 'Asia/Shanghai', 447 'North Asia East Standard Time' => 'Asia/Irkutsk', 448 'Singapore Standard Time' => 'Asia/Singapore', 449 'W. Australia Standard Time' => 'Australia/Perth', 450 'Taipei Standard Time' => 'Asia/Taipei', 451 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', 452 'Tokyo Standard Time' => 'Asia/Tokyo', 453 'Korea Standard Time' => 'Asia/Seoul', 454 'Yakutsk Standard Time' => 'Asia/Yakutsk', 455 'Cen. Australia Standard Time' => 'Australia/Adelaide', 456 'AUS Central Standard Time' => 'Australia/Darwin', 457 'A.U.S. Central Standard Time' => 'Australia/Darwin', 458 'E. Australia Standard Time' => 'Australia/Brisbane', 459 'AUS Eastern Standard Time' => 'Australia/Sydney', 460 'A.U.S. Eastern Standard Time' => 'Australia/Sydney', 461 'West Pacific Standard Time' => 'Pacific/Port_Moresby', 462 'Tasmania Standard Time' => 'Australia/Hobart', 463 'Magadan Standard Time' => 'Asia/Magadan', 464 'Vladivostok Standard Time' => 'Asia/Vladivostok', 465 'Russia Time Zone 10' => 'Asia/Srednekolymsk', 466 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', 467 'Russia Time Zone 11' => 'Asia/Kamchatka', 468 'New Zealand Standard Time' => 'Pacific/Auckland', 469 'Fiji Standard Time' => 'Pacific/Fiji', 470 'Fiji Islands Standard Time' => 'Pacific/Fiji', 471 'Tonga Standard Time' => 'Pacific/Tongatapu', 472 'Samoa Standard Time' => 'Pacific/Apia', 473 'Line Islands Standard Time' => 'Pacific/Kiritimati', 474 'Mexico Standard Time 2' => 'America/Chihuahua', 475 'Mexico Standard Time' => 'America/Mexico_City', 476 'U.S. Mountain Standard Time' => 'America/Phoenix', 477 'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia', 478 'E. Europe Standard Time' => 'Europe/Minsk', 479 'Transitional Islamic State of Afghanistan Standard Time' => 'Asia/Kabul', 480 'Armenian Standard Time' => 'Asia/Yerevan', 481 'Kamchatka Standard Time' => 'Asia/Kamchatka', 482 483 // A lot more bad legacy (deprecated) time zones. 484 'Australia/ACT' => 'Australia/Sydney', 485 'Australia/LHI' => 'Australia/Lord_Howe', 486 'Australia/North' => 'Australia/Darwin', 487 'Australia/NSW' => 'Australia/Sydney', 488 'Australia/Queensland' => 'Australia/Brisbane', 489 'Australia/South' => 'Australia/Adelaide', 490 'Australia/Tasmania' => 'Australia/Hobart', 491 'Australia/Victoria' => 'Australia/Melbourne', 492 'Australia/West' => 'Australia/Perth', 493 'Brazil/Acre' => 'America/Rio_Branco', 494 'Brazil/DeNoronha' => 'America/Noronha', 495 'Brazil/East' => 'America/Sao_Paulo', 496 'Brazil/West' => 'America/Manaus', 497 'Canada/Atlantic' => 'America/Halifax', 498 'Canada/Central' => 'America/Winnipeg', 499 'Canada/Eastern' => 'America/Toronto', 500 'Canada/Mountain' => 'America/Edmonton', 501 'Canada/Newfoundland' => 'America/St_Johns', 502 'Canada/Pacific' => 'America/Vancouver', 503 'Canada/Saskatchewan' => 'America/Regina', 504 'Canada/Yukon' => 'America/Whitehorse', 505 'CDT' => 'America/Chicago', 506 'CET' => 'Europe/Berlin', 507 'Central European Time' => 'Europe/Berlin', 508 'Central Time' => 'America/Chicago', 509 'Chile/Continental' => 'America/Santiago', 510 'Chile/EasterIsland' => 'Pacific/Easter', 511 'China Time' => 'Asia/Shanghai', 512 'CST' => 'America/Chicago', 513 'CST6CDT' => 'America/Chicago', 514 'Cuba' => 'America/Havana', 515 'EDT' => 'America/New_York', 516 'EET' => 'Europe/Kiev', 517 'Egypt' => 'Africa/Cairo', 518 'Eire' => 'Europe/Dublin', 519 'EST' => 'America/Cancun', 520 'EST5EDT' => 'America/New_York', 521 'Eastern Time' => 'America/New_York', 522 'Etc/Greenwich' => 'Etc/GMT', 523 'Etc/UCT' => 'Etc/UTC', 524 'Etc/Universal' => 'Etc/UTC', 525 'Etc/Zulu' => 'Etc/UTC', 526 'FET' => 'Europe/Minsk', 527 'GB' => 'Europe/London', 528 'GB-Eire' => 'Europe/London', 529 'Greenwich' => 'Etc/GMT', 530 'Hongkong' => 'Asia/Hong_Kong', 531 'HST' => 'Pacific/Honolulu', 532 'Iceland' => 'Atlantic/Reykjavik', 533 'India Time' => 'Asia/Kolkata', 534 'Iran' => 'Asia/Tehran', 535 'Israel' => 'Asia/Jerusalem', 536 'IST' => 'Asia/Kolkata', 537 'Jamaica' => 'America/Jamaica', 538 'Japan' => 'Asia/Tokyo', 539 'Japan Standard Time' => 'Asia/Tokyo', 540 'Japan Time' => 'Asia/Tokyo', 541 'JST' => 'Asia/Tokyo', 542 'Kwajalein' => 'Pacific/Kwajalein', 543 'Libya' => 'Africa/Tripoli', 544 'MDT' => 'America/Denver', 545 'MET' => 'Europe/Paris', 546 'Mexico/BajaNorte' => 'America/Tijuana', 547 'Mexico/BajaSur' => 'America/Mazatlan', 548 'Mexico/General' => 'America/Mexico_City', 549 'MST' => 'America/Phoenix', 550 'MST7MDT' => 'America/Denver', 551 'Navajo' => 'America/Denver', 552 'NZ' => 'Pacific/Auckland', 553 'NZ-CHAT' => 'Pacific/Chatham', 554 'Pacific Time' => 'America/Los_Angeles', 555 'PDT' => 'America/Los_Angeles', 556 'Poland' => 'Europe/Warsaw', 557 'Portugal' => 'Europe/Lisbon', 558 'PRC' => 'Asia/Shanghai', 559 'PST' => 'America/Los_Angeles', 560 'PST8PDT' => 'America/Los_Angeles', 561 'ROC' => 'Asia/Taipei', 562 'ROK' => 'Asia/Seoul', 563 'Singapore' => 'Asia/Singapore', 564 'Turkey' => 'Europe/Istanbul', 565 'UCT' => 'Etc/UTC', 566 'Universal' => 'Etc/UTC', 567 'US/Alaska' => 'America/Anchorage', 568 'US/Aleutian' => 'America/Adak', 569 'US/Arizona' => 'America/Phoenix', 570 'US/Central' => 'America/Chicago', 571 'US/East-Indiana' => 'America/Indiana/Indianapolis', 572 'US/Eastern' => 'America/New_York', 573 'US/Hawaii' => 'Pacific/Honolulu', 574 'US/Indiana-Starke' => 'America/Indiana/Knox', 575 'US/Michigan' => 'America/Detroit', 576 'US/Mountain' => 'America/Denver', 577 'US/Pacific' => 'America/Los_Angeles', 578 'US/Pacific-New' => 'America/Los_Angeles', 579 'US/Samoa' => 'Pacific/Pago_Pago', 580 'W-SU' => 'Europe/Moscow', 581 'WET' => 'Europe/London', 582 'Zulu' => 'Etc/UTC', 583 584 // Some UTC variations. 585 'UTC-01' => 'Etc/GMT+1', 586 'UTC-02' => 'Etc/GMT+2', 587 'UTC-03' => 'Etc/GMT+3', 588 'UTC-04' => 'Etc/GMT+4', 589 'UTC-05' => 'Etc/GMT+5', 590 'UTC-06' => 'Etc/GMT+6', 591 'UTC-07' => 'Etc/GMT+7', 592 'UTC-08' => 'Etc/GMT+8', 593 'UTC-09' => 'Etc/GMT+9', 594 595 // Some weird GMTs. 596 'Etc/GMT+0' => 'Etc/GMT', 597 'Etc/GMT-0' => 'Etc/GMT', 598 'Etc/GMT0' => 'Etc/GMT', 599 600 // Link old timezone names with their new names. 601 'Africa/Asmera' => 'Africa/Asmara', 602 'Africa/Timbuktu' => 'Africa/Abidjan', 603 'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca', 604 'America/Atka' => 'America/Adak', 605 'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires', 606 'America/Catamarca' => 'America/Argentina/Catamarca', 607 'America/Coral_Harbour' => 'America/Atikokan', 608 'America/Cordoba' => 'America/Argentina/Cordoba', 609 'America/Ensenada' => 'America/Tijuana', 610 'America/Fort_Wayne' => 'America/Indiana/Indianapolis', 611 'America/Godthab' => 'America/Nuuk', 612 'America/Indianapolis' => 'America/Indiana/Indianapolis', 613 'America/Jujuy' => 'America/Argentina/Jujuy', 614 'America/Knox_IN' => 'America/Indiana/Knox', 615 'America/Louisville' => 'America/Kentucky/Louisville', 616 'America/Mendoza' => 'America/Argentina/Mendoza', 617 'America/Montreal' => 'America/Toronto', 618 'America/Porto_Acre' => 'America/Rio_Branco', 619 'America/Rosario' => 'America/Argentina/Cordoba', 620 'America/Santa_Isabel' => 'America/Tijuana', 621 'America/Shiprock' => 'America/Denver', 622 'America/Virgin' => 'America/Port_of_Spain', 623 'Antarctica/South_Pole' => 'Pacific/Auckland', 624 'Asia/Ashkhabad' => 'Asia/Ashgabat', 625 'Asia/Calcutta' => 'Asia/Kolkata', 626 'Asia/Chongqing' => 'Asia/Shanghai', 627 'Asia/Chungking' => 'Asia/Shanghai', 628 'Asia/Dacca' => 'Asia/Dhaka', 629 'Asia/Harbin' => 'Asia/Shanghai', 630 'Asia/Istanbul' => 'Europe/Istanbul', 631 'Asia/Kashgar' => 'Asia/Urumqi', 632 'Asia/Katmandu' => 'Asia/Kathmandu', 633 'Asia/Macao' => 'Asia/Macau', 634 'Asia/Rangoon' => 'Asia/Yangon', 635 'Asia/Saigon' => 'Asia/Ho_Chi_Minh', 636 'Asia/Tel_Aviv' => 'Asia/Jerusalem', 637 'Asia/Thimbu' => 'Asia/Thimphu', 638 'Asia/Ujung_Pandang' => 'Asia/Makassar', 639 'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar', 640 'Atlantic/Faeroe' => 'Atlantic/Faroe', 641 'Atlantic/Jan_Mayen' => 'Europe/Oslo', 642 'Australia/Canberra' => 'Australia/Sydney', 643 'Australia/Yancowinna' => 'Australia/Broken_Hill', 644 'Europe/Belfast' => 'Europe/London', 645 'Europe/Kiev' => 'Europe/Kyiv', 646 'Europe/Nicosia' => 'Asia/Nicosia', 647 'Europe/Tiraspol' => 'Europe/Chisinau', 648 'Pacific/Enderbury' => 'Pacific/Kanton', 649 'Pacific/Johnston' => 'Pacific/Honolulu', 650 'Pacific/Ponape' => 'Pacific/Pohnpei', 651 'Pacific/Samoa' => 'Pacific/Pago_Pago', 652 'Pacific/Truk' => 'Pacific/Chuuk', 653 'Pacific/Yap' => 'Pacific/Chuuk', 654 ); 655 656 // Legacy GMT fallback. 657 for ($i = -12; $i <= 14; $i++) { 658 $off = abs($i); 659 if ($i < 0) { 660 $mapto = 'Etc/GMT+' . $off; 661 $utc = 'UTC-' . $off; 662 $gmt = 'GMT-' . $off; 663 } else if ($i > 0) { 664 $mapto = 'Etc/GMT-' . $off; 665 $utc = 'UTC+' . $off; 666 $gmt = 'GMT+' . $off; 667 } else { 668 $mapto = 'Etc/GMT'; 669 $utc = 'UTC'; 670 $gmt = 'GMT'; 671 } 672 if (isset(self::$bczones[$mapto])) { 673 self::$badzones[$i . ''] = $mapto; 674 self::$badzones[$i . '.0'] = $mapto; 675 self::$badzones[$utc] = $mapto; 676 self::$badzones[$gmt] = $mapto; 677 } 678 } 679 680 // Legacy Moodle half an hour offsets - pick any city nearby, ideally without DST. 681 self::$badzones['4.5'] = 'Asia/Kabul'; 682 self::$badzones['5.5'] = 'Asia/Kolkata'; 683 self::$badzones['6.5'] = 'Asia/Rangoon'; 684 self::$badzones['9.5'] = 'Australia/Darwin'; 685 686 // Remove bad zones that are elsewhere. 687 foreach (self::$bczones as $zone => $unused) { 688 if (isset(self::$badzones[$zone])) { 689 unset(self::$badzones[$zone]); 690 } 691 } 692 foreach (self::$goodzones as $zone => $unused) { 693 if (isset(self::$badzones[$zone])) { 694 unset(self::$badzones[$zone]); 695 } 696 } 697 } 698 699 /** 700 * Locale-formatted strftime using IntlDateFormatter (PHP 8.1 compatible) 701 * This provides a cross-platform alternative to strftime() for when it will be removed from PHP. 702 * Note that output can be slightly different between libc sprintf and this function as it is using ICU. 703 * 704 * From: 705 * https://github.com/alphp/strftime 706 * 707 * @param string $format Date format 708 * @param int|string|DateTime $timestamp Timestamp 709 * @param string|null $locale 710 * @return string 711 * @author BohwaZ <https://bohwaz.net/> 712 */ 713 public static function strftime(string $format, $timestamp = null, ?string $locale = null) : string { 714 // Moodle-specific modification. For the IntlDateFormatter we need to use unix-style locale 715 // from the string 'locale' even for Windows, so we can neither use moodle_getlocale(). 716 // nor rely on the setlocale() use below. We also ignore $CFG->locale because it can use 717 // Windows format. 718 $locale = $locale ?: get_string('locale', 'langconfig'); 719 720 // The following code is taken from https://github.com/alphp/strftime without modifications. 721 // phpcs:disable 722 if (!($timestamp instanceof DateTimeInterface)) { 723 $timestamp = is_int($timestamp) ? '@' . $timestamp : (string) $timestamp; 724 725 try { 726 $timestamp = new DateTime($timestamp); 727 } catch (Exception $e) { 728 throw new InvalidArgumentException('$timestamp argument is neither a valid UNIX timestamp, a valid date-time string or a DateTime object.', 0, $e); 729 } 730 } 731 732 $timestamp->setTimezone(new DateTimeZone(date_default_timezone_get())); 733 734 if (empty($locale)) { 735 // get current locale 736 $locale = setlocale(LC_TIME, '0'); 737 } 738 // remove trailing part not supported by ext-intl locale 739 $locale = preg_replace('/[^\w-].*$/', '', $locale); 740 741 $intl_formats = [ 742 '%a' => 'EEE', // An abbreviated textual representation of the day Sun through Sat 743 '%A' => 'EEEE', // A full textual representation of the day Sunday through Saturday 744 '%b' => 'MMM', // Abbreviated month name, based on the locale Jan through Dec 745 '%B' => 'MMMM', // Full month name, based on the locale January through December 746 '%h' => 'MMM', // Abbreviated month name, based on the locale (an alias of %b) Jan through Dec 747 ]; 748 749 $intl_formatter = function (DateTimeInterface $timestamp, string $format) use ($intl_formats, $locale) { 750 751 // Map IANA timezone DB names (used by PHP) to those used internally by the "intl" extension. The extension uses its 752 // own data based on ICU timezones, which may not necessarily be in-sync with IANA depending on the version installed 753 // on the local system. See: https://unicode-org.github.io/icu/userguide/datetime/timezone/#updating-the-time-zone-data 754 $tz = $timestamp->getTimezone(); 755 $intltz = IntlTimeZone::fromDateTimeZone($tz); 756 if ($intltz === null) { 757 // Where intl doesn't know about a recent timezone, map it to an equivalent existing zone. 758 $intltzname = strtr($tz->getName(), [ 759 'America/Ciudad_Juarez' => 'America/Denver', 760 'America/Nuuk' => 'America/Godthab', 761 'Europe/Kyiv' => 'Europe/Kiev', 762 'Pacific/Kanton' => 'Pacific/Enderbury', 763 ]); 764 $intltz = IntlTimeZone::createTimeZone($intltzname); 765 } 766 767 $date_type = IntlDateFormatter::FULL; 768 $time_type = IntlDateFormatter::FULL; 769 $pattern = ''; 770 771 switch ($format) { 772 // %c = Preferred date and time stamp based on locale 773 // Example: Tue Feb 5 00:45:10 2009 for February 5, 2009 at 12:45:10 AM 774 case '%c': 775 $date_type = IntlDateFormatter::LONG; 776 $time_type = IntlDateFormatter::SHORT; 777 break; 778 779 // %x = Preferred date representation based on locale, without the time 780 // Example: 02/05/09 for February 5, 2009 781 case '%x': 782 $date_type = IntlDateFormatter::SHORT; 783 $time_type = IntlDateFormatter::NONE; 784 break; 785 786 // Localized time format 787 case '%X': 788 $date_type = IntlDateFormatter::NONE; 789 $time_type = IntlDateFormatter::MEDIUM; 790 break; 791 792 default: 793 $pattern = $intl_formats[$format]; 794 } 795 796 // In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and 797 // the 4th October was followed by the 15th October. 798 // ICU (including IntlDateFormattter) interprets and formats dates based on this cutover. 799 // Posix (including strftime) and timelib (including DateTimeImmutable) instead use 800 // a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever. 801 // This leads to the same instants in time, as expressed in Unix time, having different representations 802 // in formatted strings. 803 // To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past. 804 $calendar = IntlGregorianCalendar::createInstance($intltz); 805 $calendar->setGregorianChange(PHP_INT_MIN); 806 807 return (new IntlDateFormatter($locale, $date_type, $time_type, $intltz, $calendar, $pattern))->format($timestamp); 808 }; 809 810 // Same order as https://www.php.net/manual/en/function.strftime.php 811 $translation_table = [ 812 // Day 813 '%a' => $intl_formatter, 814 '%A' => $intl_formatter, 815 '%d' => 'd', 816 '%e' => function ($timestamp) { 817 return sprintf('% 2u', $timestamp->format('j')); 818 }, 819 '%j' => function ($timestamp) { 820 // Day number in year, 001 to 366 821 return sprintf('%03d', $timestamp->format('z')+1); 822 }, 823 '%u' => 'N', 824 '%w' => 'w', 825 826 // Week 827 '%U' => function ($timestamp) { 828 // Number of weeks between date and first Sunday of year 829 $day = new DateTime(sprintf('%d-01 Sunday', $timestamp->format('Y'))); 830 return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); 831 }, 832 '%V' => 'W', 833 '%W' => function ($timestamp) { 834 // Number of weeks between date and first Monday of year 835 $day = new DateTime(sprintf('%d-01 Monday', $timestamp->format('Y'))); 836 return sprintf('%02u', 1 + ($timestamp->format('z') - $day->format('z')) / 7); 837 }, 838 839 // Month 840 '%b' => $intl_formatter, 841 '%B' => $intl_formatter, 842 '%h' => $intl_formatter, 843 '%m' => 'm', 844 845 // Year 846 '%C' => function ($timestamp) { 847 // Century (-1): 19 for 20th century 848 return floor($timestamp->format('Y') / 100); 849 }, 850 '%g' => function ($timestamp) { 851 return substr($timestamp->format('o'), -2); 852 }, 853 '%G' => 'o', 854 '%y' => 'y', 855 '%Y' => 'Y', 856 857 // Time 858 '%H' => 'H', 859 '%k' => function ($timestamp) { 860 return sprintf('% 2u', $timestamp->format('G')); 861 }, 862 '%I' => 'h', 863 '%l' => function ($timestamp) { 864 return sprintf('% 2u', $timestamp->format('g')); 865 }, 866 '%M' => 'i', 867 '%p' => 'A', // AM PM (this is reversed on purpose!) 868 '%P' => 'a', // am pm 869 '%r' => 'h:i:s A', // %I:%M:%S %p 870 '%R' => 'H:i', // %H:%M 871 '%S' => 's', 872 '%T' => 'H:i:s', // %H:%M:%S 873 '%X' => $intl_formatter, // Preferred time representation based on locale, without the date 874 875 // Timezone 876 '%z' => 'O', 877 '%Z' => 'T', 878 879 // Time and Date Stamps 880 '%c' => $intl_formatter, 881 '%D' => 'm/d/Y', 882 '%F' => 'Y-m-d', 883 '%s' => 'U', 884 '%x' => $intl_formatter, 885 ]; 886 887 $out = preg_replace_callback('/(?<!%)%([_#-]?)([a-zA-Z])/', function ($match) use ($translation_table, $timestamp) { 888 $prefix = $match[1]; 889 $char = $match[2]; 890 $pattern = '%'.$char; 891 if ($pattern == '%n') { 892 return "\n"; 893 } elseif ($pattern == '%t') { 894 return "\t"; 895 } 896 897 if (!isset($translation_table[$pattern])) { 898 throw new InvalidArgumentException(sprintf('Format "%s" is unknown in time format', $pattern)); 899 } 900 901 $replace = $translation_table[$pattern]; 902 903 if (is_string($replace)) { 904 $result = $timestamp->format($replace); 905 } else { 906 $result = $replace($timestamp, $pattern); 907 } 908 909 switch ($prefix) { 910 case '_': 911 // replace leading zeros with spaces but keep last char if also zero 912 return preg_replace('/\G0(?=.)/', ' ', $result); 913 case '#': 914 case '-': 915 // remove leading zeros but keep last char if also zero 916 return preg_replace('/^0+(?=.)/', '', $result); 917 } 918 919 return $result; 920 }, $format); 921 922 $out = str_replace('%%', '%', $out); 923 return $out; 924 // phpcs:enable 925 } 926 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body