1, self::REC_TYPE_WEEK => 7, self::REC_TYPE_MONTH => 1, self::REC_TYPE_YEAR => 12 ); private $_fields_values = array(); private $_recurring_start_date_stamp; private $_recurring_end_date_stamp; private $_recurring_length; private $_config = array(); public function __construct($recurringType, $recurringStartDateStamp, $recurringEndDateStamp, $recurringLength = 0, $config = array()) { if(is_array($recurringType)) $recurringType = self::parseRecurringDataArrayToString($recurringType); $this->_fields_values = self::_parseRecurringDataString($recurringType); $this->_recurring_start_date_stamp = $recurringStartDateStamp; $this->_recurring_end_date_stamp = $recurringEndDateStamp; $this->_recurring_length = $recurringLength; $this->_config = $config; } public static function getInstance($recurringTypeString, $recurringStartDateStamp, $recurringEndDateStamp) { return new self($recurringTypeString, $recurringStartDateStamp, $recurringEndDateStamp); } /** * Get field value. * @param $fieldName * @return mixed * @throws Exception */ private function _getFieldValue($fieldName) { if(!isset($this->_fields_values[$fieldName])) throw new Exception("Field '{$fieldName}' not found."); return $this->_fields_values[$fieldName]; } /** * Parse recurring data from array to string. * @param $dataArray * @return string * @throws Exception */ static function parseRecurringDataArrayToString($dataArray) { $dataFields = array( self::FLD_REC_TYPE => "each", self::FLD_REC_TYPE_STEP => "step", self::FLD_WEEK_NUMBER => "week_number", self::FLD_WEEK_DAYS_LIST => "days_of_week", self::FLD_REPEAT => "repeat" ); $recurringTypes = array(self::REC_TYPE_DAY, self::REC_TYPE_WEEK, self::REC_TYPE_MONTH, self::REC_TYPE_YEAR); $daysOfWeek = array("sunday" => 0, "monday" => 1, "tuesday" => 2, "wednesday" => 3, "thursday" => 4, "friday" => 5, "saturday" => 6); $dataFieldsValues = array(); foreach($dataArray as $field => $value) { switch($field) { case $dataFields[self::FLD_REC_TYPE_STEP]: case $dataFields[self::FLD_REPEAT]: $value = str_replace(" ", "", $value); $dataFieldsValues[$field] = $value; break; case $dataFields[self::FLD_REC_TYPE]: $value = str_replace(" ", "", $value); if(!in_array($value, $recurringTypes)) throw new Exception("Field '{$field}' will contains value of ".join(", ", $recurringTypes)); $dataFieldsValues[$field] = $value; break; case $dataFields[self::FLD_WEEK_NUMBER]: $value = str_replace(" ", "", $value); if(count(explode(",", $dataArray[$dataFields[self::FLD_WEEK_DAYS_LIST]])) > 1) throw new Exception("If field {$field} not null, then field ".$dataFields[self::FLD_WEEK_DAYS_LIST]." will contains only one value of ".join(", ", array_keys($daysOfWeek))); if(!in_array($value, $daysOfWeek)) throw new Exception("Field {$field} will contains value of ".join(",", array_keys($daysOfWeek))); $dataFieldsValues[$field] = $value; break; case $dataFields[self::FLD_WEEK_DAYS_LIST]: $weekDaysToRecurring = explode(",", $value); $days = array(); $repeatVal = str_replace(" ", "", $dataArray[$dataFields[self::FLD_REC_TYPE]]); if($repeatVal == "year"){ $daysOfWeek["sunday"] = 7; } foreach($weekDaysToRecurring as $day) { $day = str_replace(" ", "", $day); if(!isset($daysOfWeek[$day])) throw new Exception("Field {$field} will contains data like 'monday,tuesday,wednesday'."); array_push($days, $daysOfWeek[$day]); } $dataFieldsValues[$field] = join("," ,$days); break; default: $dataFieldsValues[$field] = ""; break; } } //Check required data and fill gaps of data. $requiredFields = array( self::FLD_REC_TYPE, self::FLD_REC_TYPE_STEP ); foreach($dataFields as $fieldKey => $fieldName) { if(isset($dataFieldsValues[$fieldName])) continue; if(in_array($fieldKey, $requiredFields)) throw new Exception("Field '{$fieldName}' is required"); $dataFieldsValues[$fieldName] = ""; } $recurringFormat = "%s_%s_%s_%s_%s#%s"; return sprintf( $recurringFormat, $dataFieldsValues[$dataFields[self::FLD_REC_TYPE]], $dataFieldsValues[$dataFields[self::FLD_REC_TYPE_STEP]], (!empty($dataFieldsValues[$dataFields[self::FLD_WEEK_NUMBER]])) ? $dataFieldsValues[$dataFields[self::FLD_WEEK_DAYS_LIST]] : "", $dataFieldsValues[$dataFields[self::FLD_WEEK_NUMBER]], (empty($dataFieldsValues[$dataFields[self::FLD_WEEK_NUMBER]])) ? $dataFieldsValues[$dataFields[self::FLD_WEEK_DAYS_LIST]] : "", $dataFieldsValues[$dataFields[self::FLD_REPEAT]] ); } /** * Parse recurring data from string. * @param $dataStr * @return array */ static private function _parseRecurringDataString($dataStr) { $formatPartsReg = "/(_|#)/"; $formatDaysListDelimiter = ","; $parsedData = array(); $parts = preg_split($formatPartsReg, $dataStr); list( $parsedData[self::FLD_REC_TYPE], $parsedData[self::FLD_REC_TYPE_STEP], $parsedData[self::FLD_WEEK_DAY], $parsedData[self::FLD_WEEK_NUMBER], $parsedData[self::FLD_WEEK_DAYS_LIST] ) = $parts; if(isset($parts[5])) { $parsedData[self::FLD_REPEAT] = $parts[5]; } $days = $parsedData[self::FLD_WEEK_DAYS_LIST]; // $days is a comma separated week days string ("0,1,2"). Need an extra check for `every Sunday` series - "0" - which string is considered as falsy/empty in php if(!empty($days) || $days === "0"){ $parsedData[self::FLD_WEEK_DAYS_LIST] = explode($formatDaysListDelimiter, $days); }else{ $parsedData[self::FLD_WEEK_DAYS_LIST] = array(); } return $parsedData; } public function getRecurringTypeValue() { return $this->_getFieldValue(self::FLD_REC_TYPE); } public function getRecurringTypeStepValue() { return $this->_getFieldValue(self::FLD_REC_TYPE_STEP); } public function getWeekNumberValue() { return $this->_getFieldValue(self::FLD_WEEK_NUMBER); } public function getWeekDayValue() { return $this->_getFieldValue(self::FLD_WEEK_DAY); } public function getWeekDaysListValue() { return $this->_getFieldValue(self::FLD_WEEK_DAYS_LIST); } public function getRepeatValue() { return $this->_getFieldValue(self::FLD_REPEAT); } /** * Correcting interval by recurring start($this->_recurring_start_date_stamp) * and end($this->_recurring_end_date_stamp) dates. * @param $intervalStartDateStamp * @param $intervalEndDateStamp * @return array */ private function _getCorrectedRecurringInterval($intervalStartDateStamp, $intervalEndDateStamp) { $recurringStartDateStamp = $this->_recurring_start_date_stamp; $recurringEndDateStamp = $this->_recurring_end_date_stamp; $recurringInterval = array( "start_date_stamp" => $intervalStartDateStamp, "end_date_stamp" => $intervalEndDateStamp ); //Return recurring interval without correcting if it not belongs to assigned interval. if (($intervalStartDateStamp >= $recurringEndDateStamp) || ($intervalEndDateStamp <= $recurringStartDateStamp)) return $recurringInterval; //Correct start date interval if it smaller then recurring start date. if ($intervalStartDateStamp < $recurringStartDateStamp) { $intervalStartDateStamp = $recurringStartDateStamp; $recurringInterval["start_date_stamp"] = $intervalStartDateStamp; } //Correct end date interval if it smaller then recurring end date. if ($intervalEndDateStamp > $recurringEndDateStamp) { $intervalEndDateStamp = $recurringEndDateStamp; $recurringInterval["end_date_stamp"] = $intervalEndDateStamp; } $type = $this->getRecurringTypeValue(); if ($recurringStartDateStamp < $intervalStartDateStamp) { if ($type == self::REC_TYPE_DAY || $type == self::REC_TYPE_WEEK) { $step = $this->_transpose_size[$type] * $this->getRecurringTypeStepValue(); $day = 24 * 60 * 60; $delta = floor(($intervalStartDateStamp - $recurringStartDateStamp) / ($day * $step)); $recurringInterval["start_date_stamp"] = SchedulerHelperDate::addDays($recurringStartDateStamp, $delta*$step); } else { $step = $this->_transpose_size[$type] * $this->getRecurringTypeStepValue(); $intStartDetails = SchedulerHelperDate::getDateInfo($intervalStartDateStamp); $recStartDetails = SchedulerHelperDate::getDateInfo($recurringStartDateStamp); $delta = ceil((($intStartDetails["year"] * 12 + $intStartDetails["month"]) - ($recStartDetails["year"] * 12 + $recStartDetails["month"])) / $step); $date = new DateTime(); $date->setTimestamp($recurringStartDateStamp); $date->setDate($recStartDetails["year"], $recStartDetails["month"] + $delta * $step, $recStartDetails["day"]); $recurringInterval["start_date_stamp"] = $date->getTimestamp(); $weekNumber = $this->getWeekNumberValue(); if ($weekNumber) $recurringInterval["start_date_stamp"] = $this->_getDayOnWeek($recurringInterval["start_date_stamp"], $this->getWeekDayValue(), $weekNumber); } } return $recurringInterval; } /** * Get step to recurring day from current day of week in date. * @param $dateStamp * @param $recurringWeekDay * @param $weekNumber * @return int */ private function _getDayOnWeek($dateStamp, $recurringWeekDay, $weekNumber = 0) { $date = new DateTime(); $date->setTimestamp($dateStamp); $m = $date->format('m'); $y = $date->format('Y'); $date->setDate($y, $m, 1); $weekDays = 0; if ($weekNumber != 0) { $weekDays = ($weekNumber - 1) * SchedulerHelperDate::DAYS_IN_WEEK; } $weekDay = SchedulerHelperDate::getDayOfWeek($date->getTimestamp()); $newDay = $recurringWeekDay + $weekDays - $weekDay + 1; $newDay = $newDay <= $weekDays ? $newDay + SchedulerHelperDate::DAYS_IN_WEEK : $newDay; $date->setDate($y, $m, $newDay); return $date->getTimestamp(); } /** * Get step to recurring day from current day of week in date. * @param $dateStamp * @param $recurringWeekDay * @return int */ private function _getRecurringDayStep($dateStamp, $recurringWeekDay) { $weekDay = SchedulerHelperDate::getDayOfWeek($dateStamp); if($this->_config["start_on_monday"]) { $recurringWeekDay = $recurringWeekDay == 0 ? 7 : $recurringWeekDay; } $dayStep = $recurringWeekDay - $weekDay; return $dayStep; } /** * Get recurring days for date. * @param $dateStamp * @param $start * $param $end * @return array */ private function _getRecurringDays($dateStamp, $start = NULL, $end = NULL) { $recurringDays = array(); //If recurring type has list of days, then get those days. $recurringWeekDays = $this->getWeekDaysListValue(); $weekDay = $this->getWeekDayValue(); $weekNumber = $this->getWeekNumberValue(); if($recurringWeekDays) { $daysCount = count($recurringWeekDays); for($i = 0; $i < $daysCount; $i++) { $dayStep = $this->_getRecurringDayStep($dateStamp, $recurringWeekDays[$i]); $stamp = SchedulerHelperDate::addDays($dateStamp, $dayStep); if((!$start || $stamp >= $start) && (!$end|| $stamp < $end)) array_push($recurringDays, $stamp); } } //Else if recurring type has day of week and step for it, then get this day. elseif(isset($weekDay) && $weekNumber) { $stamp = $this->_getDayOnWeek($dateStamp, $weekDay, $weekNumber); if((!$start || $stamp >= $start) && (!$end|| $stamp < $end)) array_push($recurringDays, $stamp); } //Else return recurring date without change. else { if((!$start || $dateStamp >= $start) && (!$end|| $dateStamp < $end)) array_push($recurringDays, $dateStamp); } return $recurringDays; } /** * Get recurring dates by interval or $intervalStartDateStamp and $countDates. * @param $intervalStartDateStamp * @param $intervalEndDateStamp * @param null $countDates * @return array|bool */ public function getRecurringDates($intervalStartDateStamp, $intervalEndDateStamp, $countDates = NULL) { $recurringTypeStep = $this->getRecurringTypeStepValue(); $recType = $this->getRecurringTypeValue(); if(!($recType && $recType)) return false; //Correct interval by recurring interval. $intervalStartDateStamp -= $this->_recurring_length; //If event ends before interval but event is in interval because of length $correctedInterval = $this->_getCorrectedRecurringInterval($intervalStartDateStamp, $intervalEndDateStamp); $intervalStartDateStamp = $correctedInterval["start_date_stamp"]; $intervalEndDateStamp = $correctedInterval["end_date_stamp"]; $currentRecurringStartDateStamp = $intervalStartDateStamp; $correcterRecurringStartDateStamp = $currentRecurringStartDateStamp; $recurringDates = array(); $recurringStartDateStamp = $this->_recurring_start_date_stamp; $recurringEndDateStamp = $this->_recurring_end_date_stamp; //Generate dates wile next recurring date belongs to interval. $countRecurringCycles = 0; while( (!is_null($countDates) && ($countRecurringCycles <= $countDates)) || ( ($intervalStartDateStamp <= $currentRecurringStartDateStamp) && ($correcterRecurringStartDateStamp < $intervalEndDateStamp) ) ) { $countRecurringCycles++; $recurringDays = $this->_getRecurringDays($currentRecurringStartDateStamp, $recurringStartDateStamp, $recurringEndDateStamp); $recurringDates = array_merge($recurringDates, $recurringDays); switch($recType) { case self::REC_TYPE_DAY: $currentRecurringStartDateStamp = SchedulerHelperDate::addDays($currentRecurringStartDateStamp, $recurringTypeStep); $correcterRecurringStartDateStamp = $currentRecurringStartDateStamp; break; case self::REC_TYPE_WEEK: $currentRecurringStartDateStamp = SchedulerHelperDate::addWeeks($currentRecurringStartDateStamp, $recurringTypeStep); $correcterRecurringStartDateStamp = SchedulerHelperDate::weekStart($currentRecurringStartDateStamp); break; case self::REC_TYPE_MONTH: $currentRecurringStartDateStamp = SchedulerHelperDate::addMonths($currentRecurringStartDateStamp, $recurringTypeStep); $correcterRecurringStartDateStamp = SchedulerHelperDate::monthStart($currentRecurringStartDateStamp); break; case self::REC_TYPE_YEAR: $currentRecurringStartDateStamp = SchedulerHelperDate::addYears($currentRecurringStartDateStamp, $recurringTypeStep); $correcterRecurringStartDateStamp = SchedulerHelperDate::yearStart($currentRecurringStartDateStamp); break; } } return (!is_null($countDates)) ? array_splice($recurringDates, (count($recurringDates) - $countDates)) : $recurringDates; } /** * @param $recurringType * @param $startDateStamp * @param $eventLength * @return int|NULL */ public static function getRecurringEndDate($recurringType, $startDateStamp, $eventLength) { $recurringTypeObj = self::getInstance($recurringType, $startDateStamp, NULL); $repeatValue = $recurringTypeObj->getRepeatValue(); if(empty($repeatValue)) return ($startDateStamp + $eventLength); $recurringStartDatesStamps = $recurringTypeObj->getRecurringDates($startDateStamp, NULL, $repeatValue); $maxEndDateStamp = NULL; foreach($recurringStartDatesStamps as $startDateStamp) { $endDateStamp = $startDateStamp; $maxEndDateStamp = ($endDateStamp > $maxEndDateStamp) ? $endDateStamp : $maxEndDateStamp; } return $maxEndDateStamp; } }