1<?php 2 3/* 4 * This file is part of the eluceo/iCal package. 5 * 6 * (c) Markus Poerschke <markus@eluceo.de> 7 * 8 * This source file is subject to the MIT license that is bundled 9 * with this source code in the file LICENSE. 10 */ 11 12namespace Eluceo\iCal\Property\Event; 13 14use Eluceo\iCal\ParameterBag; 15use Eluceo\iCal\Property\ValueInterface; 16use InvalidArgumentException; 17 18/** 19 * Implementation of Recurrence Rule. 20 * 21 * @see https://tools.ietf.org/html/rfc5545#section-3.8.5.3 22 */ 23class RecurrenceRule implements ValueInterface 24{ 25 const FREQ_YEARLY = 'YEARLY'; 26 const FREQ_MONTHLY = 'MONTHLY'; 27 const FREQ_WEEKLY = 'WEEKLY'; 28 const FREQ_DAILY = 'DAILY'; 29 const FREQ_HOURLY = 'HOURLY'; 30 const FREQ_MINUTELY = 'MINUTELY'; 31 const FREQ_SECONDLY = 'SECONDLY'; 32 33 const WEEKDAY_SUNDAY = 'SU'; 34 const WEEKDAY_MONDAY = 'MO'; 35 const WEEKDAY_TUESDAY = 'TU'; 36 const WEEKDAY_WEDNESDAY = 'WE'; 37 const WEEKDAY_THURSDAY = 'TH'; 38 const WEEKDAY_FRIDAY = 'FR'; 39 const WEEKDAY_SATURDAY = 'SA'; 40 41 /** 42 * The frequency of an Event. 43 * 44 * @var string 45 */ 46 protected $freq = self::FREQ_YEARLY; 47 48 /** 49 * BYSETPOS must require use of other BY*. 50 * 51 * @var bool 52 */ 53 protected $canUseBySetPos = false; 54 55 /** 56 * @var int|null 57 */ 58 protected $interval = 1; 59 60 /** 61 * @var int|null 62 */ 63 protected $count = null; 64 65 /** 66 * @var \DateTimeInterface|null 67 */ 68 protected $until = null; 69 70 /** 71 * @var string|null 72 */ 73 protected $wkst; 74 75 /** 76 * @var array|null 77 */ 78 protected $bySetPos = null; 79 80 /** 81 * @var string|null 82 */ 83 protected $byMonth; 84 85 /** 86 * @var string|null 87 */ 88 protected $byWeekNo; 89 90 /** 91 * @var string|null 92 */ 93 protected $byYearDay; 94 95 /** 96 * @var string|null 97 */ 98 protected $byMonthDay; 99 100 /** 101 * @var string|null 102 */ 103 protected $byDay; 104 105 /** 106 * @var string|null 107 */ 108 protected $byHour; 109 110 /** 111 * @var string|null 112 */ 113 protected $byMinute; 114 115 /** 116 * @var string|null 117 */ 118 protected $bySecond; 119 120 public function getEscapedValue(): string 121 { 122 return $this->buildParameterBag()->toString(); 123 } 124 125 /** 126 * @return ParameterBag 127 */ 128 protected function buildParameterBag() 129 { 130 $parameterBag = new ParameterBag(); 131 132 $parameterBag->setParam('FREQ', $this->freq); 133 134 if (null !== $this->interval) { 135 $parameterBag->setParam('INTERVAL', $this->interval); 136 } 137 138 if (null !== $this->count) { 139 $parameterBag->setParam('COUNT', $this->count); 140 } 141 142 if (null != $this->until) { 143 $parameterBag->setParam('UNTIL', $this->until->format('Ymd\THis\Z')); 144 } 145 146 if (null !== $this->wkst) { 147 $parameterBag->setParam('WKST', $this->wkst); 148 } 149 150 if (null !== $this->bySetPos && $this->canUseBySetPos) { 151 $parameterBag->setParam('BYSETPOS', $this->bySetPos); 152 } 153 154 if (null !== $this->byMonth) { 155 $parameterBag->setParam('BYMONTH', explode(',', $this->byMonth)); 156 } 157 158 if (null !== $this->byWeekNo) { 159 $parameterBag->setParam('BYWEEKNO', explode(',', $this->byWeekNo)); 160 } 161 162 if (null !== $this->byYearDay) { 163 $parameterBag->setParam('BYYEARDAY', explode(',', $this->byYearDay)); 164 } 165 166 if (null !== $this->byMonthDay) { 167 $parameterBag->setParam('BYMONTHDAY', explode(',', $this->byMonthDay)); 168 } 169 170 if (null !== $this->byDay) { 171 $parameterBag->setParam('BYDAY', explode(',', $this->byDay)); 172 } 173 174 if (null !== $this->byHour) { 175 $parameterBag->setParam('BYHOUR', explode(',', $this->byHour)); 176 } 177 178 if (null !== $this->byMinute) { 179 $parameterBag->setParam('BYMINUTE', explode(',', $this->byMinute)); 180 } 181 182 if (null !== $this->bySecond) { 183 $parameterBag->setParam('BYSECOND', explode(',', $this->bySecond)); 184 } 185 186 return $parameterBag; 187 } 188 189 /** 190 * @param int|null $count 191 * 192 * @return $this 193 */ 194 public function setCount($count) 195 { 196 $this->count = $count; 197 198 return $this; 199 } 200 201 /** 202 * @return int|null 203 */ 204 public function getCount() 205 { 206 return $this->count; 207 } 208 209 /** 210 * @return $this 211 */ 212 public function setUntil(\DateTimeInterface $until = null) 213 { 214 $this->until = $until; 215 216 return $this; 217 } 218 219 /** 220 * @return \DateTimeInterface|null 221 */ 222 public function getUntil() 223 { 224 return $this->until; 225 } 226 227 /** 228 * The FREQ rule part identifies the type of recurrence rule. This 229 * rule part MUST be specified in the recurrence rule. Valid values 230 * include. 231 * 232 * SECONDLY, to specify repeating events based on an interval of a second or more; 233 * MINUTELY, to specify repeating events based on an interval of a minute or more; 234 * HOURLY, to specify repeating events based on an interval of an hour or more; 235 * DAILY, to specify repeating events based on an interval of a day or more; 236 * WEEKLY, to specify repeating events based on an interval of a week or more; 237 * MONTHLY, to specify repeating events based on an interval of a month or more; 238 * YEARLY, to specify repeating events based on an interval of a year or more. 239 * 240 * @param string $freq 241 * 242 * @return $this 243 * 244 * @throws \InvalidArgumentException 245 */ 246 public function setFreq($freq) 247 { 248 if (@constant('static::FREQ_' . $freq) !== null) { 249 $this->freq = $freq; 250 } else { 251 throw new \InvalidArgumentException("The Frequency {$freq} is not supported."); 252 } 253 254 return $this; 255 } 256 257 /** 258 * @return string 259 */ 260 public function getFreq() 261 { 262 return $this->freq; 263 } 264 265 /** 266 * The INTERVAL rule part contains a positive integer representing at 267 * which intervals the recurrence rule repeats. 268 * 269 * @param int|null $interval 270 * 271 * @return $this 272 */ 273 public function setInterval($interval) 274 { 275 $this->interval = $interval; 276 277 return $this; 278 } 279 280 /** 281 * @return int|null 282 */ 283 public function getInterval() 284 { 285 return $this->interval; 286 } 287 288 /** 289 * The WKST rule part specifies the day on which the workweek starts. 290 * Valid values are MO, TU, WE, TH, FR, SA, and SU. 291 * 292 * @param string $value 293 * 294 * @return $this 295 */ 296 public function setWkst($value) 297 { 298 $this->wkst = $value; 299 300 return $this; 301 } 302 303 /** 304 * The BYSETPOS filters one interval of events by the specified position. 305 * A positive position will start from the beginning and go forward while 306 * a negative position will start at the end and move backward. 307 * 308 * Valid values are a comma separated string or an array of integers 309 * from 1 to 366 or negative integers from -1 to -366. 310 * 311 * @param int|string|array|null $value 312 * 313 * @throws InvalidArgumentException 314 * 315 * @return $this 316 */ 317 public function setBySetPos($value) 318 { 319 if (null === $value) { 320 $this->bySetPos = $value; 321 322 return $this; 323 } 324 325 if (!(is_string($value) || is_array($value) || is_int($value))) { 326 throw new InvalidArgumentException('Invalid value for BYSETPOS'); 327 } 328 329 $list = $value; 330 331 if (is_int($value)) { 332 if ($value === 0 || $value < -366 || $value > 366) { 333 throw new InvalidArgumentException('Invalid value for BYSETPOS'); 334 } 335 $this->bySetPos = [$value]; 336 337 return $this; 338 } 339 340 if (is_string($value)) { 341 $list = explode(',', $value); 342 } 343 344 $output = []; 345 346 foreach ($list as $item) { 347 if (is_string($item)) { 348 if (!preg_match('/^ *-?[0-9]* *$/', $item)) { 349 throw new InvalidArgumentException('Invalid value for BYSETPOS'); 350 } 351 $item = intval($item); 352 } 353 354 if (!is_int($item) || $item === 0 || $item < -366 || $item > 366) { 355 throw new InvalidArgumentException('Invalid value for BYSETPOS'); 356 } 357 358 $output[] = $item; 359 } 360 361 $this->bySetPos = $output; 362 363 return $this; 364 } 365 366 /** 367 * The BYMONTH rule part specifies a COMMA-separated list of months of the year. 368 * Valid values are 1 to 12. 369 * 370 * @param int $month 371 * 372 * @throws \InvalidArgumentException 373 * 374 * @return $this 375 */ 376 public function setByMonth($month) 377 { 378 if (!is_integer($month) || $month <= 0 || $month > 12) { 379 throw new InvalidArgumentException('Invalid value for BYMONTH'); 380 } 381 382 $this->byMonth = $month; 383 384 $this->canUseBySetPos = true; 385 386 return $this; 387 } 388 389 /** 390 * The BYWEEKNO rule part specifies a COMMA-separated list of ordinals specifying weeks of the year. 391 * Valid values are 1 to 53 or -53 to -1. 392 * 393 * @param int $value 394 * 395 * @throws \InvalidArgumentException 396 * 397 * @return $this 398 */ 399 public function setByWeekNo($value) 400 { 401 if (!is_integer($value) || $value > 53 || $value < -53 || $value === 0) { 402 throw new InvalidArgumentException('Invalid value for BYWEEKNO'); 403 } 404 405 $this->byWeekNo = $value; 406 407 $this->canUseBySetPos = true; 408 409 return $this; 410 } 411 412 /** 413 * The BYYEARDAY rule part specifies a COMMA-separated list of days of the year. 414 * Valid values are 1 to 366 or -366 to -1. 415 * 416 * @param int $day 417 * 418 * @throws \InvalidArgumentException 419 * 420 * @return $this 421 */ 422 public function setByYearDay($day) 423 { 424 if (!is_integer($day) || $day > 366 || $day < -366 || $day === 0) { 425 throw new InvalidArgumentException('Invalid value for BYYEARDAY'); 426 } 427 428 $this->byYearDay = $day; 429 430 $this->canUseBySetPos = true; 431 432 return $this; 433 } 434 435 /** 436 * The BYMONTHDAY rule part specifies a COMMA-separated list of days of the month. 437 * Valid values are 1 to 31 or -31 to -1. 438 * 439 * @param int $day 440 * 441 * @return $this 442 * 443 * @throws \InvalidArgumentException 444 */ 445 public function setByMonthDay($day) 446 { 447 if (!is_integer($day) || $day > 31 || $day < -31 || $day === 0) { 448 throw new InvalidArgumentException('Invalid value for BYMONTHDAY'); 449 } 450 451 $this->byMonthDay = $day; 452 453 $this->canUseBySetPos = true; 454 455 return $this; 456 } 457 458 /** 459 * The BYDAY rule part specifies a COMMA-separated list of days of the week;. 460 * 461 * SU indicates Sunday; MO indicates Monday; TU indicates Tuesday; 462 * WE indicates Wednesday; TH indicates Thursday; FR indicates Friday; and SA indicates Saturday. 463 * 464 * Each BYDAY value can also be preceded by a positive (+n) or negative (-n) integer. 465 * If present, this indicates the nth occurrence of a specific day within the MONTHLY or YEARLY "RRULE". 466 * 467 * @return $this 468 */ 469 public function setByDay(string $day) 470 { 471 $this->byDay = $day; 472 473 $this->canUseBySetPos = true; 474 475 return $this; 476 } 477 478 /** 479 * The BYHOUR rule part specifies a COMMA-separated list of hours of the day. 480 * Valid values are 0 to 23. 481 * 482 * @param int $value 483 * 484 * @return $this 485 * 486 * @throws \InvalidArgumentException 487 */ 488 public function setByHour($value) 489 { 490 if (!is_integer($value) || $value < 0 || $value > 23) { 491 throw new \InvalidArgumentException('Invalid value for BYHOUR'); 492 } 493 494 $this->byHour = $value; 495 496 $this->canUseBySetPos = true; 497 498 return $this; 499 } 500 501 /** 502 * The BYMINUTE rule part specifies a COMMA-separated list of minutes within an hour. 503 * Valid values are 0 to 59. 504 * 505 * @param int $value 506 * 507 * @return $this 508 * 509 * @throws \InvalidArgumentException 510 */ 511 public function setByMinute($value) 512 { 513 if (!is_integer($value) || $value < 0 || $value > 59) { 514 throw new \InvalidArgumentException('Invalid value for BYMINUTE'); 515 } 516 517 $this->byMinute = $value; 518 519 $this->canUseBySetPos = true; 520 521 return $this; 522 } 523 524 /** 525 * The BYSECOND rule part specifies a COMMA-separated list of seconds within a minute. 526 * Valid values are 0 to 60. 527 * 528 * @param int $value 529 * 530 * @return $this 531 * 532 * @throws \InvalidArgumentException 533 */ 534 public function setBySecond($value) 535 { 536 if (!is_integer($value) || $value < 0 || $value > 60) { 537 throw new \InvalidArgumentException('Invalid value for BYSECOND'); 538 } 539 540 $this->bySecond = $value; 541 542 $this->canUseBySetPos = true; 543 544 return $this; 545 } 546} 547