| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
1 <?php //$Id: olson.php,v 1.25 2007/10/02 01:43:07 mattc-catalyst Exp $ 2 3 /*** 4 *** olson_to_timezones ($filename) 5 *** 6 *** Parses the olson files for Zones and DST rules. 7 *** It updates the Moodle database with the Zones/DST rules 8 *** 9 *** Returns true/false 10 *** 11 */ 12 function olson_to_timezones ($filename) { 13 14 $zones = olson_simple_zone_parser($filename); 15 $rules = olson_simple_rule_parser($filename); 16 17 $mdl_zones = array(); 18 19 /** 20 *** To translate the combined Zone & Rule changes 21 *** in the Olson files to the Moodle single ruleset 22 *** format, we need to trasverse every year and see 23 *** if either the Zone or the relevant Rule has a 24 *** change. It's yuck but it yields a rationalized 25 *** set of data, which is arguably simpler. 26 *** 27 *** Also note that I am starting at the epoch (1970) 28 *** because I don't think we'll see many events scheduled 29 *** before that, anyway. 30 *** 31 **/ 32 $maxyear = localtime(time(), true); 33 $maxyear = $maxyear['tm_year'] + 1900 + 10; 34 35 foreach ($zones as $zname => $zbyyear) { // loop over zones 36 /** 37 *** Loop over years, only adding a rule when zone or rule 38 *** have changed. All loops preserver the last seen vars 39 *** until there's an explicit decision to delete them 40 *** 41 **/ 42 43 // clean the slate for a new zone 44 $zone = NULL; 45 $rule = NULL; 46 47 // 48 // Find the pre 1970 zone rule entries 49 // 50 for ($y = 1970 ; $y >= 0 ; $y--) { 51 if (array_key_exists((string)$y, $zbyyear )) { // we have a zone entry for the year 52 $zone = $zbyyear[$y]; 53 //print_object("Zone $zname pre1970 is in $y\n"); 54 break; // Perl's last -- get outta here 55 } 56 } 57 if (!empty($zone['rule']) && array_key_exists($zone['rule'], $rules)) { 58 $rule = NULL; 59 for ($y = 1970 ; $y > 0 ; $y--) { 60 if (array_key_exists((string)$y, $rules[$zone['rule']] )) { // we have a rule entry for the year 61 $rule = $rules[$zone['rule']][$y]; 62 //print_object("Rule $rule[name] pre1970 is $y\n"); 63 break; // Perl's last -- get outta here 64 } 65 66 } 67 if (empty($rule)) { 68 // Colombia and a few others refer to rules before they exist 69 // Perhaps we should comment out this warning... 70 // trigger_error("Cannot find rule in $zone[rule] <= 1970"); 71 $rule = array(); 72 } 73 } else { 74 // no DST this year! 75 $rule = array(); 76 } 77 78 // Prepare to insert the base 1970 zone+rule 79 if (!empty($rule) && array_key_exists($zone['rule'], $rules)) { 80 // merge the two arrays into the moodle rule 81 unset($rule['name']); // warning: $rule must NOT be a reference! 82 unset($rule['year']); 83 $mdl_tz = array_merge($zone, $rule); 84 85 //fix (de)activate_time (AT) field to be GMT 86 $mdl_tz['dst_time'] = olson_parse_at($mdl_tz['dst_time'], 'set', $mdl_tz['gmtoff']); 87 $mdl_tz['std_time'] = olson_parse_at($mdl_tz['std_time'], 'reset', $mdl_tz['gmtoff']); 88 } else { 89 // just a simple zone 90 $mdl_tz = $zone; 91 // TODO: Add other default values here! 92 $mdl_tz['dstoff'] = 0; 93 } 94 95 // Fix the from year to 1970 96 $mdl_tz['year'] = 1970; 97 98 // add to the array 99 $mdl_zones[] = $mdl_tz; 100 //print_object("Zero entry for $zone[name] added"); 101 102 $lasttimezone = $mdl_tz; 103 104 /// 105 /// 1971 onwards 106 /// 107 for ($y = 1971; $y < $maxyear ; $y++) { 108 $changed = false; 109 /// 110 /// We create a "zonerule" entry if either zone or rule change... 111 /// 112 /// force $y to string to avoid PHP 113 /// thinking of a positional array 114 /// 115 if (array_key_exists((string)$y, $zbyyear)) { // we have a zone entry for the year 116 $changed = true; 117 $zone = $zbyyear[(string)$y]; 118 } 119 if (!empty($zone['rule']) && array_key_exists($zone['rule'], $rules)) { 120 if (array_key_exists((string)$y, $rules[$zone['rule']])) { 121 $changed = true; 122 $rule = $rules[$zone['rule']][(string)$y]; 123 } 124 } else { 125 $rule = array(); 126 } 127 128 if ($changed) { 129 //print_object("CHANGE YEAR $y Zone $zone[name] Rule $zone[rule]\n"); 130 if (!empty($rule)) { 131 // merge the two arrays into the moodle rule 132 unset($rule['name']); 133 unset($rule['year']); 134 $mdl_tz = array_merge($zone, $rule); 135 136 // VERY IMPORTANT!! 137 $mdl_tz['year'] = $y; 138 139 //fix (de)activate_time (AT) field to be GMT 140 $mdl_tz['dst_time'] = olson_parse_at($mdl_tz['dst_time'], 'set', $mdl_tz['gmtoff']); 141 $mdl_tz['std_time'] = olson_parse_at($mdl_tz['std_time'], 'reset', $mdl_tz['gmtoff']); 142 } else { 143 // just a simple zone 144 $mdl_tz = $zone; 145 } 146 147 /* 148 if(isset($mdl_tz['dst_time']) && !strpos($mdl_tz['dst_time'], ':') || isset($mdl_tz['std_time']) && !strpos($mdl_tz['std_time'], ':')) { 149 print_object($mdl_tz); 150 print_object('---'); 151 } 152 */ 153 // This is the simplest way to make the != operator just below NOT take the year into account 154 $lasttimezone['year'] = $mdl_tz['year']; 155 156 // If not a duplicate, add and update $lasttimezone 157 if($lasttimezone != $mdl_tz) { 158 $mdl_zones[] = $lasttimezone = $mdl_tz; 159 } 160 } 161 } 162 163 } 164 165 /* 166 if (function_exists('memory_get_usage')) { 167 trigger_error("We are consuming this much memory: " . get_memory_usage()); 168 } 169 */ 170 171 /// Since Moodle 1.7, rule is tzrule in DB (reserved words problem), so change it here 172 /// after everything is calculated to be properly loaded to the timezone table. 173 /// Pre 1.7 users won't have the old rule if updating this from moodle.org but it 174 /// seems that such field isn't used at all by the rest of Moodle (at least I haven't 175 /// found any use when looking for it). 176 177 foreach($mdl_zones as $key=>$mdl_zone) { 178 $mdl_zones[$key]['tzrule'] = $mdl_zones[$key]['rule']; 179 } 180 181 return $mdl_zones; 182 } 183 184 185 /*** 186 *** olson_simple_rule_parser($filename) 187 *** 188 *** Parses the olson files for DST rules. 189 *** It's a simple implementation that simplifies some fields 190 *** 191 *** Returns a multidimensional array, or false on error 192 *** 193 */ 194 function olson_simple_rule_parser ($filename) { 195 196 $file = fopen($filename, 'r', 0); 197 198 if (empty($file)) { 199 return false; 200 } 201 202 // determine the maximum year for this zone 203 $maxyear = array(); 204 205 while ($line = fgets($file)) { 206 // only pay attention to rules lines 207 if(!preg_match('/^Rule\s/', $line)){ 208 continue; 209 } 210 $line = preg_replace('/\n$/', '',$line); // chomp 211 $rule = preg_split('/\s+/', $line); 212 list($discard, 213 $name, 214 $from, 215 $to, 216 $type, 217 $in, 218 $on, 219 $at, 220 $save, 221 $letter) = $rule; 222 if (isset($maxyear[$name])) { 223 if ($maxyear[$name] < $from) { 224 $maxyear[$name] = $from; 225 } 226 } else { 227 $maxyear[$name] = $from; 228 } 229 230 } 231 232 fseek($file, 0); 233 234 $rules = array(); 235 while ($line = fgets($file)) { 236 // only pay attention to rules lines 237 if(!preg_match('/^Rule\s/', $line)){ 238 continue; 239 } 240 $line = preg_replace('/\n$/', '',$line); // chomp 241 $rule = preg_split('/\s+/', $line); 242 list($discard, 243 $name, 244 $from, 245 $to, 246 $type, 247 $in, 248 $on, 249 $at, 250 $save, 251 $letter) = $rule; 252 253 $srs = ($save === '0') ? 'reset' : 'set'; 254 255 if($to == 'only') { 256 $to = $from; 257 } 258 else if($to == 'max') { 259 $to = $maxyear[$name]; 260 } 261 262 for($i = $from; $i <= $to; ++$i) { 263 $rules[$name][$i][$srs] = $rule; 264 } 265 266 } 267 268 fclose($file); 269 270 $months = array('jan' => 1, 'feb' => 2, 271 'mar' => 3, 'apr' => 4, 272 'may' => 5, 'jun' => 6, 273 'jul' => 7, 'aug' => 8, 274 'sep' => 9, 'oct' => 10, 275 'nov' => 11, 'dec' => 12); 276 277 278 // now reformat it a bit to match Moodle's DST table 279 $moodle_rules = array(); 280 foreach ($rules as $rule => $rulesbyyear) { 281 foreach ($rulesbyyear as $year => $rulesthisyear) { 282 283 if(!isset($rulesthisyear['reset'])) { 284 // No "reset" rule. We will assume that this is somewhere in the southern hemisphere 285 // after a period of not using DST, otherwise it doesn't make sense at all. 286 // With that assumption, we can put in a fake reset e.g. on Jan 1, 12:00. 287 /* 288 print_object("no reset"); 289 print_object($rules); 290 die(); 291 */ 292 $rulesthisyear['reset'] = array( 293 NULL, NULL, NULL, NULL, NULL, 'jan', 1, '12:00', '00:00', NULL 294 ); 295 } 296 297 if(!isset($rulesthisyear['set'])) { 298 // No "set" rule. We will assume that this is somewhere in the southern hemisphere 299 // and that it begins a period of not using DST, otherwise it doesn't make sense at all. 300 // With that assumption, we can put in a fake set on Dec 31, 12:00, shifting time by 0 minutes. 301 $rulesthisyear['set'] = array( 302 NULL, $rulesthisyear['reset'][1], NULL, NULL, NULL, 'dec', 31, '12:00', '00:00', NULL 303 ); 304 } 305 306 list($discard, 307 $name, 308 $from, 309 $to, 310 $type, 311 $in, 312 $on, 313 $at, 314 $save, 315 $letter) = $rulesthisyear['set']; 316 317 $moodle_rule = array(); 318 319 // $save is sometimes just minutes 320 // and othertimes HH:MM -- only 321 // parse if relevant 322 if (!preg_match('/^\d+$/', $save)) { 323 list($hours, $mins) = explode(':', $save); 324 $save = $hours * 60 + $mins; 325 } 326 327 // we'll parse $at later 328 // $at = olson_parse_at($at); 329 $in = strtolower($in); 330 if(!isset($months[$in])) { 331 trigger_error('Unknown month: '.$in); 332 } 333 334 $moodle_rule['name'] = $name; 335 $moodle_rule['year'] = $year; 336 $moodle_rule['dstoff'] = $save; // time offset 337 338 $moodle_rule['dst_month'] = $months[$in]; // the month 339 $moodle_rule['dst_time'] = $at; // the time 340 341 // Encode index and day as per Moodle's specs 342 $on = olson_parse_on($on); 343 $moodle_rule['dst_startday'] = $on['startday']; 344 $moodle_rule['dst_weekday'] = $on['weekday']; 345 $moodle_rule['dst_skipweeks'] = $on['skipweeks']; 346 347 // and now the "deactivate" data 348 list($discard, 349 $name, 350 $from, 351 $to, 352 $type, 353 $in, 354 $on, 355 $at, 356 $save, 357 $letter) = $rulesthisyear['reset']; 358 359 // we'll parse $at later 360 // $at = olson_parse_at($at); 361 $in = strtolower($in); 362 if(!isset($months[$in])) { 363 trigger_error('Unknown month: '.$in); 364 } 365 366 $moodle_rule['std_month'] = $months[$in]; // the month 367 $moodle_rule['std_time'] = $at; // the time 368 369 // Encode index and day as per Moodle's specs 370 $on = olson_parse_on($on); 371 $moodle_rule['std_startday'] = $on['startday']; 372 $moodle_rule['std_weekday'] = $on['weekday']; 373 $moodle_rule['std_skipweeks'] = $on['skipweeks']; 374 375 $moodle_rules[$moodle_rule['name']][$moodle_rule['year']] = $moodle_rule; 376 //print_object($moodle_rule); 377 378 } // end foreach year within a rule 379 380 // completed with all the entries for this rule 381 // if the last entry has a TO other than 'max' 382 // then we have to deal with closing the last rule 383 //trigger_error("Rule $name ending to $to"); 384 if (!empty($to) && $to !== 'max') { 385 // We can handle two cases for TO: 386 // a year, or "only" 387 $reset_rule = $moodle_rule; 388 $reset_rule['dstoff'] = '00'; 389 if (preg_match('/^\d+$/', $to)){ 390 $reset_rule['year'] = $to; 391 $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule; 392 } elseif ($to === 'only') { 393 $reset_rule['year'] = $reset_rule['year'] + 1; 394 $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule; 395 } else { 396 trigger_error("Strange value in TO $to rule field for rule $name"); 397 } 398 399 } // end if $to is interesting 400 401 } // end foreach rule 402 403 return $moodle_rules; 404 } 405 406 /*** 407 *** olson_simple_zone_parser($filename) 408 *** 409 *** Parses the olson files for zone info 410 *** 411 *** Returns a multidimensional array, or false on error 412 *** 413 */ 414 function olson_simple_zone_parser ($filename) { 415 416 $file = fopen($filename, 'r', 0); 417 418 if (empty($file)) { 419 return false; 420 } 421 422 $zones = array(); 423 $lastzone = NULL; 424 425 while ($line = fgets($file)) { 426 // skip obvious non-zone lines 427 if (preg_match('/^#/', $line)) { 428 continue; 429 } 430 if (preg_match('/^(?:Rule|Link|Leap)/',$line)) { 431 $lastzone = NULL; // reset lastzone 432 continue; 433 } 434 435 // If there are blanks in the start of the line but the first non-ws character is a #, 436 // assume it's an "inline comment". The funny thing is that this happens only during 437 // the definition of the Rule for Europe/Athens. 438 if(substr(trim($line), 0, 1) == '#') { 439 continue; 440 } 441 442 /*** Notes 443 *** 444 *** By splitting on space, we are only keeping the 445 *** year of the UNTIL field -- that's on purpose. 446 *** 447 *** The Zone lines are followed by continuation lines 448 *** were we reuse the info from the last one seen. 449 *** 450 *** We are transforming "until" fields into "from" fields 451 *** which make more sense from the Moodle perspective, so 452 *** each initial Zone entry is "from" the year 0, and for the 453 *** continuation lines, we shift the "until" from the previous field 454 *** into this line's "from". 455 *** 456 *** If a RULES field contains a time instead of a rule we discard it 457 *** I have no idea of how to create a DST rule out of that 458 *** (what are the start/end times?) 459 *** 460 *** We remove "until" from the data we keep, but preserve 461 *** it in $lastzone. 462 */ 463 if (preg_match('/^Zone/', $line)) { // a new zone 464 $line = trim($line); 465 $line = preg_split('/\s+/', $line); 466 $zone = array(); 467 list( $discard, // 'Zone' 468 $zone['name'], 469 $zone['gmtoff'], 470 $zone['rule'], 471 $discard // format 472 ) = $line; 473 // the things we do to avoid warnings 474 if (!empty($line[5])) { 475 $zone['until'] = $line[5]; 476 } 477 $zone['year'] = '0'; 478 479 $zones[$zone['name']] = array(); 480 481 } else if (!empty($lastzone) && preg_match('/^\s+/', $line)){ 482 // looks like a credible continuation line 483 $line = trim($line); 484 $line = preg_split('/\s+/', $line); 485 if (count($line) < 3) { 486 $lastzone = NULL; 487 continue; 488 } 489 // retrieve info from the lastzone 490 $zone = $lastzone; 491 $zone['year'] = $zone['until']; 492 // overwrite with current data 493 list( 494 $zone['gmtoff'], 495 $zone['rule'], 496 $discard // format 497 ) = $line; 498 // the things we do to avoid warnings 499 if (!empty($line[3])) { 500 $zone['until'] = $line[3]; 501 } 502 503 } else { 504 $lastzone = NULL; 505 continue; 506 } 507 508 // tidy up, we're done 509 // perhaps we should insert in the DB at this stage? 510 $lastzone = $zone; 511 unset($zone['until']); 512 $zone['gmtoff'] = olson_parse_offset($zone['gmtoff']); 513 if ($zone['rule'] === '-') { // cleanup empty rules 514 $zone['rule'] = ''; 515 } 516 if (preg_match('/:/',$zone['rule'])) { 517 // we are not handling direct SAVE rules here 518 // discard it 519 $zone['rule'] = ''; 520 } 521 522 $zones[$zone['name']][(string)$zone['year']] = $zone; 523 } 524 525 return $zones; 526 } 527 528 /*** 529 *** olson_parse_offset($offset) 530 *** 531 *** parses time offsets from the GMTOFF and SAVE 532 *** fields into +/-MINUTES 533 */ 534 function olson_parse_offset ($offset) { 535 $offset = trim($offset); 536 537 // perhaps it's just minutes 538 if (preg_match('/^(-?)(\d*)$/', $offset)) { 539 return intval($offset); 540 } 541 // (-)hours:minutes(:seconds) 542 if (preg_match('/^(-?)(\d*):(\d+)/', $offset, $matches)) { 543 // we are happy to discard the seconds 544 $sign = $matches[1]; 545 $hours = intval($matches[2]); 546 $seconds = intval($matches[3]); 547 $offset = $sign . ($hours*60 + $seconds); 548 return intval($offset); 549 } 550 551 trigger_error('Strange time format in olson_parse_offset() ' .$offset); 552 return 0; 553 554 } 555 556 557 /*** 558 *** olson_parse_on_($on) 559 *** 560 *** see `man zic`. This function translates the following formats 561 *** 5 the fifth of the month 562 *** lastSun the last Sunday in the month 563 *** lastMon the last Monday in the month 564 *** Sun>=8 first Sunday on or after the eighth 565 *** Sun<=25 last Sunday on or before the 25th 566 *** 567 *** to a moodle friendly format. Returns an array with: 568 *** 569 *** startday: the day of the month that we start counting from. 570 *** if negative, it means we start from that day and 571 *** count backwards. since -1 would be meaningless, 572 *** it means "end of month and backwards". 573 *** weekday: the day of the week that we must find. we will 574 *** scan days from the startday until we find the 575 *** first such weekday. 0...6 = Sun...Sat. 576 *** -1 means that any day of the week will do, 577 *** effectively ending the search on startday. 578 *** skipweeks:after finding our end day as outlined above, 579 *** skip this many weeks. this enables us to find 580 *** "the second sunday >= 10". usually will be 0. 581 */ 582 function olson_parse_on ($on) { 583 584 $rule = array(); 585 $days = array('sun' => 0, 'mon' => 1, 586 'tue' => 2, 'wed' => 3, 587 'thu' => 4, 'fri' => 5, 588 'sat' => 6); 589 590 if(is_numeric($on)) { 591 $rule['startday'] = intval($on); // Start searching from that day 592 $rule['weekday'] = -1; // ...and stop there, no matter what weekday 593 $rule['skipweeks'] = 0; // Don't skip any weeks. 594 } 595 else { 596 $on = strtolower($on); 597 if(substr($on, 0, 4) == 'last') { 598 // e.g. lastSun 599 if(!isset($days[substr($on, 4)])) { 600 trigger_error('Unknown last weekday: '.substr($on, 4)); 601 } 602 else { 603 $rule['startday'] = -1; // Start from end of month 604 $rule['weekday'] = $days[substr($on, 4)]; // Find the first such weekday 605 $rule['skipweeks'] = 0; // Don't skip any weeks. 606 } 607 } 608 else if(substr($on, 3, 2) == '>=') { 609 // e.g. Sun>=8 610 if(!isset($days[substr($on, 0, 3)])) { 611 trigger_error('Unknown >= weekday: '.substr($on, 0, 3)); 612 } 613 else { 614 $rule['startday'] = intval(substr($on, 5)); // Start from that day of the month 615 $rule['weekday'] = $days[substr($on, 0, 3)]; // Find the first such weekday 616 $rule['skipweeks'] = 0; // Don't skip any weeks. 617 } 618 } 619 else if(substr($on, 3, 2) == '<=') { 620 // e.g. Sun<=25 621 if(!isset($days[substr($on, 0, 3)])) { 622 trigger_error('Unknown <= weekday: '.substr($on, 0, 3)); 623 } 624 else { 625 $rule['startday'] = -intval(substr($on, 5)); // Start from that day of the month; COUNT BACKWARDS (minus sign) 626 $rule['weekday'] = $days[substr($on, 0, 3)]; // Find the first such weekday 627 $rule['skipweeks'] = 0; // Don't skip any weeks. 628 } 629 } 630 else { 631 trigger_error('unknown on '.$on); 632 } 633 } 634 return $rule; 635 } 636 637 638 /*** 639 *** olson_parse_at($at, $set, $gmtoffset) 640 *** 641 *** see `man zic`. This function translates 642 *** 643 *** 2 time in hours 644 *** 2:00 time in hours and minutes 645 *** 15:00 24-hour format time (for times after noon) 646 *** 1:28:14 time in hours, minutes, and seconds 647 *** 648 *** Any of these forms may be followed by the letter w if the given 649 *** time is local "wall clock" time, s if the given time is local 650 *** "standard" time, or u (or g or z) if the given time is univer- 651 *** sal time; in the absence of an indicator, wall clock time is 652 *** assumed. 653 *** 654 *** returns a moodle friendly $at, in GMT, which is what Moodle wants 655 *** 656 *** 657 */ 658 function olson_parse_at ($at, $set = 'set', $gmtoffset) { 659 660 // find the time "signature"; 661 $sig = ''; 662 if (preg_match('/[ugzs]$/', $at, $matches)) { 663 $sig = $matches[0]; 664 $at = substr($at, 0, strlen($at)-1); // chop 665 } 666 667 list($hours, $mins) = explode(':', $at); 668 669 // GMT -- return as is! 670 if ( !empty($sig) && ( $sig === 'u' 671 || $sig === 'g' 672 || $sig === 'z' )) { 673 return $at; 674 } 675 676 // Wall clock 677 if (empty($sig) || $sig === 'w') { 678 if ($set !== 'set'){ // wall clock is on DST, assume by 1hr 679 $hours = $hours-1; 680 } 681 $sig = 's'; 682 } 683 684 // Standard time 685 if (!empty($sig) && $sig === 's') { 686 $mins = $mins + $hours*60 + $gmtoffset; 687 $hours = $mins / 60; 688 $hours = (int)$hours; 689 $mins = abs($mins % 60); 690 return sprintf('%02d:%02d', $hours, $mins); 691 } 692 693 trigger_error('unhandled case - AT flag is ' . $matches[0]); 694 } 695 696 697 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Jan 14 11:33:29 2009 | Cross-referenced by PHPXref 0.7 |