Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / lib / olson.php
blobac8349236337001c6db042fef579f8f872f4809d
1 <?php //$Id$
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 ***
12 function olson_to_timezones ($filename) {
14 $zones = olson_simple_zone_parser($filename);
15 $rules = olson_simple_rule_parser($filename);
17 $mdl_zones = array();
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;
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 **/
43 // clean the slate for a new zone
44 $zone = NULL;
45 $rule = NULL;
48 // Find the pre 1970 zone rule entries
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
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
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();
73 } else {
74 // no DST this year!
75 $rule = array();
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);
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;
95 // Fix the from year to 1970
96 $mdl_tz['year'] = 1970;
98 // add to the array
99 $mdl_zones[] = $mdl_tz;
100 //print_object("Zero entry for $zone[name] added");
102 $lasttimezone = $mdl_tz;
105 /// 1971 onwards
106 ///
107 for ($y = 1971; $y < $maxyear ; $y++) {
108 $changed = false;
110 /// We create a "zonerule" entry if either zone or rule change...
112 /// force $y to string to avoid PHP
113 /// thinking of a positional array
115 if (array_key_exists((string)$y, $zbyyear)) { // we have a zone entry for the year
116 $changed = true;
117 $zone = $zbyyear[(string)$y];
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];
124 } else {
125 $rule = array();
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);
136 // VERY IMPORTANT!!
137 $mdl_tz['year'] = $y;
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;
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('---');
153 // This is the simplest way to make the != operator just below NOT take the year into account
154 $lasttimezone['year'] = $mdl_tz['year'];
156 // If not a duplicate, add and update $lasttimezone
157 if($lasttimezone != $mdl_tz) {
158 $mdl_zones[] = $lasttimezone = $mdl_tz;
166 if (function_exists('memory_get_usage')) {
167 trigger_error("We are consuming this much memory: " . get_memory_usage());
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).
177 foreach($mdl_zones as $key=>$mdl_zone) {
178 $mdl_zones[$key]['tzrule'] = $mdl_zones[$key]['rule'];
181 return $mdl_zones;
185 /***
186 *** olson_simple_rule_parser($filename)
188 *** Parses the olson files for DST rules.
189 *** It's a simple implementation that simplifies some fields
191 *** Returns a multidimensional array, or false on error
194 function olson_simple_rule_parser ($filename) {
196 $file = fopen($filename, 'r', 0);
198 if (empty($file)) {
199 return false;
202 $rules = array();
203 while ($line = fgets($file)) {
204 // only pay attention to rules lines
205 if(!preg_match('/^Rule\s/', $line)){
206 continue;
208 $line = preg_replace('/\n$/', '',$line); // chomp
209 $rule = preg_split('/\s+/', $line);
210 list($discard,
211 $name,
212 $from,
213 $to,
214 $type,
215 $in,
216 $on,
217 $at,
218 $save,
219 $letter) = $rule;
221 $srs = ($save === '0') ? 'reset' : 'set';
223 if($to == 'only') {
224 $to = $from;
226 else if($to == 'max') {
227 $to = date('Y');
230 for($i = $from; $i <= $to; ++$i) {
231 $rules[$name][$i][$srs] = $rule;
236 fclose($file);
238 $months = array('jan' => 1, 'feb' => 2,
239 'mar' => 3, 'apr' => 4,
240 'may' => 5, 'jun' => 6,
241 'jul' => 7, 'aug' => 8,
242 'sep' => 9, 'oct' => 10,
243 'nov' => 11, 'dec' => 12);
246 // now reformat it a bit to match Moodle's DST table
247 $moodle_rules = array();
248 foreach ($rules as $rule => $rulesbyyear) {
249 foreach ($rulesbyyear as $year => $rulesthisyear) {
251 if(!isset($rulesthisyear['reset'])) {
252 // No "reset" rule. We will assume that this is somewhere in the southern hemisphere
253 // after a period of not using DST, otherwise it doesn't make sense at all.
254 // With that assumption, we can put in a fake reset e.g. on Jan 1, 12:00.
256 print_object("no reset");
257 print_object($rules);
258 die();
260 $rulesthisyear['reset'] = array(
261 NULL, NULL, NULL, NULL, NULL, 'jan', 1, '12:00', '00:00', NULL
265 if(!isset($rulesthisyear['set'])) {
266 // No "set" rule. We will assume that this is somewhere in the southern hemisphere
267 // and that it begins a period of not using DST, otherwise it doesn't make sense at all.
268 // With that assumption, we can put in a fake set on Dec 31, 12:00, shifting time by 0 minutes.
269 $rulesthisyear['set'] = array(
270 NULL, $rulesthisyear['reset'][1], NULL, NULL, NULL, 'dec', 31, '12:00', '00:00', NULL
274 list($discard,
275 $name,
276 $from,
277 $to,
278 $type,
279 $in,
280 $on,
281 $at,
282 $save,
283 $letter) = $rulesthisyear['set'];
285 $moodle_rule = array();
287 // $save is sometimes just minutes
288 // and othertimes HH:MM -- only
289 // parse if relevant
290 if (!preg_match('/^\d+$/', $save)) {
291 list($hours, $mins) = explode(':', $save);
292 $save = $hours * 60 + $mins;
295 // we'll parse $at later
296 // $at = olson_parse_at($at);
297 $in = strtolower($in);
298 if(!isset($months[$in])) {
299 trigger_error('Unknown month: '.$in);
302 $moodle_rule['name'] = $name;
303 $moodle_rule['year'] = $year;
304 $moodle_rule['dstoff'] = $save; // time offset
306 $moodle_rule['dst_month'] = $months[$in]; // the month
307 $moodle_rule['dst_time'] = $at; // the time
309 // Encode index and day as per Moodle's specs
310 $on = olson_parse_on($on);
311 $moodle_rule['dst_startday'] = $on['startday'];
312 $moodle_rule['dst_weekday'] = $on['weekday'];
313 $moodle_rule['dst_skipweeks'] = $on['skipweeks'];
315 // and now the "deactivate" data
316 list($discard,
317 $name,
318 $from,
319 $to,
320 $type,
321 $in,
322 $on,
323 $at,
324 $save,
325 $letter) = $rulesthisyear['reset'];
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);
334 $moodle_rule['std_month'] = $months[$in]; // the month
335 $moodle_rule['std_time'] = $at; // the time
337 // Encode index and day as per Moodle's specs
338 $on = olson_parse_on($on);
339 $moodle_rule['std_startday'] = $on['startday'];
340 $moodle_rule['std_weekday'] = $on['weekday'];
341 $moodle_rule['std_skipweeks'] = $on['skipweeks'];
343 $moodle_rules[$moodle_rule['name']][$moodle_rule['year']] = $moodle_rule;
344 //print_object($moodle_rule);
346 } // end foreach year within a rule
348 // completed with all the entries for this rule
349 // if the last entry has a TO other than 'max'
350 // then we have to deal with closing the last rule
351 //trigger_error("Rule $name ending to $to");
352 if (!empty($to) && $to !== 'max') {
353 // We can handle two cases for TO:
354 // a year, or "only"
355 $reset_rule = $moodle_rule;
356 $reset_rule['dstoff'] = '00';
357 if (preg_match('/^\d+$/', $to)){
358 $reset_rule['year'] = $to;
359 $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule;
360 } elseif ($to === 'only') {
361 $reset_rule['year'] = $reset_rule['year'] + 1;
362 $moodle_rules[$reset_rule['name']][$reset_rule['year']] = $reset_rule;
363 } else {
364 trigger_error("Strange value in TO $to rule field for rule $name");
367 } // end if $to is interesting
369 } // end foreach rule
371 return $moodle_rules;
374 /***
375 *** olson_simple_zone_parser($filename)
377 *** Parses the olson files for zone info
379 *** Returns a multidimensional array, or false on error
382 function olson_simple_zone_parser ($filename) {
384 $file = fopen($filename, 'r', 0);
386 if (empty($file)) {
387 return false;
390 $zones = array();
391 $lastzone = NULL;
393 while ($line = fgets($file)) {
394 // skip obvious non-zone lines
395 if (preg_match('/^#/', $line)) {
396 continue;
398 if (preg_match('/^(?:Rule|Link|Leap)/',$line)) {
399 $lastzone = NULL; // reset lastzone
400 continue;
403 // If there are blanks in the start of the line but the first non-ws character is a #,
404 // assume it's an "inline comment". The funny thing is that this happens only during
405 // the definition of the Rule for Europe/Athens.
406 if(substr(trim($line), 0, 1) == '#') {
407 continue;
410 /*** Notes
412 *** By splitting on space, we are only keeping the
413 *** year of the UNTIL field -- that's on purpose.
415 *** The Zone lines are followed by continuation lines
416 *** were we reuse the info from the last one seen.
418 *** We are transforming "until" fields into "from" fields
419 *** which make more sense from the Moodle perspective, so
420 *** each initial Zone entry is "from" the year 0, and for the
421 *** continuation lines, we shift the "until" from the previous field
422 *** into this line's "from".
424 *** If a RULES field contains a time instead of a rule we discard it
425 *** I have no idea of how to create a DST rule out of that
426 *** (what are the start/end times?)
428 *** We remove "until" from the data we keep, but preserve
429 *** it in $lastzone.
431 if (preg_match('/^Zone/', $line)) { // a new zone
432 $line = trim($line);
433 $line = preg_split('/\s+/', $line);
434 $zone = array();
435 list( $discard, // 'Zone'
436 $zone['name'],
437 $zone['gmtoff'],
438 $zone['rule'],
439 $discard // format
440 ) = $line;
441 // the things we do to avoid warnings
442 if (!empty($line[5])) {
443 $zone['until'] = $line[5];
445 $zone['year'] = '0';
447 $zones[$zone['name']] = array();
449 } else if (!empty($lastzone) && preg_match('/^\s+/', $line)){
450 // looks like a credible continuation line
451 $line = trim($line);
452 $line = preg_split('/\s+/', $line);
453 if (count($line) < 3) {
454 $lastzone = NULL;
455 continue;
457 // retrieve info from the lastzone
458 $zone = $lastzone;
459 $zone['year'] = $zone['until'];
460 // overwrite with current data
461 list(
462 $zone['gmtoff'],
463 $zone['rule'],
464 $discard // format
465 ) = $line;
466 // the things we do to avoid warnings
467 if (!empty($line[3])) {
468 $zone['until'] = $line[3];
471 } else {
472 $lastzone = NULL;
473 continue;
476 // tidy up, we're done
477 // perhaps we should insert in the DB at this stage?
478 $lastzone = $zone;
479 unset($zone['until']);
480 $zone['gmtoff'] = olson_parse_offset($zone['gmtoff']);
481 if ($zone['rule'] === '-') { // cleanup empty rules
482 $zone['rule'] = '';
484 if (preg_match('/:/',$zone['rule'])) {
485 // we are not handling direct SAVE rules here
486 // discard it
487 $zone['rule'] = '';
490 $zones[$zone['name']][(string)$zone['year']] = $zone;
493 return $zones;
496 /***
497 *** olson_parse_offset($offset)
499 *** parses time offsets from the GMTOFF and SAVE
500 *** fields into +/-MINUTES
502 function olson_parse_offset ($offset) {
503 $offset = trim($offset);
505 // perhaps it's just minutes
506 if (preg_match('/^(-?)(\d*)$/', $offset)) {
507 return intval($offset);
509 // (-)hours:minutes(:seconds)
510 if (preg_match('/^(-?)(\d*):(\d+)/', $offset, $matches)) {
511 // we are happy to discard the seconds
512 $sign = $matches[1];
513 $hours = intval($matches[2]);
514 $seconds = intval($matches[3]);
515 $offset = $sign . ($hours*60 + $seconds);
516 return intval($offset);
519 trigger_error('Strange time format in olson_parse_offset() ' .$offset);
520 return 0;
525 /***
526 *** olson_parse_on_($on)
528 *** see `man zic`. This function translates the following formats
529 *** 5 the fifth of the month
530 *** lastSun the last Sunday in the month
531 *** lastMon the last Monday in the month
532 *** Sun>=8 first Sunday on or after the eighth
533 *** Sun<=25 last Sunday on or before the 25th
535 *** to a moodle friendly format. Returns an array with:
537 *** startday: the day of the month that we start counting from.
538 *** if negative, it means we start from that day and
539 *** count backwards. since -1 would be meaningless,
540 *** it means "end of month and backwards".
541 *** weekday: the day of the week that we must find. we will
542 *** scan days from the startday until we find the
543 *** first such weekday. 0...6 = Sun...Sat.
544 *** -1 means that any day of the week will do,
545 *** effectively ending the search on startday.
546 *** skipweeks:after finding our end day as outlined above,
547 *** skip this many weeks. this enables us to find
548 *** "the second sunday >= 10". usually will be 0.
550 function olson_parse_on ($on) {
552 $rule = array();
553 $days = array('sun' => 0, 'mon' => 1,
554 'tue' => 2, 'wed' => 3,
555 'thu' => 4, 'fri' => 5,
556 'sat' => 6);
558 if(is_numeric($on)) {
559 $rule['startday'] = intval($on); // Start searching from that day
560 $rule['weekday'] = -1; // ...and stop there, no matter what weekday
561 $rule['skipweeks'] = 0; // Don't skip any weeks.
563 else {
564 $on = strtolower($on);
565 if(substr($on, 0, 4) == 'last') {
566 // e.g. lastSun
567 if(!isset($days[substr($on, 4)])) {
568 trigger_error('Unknown last weekday: '.substr($on, 4));
570 else {
571 $rule['startday'] = -1; // Start from end of month
572 $rule['weekday'] = $days[substr($on, 4)]; // Find the first such weekday
573 $rule['skipweeks'] = 0; // Don't skip any weeks.
576 else if(substr($on, 3, 2) == '>=') {
577 // e.g. Sun>=8
578 if(!isset($days[substr($on, 0, 3)])) {
579 trigger_error('Unknown >= weekday: '.substr($on, 0, 3));
581 else {
582 $rule['startday'] = intval(substr($on, 5)); // Start from that day of the month
583 $rule['weekday'] = $days[substr($on, 0, 3)]; // Find the first such weekday
584 $rule['skipweeks'] = 0; // Don't skip any weeks.
587 else if(substr($on, 3, 2) == '<=') {
588 // e.g. Sun<=25
589 if(!isset($days[substr($on, 0, 3)])) {
590 trigger_error('Unknown <= weekday: '.substr($on, 0, 3));
592 else {
593 $rule['startday'] = -intval(substr($on, 5)); // Start from that day of the month; COUNT BACKWARDS (minus sign)
594 $rule['weekday'] = $days[substr($on, 0, 3)]; // Find the first such weekday
595 $rule['skipweeks'] = 0; // Don't skip any weeks.
598 else {
599 trigger_error('unknown on '.$on);
602 return $rule;
606 /***
607 *** olson_parse_at($at, $set, $gmtoffset)
609 *** see `man zic`. This function translates
611 *** 2 time in hours
612 *** 2:00 time in hours and minutes
613 *** 15:00 24-hour format time (for times after noon)
614 *** 1:28:14 time in hours, minutes, and seconds
616 *** Any of these forms may be followed by the letter w if the given
617 *** time is local "wall clock" time, s if the given time is local
618 *** "standard" time, or u (or g or z) if the given time is univer-
619 *** sal time; in the absence of an indicator, wall clock time is
620 *** assumed.
622 *** returns a moodle friendly $at, in GMT, which is what Moodle wants
624 ***
626 function olson_parse_at ($at, $set = 'set', $gmtoffset) {
628 // find the time "signature";
629 $sig = '';
630 if (preg_match('/[ugzs]$/', $at, $matches)) {
631 $sig = $matches[0];
632 $at = substr($at, 0, strlen($at)-1); // chop
635 list($hours, $mins) = explode(':', $at);
637 // GMT -- return as is!
638 if ( !empty($sig) && ( $sig === 'u'
639 || $sig === 'g'
640 || $sig === 'z' )) {
641 return $at;
644 // Wall clock
645 if (empty($sig) || $sig === 'w') {
646 if ($set !== 'set'){ // wall clock is on DST, assume by 1hr
647 $hours = $hours-1;
649 $sig = 's';
652 // Standard time
653 if (!empty($sig) && $sig === 's') {
654 $mins = $mins + $hours*60 + $gmtoffset;
655 $hours = $mins / 60;
656 $hours = (int)$hours;
657 $mins = abs($mins % 60);
658 return sprintf('%02d:%02d', $hours, $mins);
661 trigger_error('unhandled case - AT flag is ' . $matches[0]);