From 830a2bbd82fa208e12667c6ffb3debc8fa416f54 Mon Sep 17 00:00:00 2001 From: defacer Date: Sat, 26 Mar 2005 18:43:58 +0000 Subject: [PATCH] Yay! Major DST support progress! 1. calculate_user_dst_table() works correctly and offers many features: Can create or extend on-the-fly a table of pre-computed timestamps on which DST changes happen, keeps track of these changes and the current computed year range in $USER->dstoffsets and $USER->dstrange. Initially the computation is done in a current year +/- 3 years range, to keep the amount of serialized data small and to make traversals of $USER->dstoffsets faster. 2. dst_offset_on() works correctly, and can dynamically instruct the above function to extend the pre-calculated region on demand (i.e., when it's called for a timestamp which falls outside said region). 3. Some other improvements. I have made a few preliminary tests on my dev machine and It Works(tm!) :D --- lib/moodlelib.php | 167 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 128 insertions(+), 39 deletions(-) diff --git a/lib/moodlelib.php b/lib/moodlelib.php index fc508d99ae7..6caf2289ec1 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -861,54 +861,118 @@ function get_user_timezone($tz = 99) { return $tz; } -function calculate_user_dst_table() { +function calculate_user_dst_table($from_year = NULL, $to_year = NULL) { global $CFG, $USER; - static $presetname = NULL; - if (!empty($presetname)) { - return true; + if (empty($USER)) { + return false; } if (empty($CFG->forcetimezone)) { if (empty($USER->timezonename)) { return false; } - $presetname = $USER->timezonename; + $timezonename = $USER->timezonename; } else { - $presetname = $CFG->forcetimezone; + $timezonename = $CFG->forcetimezone; } - $presetrecords = get_records('timezone', 'name', $presetname, 'year ASC'); + if (!empty($USER->dstoffsets) && empty($from_year) && empty($to_year)) { + // Repeat calls which do not request specific year ranges stop here, we have already calculated the table + // This will be the return path most of the time, pretty light computationally + return true; + } + + // Reaching here means we either need to extend our table or create it from scratch + if(empty($USER->dstoffsets)) { + // If we 're creating from scratch, put the two guard elements in there + $USER->dstoffsets = array(1 => NULL, 0 => NULL); + } + if(empty($USER->dstrange)) { + // If creating from scratch + $from = max((empty($from_year) ? intval(date('Y')) - 3 : $from_year), 1971); + $to = min((empty($to_year) ? intval(date('Y')) + 3 : $to_year), 2035); + + // Fill in the array with the extra years we need to process + $yearstoprocess = array(); + for($i = $from; $i <= $to; ++$i) { + $yearstoprocess[] = $i; + } + + // Take note of which years we have processed for future calls + $USER->dstrange = array($from, $to); + } + else { + // If needing to extend the table, do the same + $yearstoprocess = array(); + + $from = max((empty($from_year) ? $USER->dstrange[0] : $from_year), 1971); + $to = min((empty($to_year) ? $USER->dstrange[1] : $to_year), 2035); + + if($from < $USER->dstrange[0]) { + // Take note of which years we need to process and then note that we have processed them for future calls + for($i = $from; $i < $USER->dstrange[0]; ++$i) { + $yearstoprocess[] = $i; + } + $USER->dstrange[0] = $from; + } + if($to > $USER->dstrange[1]) { + // Take note of which years we need to process and then note that we have processed them for future calls + for($i = $USER->dstrange[1] + 1; $i <= $to; ++$i) { + $yearstoprocess[] = $i; + } + $USER->dstrange[1] = $to; + } + } + + if(empty($yearstoprocess)) { + // This means that there was a call requesting a SMALLER range than we have already calculated + return true; + } + + // From now on, we know that the array has at least the two guard elements, and $yearstoprocess has the years we need + // Also, the array is sorted in descending timestamp order! + + // Get DB data + $presetrecords = get_records('timezone', 'name', $timezonename, 'year DESC', 'year, gmtoff, dstoff, dst_month, dst_startday, dst_weekday, dst_skipweeks, dst_time, std_month, std_startday, std_weekday, std_skipweeks, std_time'); if(empty($presetrecords)) { return false; } - if(!empty($USER)) { - if(empty($USER->dstoffsets)) { - $USER->dstoffsets = array(0 => NULL); - foreach($presetrecords as $presetrecord) { - $changes = dst_changes_for_year($presetrecord->year, $presetrecord); - - if($changes === NULL) { - continue; - } - if($changes['dst'] != 0) { - $USER->dstoffsets[$changes['dst']] = $presetrecord->dstoff * MINSECS; - } - if($changes['std'] != 0) { - $USER->dstoffsets[$changes['std']] = 0; - } + // Remove ending guard (first element of the array) + reset($USER->dstoffsets); + unset($USER->dstoffsets[key($USER->dstoffsets)]); + + // Add all required change timestamps + foreach($yearstoprocess as $y) { + // Find the record which is in effect for the year $y + foreach($presetrecords as $year => $preset) { + if($year <= $y) { + break; } - $calcuntil = make_timestamp(2031, 1, 1, 00, 00, 00, get_user_timezone(99), false); - $USER->dstoffsets[$calcuntil] = NULL; - krsort($USER->dstoffsets); + } + + $changes = dst_changes_for_year($y, $preset); + + if($changes === NULL) { + continue; + } + if($changes['dst'] != 0) { + $USER->dstoffsets[$changes['dst']] = $preset->dstoff * MINSECS; + } + if($changes['std'] != 0) { + $USER->dstoffsets[$changes['std']] = 0; } } - //print_object($presetrecords); - //print_object($USER->dstoffsets); - + // Put in a guard element at the top + $maxtimestamp = max(array_keys($USER->dstoffsets)); + $USER->dstoffsets[($maxtimestamp + DAYSECS)] = NULL; // DAYSECS is arbitrary, any "small" number will do + + // Sort again + krsort($USER->dstoffsets); + return true; } @@ -925,14 +989,23 @@ function dst_changes_for_year($year, $timezone) { list($std_hour, $std_min) = explode(':', $timezone->std_time); $tz = get_user_timezone(99); - $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, $dst_hour, $dst_min, 0, $tz, false); - $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, $std_hour, $std_min, 0, $tz, false); + $timedst = make_timestamp($year, $timezone->dst_month, $monthdaydst, 0, 0, 0, $tz, false); + $timestd = make_timestamp($year, $timezone->std_month, $monthdaystd, 0, 0, 0, $tz, false); + + // Instead of putting hour and minute in make_timestamp(), we add them afterwards. + // This has the advantage of being able to have negative values for hour, i.e. for timezones + // where GMT time would be in the PREVIOUS day than the local one on which DST changes. + + $timedst += $dst_hour * HOURSECS + $dst_min * MINSECS; + $timestd += $std_hour * HOURSECS + $std_min * MINSECS; return array('dst' => $timedst, 0 => $timedst, 'std' => $timestd, 1 => $timestd); } // $time must NOT be compensated at all, it has to be a pure timestamp function dst_offset_on($time) { + global $USER; + if(!calculate_user_dst_table()) { return 0; } @@ -941,22 +1014,38 @@ function dst_offset_on($time) { return 0; } - $finaloffset = NULL; - - foreach($USER->dstoffsets as $from => $offset) { + reset($USER->dstoffsets); + while(list($from, $offset) = each($USER->dstoffsets)) { if($from <= $time) { - $finaloffset = $offset; break; } } - if($finaloffset === NULL) { - // This means we haven't calculated far enough ahead, do it now? - print_object($USER->dstoffsets); - error('Error in calculating DST offset for timestamp '.$time); + // This is the normal return path + if($offset !== NULL) { + return $offset; } - return $finaloffset; + // Reaching this point means we haven't calculated far enough, do it now: + // Calculate extra DST changes if needed and recurse. The recursion always + // moves toward the stopping condition, so will always end. + + if($from == 0) { + // We need a year smaller than $USER->dstrange[0] + if($USER->dstrange[0] == 1971) { + return 0; + } + calculate_user_dst_table($USER->dstrange[0] - 5, NULL); + return dst_offset_on($time); + } + else { + // We need a year larger than $USER->dstrange[1] + if($USER->dstrange[1] == 2035) { + return 0; + } + calculate_user_dst_table(NULL, $USER->dstrange[1] + 5); + return dst_offset_on($time); + } } function find_day_in_month($startday, $weekday, $month, $year) {