DateTimeParser.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <?php
  2. namespace Sabre\VObject;
  3. use DateTime;
  4. use DateTimeZone;
  5. use DateInterval;
  6. use InvalidArgumentException;
  7. use LogicException;
  8. /**
  9. * DateTimeParser
  10. *
  11. * This class is responsible for parsing the several different date and time
  12. * formats iCalendar and vCards have.
  13. *
  14. * @copyright Copyright (C) 2007-2014 fruux GmbH (https://fruux.com/).
  15. * @author Evert Pot (http://evertpot.com/)
  16. * @license http://sabre.io/license/ Modified BSD License
  17. */
  18. class DateTimeParser {
  19. /**
  20. * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
  21. *
  22. * Specifying a reference timezone is optional. It will only be used
  23. * if the non-UTC format is used. The argument is used as a reference, the
  24. * returned DateTime object will still be in the UTC timezone.
  25. *
  26. * @param string $dt
  27. * @param DateTimeZone $tz
  28. * @return DateTime
  29. */
  30. static public function parseDateTime($dt, DateTimeZone $tz = null) {
  31. // Format is YYYYMMDD + "T" + hhmmss
  32. $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
  33. if (!$result) {
  34. throw new LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
  35. }
  36. if ($matches[7]==='Z' || is_null($tz)) {
  37. $tz = new DateTimeZone('UTC');
  38. }
  39. $date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
  40. // Still resetting the timezone, to normalize everything to UTC
  41. // $date->setTimeZone(new \DateTimeZone('UTC'));
  42. return $date;
  43. }
  44. /**
  45. * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object.
  46. *
  47. * @param string $date
  48. * @param DateTimeZone $tz
  49. * @return DateTime
  50. */
  51. static public function parseDate($date, DateTimeZone $tz = null) {
  52. // Format is YYYYMMDD
  53. $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
  54. if (!$result) {
  55. throw new LogicException('The supplied iCalendar date value is incorrect: ' . $date);
  56. }
  57. if (is_null($tz)) {
  58. $tz = new DateTimeZone('UTC');
  59. }
  60. $date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], $tz);
  61. return $date;
  62. }
  63. /**
  64. * Parses an iCalendar (RFC5545) formatted duration value.
  65. *
  66. * This method will either return a DateTimeInterval object, or a string
  67. * suitable for strtotime or DateTime::modify.
  68. *
  69. * @param string $duration
  70. * @param bool $asString
  71. * @return DateInterval|string
  72. */
  73. static public function parseDuration($duration, $asString = false) {
  74. $result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches);
  75. if (!$result) {
  76. throw new LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
  77. }
  78. if (!$asString) {
  79. $invert = false;
  80. if ($matches['plusminus']==='-') {
  81. $invert = true;
  82. }
  83. $parts = array(
  84. 'week',
  85. 'day',
  86. 'hour',
  87. 'minute',
  88. 'second',
  89. );
  90. foreach($parts as $part) {
  91. $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
  92. }
  93. // We need to re-construct the $duration string, because weeks and
  94. // days are not supported by DateInterval in the same string.
  95. $duration = 'P';
  96. $days = $matches['day'];
  97. if ($matches['week']) {
  98. $days+=$matches['week']*7;
  99. }
  100. if ($days)
  101. $duration.=$days . 'D';
  102. if ($matches['minute'] || $matches['second'] || $matches['hour']) {
  103. $duration.='T';
  104. if ($matches['hour'])
  105. $duration.=$matches['hour'].'H';
  106. if ($matches['minute'])
  107. $duration.=$matches['minute'].'M';
  108. if ($matches['second'])
  109. $duration.=$matches['second'].'S';
  110. }
  111. if ($duration==='P') {
  112. $duration = 'PT0S';
  113. }
  114. $iv = new DateInterval($duration);
  115. if ($invert) $iv->invert = true;
  116. return $iv;
  117. }
  118. $parts = array(
  119. 'week',
  120. 'day',
  121. 'hour',
  122. 'minute',
  123. 'second',
  124. );
  125. $newDur = '';
  126. foreach($parts as $part) {
  127. if (isset($matches[$part]) && $matches[$part]) {
  128. $newDur.=' '.$matches[$part] . ' ' . $part . 's';
  129. }
  130. }
  131. $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
  132. if ($newDur === '+') {
  133. $newDur = '+0 seconds';
  134. };
  135. return $newDur;
  136. }
  137. /**
  138. * Parses either a Date or DateTime, or Duration value.
  139. *
  140. * @param string $date
  141. * @param DateTimeZone|string $referenceTz
  142. * @return DateTime|DateInterval
  143. */
  144. static public function parse($date, $referenceTz = null) {
  145. if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
  146. return self::parseDuration($date);
  147. } elseif (strlen($date)===8) {
  148. return self::parseDate($date, $referenceTz);
  149. } else {
  150. return self::parseDateTime($date, $referenceTz);
  151. }
  152. }
  153. /**
  154. * This method parses a vCard date and or time value.
  155. *
  156. * This can be used for the DATE, DATE-TIME, TIMESTAMP and
  157. * DATE-AND-OR-TIME value.
  158. *
  159. * This method returns an array, not a DateTime value.
  160. *
  161. * The elements in the array are in the following order:
  162. * year, month, date, hour, minute, second, timezone
  163. *
  164. * Almost any part of the string may be omitted. It's for example legal to
  165. * just specify seconds, leave out the year, etc.
  166. *
  167. * Timezone is either returned as 'Z' or as '+08:00'
  168. *
  169. * For any non-specified values null is returned.
  170. *
  171. * List of date formats that are supported:
  172. * YYYY
  173. * YYYY-MM
  174. * YYYYMMDD
  175. * --MMDD
  176. * ---DD
  177. *
  178. * YYYY-MM-DD
  179. * --MM-DD
  180. * ---DD
  181. *
  182. * List of supported time formats:
  183. *
  184. * HH
  185. * HHMM
  186. * HHMMSS
  187. * -MMSS
  188. * --SS
  189. *
  190. * HH
  191. * HH:MM
  192. * HH:MM:SS
  193. * -MM:SS
  194. * --SS
  195. *
  196. * A full basic-format date-time string looks like :
  197. * 20130603T133901
  198. *
  199. * A full extended-format date-time string looks like :
  200. * 2013-06-03T13:39:01
  201. *
  202. * Times may be postfixed by a timezone offset. This can be either 'Z' for
  203. * UTC, or a string like -0500 or +1100.
  204. *
  205. * @param string $date
  206. * @return array
  207. */
  208. static public function parseVCardDateTime($date) {
  209. $regex = '/^
  210. (?: # date part
  211. (?:
  212. (?: (?P<year> [0-9]{4}) (?: -)?| --)
  213. (?P<month> [0-9]{2})?
  214. |---)
  215. (?P<date> [0-9]{2})?
  216. )?
  217. (?:T # time part
  218. (?P<hour> [0-9]{2} | -)
  219. (?P<minute> [0-9]{2} | -)?
  220. (?P<second> [0-9]{2})?
  221. (?P<timezone> # timezone offset
  222. Z | (?: \+|-)(?: [0-9]{4})
  223. )?
  224. )?
  225. $/x';
  226. if (!preg_match($regex, $date, $matches)) {
  227. // Attempting to parse the extended format.
  228. $regex = '/^
  229. (?: # date part
  230. (?: (?P<year> [0-9]{4}) - | -- )
  231. (?P<month> [0-9]{2}) -
  232. (?P<date> [0-9]{2})
  233. )?
  234. (?:T # time part
  235. (?: (?P<hour> [0-9]{2}) : | -)
  236. (?: (?P<minute> [0-9]{2}) : | -)?
  237. (?P<second> [0-9]{2})?
  238. (?P<timezone> # timezone offset
  239. Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
  240. )?
  241. )?
  242. $/x';
  243. if (!preg_match($regex, $date, $matches)) {
  244. throw new InvalidArgumentException('Invalid vCard date-time string: ' . $date);
  245. }
  246. }
  247. $parts = array(
  248. 'year',
  249. 'month',
  250. 'date',
  251. 'hour',
  252. 'minute',
  253. 'second',
  254. 'timezone'
  255. );
  256. $result = array();
  257. foreach($parts as $part) {
  258. if (empty($matches[$part])) {
  259. $result[$part] = null;
  260. } elseif ($matches[$part] === '-' || $matches[$part] === '--') {
  261. $result[$part] = null;
  262. } else {
  263. $result[$part] = $matches[$part];
  264. }
  265. }
  266. return $result;
  267. }
  268. /**
  269. * This method parses a vCard TIME value.
  270. *
  271. * This method returns an array, not a DateTime value.
  272. *
  273. * The elements in the array are in the following order:
  274. * hour, minute, second, timezone
  275. *
  276. * Almost any part of the string may be omitted. It's for example legal to
  277. * just specify seconds, leave out the hour etc.
  278. *
  279. * Timezone is either returned as 'Z' or as '+08:00'
  280. *
  281. * For any non-specified values null is returned.
  282. *
  283. * List of supported time formats:
  284. *
  285. * HH
  286. * HHMM
  287. * HHMMSS
  288. * -MMSS
  289. * --SS
  290. *
  291. * HH
  292. * HH:MM
  293. * HH:MM:SS
  294. * -MM:SS
  295. * --SS
  296. *
  297. * A full basic-format time string looks like :
  298. * 133901
  299. *
  300. * A full extended-format time string looks like :
  301. * 13:39:01
  302. *
  303. * Times may be postfixed by a timezone offset. This can be either 'Z' for
  304. * UTC, or a string like -0500 or +11:00.
  305. *
  306. * @param string $date
  307. * @return array
  308. */
  309. static public function parseVCardTime($date) {
  310. $regex = '/^
  311. (?P<hour> [0-9]{2} | -)
  312. (?P<minute> [0-9]{2} | -)?
  313. (?P<second> [0-9]{2})?
  314. (?P<timezone> # timezone offset
  315. Z | (?: \+|-)(?: [0-9]{4})
  316. )?
  317. $/x';
  318. if (!preg_match($regex, $date, $matches)) {
  319. // Attempting to parse the extended format.
  320. $regex = '/^
  321. (?: (?P<hour> [0-9]{2}) : | -)
  322. (?: (?P<minute> [0-9]{2}) : | -)?
  323. (?P<second> [0-9]{2})?
  324. (?P<timezone> # timezone offset
  325. Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
  326. )?
  327. $/x';
  328. if (!preg_match($regex, $date, $matches)) {
  329. throw new InvalidArgumentException('Invalid vCard time string: ' . $date);
  330. }
  331. }
  332. $parts = array(
  333. 'hour',
  334. 'minute',
  335. 'second',
  336. 'timezone'
  337. );
  338. $result = array();
  339. foreach($parts as $part) {
  340. if (empty($matches[$part])) {
  341. $result[$part] = null;
  342. } elseif ($matches[$part] === '-') {
  343. $result[$part] = null;
  344. } else {
  345. $result[$part] = $matches[$part];
  346. }
  347. }
  348. return $result;
  349. }
  350. }