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