Extension:TimeZoneInfo/1.0

From MediaWiki.org
Jump to navigation Jump to search
<?php

/*
 * Timezone info extension.
 *
 * This extension displays several types of useful (?) information relating
 * to timezones.  It is expected to be of some use in Wikis which serve a
 * distributed community of users, where people want to know what time it
 * is for other people.
 *
 * Install in the usual way: put this file (TimeZoneInfo.php) in your
 * extensions directory, and add this to LocalSettings.php:
 *
 *   require_once( "extensions/TimeZoneInfo.php" );
 *
 * This extension adds three tags:
 *
 *   <tzchart class=wikitable>
 *   zonename
 *   zonename
 *   ...
 *   </tzchart>
 *
 * displays a chart of time conversions for the listed zones.
 *
 *   <tztrans class=wikitable>
 *   zonename
 *   zonename
 *   ...
 *   </tztrans>
 *
 * displays a table of daylight <--> standard time transitions for the
 * listed time zones.
 *
 *   <tzlist filter="foo" />
 *
 * displays a table of all timezones (or zones matching the filter).
 *
 * For more info on each tag, see the hook function comments below.
 */

$wgExtensionFunctions[] = 'wfTimeZoneInfo';
$wgExtensionCredits['parserhook'][] = array(
  'name'=>'TimeZoneInfo',
  'author'=>'Johan the Ghost',
  'url'=>'http://www.mediawiki.org/wiki/Extension:TimeZoneInfo',
  'description'=>'display a variety of timezone information',
);


/*
 * Install TimeZoneInfo extension.
 */
function wfTimeZoneInfo() {
	new TimeZoneInfo();
}


class TimeZoneInfo {

	/*
	 * Set up the colours used by the timezone chart.  These are the colours
	 * used to represent local nighttime, twilight (morning/evening),
	 * and daytime.
	 */
	var $colours = array(
		"#c0c0c0", "#d0d0ff", "#c0ffff"
	);

	/*
	 * Set up the time bands used by the timezone chart.  Each hour of
	 * local time is set to 0 to make it show as night, 1 for twilight,
	 * 2 for daytime.
	 */
	var $bands = array(
	    0, 0, 0, 0, 0, 0, 0, 0,				// 0-7: night
	    1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1,	// 8-18: twi, day, twi
	    0, 0, 0, 0, 0						// 19-23: night
	);


    ////////////////////////////////////////////////////////////////////////
    // Constructor
    ////////////////////////////////////////////////////////////////////////

    /*
     * Set up the TimeZoneInfo extension.
     */
	function TimeZoneInfo() {
        global $wgParser;

        // Install parser hooks for the new tags we're creating.
        $wgParser->setHook( 'tzchart', array($this, 'hookTimeZoneChart') );
        $wgParser->setHook( 'tztrans', array($this, 'hookTimeZoneTrans') );
        $wgParser->setHook( 'tzlist',  array($this, 'hookTimeZoneList') );
	}


    ////////////////////////////////////////////////////////////////////////
    // The <tzchart> Tag
    ////////////////////////////////////////////////////////////////////////

    /*
     * Parser hook for the "<tzchart> ... </tzchart>" tag.  This tag
     * creates a conversion chart showing the relationships between a
     * number of specified timezones.  Timezones with fractional hour
     * offsets (eg. India) are handled quite well.
     *
     * The tag takes the following parameters:
     *     align=xxx		passed to the generated table
     *     start=n			start the chart at the given UTC hour;
     *                      default 0 (midnight UTC).
     *
     * The tag text consists of the timezone list, one name per line.
     * Each name may be followed by a user-friendly label.
     *
     * Example:
     *     <tzchart align=right start=2>
     *     America/Los_Angeles
     *     Europe/Paris
     *     Asia/Calcutta
     *     </tzchart>
     */
    function hookTimeZoneChart($tagText, $argv, &$parser) {
    	try {
    		$utc = new DateTimeZone('UTC');
    	} catch (Exception $e) {
    		return "Timezone UTC unknown???";
    	}

    	// Get the timezone definitions based on the tag text.
    	$zones = $this->getZones($tagText);

    	// See if we have mixed zones; ie. some zones with whole-hour
    	// offsets, and some zones with part-hour offsets.
    	$mixed = false;
        foreach ($zones as $zone)
    		if ($zone['split'] != $zones[0]['split'])
        		$mixed = true;

    	// See what the start and end times are.
    	if (isset($argv['start']))
			$start = $argv['start'];
		else
			$start = 0;
		$end = $start + 24;

    	$tparms = '';
    	if (@$argv['align'])
			$tparms .= "align=" . $argv['align'];
        $wikiText = "{| class=wikitable $tparms\n";

        // Do the table header.
        $wikiText .= "|-\n";
        foreach ($zones as $zone) {
        	$wikiText .= "!" . $zone['abbr'] . "\n";
        }

        // Now do the chart.
        $time = new DateTime('now', $utc);
        for ($h = $start; $h < $end; ++$h)
        	for ($m = 0; $m < 60; $m += $mixed ? 30 : 60)
    	    	$wikiText .= $this->doChartRow($zones, $mixed, $h, $m, $h == $start, $time);

        // For a mixed table, do the final half-cells.  Ordering doesn't
        // matter; these will fill in the odd gaps.
	    if ($mixed) {
        	$wikiText .= "|- style=\"height:7px\"\n";
        	foreach ($zones as $zone)
    			if (!$zone['split'])
        			$wikiText .= "|\n";
    	}

        $wikiText .= "|}\n";

        // Done.
    	$localParser = new Parser();
    	$output = $localParser->parse($wikiText, $parser->mTitle, $parser->mOptions, false);
    	return $output->getText();
    }


    function doChartRow($zones, $mixed, $h, $m, $first, &$time) {
	    /*
	     * It would be great not to have to do non-mixed tables differently.
	     * However, this fails because an empty row is ignored; ie. in this:
	     *    |-
	     *    |-
	     *    | foo...
	     * the first row (|-) is ignored.  So instead, for non-mixed tables,
	     * we drop the rowspan.
	     */

    	$wikiText = '';
		$span = $mixed ? "rowspan=2" : "";
		$style = "style=\"padding-top: 0; padding-bottom:0;\"";

        $wikiText .= "|- style=\"height:7px\"\n";
        foreach ($zones as $zone) {
	        if ($mixed) {
    			if ($first && $m == 0 && $zone['split']) {
	    			// Do an initial half-cell for a split zone.  Ordering
	    			// matters.
        			$wikiText .= "|\n";
    				continue;
    			} else if (($zone['split'] == 0) != ($m == 0))
    				continue;
			}
    		$time->setTime($h % 24, $m, 0);
    		$time->modify("+" . $zone['off'] . " seconds");
    		$band = $this->bands[$time->format('G')];
    		$col = $this->colours[$band];
        	$wikiText .= "| $span bgcolor=\"$col\" $style | " . $time->format('H:i') . "\n";
        }

        return $wikiText;
    }


    ////////////////////////////////////////////////////////////////////////
    // The <tztrans> Tag
    ////////////////////////////////////////////////////////////////////////

    /*
     * Parser hook for the "<tztrans> ... </tztrans>" tag.  This tag formats
     * a list of timezone standard<->daylight time transitions as a Wiki table.
     *
     * The tag takes the following parameters:
     *     align=xxx		passed to the generated table
     *
     * The tag text consists of the timezone list, one name per line.
     * Each name may be followed by a user-friendly label.
     *
     * Example:
     *     <tztrans align=right>
     *     America/Los_Angeles LA
     *     Europe/Paris
     *     </tztrans>
     */
    function hookTimeZoneTrans($tagText, $argv, &$parser) {
    	// Get the timezone definitions based on the tag text.
    	$zones = $this->getZones($tagText);
	    $now = time();

    	$tparms = '';
    	if (@$argv['align'])
			$tparms .= "align=" . $argv['align'];
        $wikiText = "{| class=wikitable $tparms\n";

        $wikiText .= "|\n";
        foreach ($zones as $zone)
    		$wikiText .= $this->formatTransitions($zone, $now);

        $wikiText .= "|}\n";

        $output = $parser->parse($wikiText, $parser->mTitle, $parser->mOptions, true, false );
        return $output->getText();
	}


    function formatTransitions(&$zone, $now) {
	    $name = $zone['name'];
	    $abbr = $zone['abbr'];
	    $wikiText = '';

	    $transitions = $this->getTransitions($zone, $now, $now + 3600 * 24 * 366, 2);
	 	$wikiText .= ";$name ($abbr)\n";

		if (count($transitions) == 0)
			$wikiText .= ":No change\n";
		else {
			foreach ($transitions as $tinfo)
   	    		$wikiText .= ":" . $tinfo['date'] . ": " . $tinfo['text'] . "\n";
		}

		return $wikiText;
    }


    ////////////////////////////////////////////////////////////////////////
    // The <tzlist> Tag
    ////////////////////////////////////////////////////////////////////////

    /*
     * Parser hook for the "<tzlist />" tag.  This tag produces a table
     * of known timezones.
     *
     * The tag takes the following parameters:
     *     filter=xxx		if supplied, only zones whose name contains
     *                      the given string are shown.
     *
     * The tag text is ignored.
     *
     * Example:
     *     <tzlist filter="Atlantic/" />
     */
    function hookTimeZoneList($tagText, $argv, &$parser) {
	    $wikiText = '';
	    $now = time();

        $wikiText .= "{| class=wikitable\n";

	    $wikiText .= "! Zone\n";
	    $wikiText .= "! Current offset\n";
	    $wikiText .= "! Next change\n";
	    $wikiText .= "! Second change\n";

	    $zoneIds = DateTimeZone::listIdentifiers();
		foreach ($zoneIds as $zone) {
			if (@$argv['filter'] && strpos($zone, $argv['filter']) === false)
				continue;

			$zinfo = $this->getZone($zone);
			if (!$zinfo)
				continue;

	    	$transitions = $this->getTransitions($zinfo, $now, $now + 3600 * 24 * 366, 2);
	    	$tinfo1 = @$transitions[0];
	    	$tinfo2 = @$transitions[1];

			$off = $this->usertime($zinfo['off'],
								   "ahead of",
								   "same as",
								   "behind") . " UTC";

	    	$wikiText .= "|-\n";
	    	$wikiText .= "| " . $zone . "\n";
	    	$wikiText .= "| " . $off . ($zinfo['dst'] ? " (dst)" : "") . "\n";
	    	if ($tinfo1) {
	    		$wikiText .= "| " . $tinfo1['date'] . ": " . $tinfo1['text'] . "\n";
	    		if ($tinfo2)
	    			$wikiText .= "| " . $tinfo2['date'] . ": " . $tinfo2['text'] . "\n";
    		}
		}

        $wikiText .= "|}\n";

        $output = $parser->parse($wikiText, $parser->mTitle, $parser->mOptions, true, false );
        return $output->getText();
    }


    ////////////////////////////////////////////////////////////////////////
    // Zone Utilities
    ////////////////////////////////////////////////////////////////////////

    /*
     * Given a list of timezones, get the zone info for them.
     *
     * $text               timezone list, one name per line.  Each name may
     *                     be followed by a user-friendly label.
     *
     * Returns an array of records, each containing the following fields:
     *     'rec'		   the DateTimeZone object for this zone.
     *     'dst'		   true iff the zone is currently in DST.
     *     'off'		   the current UTC offset in seconds.
     *     'split'		   true iff the offset is not a whole number of hours.
     *     'name'		   the user-friendly name of this zone.
     *     'label'		   a label for the zone and DST/standard status.
     *     'abbr'		   abbreviation for the zone.
     */
	function getZones($text) {
		$zones = array();

    	// Scan the text for the list of timezones.
    	$zonenames = explode("\n", $text);
    	foreach ($zonenames as $zn) {
	    	// Break the entry into zonename and user-friendly name.
    		$zn = trim($zn);
    		$parts = explode(" ", $zn);
    		if (!@$parts[0])
    			continue;

    		// Get the zone info.
    		$zinfo = $this->getZone($parts[0], @$parts[1]);
    		if ($zinfo)
    			$zones[] = $zinfo;
    	}

    	return $zones;
	}


	/*
	 * Given a timezone name, get the zone info, and enquire for some
	 * additional info about it.
     *
     * $zn                 the timezone name, as in "America/Los_Angeles".
     * $name               a user-supplied name for the zone; if omitted,
     *                     $zn is used.
     *
     * Returns null if the zone doesn't exist; otherwise, a record
     * containing the following fields:
     *     'rec'		   the DateTimeZone object for this zone.
     *     'dst'		   true iff the zone is currently in DST.
     *     'off'		   the current UTC offset in seconds.
     *     'split'		   true iff the offset is not a whole number of hours.
     *     'name'		   the user-friendly name of this zone.
     *     'label'		   a label for the zone and DST/standard status.
     *     'abbr'		   abbreviation for the zone.
	 */
	function getZone($zn, $name=null) {
    	try {
    		$zone = new DateTimeZone($zn);
    	} catch (Exception $e) {
    		return null;
    	}

    	if (!$name)
    		$name = $zn;
    	$now = new DateTime('now', $zone);
    	$offset = $zone->getOffset($now);
    	$dst = $now->format('I');
    	$abbr = $now->format('T');
    	$label = $name . "<br>" . ($dst ? "DST" : "Std");

    	return array('rec' => $zone,
    				 'dst' => $dst ? true : false,
    	             'off' => $offset,
    	             'split' => $offset / 60 % 60 != 0,
    	             'name' => $name,
    	             'label' => $label,
    	             'abbr' => $abbr);
	}


    ////////////////////////////////////////////////////////////////////////
    // Transition Utilities
    ////////////////////////////////////////////////////////////////////////

    /*
     * Get the standard/daylight time transitions for a given tmiezone.
     *
     * $zone               the zone info record, as returned by getZone().
     * $start              if not null, must be a timestamp; return
     *                     transitions starting at that time.
     * $end                if not null, must be a timestamp; return
     *                     transitions up to that time.
     * $max                if not null, the maximum nunber of transitions
     *                     we want.
     *
     * Returns a array of records, each containing the following fields:
     *     'date'		   the date of the transition
     *     'text'          text string descbribing the transition
     *     'type'          the type of time we're moving to: daylight or
     *                     standard
     *     'offset'        the UTC offset after the transition
     */
    function getTransitions(&$zone, $start, $end, $max) {
	    $timezone = $zone['rec'];
		$transitions = $timezone->getTransitions();

	    $trans = array();
		$prevOff = null;
		foreach ($transitions as $tran) {
			if ($start && $tran['ts'] < $start || $end && $tran['ts'] > $end) {
				$prevOff = $tran['offset'];
				continue;
			}

			// Now get the transition data.
	    	$trans[] = $this->getTransition($zone, $tran, $prevOff);

			// See if we're done.
			if ($max > 0 && count($trans) >= $max)
				break;

			$prevOff = $tran['offset'];
		}

        return $trans;
    }


    /*
     * Format the next standard/daylight time transition for a given timezone.
     *
     * $zone               the zone info record, as returned by getZone().
     * $tran               the transition to format.
     * $prevOff            the previous UTC offset in seconds.
     *
     * Returns a record containing the following fields:
     *     'date'		   the date of the transition
     *     'text'          text string descbribing the transition
     *     'type'          the type of time we're moving to: daylight or
     *                     standard
     *     'offset'        the UTC offset after the transition
     */
    function getTransition(&$zone, &$tran, $prevOff) {
		// When is the transition?
		$when = new DateTime("@" . ($tran['ts'] + $prevOff), $zone['rec']);
		$date = $when->format("j M");

		// How do the clocks move?
		$diff = null;
		if ($prevOff !== null) {
			$ds = $tran['offset'] - $prevOff;
			$diff = $this->usertime($ds, "forward", "", "back");
		}

		// What is the new zone?
		$type = $tran['isdst'] ? "daylight" : "standard";

		// Now format it.
	    if ($diff)
	    	$text = "$diff";
	    else
	    	$text = "to $type time";

	    // Return the textual description, and the new offset.
    	return array('date' => $date,
    				 'text' => $text,
    				 'type' => $type,
    				 'offset' => $tran['offset']);
    }


    ////////////////////////////////////////////////////////////////////////
    // Basic Utilities
    ////////////////////////////////////////////////////////////////////////

    /*
     * Convert a time interval in seconds to a user-friendly string;
     * such as "17 hours ago", "1 day 17 hours ago", "3 days ago".
     *
     * $secs               the interval, in seconds; may be negative.
     * $pos                the suffix used if the value is positive.
     * $pos                the string returned if the value is zero.
     * $neg                the suffix used if the value is negative.
     */
    function usertime($secs, $pos, $zero, $neg) {
	    if ($secs == 0)
	    	return $zero;

		$sign = $secs > 0 ? $pos : $neg;
		$secs = abs($secs);

		$s = (int) ($secs % 60);
		$rs = round($secs % 60);
		$secs /= 60;
		$m = (int) ($secs % 60);
		$rm = round($secs % 60);
		$secs /= 60;
		$h = (int) ($secs % 24);
		$rh = round($secs % 24);
		$secs /= 24;
		$d = (int) $secs;
		$rd = round($secs);

	    $res = array();

	    if ($d >= 2)
	    	$res[] = $rd . " day" . ($d > 1 ? "s" : "");
	    else {
			if ($d != 0)
	    		$res[] = $d . " day" . ($d > 1 ? "s" : "");
	    	if ($h >= 2)
	    		$res[] = $rh . " hour" . ($h > 1 ? "s" : "");
	    	else {
				if ($h != 0)
	    			$res[] = $h . " hour" . ($h > 1 ? "s" : "");
				if ($m >= 2)
					$res[] = $rm . " min" . ($m > 1 ? "s" : "");
	    		else {
					if ($m != 0)
	    				$res[] = $m . " min" . ($m > 1 ? "s" : "");
					if ($s > 0)
						$res[] = $rs . " sec" . ($s > 1 ? "s" : "");
				}
			}
	    }
		$res[] = $sign;

		return implode(" ", $res);
    }

}