Your IP : 216.73.216.240
<?php
/**
* iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
*
* This file is a part of iCalcreator.
*
* @author Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
* @copyright 2007-2024 Kjell-Inge Gustafsson, kigkonsult AB, All rights reserved
* @link https://kigkonsult.se
* @license Subject matter of licence is the software iCalcreator.
* The above copyright, link, package and version notices,
* this licence notice and the invariant [rfc5545] PRODID result use
* as implemented and invoked in iCalcreator shall be included in
* all copies or substantial portions of the iCalcreator.
*
* iCalcreator is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* iCalcreator is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
*/
declare( strict_types = 1 );
namespace Kigkonsult\Icalcreator\Util;
use DateInterval;
use DateTime;
use DateTimeInterface;
use Exception;
use Kigkonsult\Icalcreator\CalendarComponent;
use Kigkonsult\Icalcreator\Vavailability;
use Kigkonsult\Icalcreator\Vcalendar;
use Kigkonsult\Icalcreator\Vevent;
use Kigkonsult\Icalcreator\Vfreebusy;
use Kigkonsult\Icalcreator\Vjournal;
use Kigkonsult\Icalcreator\Vtodo;
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 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
*
* @since 2.41.88 - 2024-01-18
*/
class SelectFactory
{
/**
* @var string component end date properties
*/
private static string $DTENDEXIST = 'dtendExist';
/**
* @var string dito
*/
private static string $DUEEXIST = 'dueExist';
/**
* @var string dito
*/
private static string $DURATIONEXIST = 'durationExist';
/**
* @var string dito
*/
private static string $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 null|int|array|DateTimeInterface $startY (int) start Year, default current Year
* ALT. (object) DateTimeInterface start date
* ALT. array selectOptions ( *[ <propName> => <uniqueValue> ] )
* @param null|int|DateTimeInterface $startM (int) start Month, default current Month
* ALT. (object) DateTimeInterface end date
* @param null|int $startD start Day, default current Day
* @param null|int $endY end Year, default $startY
* @param null|int $endM end Month, default $startM
* @param null|int $endD end Day, default $startD
* @param mixed $cType calendar component type(-s), default false=all else string/array type(-s)
* @param null|bool $flat false (default) => output : array[Year][Month][Day][]
* true => output : array[] (ignores split)
* @param null|bool $any true (default) - select component(-s) that occurs within period
* false - only component(-s) that starts within period
* @param null|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|bool false on select error
* @throws RuntimeException
* @throws Exception
* @since 2.41.45 - 2022-04-27
*/
public static function selectComponents(
Vcalendar $calendar,
null|int|array|DateTimeInterface $startY = null,
null|int|DateTimeInterface $startM = null,
null|int $startD = null,
null|int $endY = null,
null|int $endM = null,
null|int $endD = null,
mixed $cType = null,
null|bool $flat = null,
null|bool $any = null,
null|bool $split = null
) : bool | array
{
static $P1D = 'P1D';
static $YMDHIS2 = 'YmdHis';
static $YMDn = 'Ymd';
static $HIS = '%02d%02d%02d';
static $DAYOFDAYS = 'day %d of %d';
/* 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 );
$rscaleUids = RecurFactory::rruleRscaleCheck( $calendar, $cType );
$compUIDold = null;
$exdateList = $recurIdList = [];
$INTERVAL_P1D = DateIntervalFactory::factory( $P1D );
$calendar->resetCompCounter();
while( $component = $calendar->getComponent()) {
if( empty( $component )) {
continue;
}
/* skip invalid type components */
if( ! in_array( $component->getCompType(), $cType, true )) {
continue;
}
/* get UID */
$compUID = $component->getUid();
if( in_array( $compUID, $rscaleUids, true )) { // UIDs to skip, rfc7529 6. Compatibility, option 2
continue;
}
/* check UID */
if( $compUIDold !== $compUID ) {
$compUIDold = $compUID;
$exdateList = $recurIdList = [];
}
/* 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->value, $prop->params );
$dtStartTz = $compStart->getTimezoneName();
$compStartHis = StringFactory::$SP0;
if( ! $prop->hasParamValue( Vcalendar::DATE )) {
$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( $compEnd === null ) {
$compDuration = null; // DateInterval: no duration
$compEnd = $compStart->getClone();
$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
}
$propEndName = ( isset( $compEnd->SCbools[self::$DUEEXIST] ))
? Vcalendar::X_CURRENT_DUE
: Vcalendar::X_CURRENT_DTEND;
$compType = $component->getCompType();
$isFreebusyCompType = ( Vcalendar::VFREEBUSY === $compType );
/**
* Component with recurrence-id sorted before any rDate/rRule comp
* Used to alter date(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->value, $prop->params, $dtStartTz );
$rangeSet = $prop->hasParamKey( 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 ))) {
/* 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] )) {
--$xRecurrence;
}
else { // 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 reccurence in days (incl start day)
$occurenceDays = DateTimeFactory::getDayDiff( $rStart, $rEnd );
$rEndYmd = $rEnd->format( $YMDn );
while( $rStart->format( $YMDn ) <= $rEndYmd ) {
++$cnt;
if( 1 < $occurenceDays ) {
$xPropVal = sprintf( $DAYOFDAYS, $cnt, $occurenceDays );
$component2->setXprop( Vcalendar::X_OCCURENCE, $xPropVal );
}
if( 1 < $cnt ) {
$rStart->setTime( 0, 0 ); // 0: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
self::nonFlatAppend( clone $component2, $result, $rStart, $compUID ); // 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 = $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
self::nonFlatAppend( clone $component2, $result, $rStart, $compUID ); // 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()->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((string) $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((string) $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( str_starts_with((string) $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 reccurence in days (incl start day)
$occurenceDays = DateTimeFactory::getDayDiff( $rStart, $rEnd );
$rEndYmd = $rEnd->format( $YMDn );
while( $rStart->format( $YMDn ) <= $rEndYmd ) { // iterate.. .
++$cnt;
if( $rStart->format( $YMDn ) < $fcnStartYmd ) { // date before dtstart
$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->setXprop( Vcalendar::X_RECURRENCE, $xRecurrence );
if( 1 < $occurenceDays ) {
$xPropVal = sprintf( $DAYOFDAYS, $cnt, $occurenceDays );
$component3->setXprop( Vcalendar::X_OCCURENCE, $xPropVal );
}
$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 ));
}
self::nonFlatAppend( clone $component3, $result, $rStart, $compUID ); // 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 )
);
if( empty( $durationInterval )) {
$component2->deleteXprop( $propEndName );
}
else {
$component2->setXprop(
$propEndName,
$rStart->getClone()->add( $durationInterval )->format( $compEnd->dateFormat )
);
}
self::nonFlatAppend( clone $component2, $result, $rStart, $compUID ); // 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 ) {
self::ymdSort( $result );
} // end elseif( !$flat )
return $result;
}
/**
* @param CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param array $result
* @param DateTimeInterface $date
* @param null|string $compUID
* @return void
*/
private static function nonFlatAppend(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
array & $result,
DateTimeInterface $date,
? string $compUID = ''
) : void
{
static $Y = 'Y';
static $M = 'm';
static $D = 'd';
$xY = (int) $date->format( $Y );
$xM = (int) $date->format( $M );
$xD = (int) $date->format( $D );
if( ! isset( $result[$xY][$xM][$xD][$compUID] )) {
$result[$xY][$xM][$xD][$compUID] = [];
}
$result[$xY][$xM][$xD][$compUID][] = $component;
}
/**
* @param array $result
* @return void
*/
private static function ymdSort( array & $result ) : void
{
static $SORTER = [ SortFactory::class, 'cmpfcn' ];
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 );
}
}
/**
* 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
*/
private static function inScope(
UtilDateTime $start,
UtilDateTime $scopeStart,
UtilDateTime $end,
UtilDateTime $scopeEnd,
string $format
) : bool
{
return (( $start->format( $format ) >= $scopeStart->format( $format )) &&
( $end->format( $format ) <= $scopeEnd->format( $format )));
}
/**
* Get all EXRULE dates (multiple values allowed)
*
* @param CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param array $exdateList
* @param string $dtStartTz
* @param UtilDateTime $compStart
* @param UtilDateTime $workStart
* @param UtilDateTime $workEnd
* @param string $compStartHis
* @throws Exception
* @since 2.41.88 - 2024-01-18
*/
private static function getAllEXRULEdates(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
array & $exdateList,
string $dtStartTz,
UtilDateTime $compStart,
UtilDateTime $workStart,
UtilDateTime $workEnd,
string $compStartHis
) : void
{
if( in_array( $component->getCompType(), [ Vcalendar::VAVAILABILITY, Vcalendar::VFREEBUSY ], true )) {
return;
}
if( false !== ( $prop = $component->getExrule( true ))) {
$isValueDate = $prop->hasParamValue( Vcalendar::DATE );
$pcValue = $prop->getValue();
if( isset( $pcValue[Vcalendar::UNTIL] ) && ! $isValueDate ) {
// convert UNTIL date to DTSTART timezone
$pcValue[Vcalendar::UNTIL] = UtilDateTime::factory(
$pcValue[Vcalendar::UNTIL],
[ Vcalendar::TZID => Vcalendar::UTC ],
$dtStartTz
);
}
$exdateList2 = [];
RecurFactory::recur2date(
$exdateList2,
$pcValue,
$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|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param array $exdateList
* @param string $dtStartTz
* @throws Exception
* @since 2.41.88 - 2024-01-18
*/
private static function getAllEXDATEdates(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
array & $exdateList,
string $dtStartTz
) : void
{
if( in_array( $component->getCompType(), [ Vcalendar::VAVAILABILITY, Vcalendar::VFREEBUSY ], true )) {
return;
}
while( false !== ( $prop = $component->getExdate( null, true ))) {
foreach( $prop->getValue() as $exdate ) {
$exdate = UtilDateTime::factory( $exdate, $prop->params, $dtStartTz );
$exdateList[$exdate->key] = true;
} // end - foreach( $exdate as $exdate )
} // end while
}
/**
* Update $recurList all RRULE dates (multiple values allowed)
*
* @param CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param array $recurList
* @param string $dtStartTz
* @param UtilDateTime $compStart
* @param UtilDateTime $workStart
* @param UtilDateTime $workEnd
* @param string $compStartHis
* @param array $exdateList
* @param null|DateInterval $compDuration
* @throws Exception
* @since 2.41.88 - 2024-01-18
*/
private static function getAllRRULEdates(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
array & $recurList,
string $dtStartTz,
UtilDateTime $compStart,
UtilDateTime $workStart,
UtilDateTime $workEnd,
string $compStartHis,
array & $exdateList,
? DateInterval $compDuration = null
) : void
{
if( in_array( $component->getCompType(), [ Vcalendar::VAVAILABILITY, Vcalendar::VFREEBUSY ], true )) {
return;
}
$exdateYmdList = self::getYmdList( $exdateList );
$recurYmdList = self::getYmdList( $recurList );
if( false === ( $prop = $component->getRrule( true ))) {
return;
}
$isValueDate = $prop->hasParamValue( Vcalendar::DATE );
$pcValue = $prop->getValue();
if( isset( $pcValue[Vcalendar::UNTIL] ) && ! $isValueDate ) {
// convert RRULE['UNTIL'] to same timezone as DTSTART !!
$pcValue[Vcalendar::UNTIL] = UtilDateTime::factory(
$pcValue[Vcalendar::UNTIL],
[ Vcalendar::TZID => Vcalendar::UTC ],
$dtStartTz
);
}
$recurList2 = [];
RecurFactory::recur2date( $recurList2, $pcValue, $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
}
/**
* Update $recurList with RDATE dates (overwrite if exists)
*
* @param CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param array $recurList
* @param string $dtStartTz
* @param UtilDateTime $workStart
* @param UtilDateTime $fcnEnd
* @param string $format
* @param array $exdateList
* @param string $compStartHis
* @param null|DateInterval $compDuration
* @throws Exception
* @since 2.41.88 - 2024-01-18
*/
private static function getAllRDATEdates(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
array & $recurList,
string $dtStartTz,
UtilDateTime $workStart,
UtilDateTime $fcnEnd,
string $format,
array & $exdateList,
string $compStartHis,
? DateInterval $compDuration = null
) : void
{
if( in_array( $component->getCompType(), [ Vcalendar::VAVAILABILITY, Vcalendar::VFREEBUSY ], true )) {
return;
}
$exdateYmdList = self::getYmdList( $exdateList );
$recurYmdList = self::getYmdList( $recurList );
while( false !== ( $prop = $component->getRdate( null, true ))) {
$rDateFmt = $prop->getValueParam() ?? Vcalendar::DATE_TIME;
// DATE or PERIOD
foreach( $prop->getValue() as $theRdate ) {
if( Vcalendar::PERIOD === $rDateFmt ) { // all days within PERIOD
$rDate = UtilDateTime::factory( $theRdate[0], $prop->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], $prop->params, $dtStartTz )
);
continue;
}
// period duration
$recurList[$rDate->key] = $theRdate[1];
continue;
} // end if( Vcalendar::PERIOD == $rDateFmt )
if( Vcalendar::DATE === $rDateFmt ) { // single recurrence, DATE (=Ymd)
$rDate = UtilDateTime::factory(
$theRdate,
array_merge( $prop->params, [ Vcalendar::TZID => $dtStartTz ] ),
$dtStartTz
);
$rDateYmdHisKey = $rDate->key . $compStartHis;
}
else { // single recurrence, DATETIME
$rDate = UtilDateTime::factory( $theRdate, $prop->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 Ymd-List from YmdHis-keyed array
*
* @param array $YmdHisArr
* @return array
* @since 2.26.2 - 2018-11-15
*/
private static function getYmdList( array $YmdHisArr ) : array
{
$res = [];
foreach( $YmdHisArr as $key => $value ) {
$res[substr((string) $key, 0, 8 )] = $key;
}
return $res;
}
/**
* Assert date arguments
*
* @param int|DateTimeInterface $startY
* @param int|DateTimeInterface $startM
* @param null|int $startD
* @param null|int $endY
* @param null|int $endM
* @param null|int $endD
* @since 2.29.16 - 2020-01-24
*/
private static function assertDateArguments(
mixed & $startY,
mixed & $startM,
? int & $startD = null,
? int & $endY = null,
? int & $endM = null,
? int & $endD = null
) : void
{
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) date( $Y );
}
if( empty( $startM )) {
$startM = (int) date( $M );
}
if( $startD === null ) {
$startD = (int) date( $D );
}
if( $endY === null ) {
$endY = (int) $startY;
}
if( $endM === null ) {
$endM = (int) $startM;
}
if( $endD === null ) {
$endD = $startD;
}
}
}
/**
* Return reviewed component types
*
* @param null|string|string[] $cType
* @return string[]
* @since 2.41.68 - 2022-10-03
*/
private static function assertComponentTypes( null | array | string $cType = null ) : array
{
if( empty( $cType )) {
return Vcalendar::$VCOMPS;
}
if( ! is_array( $cType )) {
$cType = [ $cType ];
}
foreach( $cType as & $theType ) {
$theType = ucfirst( strtolower( $theType ));
if( ! Vcalendar::isVcalendarVcomp( $theType )) {
$theType = Vcalendar::VEVENT;
}
}
return array_unique( $cType );
}
/**
* Assert bool arguments
*
* @param bool $flat
* @param bool $any
* @param bool $split
* @since 2.26.2 - 2018-11-15
*/
private static function assertBoolArguments(
? bool & $flat = null,
? bool & $any = null,
? bool & $split = null
) : void
{
// defaults
$flat = $flat ?? false;
$any = $any ?? true;
$split = $split ?? true;
if(( false === $flat ) && ( false === $any )) {
// invalid combination
$split = false;
}
if(( true === $flat ) && ( true === $split )) {
// invalid combination
$split = false;
}
}
/**
* Return comp end date(time) from dtend/due/duration properties
*
* @param CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component
* @param string $dtStartTz
* @return null|UtilDateTime
* @throws Exception
* @since 2.41.88 - 2024-01-18
*/
private static function getCompEndDate(
CalendarComponent|Vavailability|Vevent|Vfreebusy|Vjournal|Vtodo $component,
string $dtStartTz
) : null | UtilDateTime
{
static $MINUS1DAY = '-1 day';
$prop = null;
$compType = $component->getCompType();
$isVavailabilty = ( Vcalendar::VAVAILABILITY === $compType );
$isVevent = ( Vcalendar::VEVENT === $compType );
$isVfreebusy = ( Vcalendar::VFREEBUSY === $compType );
if(( $isVavailabilty || $isVevent || $isVfreebusy ) &&
( false !== ( $prop = $component->getDtend( true )))) {
$compEnd = UtilDateTime::factory( $prop->getValue(), $prop->getParams(), $dtStartTz );
$compEnd->SCbools[self::$DTENDEXIST] = true;
}
if( empty( $prop ) &&
( Vcalendar::VTODO === $compType ) &&
( false !== ( $prop = $component->getDue( true )))) {
$compEnd = UtilDateTime::factory( $prop->getValue(), $prop->getParams(), $dtStartTz );
$compEnd->SCbools[self::$DUEEXIST] = true;
}
if( empty( $prop ) && // duration in dtend format
( Vcalendar::VJOURNAL !== $compType ) &&
( false !== ( $prop = $component->getDuration( true, true )))) {
$compEnd = UtilDateTime::factory( $prop->getValue(), $prop->getParams(), $dtStartTz );
$compEnd->SCbools[self::$DURATIONEXIST] = true;
}
if( ! empty( $prop ) && $prop->hasParamValue( 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 ?? null;
}
/**
* Set duration end time
*
* @param UtilDateTime $rWdate
* @param UtilDateTime $rEnd
* @param int $cnt
* @param int $occurenceDays
* @param array $endHis
* @since 2.26 - 2018-11-10
*/
private static function setDurationEndTime(
UtilDateTime $rWdate,
UtilDateTime $rEnd,
int $cnt,
int $occurenceDays,
array $endHis
) : void
{
static $YMDn = 'Ymd';
switch( true ) {
case ( $cnt < $occurenceDays ) :
$rWdate->setTime( 23, 59, 59 );
break;
case ( $rWdate->format( $YMDn ) < $rEnd->format( $YMDn )) :
if( ( 0 === $endHis[0] ) && ( 0 === $endHis[1] ) && ( 0 === $endHis[2] )) {
$rWdate->setTime( 24, 0 ); // end exactly at midnight // 24:00:00
}
else {
$rWdate->setTime( 23, 59, 59 ); // break at midnight, cont next day // 23:59:59
}
break;
default :
$rWdate->setTime( $endHis[0], $endHis[1], $endHis[2] );
} // end switch
}
/**
* Update recurr-id-comps properties summary, description and comment if missing
*
* @param CalendarComponent|Vavailability|Vevent|Vtodo|Vjournal|Vfreebusy $component
* @param array $recurIdComps
* @since 2.41.88 - 2024-01-18
*/
private static function updateRecurrIdComps(
CalendarComponent|Vavailability|Vevent|Vtodo|Vjournal|Vfreebusy $component,
array $recurIdComps
) : void
{
if( empty( $recurIdComps )) {
return;
}
if( in_array( $component->getCompType(), [ Vcalendar::VAVAILABILITY, Vcalendar::VFREEBUSY ], true )) {
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->getValue(), $summary->getParams());
}
}
if( ! empty( $description )) {
$value = $recurIdComps[$RecurrIdKey][4]->getDescription();
if( empty( $value )) {
$recurIdComps[$RecurrIdKey][4]->setDescription( $description->getValue(), $description->getParams());
}
} // end if
if( empty( $comments )) {
continue;
}
$value = $recurIdComps[$RecurrIdKey][4]->getComment();
if( ! empty( $value )) {
continue;
}
foreach( $comments as $prop ) {
$recurIdComps[$RecurrIdKey][4]->setComment( $prop->getValue(), $prop->getParams());
}
} // 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
* @since 2.41.68 - 2022-10-03
*/
private static function selectComponents2( Vcalendar $calendar, array $selectOptions ) : array
{
$output = [];
$selectOptions = array_change_key_case( $selectOptions, CASE_UPPER );
while( $component3 = $calendar->getComponent()) {
if( empty( $component3 )) {
continue;
}
if( ! Vcalendar::isVcalendarVcomp( $component3->getCompType())) {
continue;
}
$uid = $component3->getUid();
foreach( $selectOptions as $propName => $propValue ) {
if( ! Vcalendar::isSelectSortProp( $propName )) {
continue;
}
if( ! is_array( $propValue )) {
$propValue = [ $propValue ];
}
if(( Vcalendar::UID === $propName ) && in_array( $uid, $propValue, true )) {
$output[$uid][] = $component3;
continue;
}
if( Vcalendar::isMultiProp1( $propName )) {
$propValues = [];
$component3->getProperties( $propName, $propValues );
$propValues = array_keys( $propValues );
foreach( $propValue as $theValue ) {
if( in_array( $theValue, $propValues, true )) { // && ! isset( $output[$uid] )) {
$output[$uid][] = $component3;
break;
}
}
continue;
} // end if( // multiple occurrence?
$method = StringFactory::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, true )) {
$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, true )) {
$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 ) {
foreach( $uList as $uValue ) {
$output2[] = $uValue;
}
}
$output = $output2;
}
return $output;
}
}