浏览代码

Migrate some more classes

euromark 11 年之前
父节点
当前提交
0f7f75039a

+ 79 - 0
src/Utility/Multibyte.php

@@ -0,0 +1,79 @@
+<?php
+namespace Tools\Utility;
+
+/**
+ * Multibyte handling methods.
+ *
+ * Shim class for 3.x built from 2.x core file.
+ */
+
+/**
+ * Multibyte handling methods.
+ */
+class Multibyte {
+
+	/**
+	 * Converts a multibyte character string
+	 * to the decimal value of the character
+	 *
+	 * @param string $string String to convert.
+	 * @return array
+	 */
+	public static function utf8($string) {
+		$map = array();
+
+		$values = array();
+		$find = 1;
+		$length = strlen($string);
+
+		for ($i = 0; $i < $length; $i++) {
+			$value = ord($string[$i]);
+
+			if ($value < 128) {
+				$map[] = $value;
+			} else {
+				if (empty($values)) {
+					$find = ($value < 224) ? 2 : 3;
+				}
+				$values[] = $value;
+
+				if (count($values) === $find) {
+					if ($find == 3) {
+						$map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64);
+					} else {
+						$map[] = (($values[0] % 32) * 64) + ($values[1] % 64);
+					}
+					$values = array();
+					$find = 1;
+				}
+			}
+		}
+		return $map;
+	}
+
+	/**
+	 * Converts the decimal value of a multibyte character string
+	 * to a string
+	 *
+	 * @param array $array Values array.
+	 * @return string
+	 */
+	public static function ascii($array) {
+		$ascii = '';
+
+		foreach ($array as $utf8) {
+			if ($utf8 < 128) {
+				$ascii .= chr($utf8);
+			} elseif ($utf8 < 2048) {
+				$ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64));
+				$ascii .= chr(128 + ($utf8 % 64));
+			} else {
+				$ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096));
+				$ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64));
+				$ascii .= chr(128 + ($utf8 % 64));
+			}
+		}
+		return $ascii;
+	}
+
+}

+ 360 - 0
src/Utility/Number.php

@@ -0,0 +1,360 @@
+<?php
+namespace Tools\Utility;
+
+use Cake\I18n\Number as CakeNumber;
+use Cake\Core\Configure;
+
+/**
+ * Extend CakeNumber with a few important improvements:
+ * - config setting for format()
+ * - spacer char for currency (initially from https://github.com/cakephp/cakephp/pull/1148)
+ * - signed values possible
+ *
+ */
+class Number extends CakeNumber {
+
+	protected static $_currency = 'EUR';
+
+	protected static $_symbolRight = '€';
+
+	protected static $_symbolLeft = '';
+
+	protected static $_decimals = ',';
+
+	protected static $_thousands = '.';
+
+	/**
+	 * Correct the defaul values according to localization
+	 *
+	 * @return void
+	 */
+	public static function config($options = array()) {
+		$config = $options + (array)Configure::read('Localization');
+		foreach ($config as $key => $value) {
+			$key = '_' . $key;
+			if (!isset(static::${$key})) {
+				continue;
+			}
+			static::${$key} = $value;
+		}
+	}
+
+	/**
+	 * Display price (or was price if available)
+	 * Without allowNegative it will always default all non-positive values to 0
+	 *
+	 * @param price
+	 * @param specialPrice (outranks the price)
+	 * @param options
+	 * - places
+	 * - allowNegative (defaults to false - price needs to be > 0)
+	 *
+	 * @deprecated use currency()
+	 * @return string
+	 */
+	public static function price($price, $specialPrice = null, $formatOptions = array()) {
+		if ($specialPrice !== null && $specialPrice > 0) {
+			$val = $specialPrice;
+		} elseif ($price > 0 || !empty($formatOptions['allowNegative'])) {
+			$val = $price;
+		} else {
+			if (isset($formatOptions['default'])) {
+				return $formatOptions['default'];
+			}
+			$val = max(0, $price);
+		}
+		return static::money($val, $formatOptions);
+	}
+
+	/**
+	 * Convenience method to display the default currency
+	 *
+	 * @param mixed $amount
+	 * @param array $formatOptions
+	 * @return string
+	 */
+	public static function money($amount, array $formatOptions = array()) {
+		return static::currency($amount, null, $formatOptions);
+	}
+
+	/**
+	 * Format numeric values
+	 * should not be used for currencies
+	 * //TODO: automize per localeconv() ?
+	 *
+	 * @param float $number
+	 * @param array $options : currency=true/false, ... (leave empty for no special treatment)
+	 * @return string
+	 */
+	public static function format($number, array $formatOptions = array()) {
+		if (!is_numeric($number)) {
+			$default = '---';
+			if (!empty($options['default'])) {
+				$default = $options['default'];
+			}
+			return $default;
+		}
+		if ($formatOptions === false) {
+			$formatOptions = array();
+		} elseif (!is_array($formatOptions)) {
+			$formatOptions = array('places' => $formatOptions);
+		}
+		$options = array('before' => '', 'after' => '', 'places' => 2, 'thousands' => static::$_thousands, 'decimals' => static::$_decimals, 'escape' => false);
+		$options = $formatOptions + $options;
+
+		if (!empty($options['currency'])) {
+			if (!empty(static::$_symbolRight)) {
+				$options['after'] = ' ' . static::$_symbolRight;
+			} elseif (!empty(static::$_symbolLeft)) {
+				$options['before'] = static::$_symbolLeft . ' ';
+			}
+		}
+
+		/*
+		if ($spacer !== false) {
+			$spacer = ($spacer === true) ? ' ' : $spacer;
+			if ((string)$before !== '') {
+				$before .= $spacer;
+			}
+			if ((string)$after !== '') {
+				$after = $spacer . $after;
+			}
+		}
+
+		*/
+		if ($options['places'] < 0) {
+			$number = round($number, $options['places']);
+		}
+		$sign = '';
+		if ($number > 0 && !empty($options['signed'])) {
+			$sign = '+';
+		}
+		if (isset($options['signed'])) {
+			unset($options['signed']);
+		}
+		return $sign . parent::format($number, $options);
+	}
+
+	/**
+	 * Correct the default for European countries
+	 *
+	 * @param mixed $number
+	 * @param string $currency
+	 * @param array $formatOptions
+	 * @return string
+	 */
+	public static function currency($number, $currency = null, array $formatOptions = array()) {
+		if ($currency === null) {
+			$currency = static::$_currency;
+		}
+		$defaults = array();
+		if ($currency !== 'EUR' && isset(static::$_currencies[$currency])) {
+			$defaults = static::$_currencies[$currency];
+		} elseif ($currency !== 'EUR' && is_string($currency)) {
+			$defaults['wholeSymbol'] = $currency;
+			$defaults['wholePosition'] = 'before';
+			$defaults['spacer'] = true;
+		}
+		$defaults += array(
+			'wholeSymbol' => '€', 'wholePosition' => 'after',
+			'negative' => '-', 'positive' => '+', 'escape' => true,
+			'decimals' => ',', 'thousands' => '.',
+			'spacer' => $currency === 'EUR' ? true : false
+		);
+		$options = $formatOptions + $defaults;
+
+		if (!empty($options['spacer'])) {
+			$spacer = is_string($options['spacer']) ? $options['spacer'] : ' ';
+
+			if ($options['wholePosition'] === 'after') {
+				$options['wholeSymbol'] = $spacer . $options['wholeSymbol'];
+			} elseif ($options['wholePosition'] === 'before') {
+				$options['wholeSymbol'] .= $spacer;
+			}
+		}
+
+		$sign = '';
+		if ($number > 0 && !empty($options['signed'])) {
+			$sign = $options['positive'];
+		}
+		return $sign . parent::currency($number, null, $options);
+	}
+
+	/**
+	 * Formats a number with a level of precision.
+	 *
+	 * @param float $number	A floating point number.
+	 * @param int $precision The precision of the returned number.
+	 * @param string $decimals
+	 * @return float Formatted float.
+	 * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::precision
+	 */
+	public static function precision($number, $precision = 3, $decimals = '.') {
+		$number = parent::precision($number, $precision);
+		if ($decimals !== '.' && $precision > 0) {
+			$number = str_replace('.', $decimals, $number);
+		}
+		return $number;
+	}
+
+	/**
+	 * Returns a formatted-for-humans file size.
+	 *
+	 * @param int $size Size in bytes
+	 * @return string Human readable size
+	 * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toReadableSize
+	 */
+	public static function toReadableSize($size, $decimals = '.') {
+		$size = parent::toReadableSize($size);
+		if ($decimals !== '.') {
+			$size = str_replace('.', $decimals, $size);
+		}
+		return $size;
+	}
+
+	/**
+	 * Formats a number into a percentage string.
+	 *
+	 * Options:
+	 *
+	 * - `multiply`: Multiply the input value by 100 for decimal percentages.
+	 * - `decimals`: Decimal character.
+	 *
+	 * @param float $number A floating point number
+	 * @param int $precision The precision of the returned number
+	 * @param string $decimals
+	 * @return string Percentage string
+	 * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toPercentage
+	 */
+	public static function toPercentage($number, $precision = 2, array $options = array()) {
+		$options += array('multiply' => false, 'decimals' => '.');
+		if ($options['multiply']) {
+			$number *= 100;
+		}
+		return static::precision($number, $precision, $options['decimals']) . '%';
+	}
+
+	/**
+	 * Get the rounded average.
+	 *
+	 * @param array $values: int or float values
+	 * @param int $precision
+	 * @return float Average
+	 */
+	public static function average($values, $precision = 0) {
+		if (empty($values)) {
+			return 0.0;
+		}
+		return round(array_sum($values) / count($values), $precision);
+	}
+
+	/**
+	 * Round value.
+	 *
+	 * @param float $number
+	 * @param float $increment
+	 * @return float result
+	 */
+	public static function roundTo($number, $increments = 1.0) {
+		$precision = static::getDecimalPlaces($increments);
+		$res = round($number, $precision);
+		if ($precision <= 0) {
+			$res = (int)$res;
+		}
+		return $res;
+	}
+
+	/**
+	 * Round value up.
+	 *
+	 * @param float $number
+	 * @param int $increment
+	 * @return float result
+	 */
+	public static function roundUpTo($number, $increments = 1) {
+		return (ceil($number / $increments) * $increments);
+	}
+
+	/**
+	 * Round value down.
+	 *
+	 * @param float $number
+	 * @param int $increment
+	 * @return float result
+	 */
+	public static function roundDownTo($number, $increments = 1) {
+		return (floor($number / $increments) * $increments);
+	}
+
+	/**
+	 * Get decimal places
+	 *
+	 * @param float $number
+	 * @return int decimalPlaces
+	 */
+	public static function getDecimalPlaces($number) {
+		$decimalPlaces = 0;
+		while ($number > 1 && $number != 0) {
+			$number /= 10;
+			$decimalPlaces -= 1;
+		}
+		while ($number < 1 && $number != 0) {
+			$number *= 10;
+			$decimalPlaces += 1;
+		}
+		return $decimalPlaces;
+	}
+
+	/**
+	 * Returns the English ordinal suffix (th, st, nd, etc) of a number.
+	 *
+	 * echo NumberLib::ordinal(2); // "nd"
+	 * echo NumberLib::ordinal(10); // "th"
+	 * echo NumberLib::ordinal(33); // "rd"
+	 *
+	 * @param int $number
+	 * @return string
+	 */
+	public static function ordinal($number) {
+		if ($number % 100 > 10 && $number % 100 < 14) {
+			return 'th';
+		}
+		switch ($number % 10) {
+			case 1:
+				return 'st';
+			case 2:
+				return 'nd';
+			case 3:
+				return 'rd';
+			default:
+				return 'th';
+		}
+	}
+
+	/**
+	 * Can compare two float values
+	 *
+	 * @link http://php.net/manual/en/language.types.float.php
+	 * @param float $x
+	 * @param float $y
+	 * @param float $precision
+	 * @return bool
+	 */
+	public static function isFloatEqual($x, $y, $precision = 0.0000001) {
+		return ($x + $precision >= $y) && ($x - $precision <= $y);
+	}
+
+	/**
+	 * Get the settings for a specific formatName
+	 *
+	 * @param string $formatName (EUR, ...)
+	 * @return array currencySettings or null on failure
+	 */
+	public static function getFormat($formatName) {
+		if (!isset(static::$_currencies[$formatName])) {
+			return null;
+		}
+		return static::$_currencies[$formatName];
+	}
+
+}

+ 425 - 0
src/Utility/Text.php

@@ -0,0 +1,425 @@
+<?php
+namespace Tools\Utility;
+
+use Cake\Utility\String;
+use Cake\Core\Configure;
+
+/**
+ * Extend String.
+ * //TODO: cleanup
+ *
+ */
+class Text extends String {
+
+	public $text, $length, $char, $letter, $space, $word, $rWord, $sen, $rSen, $para, $rPara, $beautified;
+
+	public function __construct($text = null) {
+		$this->text = $text;
+	}
+
+	/**
+	 * Read tab data (tab-separated data).
+	 *
+	 * @return array
+	 */
+	public function readTab() {
+		$pieces = explode("\n", $this->text);
+		$result = array();
+		foreach ($pieces as $piece) {
+			$tmp = explode("\t", trim($piece, "\r\n"));
+			$result[] = $tmp;
+		}
+		return $result;
+	}
+
+	/**
+	 * Read with a specific pattern.
+	 *
+	 * E.g.: '%s,%s,%s'
+	 *
+	 * @param string $pattern
+	 * @return array
+	 */
+	public function readWithPattern($pattern) {
+		$pieces = explode("\n", $this->text);
+		$result = array();
+		foreach ($pieces as $piece) {
+			$result[] = sscanf(trim($piece, "\r\n"), $pattern);
+		}
+		return $result;
+	}
+
+	/**
+	 * Count words in a text.
+	 *
+	 * //TODO use str_word_count() instead!!!
+	 *
+	 * @param string $text
+	 * @return int
+	 */
+	public static function numberOfWords($text) {
+		$count = 0;
+		$words = explode(' ', $text);
+		foreach ($words as $word) {
+			$word = trim($word);
+			if (!empty($word)) {
+				$count++;
+			}
+		}
+		return $count;
+	}
+
+	/**
+	 * Count chars in a text.
+	 *
+	 * Options:
+	 * - 'whitespace': If whitespace should be counted, as well, defaults to false
+	 *
+	 * @param string $text
+	 * @return int
+	 */
+	public static function numberOfChars($text, $options = array()) {
+		$text = str_replace(array("\r", "\n", "\t", ' '), '', $text);
+		$count = mb_strlen($text);
+		return $count;
+	}
+
+	/**
+	 * Return an abbreviated string, with characters in the middle of the
+	 * excessively long string replaced by $ending.
+	 *
+	 * @param string $text The original string.
+	 * @param int $length The length at which to abbreviate.
+	 * @return string The abbreviated string, if longer than $length.
+	 */
+	public static function abbreviate($text, $length = 20, $ending = '...') {
+		if (mb_strlen($text) <= $length) {
+			return $text;
+		}
+		return rtrim(mb_substr($text, 0, round(($length - 3) / 2))) . $ending . ltrim(mb_substr($text, (($length - 3) / 2) * -1));
+	}
+
+	/**
+	 * TextLib::convertToOrd()
+	 *
+	 * @param string $str
+	 * @param string $separator
+	 * @return string
+	 */
+	public function convertToOrd($str = null, $separator = '-') {
+		/*
+		if (!class_exists('UnicodeLib')) {
+			App::uses('UnicodeLib', 'Tools.Lib');
+		}
+		*/
+		if ($str === null) {
+			$str = $this->text;
+		}
+		$chars = preg_split('//', $str, -1);
+		$res = array();
+		foreach ($chars as $char) {
+			//$res[] = UnicodeLib::ord($char);
+			$res[] = ord($char);
+		}
+		return implode($separator, $res);
+	}
+
+	public static function convertToOrdTable($str, $maxCols = 20) {
+		$res = '<table>';
+		$r = array('chr' => array(), 'ord' => array());
+		$chars = preg_split('//', $str, -1);
+		$count = 0;
+		foreach ($chars as $key => $char) {
+			if ($maxCols && $maxCols < $count || $key === count($chars) - 1) {
+				$res .= '<tr><th>' . implode('</th><th>', $r['chr']) . '</th>';
+				$res .= '</tr>';
+				$res .= '<tr>';
+				$res .= '<td>' . implode('</th><th>', $r['ord']) . '</td></tr>';
+				$count = 0;
+				$r = array('chr' => array(), 'ord' => array());
+			}
+			$count++;
+			//$res[] = UnicodeLib::ord($char);
+			$r['ord'][] = ord($char);
+			$r['chr'][] = $char;
+		}
+
+		$res .= '</table>';
+		return $res;
+	}
+
+	/**
+	 * Explode a string of given tags into an array.
+	 */
+	public function explodeTags($tags) {
+		// This regexp allows the following types of user input:
+		// this, "somecompany, llc", "and ""this"" w,o.rks", foo bar
+		$regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
+		preg_match_all($regexp, $tags, $matches);
+		$typedTags = array_unique($matches[1]);
+
+		$tags = array();
+		foreach ($typedTags as $tag) {
+		// If a user has escaped a term (to demonstrate that it is a group,
+		// or includes a comma or quote character), we remove the escape
+		// formatting so to save the term into the database as the user intends.
+		$tag = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $tag)));
+		if ($tag) {
+			$tags[] = $tag;
+		}
+		}
+
+		return $tags;
+	}
+
+	/**
+	 * Implode an array of tags into a string.
+	 */
+	public function implodeTags($tags) {
+		$encodedTags = array();
+		foreach ($tags as $tag) {
+		// Commas and quotes in tag names are special cases, so encode them.
+		if (strpos($tag, ',') !== false || strpos($tag, '"') !== false) {
+			$tag = '"' . str_replace('"', '""', $tag) . '"';
+		}
+
+		$encodedTags[] = $tag;
+		}
+		return implode(', ', $encodedTags);
+	}
+
+	/**
+	 * Prevents [widow words](http://www.shauninman.com/archive/2006/08/22/widont_wordpress_plugin)
+	 * by inserting a non-breaking space between the last two words.
+	 *
+	 * echo Text::widont($text);
+	 *
+	 * @param string text to remove widows from
+	 * @return string
+	 */
+	public function widont($str = null) {
+		if ($str === null) {
+			$str = $this->text;
+		}
+		$str = rtrim($str);
+		$space = strrpos($str, ' ');
+
+		if ($space !== false) {
+			$str = substr($str, 0, $space) . '&nbsp;' . substr($str, $space + 1);
+		}
+
+		return $str;
+	}
+
+/* text object specific */
+
+	/**
+	 * Extract words
+	 *
+	 * @param options
+	 * - min_char, max_char, case_sensititive, ...
+	 * @return array
+	 */
+	public function words($options = array()) {
+		if (true || !$this->xrWord) {
+			$text = str_replace(array(PHP_EOL, NL, TB), ' ', $this->text);
+
+			$pieces = explode(' ', $text);
+			$pieces = array_unique($pieces);
+
+			// strip chars like . or ,
+			foreach ($pieces as $key => $piece) {
+				if (empty($options['case_sensitive'])) {
+					$piece = mb_strtolower($piece);
+				}
+				$search = array(',', '.', ';', ':', '#', '', '(', ')', '{', '}', '[', ']', '$', '%', '"', '!', '?', '<', '>', '=', '/');
+				$search = array_merge($search, array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0));
+				$piece = str_replace($search, '', $piece);
+				$piece = trim($piece);
+
+				if (empty($piece) || !empty($options['min_char']) && mb_strlen($piece) < $options['min_char'] || !empty($options['max_char']) && mb_strlen($piece) > $options['max_char']) {
+					unset($pieces[$key]);
+				} else {
+					$pieces[$key] = $piece;
+				}
+			}
+			$pieces = array_unique($pieces);
+			//$this->xrWord = $pieces;
+		}
+		return $pieces;
+	}
+
+	/**
+	 * Limit the number of words in a string.
+	 *
+	 * <code>
+	 *		// Returns "This is a..."
+	 *		echo TextExt::maxWords('This is a sentence.', 3);
+	 *
+	 *		// Limit the number of words and append a custom ending
+	 *		echo Str::words('This is a sentence.', 3, '---');
+	 * </code>
+	 *
+	 * @param string  $value
+	 * @param int     $words
+	 * @param array $options
+	 * - ellipsis
+	 * - html
+	 * @return string
+	 */
+	public static function maxWords($value, $words = 100, $options = array()) {
+		$defaults = array(
+			'ellipsis' => '...'
+		);
+		if (!empty($options['html']) && Configure::read('App.encoding') === 'UTF-8') {
+			$defaults['ellipsis'] = "\xe2\x80\xa6";
+		}
+		$options += $defaults;
+
+		if (trim($value) === '') {
+			return '';
+		}
+		preg_match('/^\s*+(?:\S++\s*+){1,' . $words . '}/u', $value, $matches);
+
+		$end = $options['ellipsis'];
+		if (mb_strlen($value) === mb_strlen($matches[0])) {
+			$end = '';
+		}
+		return rtrim($matches[0]) . $end;
+	}
+
+	/**
+	 * High ASCII to Entities
+	 *
+	 * Converts High ascii text and MS Word special characters to character entities
+	 *
+	 * @param string
+	 * @return string
+	 */
+	public function asciiToEntities($str) {
+		$count = 1;
+		$out = '';
+		$temp = array();
+
+		for ($i = 0, $s = strlen($str); $i < $s; $i++) {
+			$ordinal = ord($str[$i]);
+
+			if ($ordinal < 128) {
+				/*
+				If the $temp array has a value but we have moved on, then it seems only
+				fair that we output that entity and restart $temp before continuing. -Paul
+				*/
+				if (count($temp) == 1) {
+					$out .= '&#' . array_shift($temp) . ';';
+					$count = 1;
+				}
+
+				$out .= $str[$i];
+			} else {
+				if (count($temp) == 0) {
+					$count = ($ordinal < 224) ? 2 : 3;
+				}
+
+				$temp[] = $ordinal;
+
+				if (count($temp) == $count) {
+					$number = ($count == 3) ? (($temp['0'] % 16) * 4096) + (($temp['1'] % 64) * 64) + ($temp['2'] %
+						64) : (($temp['0'] % 32) * 64) + ($temp['1'] % 64);
+
+					$out .= '&#' . $number . ';';
+					$count = 1;
+					$temp = array();
+				}
+			}
+		}
+		return $out;
+	}
+
+	/**
+	 * Entities to ASCII
+	 *
+	 * Converts character entities back to ASCII
+	 *
+	 * @param string
+	 * @param bool
+	 * @return string
+	 */
+	public function EntitiesToAscii($str, $all = true) {
+		if (preg_match_all('/\&#(\d+)\;/', $str, $matches)) {
+			for ($i = 0, $s = count($matches['0']); $i < $s; $i++) {
+				$digits = $matches['1'][$i];
+
+				$out = '';
+
+				if ($digits < 128) {
+					$out .= chr($digits);
+
+				} elseif ($digits < 2048) {
+					$out .= chr(192 + (($digits - ($digits % 64)) / 64));
+					$out .= chr(128 + ($digits % 64));
+				} else {
+					$out .= chr(224 + (($digits - ($digits % 4096)) / 4096));
+					$out .= chr(128 + ((($digits % 4096) - ($digits % 64)) / 64));
+					$out .= chr(128 + ($digits % 64));
+				}
+
+				$str = str_replace($matches['0'][$i], $out, $str);
+			}
+		}
+
+		if ($all) {
+			$str = str_replace(array("&amp;", "&lt;", "&gt;", "&quot;", "&apos;", "&#45;"),
+				array("&", "<", ">", "\"", "'", "-"), $str);
+		}
+
+		return $str;
+	}
+
+	/**
+	 * Reduce Double Slashes
+	 *
+	 * Converts double slashes in a string to a single slash,
+	 * except those found in http://
+	 *
+	 * http://www.some-site.com//index.php
+	 *
+	 * becomes:
+	 *
+	 * http://www.some-site.com/index.php
+	 *
+	 * @param string
+	 * @return string
+	 */
+	public function reduce_double_slashes($str) {
+		return preg_replace("#([^:])//+#", "\\1/", $str);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Reduce Multiples
+	 *
+	 * Reduces multiple instances of a particular character. Example:
+	 *
+	 * Fred, Bill,, Joe, Jimmy
+	 *
+	 * becomes:
+	 *
+	 * Fred, Bill, Joe, Jimmy
+	 *
+	 * @param string
+	 * @param string	the character you wish to reduce
+	 * @param bool	TRUE/FALSE - whether to trim the character from the beginning/end
+	 * @return string
+	 */
+	public function reduce_multiples($str, $character = ',', $trim = false) {
+		$str = preg_replace('#' . preg_quote($character, '#') . '{2,}#', $character, $str);
+
+		if ($trim === true) {
+			$str = trim($str, $character);
+		}
+
+		return $str;
+	}
+
+}

文件差异内容过多而无法显示
+ 1259 - 0
src/Utility/Time.php


文件差异内容过多而无法显示
+ 1661 - 0
src/View/Helper/GoogleMapV3Helper.php


+ 502 - 0
src/View/Helper/JsHelper.php

@@ -0,0 +1,502 @@
+<?php
+/**
+ * Javascript Generator class file.
+ *
+ * CakePHP :  Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @package       Cake.View.Helper
+ * @since         CakePHP(tm) v 1.2
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Tools\View\Helper;
+
+use Cake\View\Helper;
+use Cake\Core\Configure;
+use Tools\Utility\Multibyte;
+/**
+ * Javascript Generator helper class for easy use of JavaScript.
+ *
+ * JsHelper provides an abstract interface for authoring JavaScript with a
+ * given client-side library.
+ */
+class JsHelper extends Helper {
+
+/**
+ * Whether or not you want scripts to be buffered or output.
+ *
+ * @var bool
+ */
+	public $bufferScripts = true;
+
+/**
+ * Helper dependencies
+ *
+ * @var array
+ */
+	public $helpers = array('Html', 'Form');
+
+/**
+ * Variables to pass to Javascript.
+ *
+ * @var array
+ * @see JsHelper::set()
+ */
+	protected $_jsVars = array();
+
+/**
+ * Scripts that are queued for output
+ *
+ * @var array
+ * @see JsHelper::buffer()
+ */
+	protected $_bufferedScripts = array();
+
+/**
+ * The javascript variable created by set() variables.
+ *
+ * @var string
+ */
+	public $setVariable = 'app';
+
+/**
+ * Generates a JavaScript object in JavaScript Object Notation (JSON)
+ * from an array. Will use native JSON encode method if available, and $useNative == true
+ *
+ * ### Options:
+ *
+ * - `prefix` - String prepended to the returned data.
+ * - `postfix` - String appended to the returned data.
+ *
+ * @param array $data Data to be converted.
+ * @param array $options Set of options, see above.
+ * @return string A JSON code block
+ */
+	public function object($data = array(), $options = array()) {
+		$defaultOptions = array(
+			'prefix' => '', 'postfix' => '',
+		);
+		$options += $defaultOptions;
+
+		return $options['prefix'] . json_encode($data) . $options['postfix'];
+	}
+
+/**
+ * Converts a PHP-native variable of any type to a JSON-equivalent representation
+ *
+ * @param mixed $val A PHP variable to be converted to JSON
+ * @param bool $quoteString If false, leaves string values unquoted
+ * @param string $key Key name.
+ * @return string a JavaScript-safe/JSON representation of $val
+ */
+	public function value($val = array(), $quoteString = null, $key = 'value') {
+		if ($quoteString === null) {
+			$quoteString = true;
+		}
+		switch (true) {
+			case (is_array($val) || is_object($val)):
+				$val = $this->object($val);
+				break;
+			case ($val === null):
+				$val = 'null';
+				break;
+			case (is_bool($val)):
+				$val = ($val === true) ? 'true' : 'false';
+				break;
+			case (is_int($val)):
+				$val = $val;
+				break;
+			case (is_float($val)):
+				$val = sprintf("%.11f", $val);
+				break;
+			default:
+				$val = $this->escape($val);
+				if ($quoteString) {
+					$val = '"' . $val . '"';
+				}
+		}
+		return $val;
+	}
+
+/**
+ * Escape a string to be JSON friendly.
+ *
+ * List of escaped elements:
+ *
+ * - "\r" => '\n'
+ * - "\n" => '\n'
+ * - '"' => '\"'
+ *
+ * @param string $string String that needs to get escaped.
+ * @return string Escaped string.
+ */
+	public function escape($string) {
+		return $this->_utf8ToHex($string);
+	}
+
+/**
+ * Encode a string into JSON. Converts and escapes necessary characters.
+ *
+ * @param string $string The string that needs to be utf8->hex encoded
+ * @return void
+ */
+	protected function _utf8ToHex($string) {
+		$length = strlen($string);
+		$return = '';
+		for ($i = 0; $i < $length; ++$i) {
+			$ord = ord($string{$i});
+			switch (true) {
+				case $ord == 0x08:
+					$return .= '\b';
+					break;
+				case $ord == 0x09:
+					$return .= '\t';
+					break;
+				case $ord == 0x0A:
+					$return .= '\n';
+					break;
+				case $ord == 0x0C:
+					$return .= '\f';
+					break;
+				case $ord == 0x0D:
+					$return .= '\r';
+					break;
+				case $ord == 0x22:
+				case $ord == 0x2F:
+				case $ord == 0x5C:
+					$return .= '\\' . $string{$i};
+					break;
+				case (($ord >= 0x20) && ($ord <= 0x7F)):
+					$return .= $string{$i};
+					break;
+				case (($ord & 0xE0) == 0xC0):
+					if ($i + 1 >= $length) {
+						$i += 1;
+						$return .= '?';
+						break;
+					}
+					$charbits = $string{$i} . $string{$i + 1};
+					$char = Multibyte::utf8($charbits);
+					$return .= sprintf('\u%04s', dechex($char[0]));
+					$i += 1;
+					break;
+				case (($ord & 0xF0) == 0xE0):
+					if ($i + 2 >= $length) {
+						$i += 2;
+						$return .= '?';
+						break;
+					}
+					$charbits = $string{$i} . $string{$i + 1} . $string{$i + 2};
+					$char = Multibyte::utf8($charbits);
+					$return .= sprintf('\u%04s', dechex($char[0]));
+					$i += 2;
+					break;
+				case (($ord & 0xF8) == 0xF0):
+					if ($i + 3 >= $length) {
+						$i += 3;
+						$return .= '?';
+						break;
+					}
+					$charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3};
+					$char = Multibyte::utf8($charbits);
+					$return .= sprintf('\u%04s', dechex($char[0]));
+					$i += 3;
+					break;
+				case (($ord & 0xFC) == 0xF8):
+					if ($i + 4 >= $length) {
+						$i += 4;
+						$return .= '?';
+						break;
+					}
+					$charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3} . $string{$i + 4};
+					$char = Multibyte::utf8($charbits);
+					$return .= sprintf('\u%04s', dechex($char[0]));
+					$i += 4;
+					break;
+				case (($ord & 0xFE) == 0xFC):
+					if ($i + 5 >= $length) {
+						$i += 5;
+						$return .= '?';
+						break;
+					}
+					$charbits = $string{$i} . $string{$i + 1} . $string{$i + 2} . $string{$i + 3} . $string{$i + 4} . $string{$i + 5};
+					$char = Multibyte::utf8($charbits);
+					$return .= sprintf('\u%04s', dechex($char[0]));
+					$i += 5;
+					break;
+			}
+		}
+		return $return;
+	}
+
+/**
+ * Writes all Javascript generated so far to a code block or
+ * caches them to a file and returns a linked script. If no scripts have been
+ * buffered this method will return null. If the request is an XHR(ajax) request
+ * onDomReady will be set to false. As the dom is already 'ready'.
+ *
+ * ### Options
+ *
+ * - `inline` - Set to true to have scripts output as a script block inline
+ *   if `cache` is also true, a script link tag will be generated. (default true)
+ * - `cache` - Set to true to have scripts cached to a file and linked in (default false)
+ * - `clear` - Set to false to prevent script cache from being cleared (default true)
+ * - `onDomReady` - wrap cached scripts in domready event (default true)
+ * - `safe` - if an inline block is generated should it be wrapped in <![CDATA[ ... ]]> (default true)
+ *
+ * @param array $options options for the code block
+ * @return mixed Completed javascript tag if there are scripts, if there are no buffered
+ *   scripts null will be returned.
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::writeBuffer
+ */
+	public function writeBuffer($options = array()) {
+		$domReady = !$this->request->is('ajax');
+		$defaults = array(
+			'onDomReady' => $domReady, 'inline' => true,
+			'cache' => false, 'clear' => true, 'safe' => true
+		);
+		$options += $defaults;
+		$script = implode("\n", $this->getBuffer($options['clear']));
+
+		if (empty($script)) {
+			return null;
+		}
+
+		if ($options['onDomReady']) {
+			$script = $this->{$this->_engineName}->domReady($script);
+		}
+		$opts = $options;
+		unset($opts['onDomReady'], $opts['cache'], $opts['clear']);
+
+		if ($options['cache'] && $options['inline']) {
+			$filename = md5($script);
+			$path = WWW_ROOT . Configure::read('App.jsBaseUrl');
+			if (file_exists($path . $filename . '.js')
+				|| cache(str_replace(WWW_ROOT, '', $path) . $filename . '.js', $script, '+999 days', 'public')
+				) {
+				return $this->Html->script($filename);
+			}
+		}
+
+		$return = $this->Html->scriptBlock($script, $opts);
+		if ($options['inline']) {
+			return $return;
+		}
+		return null;
+	}
+
+/**
+ * Write a script to the buffered scripts.
+ *
+ * @param string $script Script string to add to the buffer.
+ * @param bool $top If true the script will be added to the top of the
+ *   buffered scripts array. If false the bottom.
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::buffer
+ */
+	public function buffer($script, $top = false) {
+		if ($top) {
+			array_unshift($this->_bufferedScripts, $script);
+		} else {
+			$this->_bufferedScripts[] = $script;
+		}
+	}
+
+/**
+ * Get all the buffered scripts
+ *
+ * @param bool $clear Whether or not to clear the script caches (default true)
+ * @return array Array of scripts added to the request.
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::getBuffer
+ */
+	public function getBuffer($clear = true) {
+		$this->_createVars();
+		$scripts = $this->_bufferedScripts;
+		if ($clear) {
+			$this->_bufferedScripts = array();
+			$this->_jsVars = array();
+		}
+		return $scripts;
+	}
+
+/**
+ * Generates the object string for variables passed to javascript and adds to buffer
+ *
+ * @return void
+ */
+	protected function _createVars() {
+		if (!empty($this->_jsVars)) {
+			$setVar = (strpos($this->setVariable, '.')) ? $this->setVariable : 'window.' . $this->setVariable;
+			$this->buffer($setVar . ' = ' . $this->object($this->_jsVars) . ';', true);
+		}
+	}
+
+/**
+ * Generate an 'Ajax' link. Uses the selected JS engine to create a link
+ * element that is enhanced with Javascript. Options can include
+ * both those for HtmlHelper::link() and JsBaseEngine::request(), JsBaseEngine::event();
+ *
+ * ### Options
+ *
+ * - `confirm` - Generate a confirm() dialog before sending the event.
+ * - `id` - use a custom id.
+ * - `htmlAttributes` - additional non-standard htmlAttributes. Standard attributes are class, id,
+ *    rel, title, escape, onblur and onfocus.
+ * - `buffer` - Disable the buffering and return a script tag in addition to the link.
+ *
+ * @param string $title Title for the link.
+ * @param string|array $url Mixed either a string URL or a CakePHP URL array.
+ * @param array $options Options for both the HTML element and Js::request()
+ * @return string Completed link. If buffering is disabled a script tag will be returned as well.
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::link
+ */
+	public function link($title, $url = null, $options = array()) {
+		if (!isset($options['id'])) {
+			$options['id'] = 'link-' . (int)mt_rand();
+		}
+		list($options, $htmlOptions) = $this->_getHtmlOptions($options);
+		$out = $this->Html->link($title, $url, $htmlOptions);
+		$this->get('#' . $htmlOptions['id']);
+		$requestString = $event = '';
+		if (isset($options['confirm'])) {
+			$requestString = $this->confirmReturn($options['confirm']);
+			unset($options['confirm']);
+		}
+		$buffer = isset($options['buffer']) ? $options['buffer'] : null;
+		$safe = isset($options['safe']) ? $options['safe'] : true;
+		unset($options['buffer'], $options['safe']);
+
+		$requestString .= $this->request($url, $options);
+
+		if (!empty($requestString)) {
+			$event = $this->event('click', $requestString, $options + array('buffer' => $buffer));
+		}
+		if (isset($buffer) && !$buffer) {
+			$opts = array('safe' => $safe);
+			$out .= $this->Html->scriptBlock($event, $opts);
+		}
+		return $out;
+	}
+
+/**
+ * Pass variables into Javascript. Allows you to set variables that will be
+ * output when the buffer is fetched with `JsHelper::getBuffer()` or `JsHelper::writeBuffer()`
+ * The Javascript variable used to output set variables can be controlled with `JsHelper::$setVariable`
+ *
+ * @param string|array $one Either an array of variables to set, or the name of the variable to set.
+ * @param string|array $two If $one is a string, $two is the value for that key.
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::set
+ */
+	public function set($one, $two = null) {
+		$data = null;
+		if (is_array($one)) {
+			if (is_array($two)) {
+				$data = array_combine($one, $two);
+			} else {
+				$data = $one;
+			}
+		} else {
+			$data = array($one => $two);
+		}
+		if (!$data) {
+			return false;
+		}
+		$this->_jsVars = array_merge($this->_jsVars, $data);
+	}
+
+/**
+ * Uses the selected JS engine to create a submit input
+ * element that is enhanced with Javascript. Options can include
+ * both those for FormHelper::submit() and JsBaseEngine::request(), JsBaseEngine::event();
+ *
+ * Forms submitting with this method, cannot send files. Files do not transfer over XmlHttpRequest
+ * and require an iframe or flash.
+ *
+ * ### Options
+ *
+ * - `url` The url you wish the XHR request to submit to.
+ * - `confirm` A string to use for a confirm() message prior to submitting the request.
+ * - `method` The method you wish the form to send by, defaults to POST
+ * - `buffer` Whether or not you wish the script code to be buffered, defaults to true.
+ * - Also see options for JsHelper::request() and JsHelper::event()
+ *
+ * @param string $caption The display text of the submit button.
+ * @param array $options Array of options to use. See the options for the above mentioned methods.
+ * @return string Completed submit button.
+ * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/js.html#JsHelper::submit
+ */
+	public function submit($caption = null, $options = array()) {
+		if (!isset($options['id'])) {
+			$options['id'] = 'submit-' . (int)mt_rand();
+		}
+		$formOptions = array('div');
+		list($options, $htmlOptions) = $this->_getHtmlOptions($options, $formOptions);
+		$out = $this->Form->submit($caption, $htmlOptions);
+
+		$this->get('#' . $htmlOptions['id']);
+
+		$options['data'] = $this->serializeForm(array('isForm' => false, 'inline' => true));
+		$requestString = $url = '';
+		if (isset($options['confirm'])) {
+			$requestString = $this->confirmReturn($options['confirm']);
+			unset($options['confirm']);
+		}
+		if (isset($options['url'])) {
+			$url = $options['url'];
+			unset($options['url']);
+		}
+		if (!isset($options['method'])) {
+			$options['method'] = 'post';
+		}
+		$options['dataExpression'] = true;
+
+		$buffer = isset($options['buffer']) ? $options['buffer'] : null;
+		$safe = isset($options['safe']) ? $options['safe'] : true;
+		unset($options['buffer'], $options['safe']);
+
+		$requestString .= $this->request($url, $options);
+		if (!empty($requestString)) {
+			$event = $this->event('click', $requestString, $options + array('buffer' => $buffer));
+		}
+		if (isset($buffer) && !$buffer) {
+			$opts = array('safe' => $safe);
+			$out .= $this->Html->scriptBlock($event, $opts);
+		}
+		return $out;
+	}
+
+/**
+ * Parse a set of Options and extract the Html options.
+ * Extracted Html Options are removed from the $options param.
+ *
+ * @param array $options Options to filter.
+ * @param array $additional Array of additional keys to extract and include in the return options array.
+ * @return array Array of js options and Htmloptions
+ */
+	protected function _getHtmlOptions($options, $additional = array()) {
+		$htmlKeys = array_merge(
+			array('class', 'id', 'escape', 'onblur', 'onfocus', 'rel', 'title', 'style'),
+			$additional
+		);
+		$htmlOptions = array();
+		foreach ($htmlKeys as $key) {
+			if (isset($options[$key])) {
+				$htmlOptions[$key] = $options[$key];
+			}
+			unset($options[$key]);
+		}
+		if (isset($options['htmlAttributes'])) {
+			$htmlOptions = array_merge($htmlOptions, $options['htmlAttributes']);
+			unset($options['htmlAttributes']);
+		}
+		return array($options, $htmlOptions);
+	}
+
+}

+ 19 - 0
src/View/Helper/NumberHelper.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Tools\View\Helper;
+
+use Cake\View\Helper\NumberHelper as CakeNumberHelper;
+use Cake\Utility\Hash;
+/**
+ * Todo: rename to MyNumberHelper some day?
+ * Aliasing it then as Number again in the project
+ *
+ */
+class NumberHelper extends CakeNumberHelper {
+
+	public function __construct($View = null, $options = array()) {
+		$options = Hash::merge(array('engine' => 'Tools.Number'), $options);
+		parent::__construct($View, $options);
+	}
+
+}

+ 544 - 0
src/View/Helper/TimeHelper.php

@@ -0,0 +1,544 @@
+<?php
+
+namespace Tools\View\Helper;
+
+use Cake\View\Helper\TimeHelper as CakeTimeHelper;
+
+/**
+ * Wrapper for TimeHelper and TimeLib
+ */
+class TimeHelper extends CakeTimeHelper {
+
+	public $helpers = array('Html');
+
+	public function __construct($View = null, $config = array()) {
+		$defaults = array('engine' => 'Tools.Time');
+		$config += $defaults;
+		parent::__construct($View, $config);
+	}
+
+	/**
+	 * Output the age of a person within a sane range.
+	 * Defaults to the $default string if outside of that range.
+	 *
+	 * @param string date (from db)
+	 * @return int age on success, mixed $default otherwise
+	 */
+	public function userAge($date = null, $default = '---') {
+		if ((int)$date === 0) {
+			return $default;
+		}
+		$age = $this->age($date, null);
+		if ($age >= 1 && $age <= 99) {
+			return $age;
+		}
+		return $default;
+	}
+
+	/**
+	 * Like localDate(), only with additional markup <span> and class="today", if today, etc
+	 *
+	 * @return string
+	 */
+	public function localDateMarkup($dateString = null, $format = null, $options = array()) {
+		$date = $this->localDate($dateString, $format, $options);
+		$date = '<span' . ($this->isToday($dateString, (isset($options['userOffset']) ? $options['userOffset'] : null)) ? ' class="today"' : '') . '>' . $date . '</span>';
+		return $date;
+	}
+
+	/**
+	 * Like niceDate(), only with additional markup <span> and class="today", if today, etc
+	 *
+	 * @return string
+	 */
+	public function niceDateMarkup($dateString = null, $format = null, $options = array()) {
+		$date = $this->niceDate($dateString, $format, $options);
+		$date = '<span' . ($this->isToday($dateString, (isset($options['userOffset']) ? $options['userOffset'] : null)) ? ' class="today"' : '') . '>' . $date . '</span>';
+		return $date;
+	}
+
+	/**
+	 * Returns red/specialGreen/green date depending on the current day
+	 * // TODO refactor! $userOffset is deprecated!
+	 *
+	 * @param date in DB Format (xxxx-xx-xx)
+	 * ...
+	 * @param array $options
+	 * @param array $attr: html attributes
+	 * @return nicely formatted date
+	 */
+	public function published($dateString = null, $userOffset = null, $options = array(), $attr = array()) {
+		$date = $dateString ? $this->fromString($dateString, $userOffset) : null; // time() ?
+		$niceDate = '';
+		$when = null;
+		$span = '';
+		$spanEnd = '';
+		$whenArray = array('-1' => 'already', '0' => 'today', '1' => 'notyet');
+		$titles = array('-1' => __d('tools', 'publishedAlready'), '0' => __d('tools', 'publishedToday'), '1' => __d('tools', 'publishedNotYet'));
+
+		if (!empty($date)) {
+
+			$y = $this->isThisYear($date) ? '' : ' Y';
+
+			$format = (!empty($options['format']) ? $options['format'] : FORMAT_NICE_YMD);
+
+			// Hack
+			// //TODO: get this to work with datetime - somehow cleaner
+			$timeAttachment = '';
+			if (isset($options['niceDateTime'])) {
+				$timeAttachment = ', ' . $this->niceDate($date, $options['niceDateTime']);
+				$whenOverride = true;
+			}
+
+			if ($this->isToday($date)) {
+				$when = 0;
+				$niceDate = __d('tools', 'Today') . $timeAttachment;
+			} elseif ($this->isTomorrow($date)) {
+				$when = 1;
+				$niceDate = __d('tools', 'Tomorrow') . $timeAttachment;
+			} elseif ($this->wasYesterday($date)) {
+				$when = -1;
+				$niceDate = __d('tools', 'Yesterday') . $timeAttachment;
+			} else {
+				// before or after?
+				if ($this->isNotTodayAndInTheFuture($date)) {
+					$when = 1;
+				} else {
+					$when = -1;
+				}
+				$niceDate = $this->niceDate($date, $format) . $timeAttachment; //date("M jS{$y}", $date);
+			}
+
+			if (!empty($whenOverride) && $when == 0) {
+				if ($this->isInTheFuture($date)) {
+					$when = 1;
+				} else {
+					$when = -1;
+				}
+			}
+
+		}
+
+		if (empty($niceDate) || $when === null) {
+			$niceDate = '<i>n/a</i>';
+		} else {
+			if (!isset($attr['title'])) {
+				$attr['title'] = $titles[$when];
+			}
+			$attr['class'] = 'published ' . $whenArray[$when];
+		}
+		return $this->Html->tag('span', $niceDate, $attr);
+	}
+
+	/**
+	 * DatetimeHelper::timezones()
+	 *
+	 * @return array
+	 */
+	public function timezones() {
+		$timezones = array(
+			'America/Adak' => '(GMT-10:00) America/Adak (Hawaii-Aleutian Standard Time)',
+			'America/Atka' => '(GMT-10:00) America/Atka (Hawaii-Aleutian Standard Time)',
+			'America/Anchorage' => '(GMT-9:00) America/Anchorage (Alaska Standard Time)',
+			'America/Juneau' => '(GMT-9:00) America/Juneau (Alaska Standard Time)',
+			'America/Nome' => '(GMT-9:00) America/Nome (Alaska Standard Time)',
+			'America/Yakutat' => '(GMT-9:00) America/Yakutat (Alaska Standard Time)',
+			'America/Dawson' => '(GMT-8:00) America/Dawson (Pacific Standard Time)',
+			'America/Ensenada' => '(GMT-8:00) America/Ensenada (Pacific Standard Time)',
+			'America/Los_Angeles' => '(GMT-8:00) America/Los_Angeles (Pacific Standard Time)',
+			'America/Tijuana' => '(GMT-8:00) America/Tijuana (Pacific Standard Time)',
+			'America/Vancouver' => '(GMT-8:00) America/Vancouver (Pacific Standard Time)',
+			'America/Whitehorse' => '(GMT-8:00) America/Whitehorse (Pacific Standard Time)',
+			'Canada/Pacific' => '(GMT-8:00) Canada/Pacific (Pacific Standard Time)',
+			'Canada/Yukon' => '(GMT-8:00) Canada/Yukon (Pacific Standard Time)',
+			'Mexico/BajaNorte' => '(GMT-8:00) Mexico/BajaNorte (Pacific Standard Time)',
+			'America/Boise' => '(GMT-7:00) America/Boise (Mountain Standard Time)',
+			'America/Cambridge_Bay' => '(GMT-7:00) America/Cambridge_Bay (Mountain Standard Time)',
+			'America/Chihuahua' => '(GMT-7:00) America/Chihuahua (Mountain Standard Time)',
+			'America/Dawson_Creek' => '(GMT-7:00) America/Dawson_Creek (Mountain Standard Time)',
+			'America/Denver' => '(GMT-7:00) America/Denver (Mountain Standard Time)',
+			'America/Edmonton' => '(GMT-7:00) America/Edmonton (Mountain Standard Time)',
+			'America/Hermosillo' => '(GMT-7:00) America/Hermosillo (Mountain Standard Time)',
+			'America/Inuvik' => '(GMT-7:00) America/Inuvik (Mountain Standard Time)',
+			'America/Mazatlan' => '(GMT-7:00) America/Mazatlan (Mountain Standard Time)',
+			'America/Phoenix' => '(GMT-7:00) America/Phoenix (Mountain Standard Time)',
+			'America/Shiprock' => '(GMT-7:00) America/Shiprock (Mountain Standard Time)',
+			'America/Yellowknife' => '(GMT-7:00) America/Yellowknife (Mountain Standard Time)',
+			'Canada/Mountain' => '(GMT-7:00) Canada/Mountain (Mountain Standard Time)',
+			'Mexico/BajaSur' => '(GMT-7:00) Mexico/BajaSur (Mountain Standard Time)',
+			'America/Belize' => '(GMT-6:00) America/Belize (Central Standard Time)',
+			'America/Cancun' => '(GMT-6:00) America/Cancun (Central Standard Time)',
+			'America/Chicago' => '(GMT-6:00) America/Chicago (Central Standard Time)',
+			'America/Costa_Rica' => '(GMT-6:00) America/Costa_Rica (Central Standard Time)',
+			'America/El_Salvador' => '(GMT-6:00) America/El_Salvador (Central Standard Time)',
+			'America/Guatemala' => '(GMT-6:00) America/Guatemala (Central Standard Time)',
+			'America/Knox_IN' => '(GMT-6:00) America/Knox_IN (Central Standard Time)',
+			'America/Managua' => '(GMT-6:00) America/Managua (Central Standard Time)',
+			'America/Menominee' => '(GMT-6:00) America/Menominee (Central Standard Time)',
+			'America/Merida' => '(GMT-6:00) America/Merida (Central Standard Time)',
+			'America/Mexico_City' => '(GMT-6:00) America/Mexico_City (Central Standard Time)',
+			'America/Monterrey' => '(GMT-6:00) America/Monterrey (Central Standard Time)',
+			'America/Rainy_River' => '(GMT-6:00) America/Rainy_River (Central Standard Time)',
+			'America/Rankin_Inlet' => '(GMT-6:00) America/Rankin_Inlet (Central Standard Time)',
+			'America/Regina' => '(GMT-6:00) America/Regina (Central Standard Time)',
+			'America/Swift_Current' => '(GMT-6:00) America/Swift_Current (Central Standard Time)',
+			'America/Tegucigalpa' => '(GMT-6:00) America/Tegucigalpa (Central Standard Time)',
+			'America/Winnipeg' => '(GMT-6:00) America/Winnipeg (Central Standard Time)',
+			'Canada/Central' => '(GMT-6:00) Canada/Central (Central Standard Time)',
+			'Canada/East-Saskatchewan' => '(GMT-6:00) Canada/East-Saskatchewan (Central Standard Time)',
+			'Canada/Saskatchewan' => '(GMT-6:00) Canada/Saskatchewan (Central Standard Time)',
+			'Chile/EasterIsland' => '(GMT-6:00) Chile/EasterIsland (Easter Is. Time)',
+			'Mexico/General' => '(GMT-6:00) Mexico/General (Central Standard Time)',
+			'America/Atikokan' => '(GMT-5:00) America/Atikokan (Eastern Standard Time)',
+			'America/Bogota' => '(GMT-5:00) America/Bogota (Colombia Time)',
+			'America/Cayman' => '(GMT-5:00) America/Cayman (Eastern Standard Time)',
+			'America/Coral_Harbour' => '(GMT-5:00) America/Coral_Harbour (Eastern Standard Time)',
+			'America/Detroit' => '(GMT-5:00) America/Detroit (Eastern Standard Time)',
+			'America/Fort_Wayne' => '(GMT-5:00) America/Fort_Wayne (Eastern Standard Time)',
+			'America/Grand_Turk' => '(GMT-5:00) America/Grand_Turk (Eastern Standard Time)',
+			'America/Guayaquil' => '(GMT-5:00) America/Guayaquil (Ecuador Time)',
+			'America/Havana' => '(GMT-5:00) America/Havana (Cuba Standard Time)',
+			'America/Indianapolis' => '(GMT-5:00) America/Indianapolis (Eastern Standard Time)',
+			'America/Iqaluit' => '(GMT-5:00) America/Iqaluit (Eastern Standard Time)',
+			'America/Jamaica' => '(GMT-5:00) America/Jamaica (Eastern Standard Time)',
+			'America/Lima' => '(GMT-5:00) America/Lima (Peru Time)',
+			'America/Louisville' => '(GMT-5:00) America/Louisville (Eastern Standard Time)',
+			'America/Montreal' => '(GMT-5:00) America/Montreal (Eastern Standard Time)',
+			'America/Nassau' => '(GMT-5:00) America/Nassau (Eastern Standard Time)',
+			'America/New_York' => '(GMT-5:00) America/New_York (Eastern Standard Time)',
+			'America/Nipigon' => '(GMT-5:00) America/Nipigon (Eastern Standard Time)',
+			'America/Panama' => '(GMT-5:00) America/Panama (Eastern Standard Time)',
+			'America/Pangnirtung' => '(GMT-5:00) America/Pangnirtung (Eastern Standard Time)',
+			'America/Port-au-Prince' => '(GMT-5:00) America/Port-au-Prince (Eastern Standard Time)',
+			'America/Resolute' => '(GMT-5:00) America/Resolute (Eastern Standard Time)',
+			'America/Thunder_Bay' => '(GMT-5:00) America/Thunder_Bay (Eastern Standard Time)',
+			'America/Toronto' => '(GMT-5:00) America/Toronto (Eastern Standard Time)',
+			'Canada/Eastern' => '(GMT-5:00) Canada/Eastern (Eastern Standard Time)',
+			'America/Caracas' => '(GMT-4:-30) America/Caracas (Venezuela Time)',
+			'America/Anguilla' => '(GMT-4:00) America/Anguilla (Atlantic Standard Time)',
+			'America/Antigua' => '(GMT-4:00) America/Antigua (Atlantic Standard Time)',
+			'America/Aruba' => '(GMT-4:00) America/Aruba (Atlantic Standard Time)',
+			'America/Asuncion' => '(GMT-4:00) America/Asuncion (Paraguay Time)',
+			'America/Barbados' => '(GMT-4:00) America/Barbados (Atlantic Standard Time)',
+			'America/Blanc-Sablon' => '(GMT-4:00) America/Blanc-Sablon (Atlantic Standard Time)',
+			'America/Boa_Vista' => '(GMT-4:00) America/Boa_Vista (Amazon Time)',
+			'America/Campo_Grande' => '(GMT-4:00) America/Campo_Grande (Amazon Time)',
+			'America/Cuiaba' => '(GMT-4:00) America/Cuiaba (Amazon Time)',
+			'America/Curacao' => '(GMT-4:00) America/Curacao (Atlantic Standard Time)',
+			'America/Dominica' => '(GMT-4:00) America/Dominica (Atlantic Standard Time)',
+			'America/Eirunepe' => '(GMT-4:00) America/Eirunepe (Amazon Time)',
+			'America/Glace_Bay' => '(GMT-4:00) America/Glace_Bay (Atlantic Standard Time)',
+			'America/Goose_Bay' => '(GMT-4:00) America/Goose_Bay (Atlantic Standard Time)',
+			'America/Grenada' => '(GMT-4:00) America/Grenada (Atlantic Standard Time)',
+			'America/Guadeloupe' => '(GMT-4:00) America/Guadeloupe (Atlantic Standard Time)',
+			'America/Guyana' => '(GMT-4:00) America/Guyana (Guyana Time)',
+			'America/Halifax' => '(GMT-4:00) America/Halifax (Atlantic Standard Time)',
+			'America/La_Paz' => '(GMT-4:00) America/La_Paz (Bolivia Time)',
+			'America/Manaus' => '(GMT-4:00) America/Manaus (Amazon Time)',
+			'America/Marigot' => '(GMT-4:00) America/Marigot (Atlantic Standard Time)',
+			'America/Martinique' => '(GMT-4:00) America/Martinique (Atlantic Standard Time)',
+			'America/Moncton' => '(GMT-4:00) America/Moncton (Atlantic Standard Time)',
+			'America/Montserrat' => '(GMT-4:00) America/Montserrat (Atlantic Standard Time)',
+			'America/Port_of_Spain' => '(GMT-4:00) America/Port_of_Spain (Atlantic Standard Time)',
+			'America/Porto_Acre' => '(GMT-4:00) America/Porto_Acre (Amazon Time)',
+			'America/Porto_Velho' => '(GMT-4:00) America/Porto_Velho (Amazon Time)',
+			'America/Puerto_Rico' => '(GMT-4:00) America/Puerto_Rico (Atlantic Standard Time)',
+			'America/Rio_Branco' => '(GMT-4:00) America/Rio_Branco (Amazon Time)',
+			'America/Santiago' => '(GMT-4:00) America/Santiago (Chile Time)',
+			'America/Santo_Domingo' => '(GMT-4:00) America/Santo_Domingo (Atlantic Standard Time)',
+			'America/St_Barthelemy' => '(GMT-4:00) America/St_Barthelemy (Atlantic Standard Time)',
+			'America/St_Kitts' => '(GMT-4:00) America/St_Kitts (Atlantic Standard Time)',
+			'America/St_Lucia' => '(GMT-4:00) America/St_Lucia (Atlantic Standard Time)',
+			'America/St_Thomas' => '(GMT-4:00) America/St_Thomas (Atlantic Standard Time)',
+			'America/St_Vincent' => '(GMT-4:00) America/St_Vincent (Atlantic Standard Time)',
+			'America/Thule' => '(GMT-4:00) America/Thule (Atlantic Standard Time)',
+			'America/Tortola' => '(GMT-4:00) America/Tortola (Atlantic Standard Time)',
+			'America/Virgin' => '(GMT-4:00) America/Virgin (Atlantic Standard Time)',
+			'Antarctica/Palmer' => '(GMT-4:00) Antarctica/Palmer (Chile Time)',
+			'Atlantic/Bermuda' => '(GMT-4:00) Atlantic/Bermuda (Atlantic Standard Time)',
+			'Atlantic/Stanley' => '(GMT-4:00) Atlantic/Stanley (Falkland Is. Time)',
+			'Brazil/Acre' => '(GMT-4:00) Brazil/Acre (Amazon Time)',
+			'Brazil/West' => '(GMT-4:00) Brazil/West (Amazon Time)',
+			'Canada/Atlantic' => '(GMT-4:00) Canada/Atlantic (Atlantic Standard Time)',
+			'Chile/Continental' => '(GMT-4:00) Chile/Continental (Chile Time)',
+			'America/St_Johns' => '(GMT-3:-30) America/St_Johns (Newfoundland Standard Time)',
+			'Canada/Newfoundland' => '(GMT-3:-30) Canada/Newfoundland (Newfoundland Standard Time)',
+			'America/Araguaina' => '(GMT-3:00) America/Araguaina (Brasilia Time)',
+			'America/Bahia' => '(GMT-3:00) America/Bahia (Brasilia Time)',
+			'America/Belem' => '(GMT-3:00) America/Belem (Brasilia Time)',
+			'America/Buenos_Aires' => '(GMT-3:00) America/Buenos_Aires (Argentine Time)',
+			'America/Catamarca' => '(GMT-3:00) America/Catamarca (Argentine Time)',
+			'America/Cayenne' => '(GMT-3:00) America/Cayenne (French Guiana Time)',
+			'America/Cordoba' => '(GMT-3:00) America/Cordoba (Argentine Time)',
+			'America/Fortaleza' => '(GMT-3:00) America/Fortaleza (Brasilia Time)',
+			'America/Godthab' => '(GMT-3:00) America/Godthab (Western Greenland Time)',
+			'America/Jujuy' => '(GMT-3:00) America/Jujuy (Argentine Time)',
+			'America/Maceio' => '(GMT-3:00) America/Maceio (Brasilia Time)',
+			'America/Mendoza' => '(GMT-3:00) America/Mendoza (Argentine Time)',
+			'America/Miquelon' => '(GMT-3:00) America/Miquelon (Pierre & Miquelon Standard Time)',
+			'America/Montevideo' => '(GMT-3:00) America/Montevideo (Uruguay Time)',
+			'America/Paramaribo' => '(GMT-3:00) America/Paramaribo (Suriname Time)',
+			'America/Recife' => '(GMT-3:00) America/Recife (Brasilia Time)',
+			'America/Rosario' => '(GMT-3:00) America/Rosario (Argentine Time)',
+			'America/Santarem' => '(GMT-3:00) America/Santarem (Brasilia Time)',
+			'America/Sao_Paulo' => '(GMT-3:00) America/Sao_Paulo (Brasilia Time)',
+			'Antarctica/Rothera' => '(GMT-3:00) Antarctica/Rothera (Rothera Time)',
+			'Brazil/East' => '(GMT-3:00) Brazil/East (Brasilia Time)',
+			'America/Noronha' => '(GMT-2:00) America/Noronha (Fernando de Noronha Time)',
+			'Atlantic/South_Georgia' => '(GMT-2:00) Atlantic/South_Georgia (South Georgia Standard Time)',
+			'Brazil/DeNoronha' => '(GMT-2:00) Brazil/DeNoronha (Fernando de Noronha Time)',
+			'America/Scoresbysund' => '(GMT-1:00) America/Scoresbysund (Eastern Greenland Time)',
+			'Atlantic/Azores' => '(GMT-1:00) Atlantic/Azores (Azores Time)',
+			'Atlantic/Cape_Verde' => '(GMT-1:00) Atlantic/Cape_Verde (Cape Verde Time)',
+			'Africa/Abidjan' => '(GMT+0:00) Africa/Abidjan (Greenwich Mean Time)',
+			'Africa/Accra' => '(GMT+0:00) Africa/Accra (Ghana Mean Time)',
+			'Africa/Bamako' => '(GMT+0:00) Africa/Bamako (Greenwich Mean Time)',
+			'Africa/Banjul' => '(GMT+0:00) Africa/Banjul (Greenwich Mean Time)',
+			'Africa/Bissau' => '(GMT+0:00) Africa/Bissau (Greenwich Mean Time)',
+			'Africa/Casablanca' => '(GMT+0:00) Africa/Casablanca (Western European Time)',
+			'Africa/Conakry' => '(GMT+0:00) Africa/Conakry (Greenwich Mean Time)',
+			'Africa/Dakar' => '(GMT+0:00) Africa/Dakar (Greenwich Mean Time)',
+			'Africa/El_Aaiun' => '(GMT+0:00) Africa/El_Aaiun (Western European Time)',
+			'Africa/Freetown' => '(GMT+0:00) Africa/Freetown (Greenwich Mean Time)',
+			'Africa/Lome' => '(GMT+0:00) Africa/Lome (Greenwich Mean Time)',
+			'Africa/Monrovia' => '(GMT+0:00) Africa/Monrovia (Greenwich Mean Time)',
+			'Africa/Nouakchott' => '(GMT+0:00) Africa/Nouakchott (Greenwich Mean Time)',
+			'Africa/Ouagadougou' => '(GMT+0:00) Africa/Ouagadougou (Greenwich Mean Time)',
+			'Africa/Sao_Tome' => '(GMT+0:00) Africa/Sao_Tome (Greenwich Mean Time)',
+			'Africa/Timbuktu' => '(GMT+0:00) Africa/Timbuktu (Greenwich Mean Time)',
+			'America/Danmarkshavn' => '(GMT+0:00) America/Danmarkshavn (Greenwich Mean Time)',
+			'Atlantic/Canary' => '(GMT+0:00) Atlantic/Canary (Western European Time)',
+			'Atlantic/Faeroe' => '(GMT+0:00) Atlantic/Faeroe (Western European Time)',
+			'Atlantic/Faroe' => '(GMT+0:00) Atlantic/Faroe (Western European Time)',
+			'Atlantic/Madeira' => '(GMT+0:00) Atlantic/Madeira (Western European Time)',
+			'Atlantic/Reykjavik' => '(GMT+0:00) Atlantic/Reykjavik (Greenwich Mean Time)',
+			'Atlantic/St_Helena' => '(GMT+0:00) Atlantic/St_Helena (Greenwich Mean Time)',
+			'Europe/Belfast' => '(GMT+0:00) Europe/Belfast (Greenwich Mean Time)',
+			'Europe/Dublin' => '(GMT+0:00) Europe/Dublin (Greenwich Mean Time)',
+			'Europe/Guernsey' => '(GMT+0:00) Europe/Guernsey (Greenwich Mean Time)',
+			'Europe/Isle_of_Man' => '(GMT+0:00) Europe/Isle_of_Man (Greenwich Mean Time)',
+			'Europe/Jersey' => '(GMT+0:00) Europe/Jersey (Greenwich Mean Time)',
+			'Europe/Lisbon' => '(GMT+0:00) Europe/Lisbon (Western European Time)',
+			'Europe/London' => '(GMT+0:00) Europe/London (Greenwich Mean Time)',
+			'Africa/Algiers' => '(GMT+1:00) Africa/Algiers (Central European Time)',
+			'Africa/Bangui' => '(GMT+1:00) Africa/Bangui (Western African Time)',
+			'Africa/Brazzaville' => '(GMT+1:00) Africa/Brazzaville (Western African Time)',
+			'Africa/Ceuta' => '(GMT+1:00) Africa/Ceuta (Central European Time)',
+			'Africa/Douala' => '(GMT+1:00) Africa/Douala (Western African Time)',
+			'Africa/Kinshasa' => '(GMT+1:00) Africa/Kinshasa (Western African Time)',
+			'Africa/Lagos' => '(GMT+1:00) Africa/Lagos (Western African Time)',
+			'Africa/Libreville' => '(GMT+1:00) Africa/Libreville (Western African Time)',
+			'Africa/Luanda' => '(GMT+1:00) Africa/Luanda (Western African Time)',
+			'Africa/Malabo' => '(GMT+1:00) Africa/Malabo (Western African Time)',
+			'Africa/Ndjamena' => '(GMT+1:00) Africa/Ndjamena (Western African Time)',
+			'Africa/Niamey' => '(GMT+1:00) Africa/Niamey (Western African Time)',
+			'Africa/Porto-Novo' => '(GMT+1:00) Africa/Porto-Novo (Western African Time)',
+			'Africa/Tunis' => '(GMT+1:00) Africa/Tunis (Central European Time)',
+			'Africa/Windhoek' => '(GMT+1:00) Africa/Windhoek (Western African Time)',
+			'Arctic/Longyearbyen' => '(GMT+1:00) Arctic/Longyearbyen (Central European Time)',
+			'Atlantic/Jan_Mayen' => '(GMT+1:00) Atlantic/Jan_Mayen (Central European Time)',
+			'Europe/Amsterdam' => '(GMT+1:00) Europe/Amsterdam (Central European Time)',
+			'Europe/Andorra' => '(GMT+1:00) Europe/Andorra (Central European Time)',
+			'Europe/Belgrade' => '(GMT+1:00) Europe/Belgrade (Central European Time)',
+			'Europe/Berlin' => '(GMT+1:00) Europe/Berlin (Central European Time)',
+			'Europe/Bratislava' => '(GMT+1:00) Europe/Bratislava (Central European Time)',
+			'Europe/Brussels' => '(GMT+1:00) Europe/Brussels (Central European Time)',
+			'Europe/Budapest' => '(GMT+1:00) Europe/Budapest (Central European Time)',
+			'Europe/Copenhagen' => '(GMT+1:00) Europe/Copenhagen (Central European Time)',
+			'Europe/Gibraltar' => '(GMT+1:00) Europe/Gibraltar (Central European Time)',
+			'Europe/Ljubljana' => '(GMT+1:00) Europe/Ljubljana (Central European Time)',
+			'Europe/Luxembourg' => '(GMT+1:00) Europe/Luxembourg (Central European Time)',
+			'Europe/Madrid' => '(GMT+1:00) Europe/Madrid (Central European Time)',
+			'Europe/Malta' => '(GMT+1:00) Europe/Malta (Central European Time)',
+			'Europe/Monaco' => '(GMT+1:00) Europe/Monaco (Central European Time)',
+			'Europe/Oslo' => '(GMT+1:00) Europe/Oslo (Central European Time)',
+			'Europe/Paris' => '(GMT+1:00) Europe/Paris (Central European Time)',
+			'Europe/Podgorica' => '(GMT+1:00) Europe/Podgorica (Central European Time)',
+			'Europe/Prague' => '(GMT+1:00) Europe/Prague (Central European Time)',
+			'Europe/Rome' => '(GMT+1:00) Europe/Rome (Central European Time)',
+			'Europe/San_Marino' => '(GMT+1:00) Europe/San_Marino (Central European Time)',
+			'Europe/Sarajevo' => '(GMT+1:00) Europe/Sarajevo (Central European Time)',
+			'Europe/Skopje' => '(GMT+1:00) Europe/Skopje (Central European Time)',
+			'Europe/Stockholm' => '(GMT+1:00) Europe/Stockholm (Central European Time)',
+			'Europe/Tirane' => '(GMT+1:00) Europe/Tirane (Central European Time)',
+			'Europe/Vaduz' => '(GMT+1:00) Europe/Vaduz (Central European Time)',
+			'Europe/Vatican' => '(GMT+1:00) Europe/Vatican (Central European Time)',
+			'Europe/Vienna' => '(GMT+1:00) Europe/Vienna (Central European Time)',
+			'Europe/Warsaw' => '(GMT+1:00) Europe/Warsaw (Central European Time)',
+			'Europe/Zagreb' => '(GMT+1:00) Europe/Zagreb (Central European Time)',
+			'Europe/Zurich' => '(GMT+1:00) Europe/Zurich (Central European Time)',
+			'Africa/Blantyre' => '(GMT+2:00) Africa/Blantyre (Central African Time)',
+			'Africa/Bujumbura' => '(GMT+2:00) Africa/Bujumbura (Central African Time)',
+			'Africa/Cairo' => '(GMT+2:00) Africa/Cairo (Eastern European Time)',
+			'Africa/Gaborone' => '(GMT+2:00) Africa/Gaborone (Central African Time)',
+			'Africa/Harare' => '(GMT+2:00) Africa/Harare (Central African Time)',
+			'Africa/Johannesburg' => '(GMT+2:00) Africa/Johannesburg (South Africa Standard Time)',
+			'Africa/Kigali' => '(GMT+2:00) Africa/Kigali (Central African Time)',
+			'Africa/Lubumbashi' => '(GMT+2:00) Africa/Lubumbashi (Central African Time)',
+			'Africa/Lusaka' => '(GMT+2:00) Africa/Lusaka (Central African Time)',
+			'Africa/Maputo' => '(GMT+2:00) Africa/Maputo (Central African Time)',
+			'Africa/Maseru' => '(GMT+2:00) Africa/Maseru (South Africa Standard Time)',
+			'Africa/Mbabane' => '(GMT+2:00) Africa/Mbabane (South Africa Standard Time)',
+			'Africa/Tripoli' => '(GMT+2:00) Africa/Tripoli (Eastern European Time)',
+			'Asia/Amman' => '(GMT+2:00) Asia/Amman (Eastern European Time)',
+			'Asia/Beirut' => '(GMT+2:00) Asia/Beirut (Eastern European Time)',
+			'Asia/Damascus' => '(GMT+2:00) Asia/Damascus (Eastern European Time)',
+			'Asia/Gaza' => '(GMT+2:00) Asia/Gaza (Eastern European Time)',
+			'Asia/Istanbul' => '(GMT+2:00) Asia/Istanbul (Eastern European Time)',
+			'Asia/Jerusalem' => '(GMT+2:00) Asia/Jerusalem (Israel Standard Time)',
+			'Asia/Nicosia' => '(GMT+2:00) Asia/Nicosia (Eastern European Time)',
+			'Asia/Tel_Aviv' => '(GMT+2:00) Asia/Tel_Aviv (Israel Standard Time)',
+			'Europe/Athens' => '(GMT+2:00) Europe/Athens (Eastern European Time)',
+			'Europe/Bucharest' => '(GMT+2:00) Europe/Bucharest (Eastern European Time)',
+			'Europe/Chisinau' => '(GMT+2:00) Europe/Chisinau (Eastern European Time)',
+			'Europe/Helsinki' => '(GMT+2:00) Europe/Helsinki (Eastern European Time)',
+			'Europe/Istanbul' => '(GMT+2:00) Europe/Istanbul (Eastern European Time)',
+			'Europe/Kaliningrad' => '(GMT+2:00) Europe/Kaliningrad (Eastern European Time)',
+			'Europe/Kiev' => '(GMT+2:00) Europe/Kiev (Eastern European Time)',
+			'Europe/Mariehamn' => '(GMT+2:00) Europe/Mariehamn (Eastern European Time)',
+			'Europe/Minsk' => '(GMT+2:00) Europe/Minsk (Eastern European Time)',
+			'Europe/Nicosia' => '(GMT+2:00) Europe/Nicosia (Eastern European Time)',
+			'Europe/Riga' => '(GMT+2:00) Europe/Riga (Eastern European Time)',
+			'Europe/Simferopol' => '(GMT+2:00) Europe/Simferopol (Eastern European Time)',
+			'Europe/Sofia' => '(GMT+2:00) Europe/Sofia (Eastern European Time)',
+			'Europe/Tallinn' => '(GMT+2:00) Europe/Tallinn (Eastern European Time)',
+			'Europe/Tiraspol' => '(GMT+2:00) Europe/Tiraspol (Eastern European Time)',
+			'Europe/Uzhgorod' => '(GMT+2:00) Europe/Uzhgorod (Eastern European Time)',
+			'Europe/Vilnius' => '(GMT+2:00) Europe/Vilnius (Eastern European Time)',
+			'Europe/Zaporozhye' => '(GMT+2:00) Europe/Zaporozhye (Eastern European Time)',
+			'Africa/Addis_Ababa' => '(GMT+3:00) Africa/Addis_Ababa (Eastern African Time)',
+			'Africa/Asmara' => '(GMT+3:00) Africa/Asmara (Eastern African Time)',
+			'Africa/Asmera' => '(GMT+3:00) Africa/Asmera (Eastern African Time)',
+			'Africa/Dar_es_Salaam' => '(GMT+3:00) Africa/Dar_es_Salaam (Eastern African Time)',
+			'Africa/Djibouti' => '(GMT+3:00) Africa/Djibouti (Eastern African Time)',
+			'Africa/Kampala' => '(GMT+3:00) Africa/Kampala (Eastern African Time)',
+			'Africa/Khartoum' => '(GMT+3:00) Africa/Khartoum (Eastern African Time)',
+			'Africa/Mogadishu' => '(GMT+3:00) Africa/Mogadishu (Eastern African Time)',
+			'Africa/Nairobi' => '(GMT+3:00) Africa/Nairobi (Eastern African Time)',
+			'Antarctica/Syowa' => '(GMT+3:00) Antarctica/Syowa (Syowa Time)',
+			'Asia/Aden' => '(GMT+3:00) Asia/Aden (Arabia Standard Time)',
+			'Asia/Baghdad' => '(GMT+3:00) Asia/Baghdad (Arabia Standard Time)',
+			'Asia/Bahrain' => '(GMT+3:00) Asia/Bahrain (Arabia Standard Time)',
+			'Asia/Kuwait' => '(GMT+3:00) Asia/Kuwait (Arabia Standard Time)',
+			'Asia/Qatar' => '(GMT+3:00) Asia/Qatar (Arabia Standard Time)',
+			'Europe/Moscow' => '(GMT+3:00) Europe/Moscow (Moscow Standard Time)',
+			'Europe/Volgograd' => '(GMT+3:00) Europe/Volgograd (Volgograd Time)',
+			'Indian/Antananarivo' => '(GMT+3:00) Indian/Antananarivo (Eastern African Time)',
+			'Indian/Comoro' => '(GMT+3:00) Indian/Comoro (Eastern African Time)',
+			'Indian/Mayotte' => '(GMT+3:00) Indian/Mayotte (Eastern African Time)',
+			'Asia/Tehran' => '(GMT+3:30) Asia/Tehran (Iran Standard Time)',
+			'Asia/Baku' => '(GMT+4:00) Asia/Baku (Azerbaijan Time)',
+			'Asia/Dubai' => '(GMT+4:00) Asia/Dubai (Gulf Standard Time)',
+			'Asia/Muscat' => '(GMT+4:00) Asia/Muscat (Gulf Standard Time)',
+			'Asia/Tbilisi' => '(GMT+4:00) Asia/Tbilisi (Georgia Time)',
+			'Asia/Yerevan' => '(GMT+4:00) Asia/Yerevan (Armenia Time)',
+			'Europe/Samara' => '(GMT+4:00) Europe/Samara (Samara Time)',
+			'Indian/Mahe' => '(GMT+4:00) Indian/Mahe (Seychelles Time)',
+			'Indian/Mauritius' => '(GMT+4:00) Indian/Mauritius (Mauritius Time)',
+			'Indian/Reunion' => '(GMT+4:00) Indian/Reunion (Reunion Time)',
+			'Asia/Kabul' => '(GMT+4:30) Asia/Kabul (Afghanistan Time)',
+			'Asia/Aqtau' => '(GMT+5:00) Asia/Aqtau (Aqtau Time)',
+			'Asia/Aqtobe' => '(GMT+5:00) Asia/Aqtobe (Aqtobe Time)',
+			'Asia/Ashgabat' => '(GMT+5:00) Asia/Ashgabat (Turkmenistan Time)',
+			'Asia/Ashkhabad' => '(GMT+5:00) Asia/Ashkhabad (Turkmenistan Time)',
+			'Asia/Dushanbe' => '(GMT+5:00) Asia/Dushanbe (Tajikistan Time)',
+			'Asia/Karachi' => '(GMT+5:00) Asia/Karachi (Pakistan Time)',
+			'Asia/Oral' => '(GMT+5:00) Asia/Oral (Oral Time)',
+			'Asia/Samarkand' => '(GMT+5:00) Asia/Samarkand (Uzbekistan Time)',
+			'Asia/Tashkent' => '(GMT+5:00) Asia/Tashkent (Uzbekistan Time)',
+			'Asia/Yekaterinburg' => '(GMT+5:00) Asia/Yekaterinburg (Yekaterinburg Time)',
+			'Indian/Kerguelen' => '(GMT+5:00) Indian/Kerguelen (French Southern & Antarctic Lands Time)',
+			'Indian/Maldives' => '(GMT+5:00) Indian/Maldives (Maldives Time)',
+			'Asia/Calcutta' => '(GMT+5:30) Asia/Calcutta (India Standard Time)',
+			'Asia/Colombo' => '(GMT+5:30) Asia/Colombo (India Standard Time)',
+			'Asia/Kolkata' => '(GMT+5:30) Asia/Kolkata (India Standard Time)',
+			'Asia/Katmandu' => '(GMT+5:45) Asia/Katmandu (Nepal Time)',
+			'Antarctica/Mawson' => '(GMT+6:00) Antarctica/Mawson (Mawson Time)',
+			'Antarctica/Vostok' => '(GMT+6:00) Antarctica/Vostok (Vostok Time)',
+			'Asia/Almaty' => '(GMT+6:00) Asia/Almaty (Alma-Ata Time)',
+			'Asia/Bishkek' => '(GMT+6:00) Asia/Bishkek (Kirgizstan Time)',
+			'Asia/Dacca' => '(GMT+6:00) Asia/Dacca (Bangladesh Time)',
+			'Asia/Dhaka' => '(GMT+6:00) Asia/Dhaka (Bangladesh Time)',
+			'Asia/Novosibirsk' => '(GMT+6:00) Asia/Novosibirsk (Novosibirsk Time)',
+			'Asia/Omsk' => '(GMT+6:00) Asia/Omsk (Omsk Time)',
+			'Asia/Qyzylorda' => '(GMT+6:00) Asia/Qyzylorda (Qyzylorda Time)',
+			'Asia/Thimbu' => '(GMT+6:00) Asia/Thimbu (Bhutan Time)',
+			'Asia/Thimphu' => '(GMT+6:00) Asia/Thimphu (Bhutan Time)',
+			'Indian/Chagos' => '(GMT+6:00) Indian/Chagos (Indian Ocean Territory Time)',
+			'Asia/Rangoon' => '(GMT+6:30) Asia/Rangoon (Myanmar Time)',
+			'Indian/Cocos' => '(GMT+6:30) Indian/Cocos (Cocos Islands Time)',
+			'Antarctica/Davis' => '(GMT+7:00) Antarctica/Davis (Davis Time)',
+			'Asia/Bangkok' => '(GMT+7:00) Asia/Bangkok (Indochina Time)',
+			'Asia/Ho_Chi_Minh' => '(GMT+7:00) Asia/Ho_Chi_Minh (Indochina Time)',
+			'Asia/Hovd' => '(GMT+7:00) Asia/Hovd (Hovd Time)',
+			'Asia/Jakarta' => '(GMT+7:00) Asia/Jakarta (West Indonesia Time)',
+			'Asia/Krasnoyarsk' => '(GMT+7:00) Asia/Krasnoyarsk (Krasnoyarsk Time)',
+			'Asia/Phnom_Penh' => '(GMT+7:00) Asia/Phnom_Penh (Indochina Time)',
+			'Asia/Pontianak' => '(GMT+7:00) Asia/Pontianak (West Indonesia Time)',
+			'Asia/Saigon' => '(GMT+7:00) Asia/Saigon (Indochina Time)',
+			'Asia/Vientiane' => '(GMT+7:00) Asia/Vientiane (Indochina Time)',
+			'Indian/Christmas' => '(GMT+7:00) Indian/Christmas (Christmas Island Time)',
+			'Antarctica/Casey' => '(GMT+8:00) Antarctica/Casey (Western Standard Time (Australia))',
+			'Asia/Brunei' => '(GMT+8:00) Asia/Brunei (Brunei Time)',
+			'Asia/Choibalsan' => '(GMT+8:00) Asia/Choibalsan (Choibalsan Time)',
+			'Asia/Chongqing' => '(GMT+8:00) Asia/Chongqing (China Standard Time)',
+			'Asia/Chungking' => '(GMT+8:00) Asia/Chungking (China Standard Time)',
+			'Asia/Harbin' => '(GMT+8:00) Asia/Harbin (China Standard Time)',
+			'Asia/Hong_Kong' => '(GMT+8:00) Asia/Hong_Kong (Hong Kong Time)',
+			'Asia/Irkutsk' => '(GMT+8:00) Asia/Irkutsk (Irkutsk Time)',
+			'Asia/Kashgar' => '(GMT+8:00) Asia/Kashgar (China Standard Time)',
+			'Asia/Kuala_Lumpur' => '(GMT+8:00) Asia/Kuala_Lumpur (Malaysia Time)',
+			'Asia/Kuching' => '(GMT+8:00) Asia/Kuching (Malaysia Time)',
+			'Asia/Macao' => '(GMT+8:00) Asia/Macao (China Standard Time)',
+			'Asia/Macau' => '(GMT+8:00) Asia/Macau (China Standard Time)',
+			'Asia/Makassar' => '(GMT+8:00) Asia/Makassar (Central Indonesia Time)',
+			'Asia/Manila' => '(GMT+8:00) Asia/Manila (Philippines Time)',
+			'Asia/Shanghai' => '(GMT+8:00) Asia/Shanghai (China Standard Time)',
+			'Asia/Singapore' => '(GMT+8:00) Asia/Singapore (Singapore Time)',
+			'Asia/Taipei' => '(GMT+8:00) Asia/Taipei (China Standard Time)',
+			'Asia/Ujung_Pandang' => '(GMT+8:00) Asia/Ujung_Pandang (Central Indonesia Time)',
+			'Asia/Ulaanbaatar' => '(GMT+8:00) Asia/Ulaanbaatar (Ulaanbaatar Time)',
+			'Asia/Ulan_Bator' => '(GMT+8:00) Asia/Ulan_Bator (Ulaanbaatar Time)',
+			'Asia/Urumqi' => '(GMT+8:00) Asia/Urumqi (China Standard Time)',
+			'Australia/Perth' => '(GMT+8:00) Australia/Perth (Western Standard Time (Australia))',
+			'Australia/West' => '(GMT+8:00) Australia/West (Western Standard Time (Australia))',
+			'Australia/Eucla' => '(GMT+8:45) Australia/Eucla (Central Western Standard Time (Australia))',
+			'Asia/Dili' => '(GMT+9:00) Asia/Dili (Timor-Leste Time)',
+			'Asia/Jayapura' => '(GMT+9:00) Asia/Jayapura (East Indonesia Time)',
+			'Asia/Pyongyang' => '(GMT+9:00) Asia/Pyongyang (Korea Standard Time)',
+			'Asia/Seoul' => '(GMT+9:00) Asia/Seoul (Korea Standard Time)',
+			'Asia/Tokyo' => '(GMT+9:00) Asia/Tokyo (Japan Standard Time)',
+			'Asia/Yakutsk' => '(GMT+9:00) Asia/Yakutsk (Yakutsk Time)',
+			'Australia/Adelaide' => '(GMT+9:30) Australia/Adelaide (Central Standard Time (South Australia))',
+			'Australia/Broken_Hill' => '(GMT+9:30) Australia/Broken_Hill (Central Standard Time (South Australia/New South Wales))',
+			'Australia/Darwin' => '(GMT+9:30) Australia/Darwin (Central Standard Time (Northern Territory))',
+			'Australia/North' => '(GMT+9:30) Australia/North (Central Standard Time (Northern Territory))',
+			'Australia/South' => '(GMT+9:30) Australia/South (Central Standard Time (South Australia))',
+			'Australia/Yancowinna' => '(GMT+9:30) Australia/Yancowinna (Central Standard Time (South Australia/New South Wales))',
+			'Antarctica/DumontDUrville' => '(GMT+10:00) Antarctica/DumontDUrville (Dumont-d\'Urville Time)',
+			'Asia/Sakhalin' => '(GMT+10:00) Asia/Sakhalin (Sakhalin Time)',
+			'Asia/Vladivostok' => '(GMT+10:00) Asia/Vladivostok (Vladivostok Time)',
+			'Australia/ACT' => '(GMT+10:00) Australia/ACT (Eastern Standard Time (New South Wales))',
+			'Australia/Brisbane' => '(GMT+10:00) Australia/Brisbane (Eastern Standard Time (Queensland))',
+			'Australia/Canberra' => '(GMT+10:00) Australia/Canberra (Eastern Standard Time (New South Wales))',
+			'Australia/Currie' => '(GMT+10:00) Australia/Currie (Eastern Standard Time (New South Wales))',
+			'Australia/Hobart' => '(GMT+10:00) Australia/Hobart (Eastern Standard Time (Tasmania))',
+			'Australia/Lindeman' => '(GMT+10:00) Australia/Lindeman (Eastern Standard Time (Queensland))',
+			'Australia/Melbourne' => '(GMT+10:00) Australia/Melbourne (Eastern Standard Time (Victoria))',
+			'Australia/NSW' => '(GMT+10:00) Australia/NSW (Eastern Standard Time (New South Wales))',
+			'Australia/Queensland' => '(GMT+10:00) Australia/Queensland (Eastern Standard Time (Queensland))',
+			'Australia/Sydney' => '(GMT+10:00) Australia/Sydney (Eastern Standard Time (New South Wales))',
+			'Australia/Tasmania' => '(GMT+10:00) Australia/Tasmania (Eastern Standard Time (Tasmania))',
+			'Australia/Victoria' => '(GMT+10:00) Australia/Victoria (Eastern Standard Time (Victoria))',
+			'Australia/LHI' => '(GMT+10:30) Australia/LHI (Lord Howe Standard Time)',
+			'Australia/Lord_Howe' => '(GMT+10:30) Australia/Lord_Howe (Lord Howe Standard Time)',
+			'Asia/Magadan' => '(GMT+11:00) Asia/Magadan (Magadan Time)',
+			'Antarctica/McMurdo' => '(GMT+12:00) Antarctica/McMurdo (New Zealand Standard Time)',
+			'Antarctica/South_Pole' => '(GMT+12:00) Antarctica/South_Pole (New Zealand Standard Time)',
+			'Asia/Anadyr' => '(GMT+12:00) Asia/Anadyr (Anadyr Time)',
+			'Asia/Kamchatka' => '(GMT+12:00) Asia/Kamchatka (Petropavlovsk-Kamchatski Time)',
+		);
+		return $timezones;
+	}
+
+}

+ 240 - 0
src/View/Helper/TimelineHelper.php

@@ -0,0 +1,240 @@
+<?php
+
+namespace Tools\View\Helper;
+
+use Cake\View\View;
+use Cake\View\Helper;
+use Cake\Utility\Hash;
+
+/**
+ * TimelineHelper for easy output of a timeline with multiple items.
+ *
+ * You need to include your css and js file, manually:
+ *
+ *   echo $this->Html->script('timeline/timeline');
+ *   echo $this->Html->css('/js/timeline/timeline');
+ *
+ * @link http://almende.github.io/chap-links-library/timeline.html
+ * @author Mark Scherer
+ * @license MIT
+ */
+class TimelineHelper extends Helper {
+
+	public $helpers = array('Tools.Js');
+
+	protected $_defaultConfig = array(
+		'id' => 'mytimeline',
+		'selectable' => false,
+		'editable' => false,
+		'min' => null, // Min date.
+		'max' => null, // Max date.
+		'width' => '100%',
+		'height' => null, // Auto.
+		'style' => 'box',
+		'current' => null, // Current time.
+	);
+
+	protected $_items = array();
+
+
+	/**
+	 * Apply settings and merge them with the defaults.
+	 *
+	 * Possible values are (with their default values):
+	 *  - 'min',
+	 *  - 'max',
+	 *  - 'width'
+	 *  - 'height'
+	 *  - 'minHeight'
+	 *  - 'selectable' => false,
+	 *  - 'editable' => false,
+	 *  - 'moveable' => true
+	 *  - 'animate' => true,
+	 *  - 'animateZoom' => true,
+	 *  - 'axisOnTop' => false,
+	 *  - 'cluster' => false
+	 *  - 'locale' (string)
+	 *  - 'style' (string)
+	 *  - ...
+	 *
+	 * @link http://almende.github.io/chap-links-library/js/timeline/doc/
+	 * @param array $settings Key value pairs to merge with current settings.
+	 * @return void
+	 * @deprecated
+	 */
+	public function settings($settings) {
+		$this->config($settings);
+	}
+
+	/**
+	 * Add timeline item.
+	 *
+	 * Requires at least:
+	 * - start (date or datetime)
+	 * - content (string)
+	 * Further data options:
+	 * - end (date or datetime)
+	 * - group (string)
+	 * - className (string)
+	 * - editable (boolean)
+	 *
+	 * @link http://almende.github.io/chap-links-library/js/timeline/doc/
+	 * @param array
+	 * @return void
+	 */
+	public function addItem($item) {
+		$this->_items[] = $item;
+	}
+
+	/**
+	 * Add timeline items as an array of items.
+	 *
+	 * @see TimelineHelper::addItem()
+	 * @return void
+	 */
+	public function addItems($items) {
+		foreach ($items as $item) {
+			$this->_items[] = $item;
+		}
+	}
+
+	/**
+	 * Finalize the timeline and write the javascript to the buffer.
+	 * Make sure that your view does also output the buffer at some place!
+	 *
+	 * @param bool $return If the output should be returned instead
+	 * @return void or string Javascript if $return is true
+	 */
+	public function finalize($return = false) {
+		$settings = $this->config();
+		$timelineId = $settings['id'];
+		$data = $this->_format($this->_items);
+
+		$current = '';
+		if ($settings['current']) {
+			$dateString = date('Y-m-d H:i:s', time());
+			$current = 'timeline.setCurrentTime(' . $this->_date($dateString) . ');';
+		}
+		unset($settings['id']);
+		unset($settings['current']);
+		$options = $this->_options($settings);
+
+		$script = <<<JS
+var timeline;
+var data;
+var options;
+
+// Called when the Visualization API is loaded.
+function drawVisualization() {
+	// Create a JSON data table
+	data = $data
+	options = $options
+
+	// Instantiate our timeline object.
+	timeline = new links.Timeline(document.getElementById('$timelineId'));
+
+	// Draw our timeline with the created data and options
+	timeline.draw(data, options);
+	$current
+}
+
+drawVisualization();
+JS;
+		if ($return) {
+			return $script;
+		}
+		$this->Js->buffer($script);
+	}
+
+	/**
+	 * Format options to JS code
+	 *
+	 * @param array $options
+	 * @return string
+	 */
+	protected function _options($options) {
+		$e = array();
+		foreach ($options as $option => $value) {
+			if ($value === null) {
+				continue;
+			}
+			if (is_string($value)) {
+				$value = '\'' . $value . '\'';
+			} elseif (is_object($value)) { // Datetime?
+				$value = $this->_date($value);
+			} elseif (is_bool($value)) {
+				$value = $value ? 'true' : 'false';
+			} else {
+				$value = str_replace('\'', '\\\'', $value);
+			}
+			$e[] = '\'' . $option . '\': ' . $value;
+		}
+		$string = '{' . PHP_EOL . "\t" . implode(',' . PHP_EOL . "\t", $e) . PHP_EOL . '}';
+		return $string;
+	}
+
+	/**
+	 * Format items to JS code
+	 *
+	 * @see TimelineHelper::addItem()
+	 * @param array $items
+	 * @return string
+	 */
+	protected function _format($items) {
+		$e = array();
+		foreach ($items as $item) {
+			$tmp = array();
+			foreach ($item as $key => $row) {
+				switch ($key) {
+					case 'editable':
+						$tmp[] = $row ? 'true' : 'false';
+						break;
+					case 'start':
+					case 'end':
+						$tmp[] = '\'' . $key . '\': ' . $this->_date($row);
+						break;
+					default:
+						$tmp[] = '\'' . $key . '\': \'' . str_replace('\'', '\\\'', $row) . '\'';
+				}
+			}
+			$e[] = '{' . implode(',' . PHP_EOL, $tmp) . '}';
+		}
+		$string = '[' . implode(',' . PHP_EOL, $e) . '];';
+		return $string;
+	}
+
+	/**
+	 * Format date to JS code.
+	 *
+	 * @param string|DateTime $date
+	 * @return string
+	 */
+	protected function _date($date) {
+		if (is_object($date)) {
+			// Datetime?
+			$datePieces = array();
+			$datePieces[] = $date->format('Y');
+			// JavaScript uses 0-indexed months, so we need to subtract 1 month from PHP's output
+			$datePieces[] = (int)($date->format('m') - 1);
+			$datePieces[] = (int)$date->format('d');
+			$datePieces[] = (int)$date->format('H');
+			$datePieces[] = (int)$date->format('i');
+			$datePieces[] = (int)$date->format('s');
+		} else {
+			// As string (fallback).
+			$dateTime = explode(' ', $date, 2);
+			$datePieces = array();
+			$datePieces[] = substr($dateTime[0], 0, 4);
+			// JavaScript uses 0-indexed months, so we need to subtract 1 month from the output
+			$datePieces[] = (int)(substr($dateTime[0], 5, 2) - 1);
+			$datePieces[] = (int)substr($dateTime[0], 8, 2);
+			if (!empty($dateTime[1])) {
+				$datePieces[] = (int)substr($dateTime[1], 0, 2);
+				$datePieces[] = (int)substr($dateTime[1], 3, 2);
+				$datePieces[] = (int)substr($dateTime[1], 6, 2);
+			}
+		}
+		return 'new Date(' . implode(', ', $datePieces) . ')';
+	}
+
+}