PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/tine20/library/TimeZoneConvert/lib/TimeZoneConvert/VTimeZone.php

https://gitlab.com/rsilveira1987/Expresso
PHP | 389 lines | 227 code | 56 blank | 106 comment | 35 complexity | 5ee97ff961439b656ab47e3bd5df022f MD5 | raw file
  1. <?php
  2. /**
  3. * TimeZoneConvert
  4. *
  5. * @package TimeZoneConvert
  6. * @subpackage VTimeZone
  7. * @license MIT, BSD, and GPL
  8. * @copyright Copyright (c) 2012 Metaways Infosystems GmbH (http://www.metaways.de)
  9. * @author Cornelius Weiß <c.weiss@metaways.de>
  10. */
  11. /**
  12. * convert vtimezone to php timezone
  13. * http://tools.ietf.org/html/rfc5545#section-3.6.5
  14. *
  15. */
  16. class TimeZoneConvert_VTimeZone
  17. {
  18. const EOL = "\r\n";
  19. /**
  20. * gets php's DateTimeZone identifier from given VTIMEZONE and optional prodid
  21. *
  22. * @param string $VTimeZone
  23. * @param string $prodId
  24. * @param string $expectedTimezone
  25. * @return string
  26. */
  27. public function getTZIdentifier($VTimeZone, $prodId='', $expectedTimezoneId=NULL)
  28. {
  29. try {
  30. // known prodid/vtimezone -> match
  31. if ($timezone = TimeZoneConvert_VTimeZone_ChamberOfHorrors::getMatch($VTimeZone, $prodId)) {
  32. return $timezone;
  33. }
  34. // well known tz name -> match
  35. if ($timezone = $this->matchByName($VTimeZone)) {
  36. return $timezone;
  37. }
  38. // compute timezone
  39. $timezone = $this->computeTimezone($VTimeZone, $expectedTimezoneId=NULL);
  40. return $timezone->getName();
  41. } catch (Exception $e) {
  42. return NULL;
  43. }
  44. }
  45. /**
  46. * gets vtimezone string from php timezone identifier
  47. *
  48. * @param string $tzid
  49. * @param DateTime $from
  50. * @param DateTime $until
  51. * @return string
  52. */
  53. public function getVTimezone($tzid, $from=NULL, $until=NULL)
  54. {
  55. $VTimeZone = 'BEGIN:VTIMEZONE' . self::EOL;
  56. $VTimeZone .= 'TZID:' . $tzid . self::EOL;
  57. $from = $from ?: date_create('now', new DateTimeZone('UTC'));
  58. $timezone = new DateTimeZone($tzid);
  59. $transitions = TimeZoneConvert_Transition::getTransitions($timezone, $from, $until);
  60. $splitedTransitions = array(
  61. 'DAYLIGHT' => $transitions->filter('isdst', TRUE),
  62. 'STANDART' => $transitions->filter('isdst', FALSE),
  63. );
  64. foreach($splitedTransitions as $component => $transitions) {
  65. // don't add component when there are no transitions
  66. if (count ($transitions) == 0) continue;
  67. // check if rrule is OK: compute recurring rule and check all transitions
  68. $useRrule = TRUE;
  69. $transitionRule = TimeZoneConvert_TransitionRule::createFromTransition($transitions->getFirst());
  70. foreach($transitions as $transition) {
  71. $expectedTransitionDate = $transitionRule->computeTransitionDate(substr($transition['time'], 0, 4));
  72. if ($expectedTransitionDate->format(DateTime::ISO8601) != $transition['time']) {
  73. $useRrule = FALSE;
  74. break;
  75. }
  76. }
  77. // insert via rrule
  78. if ($useRrule) {
  79. // try to find rrule start
  80. $backTransitions = TimeZoneConvert_Transition::getTransitions($timezone, NULL, $transitionRule->from)
  81. ->filter('isdst', $transitionRule->isdst)
  82. ->sort('time', 'DESC');
  83. foreach ($backTransitions as $backTransition) {
  84. $expectedTransitionDate = $transitionRule->computeTransitionDate(substr($backTransition['time'], 0, 4));
  85. if ($expectedTransitionDate->format(DateTime::ISO8601) != $backTransition['time']) {
  86. break;
  87. }
  88. $transitionRule->from = $expectedTransitionDate;
  89. }
  90. }
  91. // insert rdates
  92. else {
  93. $transitionRule = TimeZoneConvert_TransitionRule::createFromTransition($transitions->getFirst(), FALSE);
  94. $transitionRule->clearTransitionDates();
  95. foreach($transitions as $transition) {
  96. $transitionRule->addTransitionDate(new DateTime($transition['time'], new DateTimeZone('UTC')));
  97. }
  98. }
  99. // compute offsetFrom
  100. $offsetFromDate = clone $transitionRule->from;
  101. $offsetFromDate->setTimezone($timezone);
  102. $offsetFromDate->modify("-1 day");
  103. $offsetFrom = $offsetFromDate->getOffset();
  104. $VTimeZone .= $this->transitionRuleToVTransitionRule($transitionRule, $offsetFrom);
  105. }
  106. $VTimeZone .= 'END:VTIMEZONE' . self::EOL;
  107. return $VTimeZone;
  108. }
  109. /**
  110. * tries to match a timezone by name from
  111. * TZID
  112. * X-LIC-LOCATION
  113. * X-MICROSOFT-CDO-TZID
  114. *
  115. * @param string $VTimeZone
  116. * @return string
  117. */
  118. public function matchByName($VTimeZone)
  119. {
  120. $phpTZIDs = DateTimeZone::listIdentifiers();
  121. if (preg_match('/TZID:(.*)/', $VTimeZone, $tzid)) {
  122. // php timezones
  123. if (in_array($tzid[1], $phpTZIDs)) {
  124. return $tzid[1];
  125. }
  126. // known/m$ timezones
  127. if (array_key_exists($tzid[1], TimeZoneConvert_KnownNamesMap::$map)) {
  128. return TimeZoneConvert_KnownNamesMap::$map[$tzid[1]];
  129. }
  130. }
  131. // eventually an X-LIC-LOCATION is included
  132. if (preg_match('/X-LIC-LOCATION:(.*)/', $VTimeZone, $tzid)) {
  133. // php timezones
  134. if (in_array($tzid[1], $phpTZIDs)) {
  135. return $tzid[1];
  136. }
  137. }
  138. // X-MICROSOFT-CDO-TZID (Exchange)
  139. if (preg_match('/X-MICROSOFT-CDO-TZID:(.*)/', $VTimeZone, $tzid)) {
  140. if (array_key_exists($tzid[1], TimeZoneConvert_KnownNamesMap::$microsoftExchangeMap)) {
  141. return TimeZoneConvert_KnownNamesMap::$microsoftExchangeMap[$tzid[1]];
  142. }
  143. }
  144. }
  145. /**
  146. * compute timezone from vtimezone
  147. *
  148. * @param string $VTimeZone
  149. * @throws TimeZoneConvert_Exception
  150. */
  151. public function computeTimezone($VTimeZone, $expectedTimezoneId=NULL)
  152. {
  153. // get transition rules
  154. $transitionRules = $this->getTransitionRules($VTimeZone);
  155. // get transitions
  156. $transitions = $this->getTransitions($transitionRules);
  157. $expectedTimezone = $expectedTimezoneId ? new DateTimeZone($expectedTimezoneId) : NULL;
  158. $timezone = TimeZoneConvert_Transition::getMatchingTimezone($transitions, $expectedTimezone);
  159. if (! $timezone instanceof DateTimeZone) {
  160. throw new TimeZoneConvert_Exception('no timezone matched');
  161. }
  162. return $timezone;
  163. }
  164. /**
  165. * compute transitions from transition rules
  166. *
  167. * @param TimeZoneConvert_Set $transitionRules
  168. * @return TimeZoneConvert_Set
  169. */
  170. public function getTransitions($transitionRules)
  171. {
  172. $transitions = new TimeZoneConvert_Set();
  173. foreach($transitionRules as $transitionRule) {
  174. if ($transitionRule->isRecurringRule()) {
  175. $transitionDates = array();
  176. $startYear = $transitionRule->from->format('Y');
  177. // NOTE: buggy clients such as lightning always start 1970 (start of unix timestamp)
  178. // we can't take those transitions
  179. if (! in_array($startYear, array('1970', '1601')) || count($transitionRules) != 2) {
  180. $transitionDates[] = $transitionRule->computeTransitionDate($startYear);
  181. // for ($i=1;$i<20;$i++) {
  182. // $transitionDates[] = $transitionRule->computeTransitionDate($startYear + $i);
  183. // }
  184. }
  185. $until = $transitionRule->until ? $transitionRule->until : new DateTime('now', new DateTimeZone('UTC'));
  186. $transitionDates[] = $transitionRule->computeTransitionDate($until->format('Y'));
  187. // if (! $transitionRule->until) {
  188. // for ($i=1;$i<10;$i++) {
  189. // $transitionDates[] = $transitionRule->computeTransitionDate($until->format('Y') + $i);
  190. // }
  191. // }
  192. } else {
  193. // for rules having rdates/no ruule, take all rdates/dtstart
  194. $transitionDates = $transitionRule->getTransitionDates();
  195. }
  196. // create transitions
  197. foreach($transitionDates as $transitionDate) {
  198. $transitions->addModel(new TimeZoneConvert_Transition(array(
  199. 'ts' => $transitionDate->getTimestamp(),
  200. 'date' => $transitionDate->format(DateTime::ISO8601),
  201. 'offset' => $transitionRule->offset,
  202. 'isdst' => $transitionRule->isdst,
  203. 'abbr' => $transitionRule->abbr,
  204. )));
  205. }
  206. }
  207. return $transitions;
  208. }
  209. /**
  210. * get all transition rules from given vtimezone
  211. *
  212. * @param string $VTimeZone
  213. * @return array of TimeZoneConvert_TransitionRule
  214. */
  215. public function getTransitionRules($VTimeZone)
  216. {
  217. $transitionRules = array();
  218. preg_match_all('/BEGIN:(?:STANDARD|DAYLIGHT)(?:.|\s)*END:(?:STANDARD|DAYLIGHT)/U', $VTimeZone, $vTransitionRules);
  219. foreach((array) $vTransitionRules[0] as $vTransitionRule) {
  220. $transitionRules[] = $this->vTransitionRuleToTransitionRule($vTransitionRule);
  221. }
  222. return $transitionRules;
  223. }
  224. /**
  225. * convert a single vTransitionRule
  226. *
  227. * @param string $vTransitionRule
  228. * @return TimeZoneConvert_TransitionRule
  229. */
  230. public function vTransitionRuleToTransitionRule($vTransitionRule)
  231. {
  232. // parse mandentory DTSTART
  233. if (! preg_match('/DTSTART:(.*)/', $vTransitionRule, $dtstart)) {
  234. throw new TimeZoneConvert_Exception('DTSTART missing');
  235. }
  236. $dtstart = new DateTime($dtstart[1], new DateTimeZone('UTC'));
  237. if ($dtstart === FALSE) {
  238. throw new TimeZoneConvert_Exception('could not parse dtstart');
  239. }
  240. // parse mandentory TZOFFSETFROM
  241. if (! preg_match('/TZOFFSETFROM:(.*)/', $vTransitionRule, $offsetFrom)) {
  242. throw new TimeZoneConvert_Exception('TZOFFSETFROM missing');
  243. }
  244. $invertedOffsetFromSign = $offsetFrom[1][0] == '-' ? '+' : '-';
  245. $offsetFromSeconds = substr($offsetFrom[1], -4, -2) * 3600 + substr($offsetFrom[1], -2) * 60;
  246. $dtstart->modify("$invertedOffsetFromSign $offsetFromSeconds seconds");
  247. // parse TZOFFSETTO
  248. if (preg_match('/TZOFFSETTO:(.*)/', $vTransitionRule, $offsetTo)) {
  249. $offsetToSign = $offsetTo[1][0] == '-' ? '-' : '+';
  250. $offsetToSeconds = substr($offsetTo[1], -4, -2) * 3600 + substr($offsetTo[1], -2) * 60;
  251. }
  252. $offsetTo = $offsetTo ? ($offsetToSign . '1') * $offsetToSeconds : 0;
  253. // parse TZNAME
  254. $abbr = preg_match('/TZNAME:(.*)/', $vTransitionRule, $abbr) ? $abbr[1] : '';
  255. $isdst = (bool) preg_match('/BEGIN:DAYLIGHT/', $vTransitionRule);
  256. $transitionRule = new TimeZoneConvert_TransitionRule(array(
  257. 'from' => $dtstart,
  258. 'offset' => $offsetTo,
  259. 'isdst' => $isdst,
  260. 'abbr' => $abbr,
  261. ));
  262. // transitions by RRULE
  263. if (preg_match('/RRULE:(.*)/', $vTransitionRule, $rrule)) {
  264. $rrule = TimeZoneConvert_VTimeZone_Rrule::createFromString($rrule[1]);
  265. $transitionRule->append(array(
  266. 'hour' => (int) $dtstart->format('G'),
  267. 'minute' => (int) $dtstart->format('i'),
  268. 'second' => (int) $dtstart->format('s'),
  269. 'month' => $rrule->month,
  270. 'wkday' => $rrule->wkday,
  271. 'numwk' => $rrule->numwk,
  272. 'until' => $rrule->until,
  273. ));
  274. }
  275. // transitions by RDATE
  276. else if (preg_match('/RDATE.*:(?:\d{8}T\d{6}[^0-9A-Z]*)+/', $vTransitionRule, $rdate)) {
  277. preg_match_all('/(?:\d{8}T\d{6}[^0-9A-Z]*)+/U', $rdate[0], $rdates);
  278. foreach((array) $rdates[0] as $transitionDateString) {
  279. $transitionDate = new DateTime($transitionDateString, new DateTimeZone('UTC'));
  280. // echo $offsetFrom . "\n";
  281. $transitionDate->modify("$invertedOffsetFromSign $offsetFromSeconds seconds");
  282. $transitionRule->addTransitionDate($transitionDate);
  283. }
  284. }
  285. // single transition
  286. else {
  287. $transitionRule->until = $dtstart;
  288. $transitionRule->addTransitionDate($dtstart);
  289. }
  290. return $transitionRule;
  291. }
  292. /**
  293. * assembles VTransitionRule
  294. *
  295. * @param TimeZoneConvert_TransitionRule $transitionRule
  296. * @param int $offsetFrom
  297. * @return string
  298. */
  299. public function transitionRuleToVTransitionRule($transitionRule, $offsetFrom)
  300. {
  301. $zone = $transitionRule->isdst ? 'DAYLIGHT' : 'STANDARD';
  302. $dtstart = clone $transitionRule->from;
  303. $offsetFromSign = $offsetFrom >=0 ? '+' : '-';
  304. $offsetFromString = $offsetFromSign .
  305. str_pad(floor(abs($offsetFrom)/3600), 2, '0', STR_PAD_LEFT) .
  306. str_pad((abs($offsetFrom)%3600)/60, 2, '0', STR_PAD_LEFT);
  307. $offsetToSign = $transitionRule->offset >=0 ? '+' : '-';
  308. $offsetToString = $offsetToSign .
  309. str_pad(floor(abs($transitionRule->offset)/3600), 2, '0', STR_PAD_LEFT) .
  310. str_pad((abs($transitionRule->offset)%3600)/60, 2, '0', STR_PAD_LEFT);
  311. $dtstart = $dtstart->modify("$offsetFrom seconds");
  312. if ($transitionRule->isRecurringRule()) {
  313. $rule = 'RRULE:' . TimeZoneConvert_VTimeZone_Rrule::createFromTransitionRule($transitionRule);
  314. } else {
  315. $rdates = $transitionRule->getTransitionDates();
  316. $rdatesArray = array();
  317. foreach($rdates as $rdate) {
  318. $rdate = clone $rdate;
  319. $rdate->modify("$offsetFrom seconds");
  320. $rdatesArray[] = $rdate->format('Ymd\THis');
  321. }
  322. $rule = str_replace(' ', '', 'RDATE;VALUE=DATE-TIME:'. implode(', ', $rdatesArray));
  323. }
  324. $vTransitionRule = "BEGIN:$zone" . self::EOL;
  325. $vTransitionRule .= "TZOFFSETFROM:$offsetFromString" . self::EOL;
  326. $vTransitionRule .= "$rule" . self::EOL;
  327. $vTransitionRule .= "DTSTART:{$dtstart->format('Ymd\THis')}" . self::EOL;
  328. $vTransitionRule .= "TZNAME:{$transitionRule->abbr}" . self::EOL;
  329. $vTransitionRule .= "TZOFFSETTO:$offsetToString" . self::EOL;
  330. $vTransitionRule .= "END:$zone" . self::EOL;
  331. return $vTransitionRule;
  332. }
  333. }