. * * This file is a part of iCalcreator. */ namespace Kigkonsult\Icalcreator\Util; use DateInterval; use DateTime; use DateTimeInterface; use Exception; use Kigkonsult\Icalcreator\CalendarComponent; use Kigkonsult\Icalcreator\Vcalendar; use RuntimeException; use function array_change_key_case; use function array_keys; use function array_unique; use function count; use function in_array; use function is_array; use function is_null; use function ksort; use function method_exists; use function sprintf; use function stripos; use function strtolower; use function substr; use function ucfirst; use function usort; /** * iCalcreator geo support class * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.30.10 - 2021-12-02 */ class SelectFactory { /** * @var string component end date properties * @static */ private static $DTENDEXIST = 'dtendExist'; private static $DUEEXIST = 'dueExist'; private static $DURATIONEXIST = 'durationExist'; private static $ENDALLDAYEVENT = 'endAllDayEvent'; /** * Return selected components from calendar on date or selectOption basis * * DTSTART MUST be set for every component. * No check of date. * * @param Vcalendar $calendar * @param int|array|DateTimeInterface $startY (int) start Year, default current Year * ALT. (object) DateTimeInterface start date * ALT. array selectOptions ( *[ => ] ) * @param int|DateTimeInterface $startM (int) start Month, default current Month * ALT. (object) DateTimeInterface end date * @param int $startD start Day, default current Day * @param int $endY end Year, default $startY * @param int $endM end Month, default $startM * @param int $endD end Day, default $startD * @param mixed $cType calendar component type(-s), default false=all else string/array type(-s) * @param bool $flat false (default) => output : array[Year][Month][Day][] * true => output : array[] (ignores split) * @param bool $any true (default) - select component(-s) that occurs within period * false - only component(-s) that starts within period * @param bool $split true (default) - one component copy every DAY it occurs during the * period (implies flat=false) * false - one occurance of component only in output array * @return array|false array on success, bool false on select error * @throws RuntimeException * @throws Exception * @static * @since 2.30.10 - 2021-12-01 */ public static function selectComponents( Vcalendar $calendar, $startY = null, $startM = null, $startD = null, $endY = null, $endM = null, $endD = null, $cType = null, $flat = null, $any = null, $split = null ) { static $P1D = 'P1D'; static $YMDHIS2 = 'YmdHis'; static $PRA = '%a'; static $YMDn = 'Ymd'; static $HIS = '%02d%02d%02d'; static $DAYOFDAYS = 'day %d of %d'; static $SORTER = [ SortFactory::class, 'cmpfcn' ]; /* check if empty calendar */ if( 1 > $calendar->countComponents()) { return false; } if( is_array( $startY )) { return self::selectComponents2( $calendar, $startY ); } /* assert boundary dates */ $argStart = ( $startY instanceof DateTimeInterface ) ? clone $startY : null; $argEnd = ( $startM instanceof DateTimeInterface ) ? clone $startM : null; self::assertDateArguments( $startY, $startM, $startD, $endY, $endM, $endD ); /* assert component types */ $cType = self::assertComponentTypes( $cType ); /* assert bool args */ self:: assertBoolArguments( $flat, $any, $split ); /* iterate components */ $result = []; $calendar = clone $calendar; $calendar->sort( Vcalendar::UID ); $compUIDold = null; $exdateList = $recurIdList = []; $INTERVAL_P1D = DateIntervalFactory::factory( $P1D ); while( $component = $calendar->getComponent()) { if( empty( $component )) { continue; } /* skip invalid type components */ if( ! Util::isCompInList( $component->getCompType(), $cType )) { continue; } /* select start from dtstart or due if dtstart is missing */ if(( false === ( $prop = $component->getDtstart( true ))) && (( Vcalendar::VTODO === $component->getCompType()) && ( false === ( $prop = $component->getDue( true ))))) { continue; } $compStart = UtilDateTime::factory( $prop[Util::$LCvalue], $prop[Util::$LCparams] ); $dtStartTz = $compStart->getTimezoneName(); if( ParameterFactory::isParamsValueSet( $prop, Vcalendar::DATE )) { $compStartHis = null; } else { $his = $compStart->getTime(); $compStartHis = sprintf( $HIS, $his[0], $his[1], $his[2] ); } /* get end date from dtend/due/duration properties */ $compEnd = self::getCompEndDate( $component, $dtStartTz ); if( empty( $compEnd )) { $compDuration = null; // DateInterval: no duration $compEnd = clone $compStart; $compEnd->setTime( 23, 59, 59 ); // 23:59:59 the same day as start } else { if( $compEnd->format( $YMDn ) < $compStart->format( $YMDn )) { // MUST be after start date!! $compEnd = $compStart->getClone(); $compEnd->setTime( 23, 59, 59 ); // 23:59:59 the same day as start or ??? } $compDuration = $compStart->diff( $compEnd ); // DateInterval } /* get UID */ $compUID = $component->getUid(); if( $compUIDold !== $compUID ) { $compUIDold = $compUID; $exdateList = $recurIdList = []; } $compType = $component->getCompType(); $isFreebusyCompType = ( Vcalendar::VFREEBUSY === $compType ); /** * Component with recurrence-id sorted before any rDate/rRule comp * to alter gmdate(time) when found in dtstart/recurlist. * (Note, a missing sequence (expected here) is the same as sequence=0 so don't test for sequence.) * Highest sequence always last, will replace any previous */ $recurId = null; if( ! $isFreebusyCompType && ( false !== ( $prop = $component->getRecurrenceid( true )))) { $recurId = UtilDateTime::factory( $prop[Util::$LCvalue], $prop[Util::$LCparams], $dtStartTz ); $rangeSet = Util::issetKeyAndEquals( $prop[Util::$LCparams], Vcalendar::RANGE, Vcalendar::THISANDFUTURE ); $recurIdList[$recurId->key] = [ $compStart->getClone(), $compEnd->getClone(), $compDuration, // DateInterval $rangeSet, clone $component, ]; // save recur day to altered YmdHis/duration/range continue; // ignore all but first prop in the recurrence_id component } // end HAS recurrence-id/sequence ksort( $recurIdList, SORT_STRING ); self::updateRecurrIdComps( $component, $recurIdList ); /* prepare */ if( null !== $argStart ) { $fcnStart = UtilDateTime::factory( $argStart ); } else { $fcnStart = $compStart->getClone(); $fcnStart->setDate( (int)$startY, (int)$startM, (int)$startD ); $fcnStart->setTime( 0, 0 ); } $fcnStartYmd = $fcnStart->format( $YMDn ); $fcnStartYmdHis = $fcnStart->format( $YMDHIS2 ); if( null !== $argEnd ) { $fcnEnd = UtilDateTime::factory( $argEnd ); } else { $fcnEnd = $compEnd->getClone(); $fcnEnd->setDate( (int)$endY, (int)$endM, (int)$endD ); $fcnEnd->setTime( 23, 59, 59 ); } /* set up work dates */ $workStart = $compStart->getClone(); $duration = ( empty( $compDuration )) ? $INTERVAL_P1D : $compDuration; // DateInterval $workStart->sub( $duration ); $workEnd = clone $fcnEnd;// !! $workEnd->add( $duration ); /* make a list of optional exclude dates for component occurence from exrule and exdate */ if( ! $isFreebusyCompType ) { self::getAllEXRULEdates( $component, $exdateList, $dtStartTz, $compStart, $workStart, $workEnd, $compStartHis ); self::getAllEXDATEdates( $component, $exdateList, $dtStartTz ); } /* select only components within.. . */ $xRecurrence = 1; // (dt)start within the period if(( ! $any && self::inScope( $compStart, $fcnStart, $compStart, $fcnEnd, $compStart->dateFormat )) || // occurs within the period ( $any && self::inScope( $fcnEnd, $compStart, $fcnStart, $compEnd, $compStart->dateFormat ))) { $propEndName = ( isset( $compEnd->SCbools[self::$DUEEXIST] )) ? Vcalendar::X_CURRENT_DUE : Vcalendar::X_CURRENT_DTEND; /* add the selected component (WITHIN valid dates) to output array */ if( $flat ) { // any=true/false, ignores split if( empty( $recurId )) { $result[$compUID] = clone $component; } // copy original to output (but not anyone with recurrence-id) } elseif( $split ) { // split the original component $rStart = ( $compStart->format( $YMDHIS2 ) < $fcnStart->format( $YMDHIS2 )) ? $fcnStart->getClone() : $compStart->getClone(); $rEnd = ( $compEnd->format( $YMDHIS2 ) > $fcnEnd->format( $YMDHIS2 )) ? $fcnEnd->getClone() : $compEnd->getClone(); $k = $rStart->key; if( ! isset( $exdateList[$k] )) { // not excluded in exrule/exdate if( isset( $recurIdList[$k] )) { // change start day to new YmdHis/duration $rStart = $recurIdList[$k][0]->getClone(); // UtilDateTime $startHis = $rStart->getTime(); $rEnd = $rStart->getClone(); if( ! empty( $recurIdList[$k][2] )) { // DateInterval $rEnd->add( $recurIdList[$k][2] ); } elseif( ! empty( $compDuration )) { // DateInterval $rEnd->add( $compDuration ); } $endHis = $rEnd->getTime(); $component2 = ( isset( $recurIdList[$k][4] )) ? clone $recurIdList[$k][4] : clone $component; } // end in recurIdList else { $startHis = $compStart->getTime(); $endHis = $compEnd->getTime(); $component2 = clone $component; } $cnt = 0; $tDate = clone $compStart; while( $tDate->format( $YMDn ) < $fcnStart->format( $YMDn )) { $cnt++; // in case fcnStart DAY AFTER cmpStart $tDate->add( $INTERVAL_P1D ); } // exclude any recurrence START date, found in exdatelist or recurrIdList // but accept the reccurence-id comp itself // count the days (incl start day) $occurenceDays = 1 + (int) $compStart->getClone() ->setTime( 0, 0 ) ->diff( $compEnd->getClone()->setTime( 0, 0 )) ->format( $PRA ); $rEndYmd = $rEnd->format( $YMDn ); while( $rStart->format( $YMDn ) <= $rEndYmd ) { ++$cnt; if( 1 < $occurenceDays ) { $component2->setXprop( Vcalendar::X_OCCURENCE, sprintf( $DAYOFDAYS, $cnt, $occurenceDays ) ); } if( 1 < $cnt ) { $rStart->setTime( 0, 0 ); } else { // make sure to exclude start day from the recurrence pattern $rStart->setTime( $startHis[0], $startHis[1], $startHis[2] ); $exdateList[$rStart->key] = $compDuration; // DateInterval } $component2->setXprop( Vcalendar::X_CURRENT_DTSTART, $rStart->format( $compStart->dateFormat ) ); if( ! empty( $compDuration )) { // DateInterval $rWdate = $rStart->getClone(); self::setDurationEndTime( $rWdate, $rEnd, $cnt, $occurenceDays, $endHis ); $component2->setXprop( $propEndName, $rWdate->format( $compEnd->dateFormat )); } // end if list( $xY, $xM, $xD ) = self::getArrayYMDkeys( $rStart ); if( ! isset( $result[$xY][$xM][$xD][$compUID] )) { $result[$xY][$xM][$xD][$compUID] = []; } $result[$xY][$xM][$xD][$compUID][] = clone $component2; // copy to output $rStart->add( $INTERVAL_P1D ); } // end while(( $rStart->format( 'Ymd' ) < $rEnd->format( 'Ymd' )) } // end if( ! isset( $exdateList[$rStart->key] )) } // end elseif( $split ) - else use component date else { // !$flat && !$split, i.e. no flat array and DTSTART within period if( isset( $recurIdList[$compStart->key] )) { $rStart = $recurIdList[$compStart->key][0]->getClone(); $rEnd = $recurIdList[$compStart->key][1]->getClone(); $component2 = ( isset( $recurIdList[$compStart->key][4] )) ? $recurIdList[$compStart->key][4] : clone $component; } else { $rStart = $compStart->getClone(); $rEnd = $compEnd->getClone(); $component2 = clone $component; } if( ! $any || ! isset( $exdateList[$rStart->key] )) { // exclude any recurrence date, found in exdatelist $component2->setXprop( Vcalendar::X_CURRENT_DTSTART, $rStart->format( $compStart->dateFormat )); if( ! empty( $compDuration )) { // DateInterval $component2->setXprop( $propEndName, $rEnd->format( $compEnd->dateFormat )); } // end if list( $xY, $xM, $xD ) = self::getArrayYMDkeys( $rStart ); if( ! isset( $result[$xY][$xM][$xD][$compUID] )) { $result[$xY][$xM][$xD][$compUID] = []; } $result[$xY][$xM][$xD][$compUID][] = clone $component2; // copy to output } } // end else } // end (dt)start within the period OR occurs within the period /* ************************************************************* if 'any' components, check components with reccurrence rules, removing all excluding dates *********************************************************** */ if( true === $any ) { $recurList = []; if( ! $isFreebusyCompType ) { /* make a list of optional repeating dates for component occurence, rrule, rdate */ self::getAllRRULEdates( $component, $recurList, $dtStartTz, $compStart, $workStart, $workEnd, $compStartHis, $exdateList, $compDuration ); $workStart = $fcnStart->getClone(); $workStart->sub( ( empty( $compDuration )) ? $INTERVAL_P1D : $compDuration ); self::getAllRDATEdates( $component, $recurList, $dtStartTz, $workStart, $fcnEnd, $compStart->dateFormat, $exdateList, $compStartHis, $compDuration ); unset( $workStart ); ksort( $recurList, SORT_STRING ); } // end if( Vcalendar::VFREEBUSY != $compType ) /* output all remaining components in recurlist */ if( 0 < count( $recurList )) { $component2 = clone $component; $compUID = $component2->getUid(); $YmdOld = null; $fcnEndYmdHis = $fcnEnd->format( $YMDHIS2 ); foreach( $recurList as $recurKey => $durationInterval ) { $recurKeyYmd = substr( $recurKey, 0, 8 ); if( $YmdOld === $recurKeyYmd ) { continue; // skip overlapping recur the same day, i.e. RDATE before RRULE } $YmdOld = $recurKeyYmd; $rStart = $compStart->getClone(); $rStart->setDateTimeFromString( $recurKey ); /* add recurring components within valid dates to output array, only start date set */ if( $flat ) { if( ! isset( $result[$compUID] )) { // only one comp $result[$compUID] = clone $component2; // copy to output } } /* add recurring components within valid dates to output array, split for each day */ elseif( $split ) { /* check and alter current component to recurr-comp if YMD match */ $component3 = null; $recurFound = false; foreach( $recurIdList as $k => $v ) { if( 0 === strpos( $k, $recurKeyYmd )) { $rStart = $v[0]->getClone(); $durationInterval2 = empty( $v[2] ) ? null : $v[2]; // DateInterval $component3 = clone $v[4]; $recurFound = true; break; } } // end foreach if( ! $recurFound ) { $component3 = clone $component2; $durationInterval2 = ( ! empty( $durationInterval )) ? $durationInterval : null; } $rEnd = $rStart->getClone(); if( ! empty( $durationInterval2 )) { $rEnd->add( $durationInterval2 ); } if( $rEnd->format( $YMDHIS2 ) > $fcnEndYmdHis ) { $rEnd = clone $fcnEnd; } $endHis = $rEnd->getTime(); ++$xRecurrence; $cnt = 0; // count the days (incl start day) $occurenceDays = 1 + (int) $rStart->getClone() ->setTime( 0, 0 ) ->diff( clone $rEnd->getClone()->setTime( 0, 0 )) ->format( $PRA ); $rEndYmd = $rEnd->format( $YMDn ); $propEndName = ( isset( $compEnd->SCbools[self::$DUEEXIST] )) ? Vcalendar::X_CURRENT_DUE : Vcalendar::X_CURRENT_DTEND; while( $rStart->format( $YMDn ) <= $rEndYmd ) { // iterate.. . ++$cnt; if( $rStart->format( $YMDn ) < $fcnStartYmd ) { // start date day before dtstart DAY $rStart->add( $INTERVAL_P1D ); // cycle rstart to dtstart DAY $rStart->setTime( 0, 0 ); continue; } if( 2 === $cnt ) { $rStart->setTime( 0, 0 ); } $component3->deleteXprop( $propEndName ); $component3->deleteXprop( Vcalendar::X_OCCURENCE ); $component3->setXprop( Vcalendar::X_RECURRENCE, $xRecurrence ); if( 1 < $occurenceDays ) { $component3->setXprop( Vcalendar::X_OCCURENCE, sprintf( $DAYOFDAYS, $cnt, $occurenceDays ) ); } $component3->setXprop( Vcalendar::X_CURRENT_DTSTART, $rStart->format( $compStart->dateFormat ) ); if( ! empty( $durationInterval2 )) { $rWdate = $rStart->getClone(); self::setDurationEndTime( $rWdate, $rEnd, $cnt, $occurenceDays, $endHis ); $component3->setXprop( $propEndName, $rWdate->format( $compEnd->dateFormat )); } list( $xY, $xM, $xD ) = self::getArrayYMDkeys( $rStart ); if( ! isset( $result[$xY][$xM][$xD][$compUID] )) { $result[$xY][$xM][$xD][$compUID] = []; } $result[$xY][$xM][$xD][$compUID][] = clone $component3; // copy to output $rStart->add( $INTERVAL_P1D ); } // end while( $rStart->format( 'Ymd' ) <= $rEnd->format( 'Ymd' )) unset( $rStart, $rEnd ); } // end elseif( $split ) elseif(( $rStartYmdHis = $rStart->format( $YMDHIS2 )) && ( $rStartYmdHis >= $fcnStartYmdHis ) && // date equal/after start ( $rStartYmdHis <= $fcnEndYmdHis )) { // date before/equal end // date within period, flat=false && split=false => one comp every recur startdate ++$xRecurrence; $component2->setXprop( Vcalendar::X_RECURRENCE, $xRecurrence ); $component2->setXprop( Vcalendar::X_CURRENT_DTSTART, $rStart->format( $compStart->dateFormat ) ); $propEndName = ( isset( $compEnd->SCbools[self::$DUEEXIST] )) ? Vcalendar::X_CURRENT_DUE : Vcalendar::X_CURRENT_DTEND; if( empty( $durationInterval )) { $component2->deleteXprop( $propEndName ); } else { $component2->setXprop( $propEndName, $rStart->getClone()->add( $durationInterval )->format( $compEnd->dateFormat ) ); } list( $xY, $xM, $xD ) = self::getArrayYMDkeys( $rStart ); if( ! isset( $result[$xY][$xM][$xD][$compUID] )) { $result[$xY][$xM][$xD][$compUID] = []; } $result[$xY][$xM][$xD][$compUID][] = clone $component2; // copy to output } // end elseif( $rStart >= $fcnStart ) } // end foreach( $recurList as $recurKey => $durationInterval ) } // end if( 0 < count( $recurList )) } // end if( true === $any ) } // end while( $component = $calendar->getComponent()) if( 0 >= count( $result )) { return false; } if( ! $flat ) { foreach( $result as $y => $yList ) { foreach( $yList as $m => $mList ) { foreach( $mList as $d => $dList ) { if( empty( $dList )) { unset( $result[$y][$m][$d] ); } else { // skip tricky UID-index $temp = []; foreach( $dList as $compVal ) { if( is_array( $compVal )) { foreach( $compVal as $comp ) { $temp[] = $comp; } continue; } $temp[] = $compVal; } // end foreach $result[$y][$m][$d] = $temp; if( 1 < count( $result[$y][$m][$d] )) { // sort foreach( $result[$y][$m][$d] as $comp ) { SortFactory::setSortArgs( $comp ); } usort( $result[$y][$m][$d], $SORTER ); } } // end else } // end foreach( $mList as $d => $dList ) if( empty( $result[$y][$m] )) { unset( $result[$y][$m] ); } else { ksort( $result[$y][$m] ); } } // end foreach( $yList as $m => $mList ) if( empty( $result[$y] )) { unset( $result[$y] ); } else { ksort( $result[$y] ); } } // end foreach( $result as $y => $yList ) if( empty( $result )) { unset( $result ); } else { ksort( $result ); } } // end elseif( !$flat ) return $result; } /** * Return bool true if dates are in scope * * @param UtilDateTime $start * @param UtilDateTime $scopeStart * @param UtilDateTime $end * @param UtilDateTime $scopeEnd * @param string $format * @return bool * @static */ private static function inScope( UtilDateTime $start, UtilDateTime $scopeStart, UtilDateTime $end, UtilDateTime $scopeEnd, $format ) { return (( $start->format( $format ) >= $scopeStart->format( $format )) && ( $end->format( $format ) <= $scopeEnd->format( $format ))); } /** * Get all EXRULE dates (multiple values allowed) * * @param CalendarComponent $component * @param array $exdateList * @param string $dtStartTz * @param UtilDateTime $compStart * @param UtilDateTime $workStart * @param UtilDateTime $workEnd * @param string $compStartHis * @throws Exception * @since 2.27.14 - 2019-02-27 */ private static function getAllEXRULEdates( CalendarComponent $component, array & $exdateList, $dtStartTz, UtilDateTime $compStart, UtilDateTime $workStart, UtilDateTime $workEnd, $compStartHis ) { if( false !== ( $prop = $component->getExrule( true ))) { $isValueDate = ParameterFactory::isParamsValueSet( $prop, Vcalendar::DATE ); $prop = $prop[Util::$LCvalue]; if( isset( $prop[Vcalendar::UNTIL] ) && ! $isValueDate ) { // convert UNTIL date to DTSTART timezone $prop[Vcalendar::UNTIL] = UtilDateTime::factory( $prop[Vcalendar::UNTIL], [ Vcalendar::TZID => Vcalendar::UTC ], $dtStartTz ); } $exdateList2 = []; RecurFactory::recur2date( $exdateList2, $prop, $compStart, $workStart, $workEnd ); foreach( $exdateList2 as $k => $v ) { // point out exact every excluded ocurrence (incl. opt. His) $exdateList[$k . $compStartHis] = $v; } } } /** * Get all EXDATE dates (multiple values allowed) * * @param CalendarComponent $component * @param array $exdateList * @param string $dtStartTz * @throws Exception * @since 2.27.2 - 2018-12-29 */ private static function getAllEXDATEdates( CalendarComponent $component, array & $exdateList, $dtStartTz ) { while( false !== ( $prop = $component->getExdate( false, true ))) { foreach( $prop[Util::$LCvalue] as $exdate ) { $exdate = UtilDateTime::factory( $exdate, $prop[Util::$LCparams], $dtStartTz ); $exdateList[$exdate->key] = true; } // end - foreach( $exdate as $exdate ) } // end while } /** * Update $recurList all RRULE dates (multiple values allowed) * * @param CalendarComponent $component * @param array $recurList * @param string $dtStartTz * @param UtilDateTime $compStart * @param UtilDateTime $workStart * @param UtilDateTime $workEnd * @param string $compStartHis * @param array $exdateList * @param DateInterval $compDuration * @throws Exception * @since 2.27.14 - 2019-02-27 */ private static function getAllRRULEdates( CalendarComponent $component, array & $recurList, $dtStartTz, UtilDateTime $compStart, UtilDateTime $workStart, UtilDateTime $workEnd, $compStartHis, array & $exdateList, DateInterval $compDuration = null ) { $exdateYmdList = self::getYmdList( $exdateList ); $recurYmdList = self::getYmdList( $recurList ); if( false !== ( $prop = $component->getRrule( true ))) { $isValueDate = ParameterFactory::isParamsValueSet( $prop, Vcalendar::DATE ); $prop = $prop[Util::$LCvalue]; if( isset( $prop[Vcalendar::UNTIL] ) && ! $isValueDate ) { // convert RRULE['UNTIL'] to same timezone as DTSTART !! $prop[Vcalendar::UNTIL] = UtilDateTime::factory( $prop[Vcalendar::UNTIL], [ Vcalendar::TZID => Vcalendar::UTC ], $dtStartTz ); } $recurList2 = []; RecurFactory::recur2date( $recurList2, $prop, $compStart, $workStart, $workEnd ); foreach( $recurList2 as $recurKey => $recurValue ) { // recurkey=Ymd if( isset( $exdateYmdList[$recurKey] )) { // exclude on Ymd basis continue; } $YmdHisKey = $recurKey . $compStartHis; // add opt His if( isset( $recurYmdList[$recurKey] )) { // replace on Ymd basis $exdateList[$YmdHisKey] = true; continue; } if( ! isset( $exdateList[$YmdHisKey] )) { $recurList[$YmdHisKey] = $compDuration; // DateInterval or false } } // end foreach } // end while } /** * Update $recurList with RDATE dates (overwrite if exists) * * @param CalendarComponent $component * @param array $recurList * @param string $dtStartTz * @param UtilDateTime $workStart * @param UtilDateTime $fcnEnd * @param string $format * @param array $exdateList * @param string $compStartHis * @param DateInterval $compDuration * @throws Exception * @since 2.27.2 - 2018-12-29 */ private static function getAllRDATEdates( CalendarComponent $component, array & $recurList, $dtStartTz, UtilDateTime $workStart, UtilDateTime $fcnEnd, $format, array & $exdateList, $compStartHis, DateInterval $compDuration = null ) { $exdateYmdList = self::getYmdList( $exdateList ); $recurYmdList = self::getYmdList( $recurList ); while( false !== ( $prop = $component->getRdate( false, true ))) { $rDateFmt = ( isset( $prop[Util::$LCparams][Vcalendar::VALUE] )) ? $prop[Util::$LCparams][Vcalendar::VALUE] // DATE or PERIOD : Vcalendar::DATE_TIME; $params = $prop[Util::$LCparams]; $prop = $prop[Util::$LCvalue]; foreach( $prop as $theRdate ) { if( Vcalendar::PERIOD === $rDateFmt ) { // all days within PERIOD $rDate = UtilDateTime::factory( $theRdate[0], $params, $dtStartTz ); if( ! self::inScope( $rDate, $workStart, $rDate, $fcnEnd, $format )) { continue; } $cmpKey = substr( $rDate->key, 0, 8 ); // exclude on Ymd basis (rRules already excluded) if( isset( $exdateYmdList[$cmpKey] )) { continue; } // exclude on Ymd(His) basis if( isset( $exdateList[$rDate->key] )) { continue; } // rDate replaces rRule, update excludeList if( isset( $recurYmdList[$cmpKey] )) { $exdateList[$recurYmdList[$cmpKey]] = true; } if( $theRdate[1] instanceof DateTime ) { // date-date period end $recurList[$rDate->key] = $rDate->diff( // save duration UtilDateTime::factory( $theRdate[1], $params, $dtStartTz ) ); continue; } try { // period duration $recurList[$rDate->key] = DateIntervalFactory::DateIntervalArr2DateInterval( $theRdate[1] ); } catch( Exception $e ) { throw $e; } continue; } // end if( Vcalendar::PERIOD == $rDateFmt ) if( Vcalendar::DATE === $rDateFmt ) { // single recurrence, DATE (=Ymd) $rDate = UtilDateTime::factory( $theRdate, array_merge( $params, [ Vcalendar::TZID => $dtStartTz ] ), $dtStartTz ); $rDateYmdHisKey = $rDate->key . $compStartHis; } else { // single recurrence, DATETIME $rDate = UtilDateTime::factory( $theRdate, $params, $dtStartTz ); // set start date for recurrence + DateInterval/false (+opt His) $rDateYmdHisKey = $rDate->key; } $cmpKey = substr( $rDate->key, 0, 8 ); switch( true ) { case ( isset( $exdateYmdList[$cmpKey] )) : // excluded on Ymd basis break; case ( ! self::inScope( $rDate, $workStart, $rDate, $fcnEnd, $format )) : break; default : if( isset( $recurYmdList[$cmpKey] )) { // rDate replaces rRule $exdateList[$recurYmdList[$cmpKey]] = true; } $recurList[$rDateYmdHisKey] = $compDuration; break; } // end switch } // end foreach } // end while } /** * Return YmdList from YmdHis keyed array * * @param array $YmdHisArr * @return array * @static * @since 2.26.2 - 2018-11-15 */ private static function getYmdList( array $YmdHisArr ) { $res = []; foreach( $YmdHisArr as $key => $value ) { $res[substr( $key, 0, 8 )] = $key; } return $res; } /** * Assert date arguments * * @param int|DateTimeInterface $startY * @param int|DateTimeInterface $startM * @param int $startD * @param int $endY * @param int $endM * @param int $endD * @static * @since 2.29.16 - 2020-01-24 */ private static function assertDateArguments( & $startY = null, & $startM = null, & $startD = null, & $endY = null, & $endM = null, & $endD = null ) { static $Y = 'Y'; static $M = 'm'; static $D = 'd'; if(( $startY instanceof DateTimeInterface ) && ( $startM instanceof DateTimeInterface )) { $endY = (int) $startM->format( $Y ); $endM = (int) $startM->format( $M ); $endD = (int) $startM->format( $D ); $startD = (int) $startY->format( $D ); $startM = (int) $startY->format( $M ); $startY = (int) $startY->format( $Y ); } else { if( empty( $startY )) { $startY = (int) gmdate( $Y ); } if( empty( $startM )) { $startM = (int) gmdate( $M ); } if( empty( $startD )) { $startD = (int) gmdate( $D ); } if( empty( $endY )) { $endY = $startY; } if( empty( $endM )) { $endM = $startM; } if( empty( $endD )) { $endD = $startD; } } } /** * Return reviewed component types * * @param array|string $cType * @return array * @static * @since 2.27.18 - 2019-04-07 */ private static function assertComponentTypes( $cType = null ) { if( empty( $cType )) { return Vcalendar::$VCOMPS; } if( ! is_array( $cType )) { $cType = [ $cType ]; } foreach( $cType as & $theType ) { $theType = ucfirst( strtolower( $theType )); if( ! Util::isCompInList( $theType, Vcalendar::$VCOMPS )) { $theType = Vcalendar::VEVENT; } } return array_unique( $cType ); } /** * Assert bool arguments * * @param bool $flat * @param bool $any * @param bool $split * @static * @since 2.26.2 - 2018-11-15 */ private static function assertBoolArguments( & $flat = null, & $any = null, & $split = null ) { // defaults $flat = ! is_null( $flat ) && (bool)$flat; $any = is_null( $any ) || (bool)$any; $split = is_null( $split ) || (bool)$split; if(( false === $flat ) && ( false === $any )) { // invalid combination $split = false; } if(( true === $flat ) && ( true === $split )) { // invalid combination $split = false; } } /** * Return comp end gmdate(time) from dtend/due/duration properties * * @param CalendarComponent $component * @param string $dtStartTz * @return UtilDateTime * @throws Exception * @static * @since 2.27.6 - 2018-12-29 */ private static function getCompEndDate( CalendarComponent $component, $dtStartTz ) { static $MINUS1DAY = '-1 day'; $compEnd = $prop = null; $compType = $component->getCompType(); $isFreebusyCompType = ( Vcalendar::VFREEBUSY === $compType ); $isVtodoCompType = ( Vcalendar::VTODO === $compType ); $isVeventCompType = ( Vcalendar::VEVENT === $compType ); if(( $isVeventCompType || $isFreebusyCompType ) && ( false !== ( $prop = $component->getDtend( true )))) { $compEnd = UtilDateTime::factory( $prop[Util::$LCvalue], $prop[Util::$LCparams], $dtStartTz ); $compEnd->SCbools[self::$DTENDEXIST] = true; } if( empty( $prop ) && $isVtodoCompType && ( false !== ( $prop = $component->getDue( true )))) { $compEnd = UtilDateTime::factory( $prop[Util::$LCvalue], $prop[Util::$LCparams], $dtStartTz ); $compEnd->SCbools[self::$DUEEXIST] = true; } if( empty( $prop ) && // duration in dtend (array) format ( $isVeventCompType || $isVtodoCompType ) && ( false !== ( $prop = $component->getDuration( true, true )))) { $compEnd = UtilDateTime::factory( $prop[Util::$LCvalue], $prop[Util::$LCparams], $dtStartTz ); $compEnd->SCbools[self::$DURATIONEXIST] = true; } if( ! empty( $prop ) && ParameterFactory::isParamsValueSet( $prop, Vcalendar::DATE )) { /* a DTEND without time part denotes an end of an event that actually ends the day before, for an all-day event DTSTART=20071201 DTEND=20071202, taking place 20071201!!! */ $compEnd->SCbools[self::$ENDALLDAYEVENT] = true; $compEnd->modify( $MINUS1DAY ); $compEnd->setTime( 23, 59, 59 ); } return $compEnd; } /** * Set duration end time * * @param UtilDateTime $rStart * @param UtilDateTime $rEnd * @param int $cnt * @param int $occurenceDays * @param array $endHis * @static * @since 2.26 - 2018-11-10 */ private static function setDurationEndTime( UtilDateTime $rStart, UtilDateTime $rEnd, $cnt, $occurenceDays, array $endHis ) { static $YMD = 'Ymd'; if( $cnt < $occurenceDays ) { $rStart->setTime( 23, 59, 59 ); } elseif( $rStart->format( $YMD ) < $rEnd->format( $YMD )) { if( ( 0 === $endHis[0] ) && ( 0 === $endHis[1] ) && ( 0 === $endHis[2] )) { $rStart->setTime( 24, 0 ); // end exactly at midnight // 24:00:00 } else { $rStart->setTime( 23, 59, 59 ); // break at midnight, cont next day // 23:59:59 } } else { $rStart->setTime( $endHis[0], $endHis[1], $endHis[2] ); } } /** * Get array Y, m, d keys * * @param UtilDateTime $icaldateTime * @return array * @static * @since 2.26.2 - 2018-11-15 */ private static function getArrayYMDkeys( UtilDateTime $icaldateTime ) { static $Y = 'Y'; static $M = 'm'; static $D = 'd'; return [ (int) $icaldateTime->format( $Y ), (int) $icaldateTime->format( $M ), (int) $icaldateTime->format( $D ) ]; } /** * Update recurr-id-comps properties summary, description and comment if missing * * @param CalendarComponent $component (Vevent/Vtodo/Vjournal) * @param array $recurIdComps * @static * @since 2.27.1 - 2018-12-16 */ private static function updateRecurrIdComps( CalendarComponent $component, array & $recurIdComps ) { if( empty( $recurIdComps )) { return; } if( Vcalendar::VFREEBUSY === $component->getCompType()) { return; } $summary = $component->getSummary( true ); $description = $component->getDescription( null, true ); $comments = []; while( false !== ( $comment = $component->getComment( null, true ))) { $comments[] = $comment; } foreach( array_keys( $recurIdComps ) as $RecurrIdKey ) { if( ! empty( $summary )) { $value = $recurIdComps[$RecurrIdKey][4]->getSummary(); if( empty( $value )) { $recurIdComps[$RecurrIdKey][4]->setSummary( $summary[Util::$LCvalue], $summary[Util::$LCparams] ); } } if( ! empty( $description )) { $value = $recurIdComps[$RecurrIdKey][4]->getDescription(); if( empty( $value )) { $recurIdComps[$RecurrIdKey][4]->setDescription( $description[Util::$LCvalue], $description[Util::$LCparams] ); } } // end if if( empty( $comments )) { continue; } $value = $recurIdComps[$RecurrIdKey][4]->getComment(); if( ! empty( $value )) { continue; } foreach( $comments as $prop ) { $recurIdComps[$RecurrIdKey][4]->setComment( $prop[Util::$LCvalue], $prop[Util::$LCparams] ); } } // end foreach } /** * Return array with selected components values from calendar based on specific property value(-s) * * @param Vcalendar $calendar * @param array $selectOptions (string) key => (mixed) value, (key=propertyName) * @return array * @static * @since 2.27.17 - 2020-01-25 */ private static function selectComponents2( Vcalendar $calendar, array $selectOptions ) { $output = []; $selectOptions = array_change_key_case( $selectOptions, CASE_UPPER ); while( $component3 = $calendar->getComponent()) { if( empty( $component3 )) { continue; } if( ! Util::isCompInList( $component3->getCompType(), Vcalendar::$VCOMPS )) { continue; } $uid = $component3->getUid(); foreach( $selectOptions as $propName => $propValue ) { if( ! Util::isPropInList( $propName, Vcalendar::$OTHERPROPS )) { continue; } if( ! is_array( $propValue )) { $propValue = [ $propValue ]; } if(( Vcalendar::UID === $propName ) && in_array( $uid, $propValue )) { $output[$uid][] = $component3; continue; } if( Util::isPropInList( $propName, Vcalendar::$MPROPS1 )) { $propValues = []; $component3->getProperties( $propName, $propValues ); $propValues = array_keys( $propValues ); foreach( $propValue as $theValue ) { if( in_array( $theValue, $propValues )) { // && ! isset( $output[$uid] )) { $output[$uid][] = $component3; break; } } continue; } // end elseif( // multiple occurrence? $method = Vcalendar::getGetMethodName( $propName ); if( ! method_exists( $component3, $method ) || ( false === ( $d = $component3->{$method}()))) { // single occurrence continue; } $outputUidIsSet = isset( $output[$uid] ); if( is_array( $d )) { foreach( $d as $part ) { if( ! $outputUidIsSet && in_array( $part, $propValue )) { $output[$uid][] = $component3; } } } elseif(( ! $outputUidIsSet && Vcalendar::SUMMARY === $propName )) { foreach( $propValue as $pval ) { if( false !== stripos( $d, $pval )) { $output[$uid][] = $component3; break; } } } elseif( ! $outputUidIsSet && in_array( $d, $propValue )) { $output[$uid][] = $component3; } } // end foreach( $selectOptions as $propName => $propValue ) } // end while( $component3 = $calendar->getComponent()) { if( ! empty( $output )) { ksort( $output ); // uid order $output2 = []; foreach( $output as $uList ) { // $uid => $uList foreach( $uList as $uValue ) { $output2[] = $uValue; } } $output = $output2; } return $output; } }