PluginProbe
Import Social Events / 1.8.5
Import Social Events v1.8.5
1.9.1 1.9.0 trunk 1.0.0 1.0.1 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0 1.3.0 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 All 61 releases
import-facebook-events / includes / lib / icalcreator / src / Util / RecurFactory.php

RecurFactory.php in Import Social Events 1.8.5, at includes/lib/icalcreator/src/Util/RecurFactory.php

1,430 lines 54.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
4 *
5 * copyright (c) 2007-2021 Kjell-Inge Gustafsson, kigkonsult, All rights reserved
6 * Link https://kigkonsult.se
7 * Package iCalcreator
8 * Version 2.30
9 * License Subject matter of licence is the software iCalcreator.
10 * The above copyright, link, package and version notices,
11 * this licence notice and the invariant [rfc5545] PRODID result use
12 * as implemented and invoked in iCalcreator shall be included in
13 * all copies or substantial portions of the iCalcreator.
14 *
15 * iCalcreator is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU Lesser General Public License as published
17 * by the Free Software Foundation, either version 3 of the License,
18 * or (at your option) any later version.
19 *
20 * iCalcreator is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU Lesser General Public License for more details.
24 *
25 * You should have received a copy of the GNU Lesser General Public License
26 * along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
27 *
28 * This file is a part of iCalcreator.
29 */
30
31 namespace Kigkonsult\Icalcreator\Util;
32
33 use DateTime;
34 use DateTimeInterface;
35 use Exception;
36 use InvalidArgumentException;
37 use Kigkonsult\Icalcreator\Vcalendar;
38 use LogicException;
39 use function array_change_key_case;
40 use function array_keys;
41 use function array_unique;
42 use function checkdate;
43 use function count;
44 use function ctype_alpha;
45 use function ctype_digit;
46 use function date;
47 use function end;
48 use function explode;
49 use function get_class;
50 use function implode;
51 use function in_array;
52 use function is_array;
53 use function is_null;
54 use function is_string;
55 use function ksort;
56 use function mktime;
57 use function sprintf;
58 use function strcasecmp;
59 use function strlen;
60 use function strtoupper;
61 use function substr;
62 use function trim;
63 use function usort;
64 use function var_export;
65
66 /**
67 * iCalcreator recur support class
68 *
69 * @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
70 * @since 2.29.27 - 2020-09-19
71 */
72 class RecurFactory
73 {
74 /**
75 * @const int in recur2date, years to extend startYear to create an endDate, if missing
76 */
77 const EXTENDYEAR = 2;
78
79 /**
80 * @var string iCal date/time key values ( week, tz used in test)
81 */
82 public static $LCYEAR = 'year';
83 public static $LCMONTH = 'month';
84 public static $LCWEEK = 'week';
85 public static $LCDAY = 'day';
86 public static $LCHOUR = 'hour';
87 public static $LCMIN = 'min';
88 public static $LCSEC = 'sec';
89 public static $LCtz = 'tz';
90
91 /**
92 * Static values for recur BYDAY
93 *
94 * @var array
95 */
96 public static $DAYNAMES = [
97 Vcalendar::SU,
98 Vcalendar::MO,
99 Vcalendar::TU,
100 Vcalendar::WE,
101 Vcalendar::TH,
102 Vcalendar::FR,
103 Vcalendar::SA
104 ];
105
106 /*
107 * @var string DateTime format keys
108 */
109 public static $YMDs = '%04d%02d%02d';
110 public static $HIS = '%02d%02d%02d';
111
112 /*
113 * @var string fullRecur2date keys
114 */
115 private static $YEARCNT_UP = 'yearcnt_up';
116 private static $YEARCNT_DOWN = 'yearcnt_down';
117 private static $MONTHDAYNO_UP = 'monthdayno_up';
118 private static $MONTHDAYNO_DOWN = 'monthdayno_down';
119 private static $MONTHCNT_DOWN = 'monthcnt_down';
120 private static $YEARDAYNO_UP = 'yeardayno_up';
121 private static $YEARDAYNO_DOWN = 'yeardayno_down';
122 private static $WEEKNO_UP = 'weekno_up';
123 private static $WEEKNO_DOWN = 'weekno_down';
124
125 /**
126 * Sort recur dates
127 *
128 * @param string $byDayA
129 * @param string $byDayB
130 * @return int
131 */
132 private static function recurBydaySort( $byDayA, $byDayB )
133 {
134 static $days = [
135 Vcalendar::SU => 0,
136 Vcalendar::MO => 1,
137 Vcalendar::TU => 2,
138 Vcalendar::WE => 3,
139 Vcalendar::TH => 4,
140 Vcalendar::FR => 5,
141 Vcalendar::SA => 6,
142 ];
143 return ( $days[substr( $byDayA, -2 )] < $days[substr( $byDayB, -2 )] )
144 ? -1
145 : 1;
146 }
147
148 /**
149 * Return formatted output for calendar component property data value type recur
150 *
151 * "The value of the UNTIL rule part MUST have the same value type as the "DTSTART" property.
152 * Furthermore, if the "DTSTART" property is specified as a date with local time,
153 * then the UNTIL rule part MUST also be specified as a date with local time.
154 * If the "DTSTART" property is specified as a date
155 * with UTC time
156 * or
157 * a date with local time and time zone reference,
158 * then the UNTIL rule part MUST be specified as a date with UTC time.
159 * In the case of the "STANDARD" and "DAYLIGHT" sub-components
160 * the UNTIL rule part MUST always be specified as a date with UTC time.
161 * If specified as a DATE-TIME value, then it MUST be specified in a UTC time format."
162 * @param string $recurProperty
163 * @param array $recurData
164 * @param bool $allowEmpty
165 * @return string
166 * @throws Exception
167 * @throws InvalidArgumentException
168 * @since 2.29.6 2019-06-23
169 * @todo above
170 */
171 public static function formatRecur( $recurProperty, $recurData, $allowEmpty )
172 {
173 static $FMTFREQEQ = 'FREQ=%s';
174 static $FMTDEFAULTEQ = ';%s=%s';
175 static $FMTOTHEREQ = ';%s=';
176 static $RECURBYDAYSORTER = null;
177 if( is_null( $RECURBYDAYSORTER )) {
178 $RECURBYDAYSORTER = [ get_class(), 'recurBydaySort' ];
179 }
180 if( empty( $recurData )) {
181 return null;
182 }
183 $output = null;
184 if( empty( $recurData[Util::$LCvalue] )) {
185 return ( $allowEmpty )
186 ? StringFactory::createElement( $recurProperty )
187 : null;
188 }
189 $isValueDate = ParameterFactory::isParamsValueSet( $recurData, Vcalendar::DATE );
190 if( isset( $recurData[Util::$LCparams] )) {
191 ParameterFactory::ifExistRemove(
192 $recurData[Util::$LCparams],
193 Vcalendar::VALUE
194 );
195 $attributes = ParameterFactory::createParams( $recurData[Util::$LCparams] );
196 }
197 else {
198 $attributes = null;
199 }
200 $content1 = $content2 = null;
201 foreach( $recurData[Util::$LCvalue] as $ruleLabel => $ruleValue ) {
202 $ruleLabel = strtoupper( $ruleLabel );
203 switch( $ruleLabel ) {
204 case Vcalendar::FREQ :
205 $content1 .= sprintf( $FMTFREQEQ, $ruleValue );
206 break;
207 case Vcalendar::UNTIL :
208 $content2 .= sprintf(
209 $FMTDEFAULTEQ,
210 Vcalendar::UNTIL,
211 DateTimeFactory::dateTime2Str( $ruleValue, $isValueDate )
212 );
213 break;
214 case Vcalendar::COUNT :
215 case Vcalendar::INTERVAL :
216 case Vcalendar::WKST :
217 $content2 .= sprintf( $FMTDEFAULTEQ, $ruleLabel, $ruleValue );
218 break;
219 case Vcalendar::BYDAY :
220 $byday = [ Util::$SP0 ];
221 $bx = 0;
222 foreach( $ruleValue as $bix => $bydayPart ) {
223 if( ! empty( $byday[$bx] ) && // new day
224 ! ctype_digit( substr( $byday[$bx], -1 ))) {
225 $byday[++$bx] = Util::$SP0;
226 }
227 if( ! is_array( $bydayPart )) { // day without rel pos number
228 $byday[$bx] .= (string) $bydayPart;
229 }
230 else { // day with rel pos number
231 foreach( $bydayPart as $bix2 => $bydayPart2 ) {
232 $byday[$bx] .= (string) $bydayPart2;
233 }
234 }
235 } // end foreach( $ruleValue as $bix => $bydayPart )
236 if( 1 < count( $byday )) {
237 usort( $byday, $RECURBYDAYSORTER );
238 }
239 $content2 .= sprintf(
240 $FMTDEFAULTEQ,
241 Vcalendar::BYDAY,
242 implode( Util::$COMMA, $byday )
243 );
244 break;
245 default : // BYSECOND/BYMINUTE/BYHOUR/BYMONTHDAY/BYYEARDAY/BYWEEKNO/BYMONTH/BYSETPOS...
246 if( is_array( $ruleValue )) {
247 $content2 .= sprintf( $FMTOTHEREQ, $ruleLabel );
248 $content2 .= implode( Util::$COMMA, $ruleValue );
249 }
250 else {
251 $content2 .= sprintf( $FMTDEFAULTEQ, $ruleLabel, $ruleValue );
252 }
253 break;
254 } // end switch( $ruleLabel )
255 } // end foreach( $theRule[Util::$LCvalue] )) as $ruleLabel => $ruleValue )
256 $output .= StringFactory::createElement(
257 $recurProperty,
258 $attributes,
259 $content1 . $content2
260 );
261 return $output;
262 }
263
264 /**
265 * Return (array) parsed rexrule string
266 *
267 * @param string $row
268 * @return array
269 * @since 2.27.3 - 2018-12-28
270 */
271 public static function parseRexrule( $row )
272 {
273 static $EQ = '=';
274 $recur = [];
275 $values = explode( Util::$SEMIC, $row );
276 foreach( $values as $value2 ) {
277 if( empty( $value2 )) {
278 continue;
279 } // ;-char in end position ???
280 $value3 = explode( $EQ, $value2, 2 );
281 $ruleLabel = strtoupper( $value3[0] );
282 switch( $ruleLabel ) {
283 case Vcalendar::BYDAY:
284 $value4 = explode( Util::$COMMA, $value3[1] );
285 if( 1 < count( $value4 )) {
286 foreach( $value4 as $v5ix => $value5 ) {
287 $value4[$v5ix] =
288 self::updateDayNoAndDayName( trim((string) $value5 ));
289 }
290 }
291 else {
292 $value4 = self::updateDayNoAndDayName(
293 trim((string) $value3[1] )
294 );
295 }
296 $recur[$ruleLabel] = $value4;
297 break;
298 default:
299 $value4 = explode( Util::$COMMA, $value3[1] );
300 if( 1 < count( $value4 )) {
301 $value3[1] = $value4;
302 }
303 $recur[$ruleLabel] = $value3[1];
304 break;
305 } // end - switch $ruleLabel
306 } // end - foreach( $values.. .
307 return $recur;
308 }
309
310 /*
311 * Return array, day rel pos number (opt) and day name abbr
312 *
313 * @param string $dayValueBase
314 * @return array
315 * @since 2.27.16 - 2019-03-03
316 */
317 private static function updateDayNoAndDayName( $dayValueBase )
318 {
319 $output = [];
320 $dayno = $dayName = false;
321 if(( ctype_alpha( substr( $dayValueBase, -1 ))) &&
322 ( ctype_alpha( substr( $dayValueBase, -2, 1 )))) {
323 $dayName = substr( $dayValueBase, -2, 2 );
324 if( 2 < strlen( $dayValueBase )) {
325 $dayno = (int) substr( $dayValueBase, 0, ( strlen( $dayValueBase ) - 2 ));
326 }
327 }
328 if( false !== $dayno ) {
329 $output[] = $dayno;
330 }
331 if( false !== $dayName ) {
332 $output[Vcalendar::DAY] = $dayName;
333 }
334 return $output;
335 }
336
337 /**
338 * Convert input format for EXRULE and RRULE to internal format
339 *
340 * "The value of the UNTIL rule part MUST have the same value type as the "DTSTART" property."
341 * "If specified as a DATE-TIME value, then it MUST be specified in a UTC time format."
342 * @param array $rexrule
343 * @param array $params merged with dtstart params
344 * @return array
345 * @throws Exception
346 * @throws InvalidArgumentException
347 * @since 2.29.25 - 2020-09-02
348 * @todo "The BYSECOND, BYMINUTE and BYHOUR rule parts MUST NOT be specified
349 * when the associated "DTSTART" property has a DATE value type."
350 */
351 public static function setRexrule( $rexrule, array $params )
352 {
353 static $ERR = 'Invalid input date \'%s\'';
354 $input = [];
355 if( empty( $rexrule )) {
356 return $input;
357 }
358 $params = [ Util::$LCparams => $params ];
359 $isValueDate = ParameterFactory::isParamsValueSet( $params, Vcalendar::DATE );
360 $paramTZid = ParameterFactory::getParamTzid( $params );
361 $rexrule = array_change_key_case( $rexrule, CASE_UPPER );
362 foreach( $rexrule as $ruleLabel => $ruleValue ) {
363 switch( true ) {
364 case ( Vcalendar::UNTIL != $ruleLabel ) :
365 $input[$ruleLabel] = $ruleValue;
366 break;
367 case ( $ruleValue instanceof DateTimeInterface ) :
368 $ruleValue = DateTimeFactory::cnvrtDateTimeInterface( $ruleValue );
369 $input[$ruleLabel] =
370 DateTimeFactory::setDateTimeTimeZone(
371 $ruleValue,
372 Vcalendar::UTC
373 );
374 ParameterFactory::ifExistRemove(
375 $params[Util::$LCparams],
376 Vcalendar::TZID
377 );
378 break;
379 case ( DateTimeFactory::isStringAndDate( $ruleValue )) :
380 list( $dateStr, $timezonePart ) =
381 DateTimeFactory::splitIntoDateStrAndTimezone( $ruleValue );
382 $isLocalTime = ( empty( $timezonePart ) && empty( $paramTZid ));
383 $dateTime = DateTimeFactory::getDateTimeWithTimezoneFromString(
384 $dateStr,
385 $isLocalTime ? null : $timezonePart,
386 $isLocalTime ? Vcalendar::UTC : $paramTZid,
387 true
388 );
389 if( $isValueDate ) {
390 ParameterFactory::ifExistRemove(
391 $params[Util::$LCparams],
392 Vcalendar::TZID
393 );
394 }
395 else {
396 $dateTime = DateTimeFactory::setDateTimeTimeZone(
397 $dateTime,
398 Vcalendar::UTC
399 );
400 ParameterFactory::ifExistRemove(
401 $params[Util::$LCparams],
402 Vcalendar::TZID
403 );
404 }
405 $input[$ruleLabel] = $dateTime;
406 break;
407 default :
408 throw new InvalidArgumentException(
409 sprintf( $ERR, var_export( $ruleValue, true ))
410 );
411 break;
412 } // end switch
413 } // end foreach( $rexrule as $ruleLabel => $ruleValue )
414 $output = self::orderRRuleKeys( $input );
415
416 if( ! isset( $output[Vcalendar::UNTIL] )) {
417 ParameterFactory::ifExistRemove(
418 $params[Util::$LCparams],
419 Vcalendar::TZID
420 );
421 }
422 try {
423 RecurFactory2::assertRecur( $output );
424 }
425 catch( LogicException $e ) {
426 throw new InvalidArgumentException( $e->getMessage(), null, $e );
427 }
428 return [ Util::$LCvalue => $output ] + $params;
429 }
430
431 /**
432 * @param array $input
433 * @return array
434 * @since 2.29.25 - 2020-09-02
435 */
436 private static function orderRRuleKeys( array $input )
437 {
438 static $RKEYS1 = [
439 Vcalendar::FREQ,
440 Vcalendar::UNTIL,
441 Vcalendar::COUNT,
442 Vcalendar::INTERVAL,
443 Vcalendar::BYSECOND,
444 Vcalendar::BYMINUTE,
445 Vcalendar::BYHOUR
446 ];
447 static $RKEYS2 = [
448 Vcalendar::BYMONTHDAY,
449 Vcalendar::BYYEARDAY,
450 Vcalendar::BYWEEKNO,
451 Vcalendar::BYMONTH,
452 Vcalendar::BYSETPOS,
453 Vcalendar::WKST
454 ];
455 static $RKEYS3 = [
456 Vcalendar::BYSECOND,
457 Vcalendar::BYMINUTE,
458 Vcalendar::BYHOUR,
459 Vcalendar::BYMONTHDAY,
460 Vcalendar::BYYEARDAY,
461 Vcalendar::BYWEEKNO,
462 Vcalendar::BYMONTH,
463 Vcalendar::BYSETPOS,
464 ];
465 /* set recurrence rule specification in rfc2445 order */
466 $output = [];
467 foreach( $RKEYS1 as $rKey1 ) {
468 if( isset( $input[$rKey1] )) {
469 $output[$rKey1] = $input[$rKey1];
470 }
471 }
472 if( isset( $input[Vcalendar::BYDAY] )) {
473 self::orderRRuleBydayKey( $input, $output );
474 }
475 foreach( $RKEYS2 as $rKey2 ) {
476 if( isset( $input[$rKey2] )) {
477 $output[$rKey2] = $input[$rKey2];
478 }
479 }
480 foreach( $RKEYS3 as $rKey3 ) {
481 if( ! isset( $output[$rKey3] )) {
482 continue;
483 }
484 if( is_string( $output[$rKey3] )) {
485 $temp = explode( UTIL::$COMMA, $output[$rKey3] );
486 if( 1 == count( $temp )) {
487 $output[$rKey3] = reset( $temp );
488 }
489 else {
490 sort( $temp );
491 $output[$rKey3] = array_unique( $temp );
492 }
493 }
494 elseif( is_array( $output[$rKey3] )) {
495 sort( $output[$rKey3] );
496 $output[$rKey3] = array_unique( $output[$rKey3] );
497 }
498 } // end foreach
499 return $output;
500 }
501
502 /**
503 * Ensure RRULE BYDAY array and upper case.. .
504 *
505 * @param array $input
506 * @param array $output
507 * @since 2.29.27 - 2020-09-19
508 */
509 private static function orderRRuleBydayKey( array $input, array & $output )
510 {
511 if( empty( $input[Vcalendar::BYDAY] )) {
512 // results in error
513 $output[Vcalendar::BYDAY] = [];
514 return;
515 }
516 if( ! is_array( $input[Vcalendar::BYDAY] )) {
517 // single day
518 $output[Vcalendar::BYDAY] = [
519 Vcalendar::DAY => strtoupper( $input[Vcalendar::BYDAY] ),
520 ];
521 return;
522 }
523 $cntStr = $cntNum = 0;
524 foreach( $input[Vcalendar::BYDAY] as $BYDAYx => $BYDAYv ) {
525 if( is_array( $BYDAYv )) {
526 break;
527 }
528 if( is_string( $BYDAYv ) && ctype_alpha( $BYDAYv )) {
529 $cntStr += 1;
530 continue;
531 }
532 if( empty( $BYDAYv )) {
533 $input[Vcalendar::BYDAY] = [ Util::$SP0 ];
534 $cntStr += 1;
535 continue;
536 }
537 $cntNum += 1;
538 } // end foreach
539 if(( 1 == $cntStr ) || ( 1 < $cntNum )) { // single day OR invalid format...
540 $input[Vcalendar::BYDAY] = [ $input[Vcalendar::BYDAY] ];
541 }
542 elseif( 1 < $cntStr ) { // split (single) days
543 $days = [];
544 foreach( $input[Vcalendar::BYDAY] as $BYDAYx => $BYDAYv ) {
545 $days[] = [ Vcalendar::DAY => $BYDAYv ];
546 }
547 $input[Vcalendar::BYDAY] = $days;
548 }
549 foreach( $input[Vcalendar::BYDAY] as $BYDAYx => $BYDAYv ) {
550 $nIx = 0;
551 foreach( $BYDAYv as $BYDAYx2 => $BYDAYv2 ) {
552 switch( true ) {
553 case ( 0 == strcasecmp( Vcalendar::DAY, $BYDAYx2 )) :
554 // day abbr with key
555 $output[Vcalendar::BYDAY][$BYDAYx][$BYDAYx2] = strtoupper( $BYDAYv2 );
556 break;
557 case ( is_string( $BYDAYv2 ) && ctype_alpha( $BYDAYv2 )) :
558 // day abbr without key, set key
559 $output[Vcalendar::BYDAY][$BYDAYx][Vcalendar::DAY] =
560 strtoupper( $BYDAYv2 );
561 break;
562 default :
563 // rel pos day number. force key from 0 (1++ results in error)
564 $output[Vcalendar::BYDAY][$BYDAYx][$nIx++] = $BYDAYv2;
565 break;
566 } // end switch
567 } // end foreach
568 ksort( $output[Vcalendar::BYDAY][$BYDAYx], SORT_NATURAL );
569 } // end foreach
570 ksort( $output[Vcalendar::BYDAY], SORT_NATURAL );
571 }
572
573 /**
574 * Update array $result with dates based on a recur pattern
575 *
576 * If missing, UNTIL is set 1 year from startdate (emergency break)
577 *
578 * @param array $result array to update, array([Y-m-d] => bool)
579 * @param array $recur pattern for recurrency (only value part, params ignored)
580 * @param string|DateTime $wDateIn component start date
581 * @param string|DateTime $fcnStartIn start date
582 * @param string|DateTime $fcnEndIn end date
583 * @throws Exception
584 * @since 2.29.24 - 2020-08-29
585 * @todo BYHOUR, BYMINUTE, BYSECOND, WEEKLY at year end/start OR not at all
586 */
587 public static function recur2date(
588 & $result,
589 $recur,
590 $wDateIn,
591 $fcnStartIn,
592 $fcnEndIn = null
593 ) {
594 if( ! isset( $recur[Vcalendar::FREQ] )) { // "MUST be specified.. ." ??
595 $recur[Vcalendar::FREQ] = Vcalendar::DAILY;
596 }
597 if( ! isset( $recur[Vcalendar::INTERVAL] )) {
598 $recur[Vcalendar::INTERVAL] = 1;
599 }
600 switch( true ) {
601 case RecurFactory2::isRecurDaily1( $recur ) :
602 $result = $result +
603 RecurFactory2::recurDaily1(
604 $recur,
605 $wDateIn,
606 $fcnStartIn,
607 $fcnEndIn
608 );
609 ksort( $result, SORT_NUMERIC );
610 break;
611 case RecurFactory2::isRecurDaily2( $recur ) :
612 $result = $result +
613 RecurFactory2::recurDaily2(
614 $recur,
615 $wDateIn,
616 $fcnStartIn,
617 $fcnEndIn
618 );
619 ksort( $result, SORT_NUMERIC );
620 break;
621 case RecurFactory2::isRecurMonthly1( $recur ) :
622 $result = $result +
623 RecurFactory2::recurMonthly1(
624 $recur,
625 $wDateIn,
626 $fcnStartIn,
627 $fcnEndIn
628 );
629 ksort( $result, SORT_NUMERIC );
630 break;
631 case RecurFactory2::isRecurMonthly2( $recur ) :
632 $result = $result +
633 RecurFactory2::recurMonthlyYearly3(
634 $recur,
635 $wDateIn,
636 $fcnStartIn,
637 $fcnEndIn
638 );
639 ksort( $result, SORT_NUMERIC );
640 break;
641 case RecurFactory2::isRecurWeekly1( $recur ) :
642 $result = $result +
643 RecurFactory2::recurWeekly1(
644 $recur,
645 $wDateIn,
646 $fcnStartIn,
647 $fcnEndIn
648 );
649 ksort( $result, SORT_NUMERIC );
650 break;
651 case RecurFactory2::isRecurWeekly2( $recur ) :
652 $result = $result +
653 RecurFactory2::recurWeekly2(
654 $recur,
655 $wDateIn,
656 $fcnStartIn,
657 $fcnEndIn
658 );
659 ksort( $result, SORT_NUMERIC );
660 break;
661 case RecurFactory2::isRecurYearly1( $recur ) :
662 $result = $result +
663 RecurFactory2::recurYearly1(
664 $recur,
665 $wDateIn,
666 $fcnStartIn,
667 $fcnEndIn
668 );
669 ksort( $result, SORT_NUMERIC );
670 break;
671 case RecurFactory2::isRecurYearly2( $recur ) :
672 $result = $result +
673 RecurFactory2::recurMonthlyYearly3(
674 $recur,
675 $wDateIn,
676 $fcnStartIn,
677 $fcnEndIn
678 );
679 ksort( $result, SORT_NUMERIC );
680 break;
681 default :
682 self::fullRecur2date(
683 $result,
684 $recur,
685 $wDateIn,
686 $fcnStartIn,
687 $fcnEndIn
688 );
689 } // end switch
690 }
691
692 /**
693 * Update array $result with dates based on a recur pattern
694 *
695 * If missing, UNTIL is set 1 year from startdate (emergency break)
696 *
697 * @param array $result array to update, array([Y-m-d] => bool)
698 * @param array $recur pattern for recurrency (only value part, params ignored)
699 * @param string|DateTime $wDateIn component start date
700 * @param string|DateTime $fcnStartIn start date
701 * @param string|DateTime $fcnEndIn end date
702 * @throws Exception
703 * @since 2.26 - 2018-11-10
704 * @todo BYHOUR, BYMINUTE, BYSECOND, WEEKLY at year end/start OR not at all
705 */
706 public static function fullRecur2date(
707 & $result,
708 $recur,
709 $wDateIn,
710 $fcnStartIn,
711 $fcnEndIn = null
712 ) {
713 static $YEAR2DAYARR = [ 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY' ];
714 if( ! isset( $recur[Vcalendar::FREQ] )) { // "MUST be specified.. ."
715 $recur[Vcalendar::FREQ] = Vcalendar::DAILY;
716 } // ??
717 if( ! isset( $recur[Vcalendar::INTERVAL] )) {
718 $recur[Vcalendar::INTERVAL] = 1;
719 }
720 $wDate = self::reFormatDate( $wDateIn );
721 $wDateYMD = sprintf(
722 self::$YMDs,
723 $wDate[self::$LCYEAR],
724 $wDate[self::$LCMONTH],
725 $wDate[self::$LCDAY]
726 );
727 $wDateHis = sprintf(
728 self::$HIS,
729 $wDate[self::$LCHOUR],
730 $wDate[self::$LCMIN],
731 $wDate[self::$LCSEC]
732 );
733 $untilHis = $wDateHis;
734 $fcnStart = self::reFormatDate( $fcnStartIn );
735 $fcnStartYMD = sprintf(
736 self::$YMDs,
737 $fcnStart[self::$LCYEAR],
738 $fcnStart[self::$LCMONTH],
739 $fcnStart[self::$LCDAY]
740 );
741 if( ! empty( $fcnEndIn )) {
742 $fcnEnd = self::reFormatDate( $fcnEndIn );
743 }
744 else {
745 $fcnEnd = $fcnStart;
746 $fcnEnd[self::$LCYEAR] += self::EXTENDYEAR;
747 }
748 $fcnEndYMD = sprintf(
749 self::$YMDs,
750 $fcnEnd[self::$LCYEAR],
751 $fcnEnd[self::$LCMONTH],
752 $fcnEnd[self::$LCDAY]
753 );
754 if( ! isset( $recur[Vcalendar::COUNT] ) && ! isset( $recur[Vcalendar::UNTIL] )) {
755 $recur[Vcalendar::UNTIL] = $fcnEnd; // ??
756 } // create break
757 if( isset( $recur[Vcalendar::UNTIL] )) {
758 $recur[Vcalendar::UNTIL] = self::reFormatDate( $recur[Vcalendar::UNTIL] );
759 if( $fcnEnd > $recur[Vcalendar::UNTIL] ) {
760 $fcnEnd = $recur[Vcalendar::UNTIL]; // emergency break
761 $fcnEndYMD = sprintf(
762 self::$YMDs,
763 $fcnEnd[self::$LCYEAR],
764 $fcnEnd[self::$LCMONTH],
765 $fcnEnd[self::$LCDAY]
766 );
767 }
768 if( isset( $recur[Vcalendar::UNTIL][self::$LCHOUR] )) {
769 $untilHis = sprintf(
770 self::$HIS,
771 $recur[Vcalendar::UNTIL][self::$LCHOUR],
772 $recur[Vcalendar::UNTIL][self::$LCMIN],
773 $recur[Vcalendar::UNTIL][self::$LCSEC]
774 );
775 }
776 else {
777 $untilHis = sprintf( self::$HIS, 23, 59, 59 );
778 }
779 } // end if( isset( $recur[Vcalendar::UNTIL] ))
780 if( $wDateYMD > $fcnEndYMD ) {
781 return; // nothing to do.. .
782 }
783 $recurFreqIsYearly = ( Vcalendar::YEARLY == $recur[Vcalendar::FREQ] );
784 $recurFreqIsMonthly = ( Vcalendar::MONTHLY == $recur[Vcalendar::FREQ] );
785 $recurFreqIsWeekly = ( Vcalendar::WEEKLY == $recur[Vcalendar::FREQ] );
786 $recurFreqIsDaily = ( Vcalendar::DAILY == $recur[Vcalendar::FREQ] );
787 $wkst = ( Util::issetKeyAndEquals( $recur, Vcalendar::WKST, Vcalendar::SU ))
788 ? 24 * 60 * 60
789 : 0; // ??
790 $recurCount = ( ! isset( $recur[Vcalendar::BYSETPOS] )) ? 1 : 0; // DTSTART counts as the first occurrence
791 /* find out how to step up dates and set index for interval \count */
792 $step = [];
793 if( $recurFreqIsYearly ) {
794 $step[self::$LCYEAR] = 1;
795 }
796 elseif( $recurFreqIsMonthly ) {
797 $step[self::$LCMONTH] = 1;
798 }
799 elseif( $recurFreqIsWeekly ) {
800 $step[self::$LCDAY] = 7;
801 }
802 else {
803 $step[self::$LCDAY] = 1;
804 }
805 if( isset( $step[self::$LCYEAR] ) && isset( $recur[Vcalendar::BYMONTH] )) {
806 $step = [ self::$LCMONTH => 1 ];
807 }
808 if( empty( $step ) && isset( $recur[Vcalendar::BYWEEKNO] )) { // ??
809 $step = [ self::$LCDAY => 7 ];
810 }
811 if( isset( $recur[Vcalendar::BYYEARDAY] ) ||
812 isset( $recur[Vcalendar::BYMONTHDAY] ) ||
813 isset( $recur[Vcalendar::BYDAY] )) {
814 $step = [ self::$LCDAY => 1 ];
815 }
816 $intervalArr = [];
817 if( 1 < $recur[Vcalendar::INTERVAL] ) {
818 $intervalIx = self::recurIntervalIx(
819 $recur[Vcalendar::FREQ],
820 $wDate,
821 $wkst
822 );
823 $intervalArr = [ $intervalIx => 0 ];
824 }
825 if( isset( $recur[Vcalendar::BYSETPOS] )) { // save start date + weekno
826 $bysetPosymd1 = $bysetPosymd2 = $bysetPosw1 = $bysetPosw2 = [];
827 if( is_array( $recur[Vcalendar::BYSETPOS] )) {
828 foreach( $recur[Vcalendar::BYSETPOS] as $bix => $bval ) {
829 $recur[Vcalendar::BYSETPOS][$bix] = (int) $bval;
830 }
831 }
832 else {
833 $recur[Vcalendar::BYSETPOS] = [ (int) $recur[Vcalendar::BYSETPOS] ];
834 }
835 if( $recurFreqIsYearly ) {
836 // start from beginning of year
837 $wDate[self::$LCMONTH] = $wDate[self::$LCDAY] = 1;
838 $wDateYMD = sprintf(
839 self::$YMDs,
840 $wDate[self::$LCYEAR],
841 $wDate[self::$LCMONTH],
842 $wDate[self::$LCDAY]
843 );
844 // make sure to count last year
845 self::stepDate( $fcnEnd, $fcnEndYMD, [ self::$LCYEAR => 1 ] );
846 }
847 elseif( $recurFreqIsMonthly ) {
848 // start from beginning of month
849 $wDate[self::$LCDAY] = 1;
850 $wDateYMD = sprintf(
851 self::$YMDs,
852 $wDate[self::$LCYEAR],
853 $wDate[self::$LCMONTH],
854 $wDate[self::$LCDAY]
855 );
856 // make sure to count last month
857 self::stepDate( $fcnEnd, $fcnEndYMD, [ self::$LCMONTH => 1 ] );
858 }
859 else {
860 self::stepDate( $fcnEnd, $fcnEndYMD, $step );
861 } // make sure to \count whole last period
862 $bysetPosWold = self::getWeekNumber(
863 0,
864 0,
865 $wkst,
866 $wDate[self::$LCMONTH],
867 $wDate[self::$LCDAY],
868 $wDate[self::$LCYEAR]
869 );
870 $bysetPosYold = $wDate[self::$LCYEAR];
871 $bysetPosMold = $wDate[self::$LCMONTH];
872 $bysetPosDold = $wDate[self::$LCDAY];
873 } // end if( isset( $recur[Vcalendar::BYSETPOS] ))
874 else {
875 self::stepDate( $wDate, $wDateYMD, $step );
876 }
877 $yearOld = null;
878 $dayCnts = [];
879 /* MAIN LOOP */
880 while( true ) {
881 if( $wDateYMD . $wDateHis > $fcnEndYMD . $untilHis ) {
882 break;
883 }
884 if( isset( $recur[Vcalendar::COUNT] ) &&
885 ( $recurCount >= $recur[Vcalendar::COUNT] )) {
886 break;
887 }
888 if( $yearOld != $wDate[self::$LCYEAR] ) { // $yearOld=null 1:st time
889 $yearOld = $wDate[self::$LCYEAR];
890 $dayCnts = self::initDayCnts( $wDate, $recur, $wkst );
891 }
892 /* check interval */
893 if( 1 < $recur[Vcalendar::INTERVAL] ) {
894 /* create interval index */
895 $intervalIx = self::recurIntervalIx(
896 $recur[Vcalendar::FREQ],
897 $wDate,
898 $wkst
899 );
900 /* check interval */
901 $currentKey = array_keys( $intervalArr );
902 $currentKey = end( $currentKey ); // get last index
903 if( $currentKey != $intervalIx ) {
904 $intervalArr = [ $intervalIx => ( $intervalArr[$currentKey] + 1 ) ];
905 }
906 if(( $recur[Vcalendar::INTERVAL] != $intervalArr[$intervalIx] ) &&
907 ( 0 != $intervalArr[$intervalIx] )) {
908 /* step up date */
909 self::stepDate( $wDate, $wDateYMD, $step );
910 continue;
911 }
912 else { // continue within the selected interval
913 $intervalArr[$intervalIx] = 0;
914 }
915 } // endif( 1 < $recur['INTERVAL'] )
916 $updateOK = true;
917 if( $updateOK && isset( $recur[Vcalendar::BYMONTH] )) {
918 $updateOK = self::recurBYcntcheck(
919 $recur[Vcalendar::BYMONTH],
920 $wDate[self::$LCMONTH],
921 ( $wDate[self::$LCMONTH] - 13 )
922 );
923 }
924 if( $updateOK && isset( $recur[Vcalendar::BYWEEKNO] )) {
925 $updateOK = self::recurBYcntcheck(
926 $recur[Vcalendar::BYWEEKNO],
927 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$WEEKNO_UP],
928 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$WEEKNO_DOWN]
929 );
930 }
931 if( $updateOK && isset( $recur[Vcalendar::BYYEARDAY] )) {
932 $updateOK = self::recurBYcntcheck(
933 $recur[Vcalendar::BYYEARDAY],
934 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$YEARCNT_UP],
935 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$YEARCNT_DOWN]
936 );
937 }
938 if( $updateOK && isset( $recur[Vcalendar::BYMONTHDAY] )) {
939 $updateOK = self::recurBYcntcheck(
940 $recur[Vcalendar::BYMONTHDAY],
941 $wDate[self::$LCDAY],
942 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$MONTHCNT_DOWN]
943 );
944 }
945 if( $updateOK && isset( $recur[Vcalendar::BYDAY] )) {
946 $updateOK = false;
947 $m = $wDate[self::$LCMONTH];
948 $d = $wDate[self::$LCDAY];
949 if( isset( $recur[Vcalendar::BYDAY][Vcalendar::DAY] )) { // single day, opt with year/month day order no
950 $dayNumberExists = $dayNumberSw = $dayNameSw = false;
951 if( $recur[Vcalendar::BYDAY][Vcalendar::DAY] ==
952 $dayCnts[$m][$d][Vcalendar::DAY] ) {
953 $dayNameSw = true;
954 }
955 if( isset( $recur[Vcalendar::BYDAY][0] )) {
956 $dayNumberExists = true;
957 if( $recurFreqIsMonthly || isset( $recur[Vcalendar::BYMONTH] )) {
958 $dayNumberSw = self::recurBYcntcheck(
959 $recur[Vcalendar::BYDAY][0],
960 $dayCnts[$m][$d][self::$MONTHDAYNO_UP],
961 $dayCnts[$m][$d][self::$MONTHDAYNO_DOWN]
962 );
963 }
964 elseif( $recurFreqIsYearly ) {
965 $dayNumberSw = self::recurBYcntcheck(
966 $recur[Vcalendar::BYDAY][0],
967 $dayCnts[$m][$d][self::$YEARDAYNO_UP],
968 $dayCnts[$m][$d][self::$YEARDAYNO_DOWN]
969 );
970 }
971 }
972 if(( $dayNumberExists && $dayNumberSw && $dayNameSw ) ||
973 ( ! $dayNumberExists && ! $dayNumberSw && $dayNameSw )) {
974 $updateOK = true;
975 }
976 } // end if( isset( $recur[Vcalendar::BYDAY][Vcalendar::DAY] ))
977 else { // multiple days
978 foreach( $recur[Vcalendar::BYDAY] as $byDayValue ) {
979 $dayNumberExists = $dayNumberSw = $dayNameSw = false;
980 if( isset( $byDayValue[Vcalendar::DAY] ) &&
981 ( $byDayValue[Vcalendar::DAY] ==
982 $dayCnts[$m][$d][Vcalendar::DAY] )) {
983 $dayNameSw = true;
984 }
985 if( isset( $byDayValue[0] )) {
986 $dayNumberExists = true;
987 if( $recurFreqIsMonthly ||
988 isset( $recur[Vcalendar::BYMONTH] )) {
989 $dayNumberSw = self::recurBYcntcheck(
990 $byDayValue[Util::$ZERO],
991 $dayCnts[$m][$d][self::$MONTHDAYNO_UP],
992 $dayCnts[$m][$d][self::$MONTHDAYNO_DOWN]
993 );
994 }
995 elseif( $recurFreqIsYearly ) {
996 $dayNumberSw = self::recurBYcntcheck(
997 $byDayValue[Util::$ZERO],
998 $dayCnts[$m][$d][self::$YEARDAYNO_UP],
999 $dayCnts[$m][$d][self::$YEARDAYNO_DOWN]
1000 );
1001 }
1002 } // end if( isset( $byDayValue[0] ))
1003 if(( $dayNumberExists && $dayNumberSw && $dayNameSw ) ||
1004 ( ! $dayNumberExists && ! $dayNumberSw && $dayNameSw )) {
1005 $updateOK = true;
1006 break;
1007 }
1008 } // end foreach( $recur[Vcalendar::BYDAY] as $byDayValue )
1009 } // end else
1010 } // end if( $updateOK && isset( $recur[Vcalendar::BYDAY] ))
1011 /* check BYSETPOS */
1012 if( $updateOK ) {
1013 if( isset( $recur[Vcalendar::BYSETPOS] ) &&
1014 ( in_array( $recur[Vcalendar::FREQ], $YEAR2DAYARR ))) {
1015 if( $recurFreqIsWeekly ) {
1016 if( $bysetPosWold ==
1017 $dayCnts[$wDate[self::$LCMONTH]][$wDate[self::$LCDAY]][self::$WEEKNO_UP] ) {
1018 $bysetPosw1[] = $wDateYMD;
1019 }
1020 else {
1021 $bysetPosw2[] = $wDateYMD;
1022 }
1023 }
1024 else {
1025 if(( $recurFreqIsYearly &&
1026 ( $bysetPosYold == $wDate[self::$LCYEAR] )) ||
1027 ( $recurFreqIsMonthly &&
1028 (( $bysetPosYold == $wDate[self::$LCYEAR] ) &&
1029 ( $bysetPosMold == $wDate[self::$LCMONTH] ))) ||
1030 ( $recurFreqIsDaily &&
1031 (( $bysetPosYold == $wDate[self::$LCYEAR] ) &&
1032 ( $bysetPosMold == $wDate[self::$LCMONTH] ) &&
1033 ( $bysetPosDold == $wDate[self::$LCDAY] )))) {
1034 $bysetPosymd1[] = $wDateYMD;
1035 }
1036 else {
1037 $bysetPosymd2[] = $wDateYMD;
1038 }
1039 } // end else
1040 }
1041 else { // ! isset( $recur[Vcalendar::BYSETPOS] )
1042 if( checkdate(
1043 (int) $wDate[self::$LCMONTH],
1044 (int) $wDate[self::$LCDAY],
1045 (int) $wDate[self::$LCYEAR] )) {
1046 /* update result array if BYSETPOS is not set */
1047 $recurCount++;
1048 if( $fcnStartYMD <= $wDateYMD ) { // only output within period
1049 $result[$wDateYMD] = true;
1050 }
1051 }
1052 $updateOK = false;
1053 }
1054 }
1055 /* step up date */
1056 self::stepDate( $wDate, $wDateYMD, $step );
1057 /* check if BYSETPOS is set for updating result array */
1058 if( $updateOK && isset( $recur[Vcalendar::BYSETPOS] )) {
1059 $bysetPos = false;
1060 if( $recurFreqIsYearly &&
1061 ( $bysetPosYold != $wDate[self::$LCYEAR] )) {
1062 $bysetPos = true;
1063 $bysetPosYold = $wDate[self::$LCYEAR];
1064 }
1065 elseif( $recurFreqIsMonthly &&
1066 (( $bysetPosYold != $wDate[self::$LCYEAR] ) ||
1067 ( $bysetPosMold != $wDate[self::$LCMONTH] ))) {
1068 $bysetPos = true;
1069 $bysetPosYold = $wDate[self::$LCYEAR];
1070 $bysetPosMold = $wDate[self::$LCMONTH];
1071 }
1072 elseif( $recurFreqIsWeekly ) {
1073 $weekNo = self::getWeekNumber(
1074 0,
1075 0,
1076 $wkst,
1077 $wDate[self::$LCMONTH],
1078 $wDate[self::$LCDAY],
1079 $wDate[self::$LCYEAR]
1080 );
1081 if( $bysetPosWold != $weekNo ) {
1082 $bysetPosWold = $weekNo;
1083 $bysetPos = true;
1084 }
1085 }
1086 elseif( $recurFreqIsDaily &&
1087 (( $bysetPosYold != $wDate[self::$LCYEAR] ) ||
1088 ( $bysetPosMold != $wDate[self::$LCMONTH] ) ||
1089 ( $bysetPosDold != $wDate[self::$LCDAY] ))) {
1090 $bysetPos = true;
1091 $bysetPosYold = $wDate[self::$LCYEAR];
1092 $bysetPosMold = $wDate[self::$LCMONTH];
1093 $bysetPosDold = $wDate[self::$LCDAY];
1094 }
1095 if( $bysetPos ) {
1096 if( isset( $recur[Vcalendar::BYWEEKNO] )) {
1097 $bysetPosArr1 = &$bysetPosw1;
1098 $bysetPosArr2 = &$bysetPosw2;
1099 }
1100 else {
1101 $bysetPosArr1 = &$bysetPosymd1;
1102 $bysetPosArr2 = &$bysetPosymd2;
1103 }
1104
1105 foreach( $recur[Vcalendar::BYSETPOS] as $ix ) {
1106 if( 0 > $ix ) { // both positive and negative BYSETPOS allowed
1107 $ix = ( count( $bysetPosArr1 ) + $ix + 1 );
1108 }
1109 $ix--;
1110 if( isset( $bysetPosArr1[$ix] )) {
1111 if( $fcnStartYMD <= $bysetPosArr1[$ix] ) { // only output within period
1112 $result[$bysetPosArr1[$ix]] = true;
1113 }
1114 $recurCount++;
1115 }
1116 if( isset( $recur[Vcalendar::COUNT] ) &&
1117 ( $recurCount >= $recur[Vcalendar::COUNT] )) {
1118 break;
1119 }
1120 }
1121 $bysetPosArr1 = $bysetPosArr2;
1122 $bysetPosArr2 = [];
1123 } // end if( $bysetPos )
1124 } // end if( $updateOK && isset( $recur['BYSETPOS'] ))
1125 } // end while( true )
1126 ksort( $result );
1127 }
1128
1129 /**
1130 * Checking BYDAY (etc) hits, recur2date help function
1131 *
1132 * @since 2.6.12 - 2011-01-03
1133 * @param array $BYvalue
1134 * @param int $upValue
1135 * @param int $downValue
1136 * @return bool
1137 */
1138 private static function recurBYcntcheck( $BYvalue, $upValue, $downValue )
1139 {
1140 if( is_array( $BYvalue ) &&
1141 ( in_array( $upValue, $BYvalue ) || in_array( $downValue, $BYvalue ))
1142 ) {
1143 return true;
1144 }
1145 elseif(( $BYvalue == $upValue ) || ( $BYvalue == $downValue )) {
1146 return true;
1147 }
1148 else {
1149 return false;
1150 }
1151 }
1152
1153 /**
1154 * (re-)Calculate internal index, recur2date help function
1155 *
1156 * @param string $freq
1157 * @param array $date
1158 * @param int $wkst
1159 * @return bool
1160 * @since 2.26 - 2018-11-10
1161 */
1162 private static function recurIntervalIx( $freq, $date, $wkst )
1163 {
1164 /* create interval index */
1165 switch( $freq ) {
1166 case Vcalendar::YEARLY :
1167 $intervalIx = $date[self::$LCYEAR];
1168 break;
1169 case Vcalendar::MONTHLY :
1170 $intervalIx =
1171 $date[self::$LCYEAR] . Util::$MINUS . $date[self::$LCMONTH];
1172 break;
1173 case Vcalendar::WEEKLY :
1174 $intervalIx = self::getWeekNumber(
1175 0, 0, $wkst,
1176 $date[self::$LCMONTH], $date[self::$LCDAY], $date[self::$LCYEAR]
1177 );
1178 break;
1179 case Vcalendar::DAILY :
1180 default:
1181 $intervalIx =
1182 $date[self::$LCYEAR] .
1183 Util::$MINUS .
1184 $date[self::$LCMONTH] .
1185 Util::$MINUS .
1186 $date[self::$LCDAY];
1187 break;
1188 } // end switch
1189 return $intervalIx;
1190 }
1191
1192 /**
1193 * Return updated date, array and timpstamp
1194 *
1195 * @param array $date date to step
1196 * @param string $dateYMD date YMD
1197 * @param array $step default array( Util::$LCDAY => 1 )
1198 * @return void
1199 */
1200 private static function stepDate( & $date, & $dateYMD, $step = null )
1201 {
1202 if( is_null( $step )) {
1203 $step = [ self::$LCDAY => 1 ];
1204 }
1205 if( ! isset( $date[self::$LCHOUR] )) {
1206 $date[self::$LCHOUR] = 0;
1207 }
1208 if( ! isset( $date[self::$LCMIN] )) {
1209 $date[self::$LCMIN] = 0;
1210 }
1211 if( ! isset( $date[self::$LCSEC] )) {
1212 $date[self::$LCSEC] = 0;
1213 }
1214 if( isset( $step[self::$LCDAY] )) {
1215 $daysInMonth = self::getDaysInMonth(
1216 $date[self::$LCHOUR],
1217 $date[self::$LCMIN],
1218 $date[self::$LCSEC],
1219 $date[self::$LCMONTH],
1220 $date[self::$LCDAY],
1221 $date[self::$LCYEAR]
1222 );
1223 }
1224 foreach( $step as $stepix => $stepvalue ) {
1225 $date[$stepix] += $stepvalue;
1226 }
1227 if( isset( $step[self::$LCMONTH] )) {
1228 if( 12 < $date[self::$LCMONTH] ) {
1229 $date[self::$LCYEAR] += 1;
1230 $date[self::$LCMONTH] -= 12;
1231 }
1232 }
1233 elseif( isset( $step[self::$LCDAY] )) {
1234 if( $daysInMonth < $date[self::$LCDAY] ) {
1235 $date[self::$LCDAY] -= $daysInMonth;
1236 $date[self::$LCMONTH] += 1;
1237 if( 12 < $date[self::$LCMONTH] ) {
1238 $date[self::$LCYEAR] += 1;
1239 $date[self::$LCMONTH] -= 12;
1240 }
1241 }
1242 }
1243 $dateYMD = sprintf(
1244 self::$YMDs,
1245 $date[self::$LCYEAR],
1246 $date[self::$LCMONTH],
1247 $date[self::$LCDAY]
1248 );
1249 }
1250
1251 /**
1252 * Return initiated $dayCnts
1253 *
1254 * @param array $wDate
1255 * @param array $recur
1256 * @param int $wkst
1257 * @return array
1258 */
1259 private static function initDayCnts( array $wDate, array $recur, $wkst )
1260 {
1261 $dayCnts = [];
1262 $yearDayCnt = [];
1263 $yearDays = 0;
1264 foreach( self::$DAYNAMES as $dn ) {
1265 $yearDayCnt[$dn] = 0;
1266 }
1267 for( $m = 1; $m <= 12; $m++ ) { // count up and update up-counters
1268 $dayCnts[$m] = [];
1269 $weekDayCnt = [];
1270 foreach( self::$DAYNAMES as $dn ) {
1271 $weekDayCnt[$dn] = 0;
1272 }
1273 $daysInMonth = self::getDaysInMonth( 0, 0, 0, $m, 1, $wDate[self::$LCYEAR] );
1274 for( $d = 1; $d <= $daysInMonth; $d++ ) {
1275 $dayCnts[$m][$d] = [];
1276 if( isset( $recur[Vcalendar::BYYEARDAY] )) {
1277 $yearDays++;
1278 $dayCnts[$m][$d][self::$YEARCNT_UP] = $yearDays;
1279 }
1280 if( isset( $recur[Vcalendar::BYDAY] )) {
1281 $day = self::getDayInWeek( 0, 0, 0, $m, $d, $wDate[self::$LCYEAR] );
1282 $dayCnts[$m][$d][Vcalendar::DAY] = $day;
1283 $weekDayCnt[$day]++;
1284 $dayCnts[$m][$d][self::$MONTHDAYNO_UP] = $weekDayCnt[$day];
1285 $yearDayCnt[$day]++;
1286 $dayCnts[$m][$d][self::$YEARDAYNO_UP] = $yearDayCnt[$day];
1287 }
1288 if( isset( $recur[Vcalendar::BYWEEKNO] ) ||
1289 ( $recur[Vcalendar::FREQ] == Vcalendar::WEEKLY )) {
1290 $dayCnts[$m][$d][self::$WEEKNO_UP] =
1291 self::getWeekNumber(0,0, $wkst, $m, $d, $wDate[self::$LCYEAR] );
1292 }
1293 } // end for( $d = 1; $d <= $daysInMonth; $d++ )
1294 } // end for( $m = 1; $m <= 12; $m++ )
1295 $daycnt = 0;
1296 $yearDayCnt = [];
1297 if( isset( $recur[Vcalendar::BYWEEKNO] ) ||
1298 ( $recur[Vcalendar::FREQ] == Vcalendar::WEEKLY )) {
1299 $weekNo = null;
1300 for( $d = 31; $d > 25; $d-- ) { // get last weekno for year
1301 if( ! $weekNo ) {
1302 $weekNo = $dayCnts[12][$d][self::$WEEKNO_UP];
1303 }
1304 elseif( $weekNo < $dayCnts[12][$d][self::$WEEKNO_UP] ) {
1305 $weekNo = $dayCnts[12][$d][self::$WEEKNO_UP];
1306 break;
1307 }
1308 } // end for
1309 }
1310 for( $m = 12; $m > 0; $m-- ) { // count down and update down-counters
1311 $weekDayCnt = [];
1312 foreach( self::$DAYNAMES as $dn ) {
1313 $yearDayCnt[$dn] = $weekDayCnt[$dn] = 0;
1314 }
1315 $monthCnt = 0;
1316 $daysInMonth = self::getDaysInMonth( 0, 0, 0, $m, 1, $wDate[self::$LCYEAR] );
1317 for( $d = $daysInMonth; $d > 0; $d-- ) {
1318 if( isset( $recur[Vcalendar::BYYEARDAY] )) {
1319 $daycnt -= 1;
1320 $dayCnts[$m][$d][self::$YEARCNT_DOWN] = $daycnt;
1321 }
1322 if( isset( $recur[Vcalendar::BYMONTHDAY] )) {
1323 $monthCnt -= 1;
1324 $dayCnts[$m][$d][self::$MONTHCNT_DOWN] = $monthCnt;
1325 }
1326 if( isset( $recur[Vcalendar::BYDAY] )) {
1327 $day = $dayCnts[$m][$d][Vcalendar::DAY];
1328 $weekDayCnt[$day] -= 1;
1329 $dayCnts[$m][$d][self::$MONTHDAYNO_DOWN] = $weekDayCnt[$day];
1330 $yearDayCnt[$day] -= 1;
1331 $dayCnts[$m][$d][self::$YEARDAYNO_DOWN] = $yearDayCnt[$day];
1332 }
1333 if( isset( $recur[Vcalendar::BYWEEKNO] ) ||
1334 ( $recur[Vcalendar::FREQ] == Vcalendar::WEEKLY )) {
1335 $dayCnts[$m][$d][self::$WEEKNO_DOWN] =
1336 ( $dayCnts[$m][$d][self::$WEEKNO_UP] - $weekNo - 1 );
1337 }
1338 } // end for( $d = $daysInMonth; $d > 0; $d-- )
1339 } // end for( $m = 12; $m > 0; $m-- )
1340 return $dayCnts;
1341 }
1342
1343 /**
1344 * Return a reformatted input date
1345 *
1346 * @param mixed $inputDate
1347 * @return array
1348 * @throws Exception
1349 * @since 2.29.21 - 2020-01-31
1350 */
1351 private static function reFormatDate( $inputDate )
1352 {
1353 static $Y = 'Y';
1354 static $M = 'm';
1355 static $D = 'd';
1356 static $H = 'H';
1357 static $I = 'i';
1358 static $S = 'i';
1359 if( is_array( $inputDate )) {
1360 return $inputDate;
1361 }
1362 if( ! $inputDate instanceof DateTime ) {
1363 $inputDate = DateTimeFactory::factory( $inputDate );
1364 }
1365 return [
1366 self::$LCYEAR => (int) $inputDate->format( $Y ),
1367 self::$LCMONTH => (int) $inputDate->format( $M ),
1368 self::$LCDAY => (int) $inputDate->format( $D ),
1369 self::$LCHOUR => (int) $inputDate->format( $H ),
1370 self::$LCMIN => (int) $inputDate->format( $I ),
1371 self::$LCSEC => (int) $inputDate->format( $S ),
1372 ];
1373 }
1374
1375 /**
1376 * Return week number
1377 *
1378 * @param int $hour
1379 * @param int $min
1380 * @param int $sec
1381 * @param int $month
1382 * @param int $day
1383 * @param int $year
1384 * @return int
1385 */
1386 private static function getWeekNumber( $hour, $min, $sec, $month, $day, $year )
1387 {
1388 static $UCW = 'W'; // week number
1389 return (int) date( $UCW, mktime( $hour, $min, $sec, $month, $day, $year ));
1390 }
1391
1392 /**
1393 * Return number of days in month
1394 *
1395 * @param int $hour
1396 * @param int $min
1397 * @param int $sec
1398 * @param int $month
1399 * @param int $day
1400 * @param int $year
1401 * @return int
1402 */
1403 private static function getDaysInMonth( $hour, $min, $sec, $month, $day, $year )
1404 {
1405 static $LCT = 't'; // number of days in month
1406 return (int) date( $LCT, mktime( $hour, $min, $sec, $month, $day, $year ));
1407 }
1408
1409 /**
1410 * Return (string) 2-pos day in week
1411 *
1412 * @param int $hour
1413 * @param int $min
1414 * @param int $sec
1415 * @param int $month
1416 * @param int $day
1417 * @param int $year
1418 * @return string
1419 */
1420 private static function getDayInWeek( $hour, $min, $sec, $month, $day, $year )
1421 {
1422 static $LCW = 'w'; // day of week number
1423 $dayNo = (int) date(
1424 $LCW,
1425 mktime( $hour, $min, $sec, $month, $day, $year )
1426 );
1427 return self::$DAYNAMES[$dayNo];
1428 }
1429 }
1430