У меня есть класс Calendar, который упаковывает кучу классов Day, чтобы представлять, что происходит с точки зрения событий, исключений и т. Д. Для наших приложений, связанных с расписанием.
Проблема заключается в том, что список функций ползетв, и классы используются все больше и больше, API для класса растет из-под контроля.Сами функции невелики, поскольку большинство алгоритмов делегируются другим классам, но чтобы API был простым и не заставлял пользователя знать базовую структуру класса, я заканчиваю тем, что много функций заключаю в функции.По сути, это набор функций, вызывающих один или два метода в других классах.
Единственный способ решить эту проблему - расширить класс Calendar для каждого основного типа использования (например, для обнаружения конфликтов).
Из-за запроса на более конкретный код здесь это выглядит так:
class Calendar
{
public $start_date;
public $end_date;
/**
* @var Hours_UnitDefault[]
*/
public $unit_defaults = array();
/**
* @var Hours_HoursException[]
*/
public $exceptions = array();
/**
* @var Event[]
*/
public $events = array();
/**
* The underlining data structure of this class.
* In the form of ['date'] => CalendarDay obj
* Why hash table?
* -Convinience/Speed of storage retreaval and organization.
* @var CalendarDay[]
*
*/
public $calendarHashTable = array();
public function __construct()
{
}
public function fetchAll(
$start_date,
$end_date,
array $room_ids,
array $ignore_events = array(),
array $ignore_recurrences = array()
)
{
$this->setDateRange($start_date, $end_date);
$unit_ids = $this->fetchUnitIDs($room_ids);
$this->fetchUnitDefaults($unit_ids);
$semester_ids = array_unique(array_from_key('semester_id', $this->unit_defaults));
$this->fetchExceptions($unit_ids, $semester_ids);
$this->fetchEvents($room_ids, $ignore_events, $ignore_recurrences);
}
/**
* @param Event[] $pending_events
* checks collisions against each pending event and stores the conflicts
*/
public function checkPendingEventsForConflicts(array $pending_events)
{
$this->initHashTableByPendingEvents($pending_events);
$this->hashAllDbData();
$this->selectCorrectUnitDefaults();
$this->calculateCollisionsForPendingEvents($pending_events);
}
/**
* goes through each CalendarDay and makes sure the UnitDefault stored is the one it should use
*/
public function selectCorrectUnitDefaults()
{
foreach ($this->calendarHashTable as $key => $day) {
$day->selectCorrectUnitDefault();
}
}
public function fetchUnitIDs($room_ids)
{
return array_unique(array_from_key('unit_id', RoomUnit::getRoomUnits(array('room_ids' => $room_ids)))); // Directory_));
}
public function fetchUnitDefaults($unit_ids)
{
$this->unit_defaults = Hours_UnitDefault::getUnitDefaults(array('date_range' => array($this->start_date, $this->end_date), 'library_unit_ids' => $unit_ids));
}
public function fetchExceptions($unit_ids, $semester_ids)
{
$this->exceptions = Hours_HoursException::getExceptions(
array(
'with_unit_defaults' => true,
'date_range' => (array(
$this->start_date,
$this->end_date
)),
'semester_unit_id' => $semester_ids[0],
'library_unit_ids' => $unit_ids)
);
}
public function fetchEvents($room_ids, $ignore_events = array(), $ignore_recurrences = array())
{
$this->events = Event::getEvents(array(
'start_date_after' => $this->start_date,
'end_date_before' => $this->end_date . ' 23:59:59',
'room_ids' => $room_ids,
'event_status' => array('approved', 'blocked'),
'not_events' => $ignore_events,
'not_recurrences' => $ignore_recurrences));
}
public function setDateRange($start_date, $end_date)
{
$this->start_date = date("Y-m-j", strtotime($start_date));
$this->end_date = date("Y-m-j", strtotime($end_date));
}
/**
* Gets an array of dates that are relevant to an event type to use as hash keys.
* @static
* @param EventInterface $event
* @return string[] dates
*/
public function getHashDateKeys(EventInterface $event)
{
return ConflictUtility::getDatesBetween2Dates($event->getStartDate(), $event->getEndDate());
}
/**
* Hashes any event type(Event, Exception, UnitDefault) into the hash table.
* @param EventInterface $event
*/
public function hashEvent(EventInterface $event)
{
$keys = $this->getHashDateKeys($event);
foreach ($keys as $key) {
if (!$this->isHashKeySet($key)) {
continue;
}
$day = $this->getCalendarDayByKey($key);
$day->assignEvent($event);
}
}
public function splitEvent(EventInterface $event)
{
//we need to split multi-day spanning days into single days
$split_events = array();
$dates = $this->getHashDateKeys($event);
$dates_count = count($dates);
if ($dates_count > 1) {
foreach ($dates as $date) {
array_push($split_events , clone $event);
}
for ($i = 0; $i setStartDate($dates[$i] . " " . date('g:i:s A', $split_events[$i]->getStartTimeStamp()));
$split_events[$i]->setEndDate($dates[$i] . " " . date('g:i:s A', $split_events[$i]->getEndTimeStamp()));
}
} else { array_push($split_events, $event); }
return $split_events;
}
/**
* Hashes events, exceptions and events into the hash table.
* Assumes the hash table was initialized.
*/
public function hashAllDbData()
{
//we need to split multi-day spanning exceptions into single days
foreach ($this->exceptions as $exception) {
$split_exceptions = $this->splitEvent($exception);
foreach($split_exceptions as $single_exception){
$this->hashEvent($single_exception);
}
}
foreach ($this->events as $event) {
$split_events = $this->splitEvent($event);
foreach($split_events as $single_event){
$this->hashEvent($single_event);
}
}
//splitting unit defaults into days (requires different approach then previous once)
foreach ($this->unit_defaults as $unit_default) {
$unit_default_semester_adapter = new Hours_UnitDefaultEventInterfaceAdopter($unit_default);
$unit_default_semester_adapter->treatAsSemester();
$dates = $this->getHashDateKeys($unit_default_semester_adapter);
foreach ($dates as $date) {
$unit_default_adapter = new Hours_UnitDefaultEventInterfaceAdopter($unit_default);
$unit_default_adapter->treatAsEvent();
$unit_default_adapter->setDate($date);
$this->hashEvent($unit_default_adapter);
}
}
}
/**
* @return CalendarDay
* @param string $key date
*/
public function getCalendarDayByKey($key)
{
return $this->calendarHashTable[$key];
}
/**
*
* @param string $key date
* @return bool
*/
public function isHashKeySet($key)
{
if (isset($this->calendarHashTable[$key])) return true;
else return false;
}
/**
* Inits a hash table key.
* @param string $key date
*/
public function initHashKey($key)
{
$calendar_day = new CalendarDay();
$calendar_day->setDate($key);
$this->calendarHashTable[$key] = $calendar_day;
}
/**
* Sets up hash table based on a date range
* @param string $date_start
* @param string $date_end
*/
public function initHashTableByDateRange($date_start, $date_end)
{
$keys = ConflictUtility::getDatesBetween2Dates($date_start, $date_end);
foreach ($keys as $key) {
$this->initHashKey($key);
}
}
/**
* Sets up hash table based on array of dates
* @param string[] $dates
*/
public function initHashTableByDateArray(array $dates = array())
{
foreach ($dates as $key) {
$this->initHashKey($key);
}
}
/**
* Sets up hash table based on pending events.
* @param Event[] $pending_events
*/
public function initHashTableByPendingEvents(array $pending_events = array())
{
foreach ($pending_events as $pending_event) {
$key = self::getHashDateKey($pending_event);
$this->initHashKey($key);
$calendar_day = $this->getCalendarDayByKey($key);
$calendar_day->assignPendingEvent($pending_event);
}
}
/**
* @param Event[] $modify_pending_events
* @param Event[] $skip_pending_events
*
* deletes pending events that are skipped from the calendar
* substitutes pendings events that are modified from the calendar
*/
public function modifyPendingEvents(array $modify_pending_events, array $skip_pending_events)
{
//replace old one with new one
foreach ($modify_pending_events as $modify_pending_event) {
$key = $this->getHashDateKey($modify_pending_event);
$day = $this->getCalendarDayByKey($key);
$day->emptyPendingEvent();
$day->assignPendingEvent($modify_pending_event);
}
//delete pending events that were skipped
foreach ($skip_pending_events as $skip_pending_event) {
$key = $this->getHashDateKey($skip_pending_event);
$day = $this->getCalendarDayByKey($key);
$day->emptyPendingEvent();
}
}
/**
* @param EventInterface $event
* @return string date
*/
public function getHashDateKey(EventInterface $event)
{
$keys = self::getHashDateKeys($event);
$key = $keys[0];
return $key;
}
/**
* For each pending event, checks even there are collisions in the appropriate date key.
* Behind the scenes it populates the conlficts EventContainer.
* @param Event[] $pending_events
*/
public function calculateCollisionsForPendingEvents(array $pending_events)
{
foreach ($pending_events as $pending_event) {
$key = self::getHashDateKey($pending_event);
$day = $this->getCalendarDayByKey($key);
$day->calculateCollisions();
}
}
/**
* Removes date keys without conflicts.
* Returns a hash table with dates that have conflicts.
* @return array HashTable
*/
public function getConflictingDays()
{
$conflicting_days = array();
foreach ($this->calendarHashTable as $date => $day) {
if ($day->hasConflicts()) {
array_push($conflicting_days, $day);
}
}
return $conflicting_days;
}
public function getConflictFreeDays()
{
$conflict_free_days = array();
foreach ($this->calendarHashTable as $date => $day) {
if (!$day->hasConflicts()) {
array_push($conflict_free_days, $day);
}
}
return $conflict_free_days;
}
/**
* @return Event[]
* returns all of the pending events that are stored on the calendar
*/
public function getPendingEvents()
{
$pending_events = array();
foreach ($this->calendarHashTable as $date => $day) {
if (!$day->isEmptyPendingEvent()) {
array_push($pending_events, $day->getPendingEvent());
}
}
return $pending_events;
}
}
Идея в том, что все хэшируется в [date] => CalendarDay, так что все повторения и т.д ...только один деньЕсли они охватывают много дней, они разбиваются на каждый охватываемый день.
class CalendarDay
{
/**
* @var EventContainer
*/
public $schedule;
/**
* @var EventContainer
*/
public $conflicts;
public $pending_event;
public $date;
public function __construct()
{
$this->conflicts = new EventContainer();
$this->schedule = new EventContainer();
$this->pending_event = new EventContainer();
}
/**
* Checks whether there are any conflicts in the conflicts container.
* Only relevant if a check for conflicts was performed and conflicts populated.
* @return bool
*/
public function hasConflicts()
{
if ($this->conflicts->isEmpty())
return false;
else
return true;
}
/**
* Figures out if there are collisions between each event type and
* the pending event. In case there is a conflict it appends it to the
* EventContainer conlflicts.
*/
public function calculateCollisions()
{
//if there is no pending events, do nothing.
if ($this->pending_event->isEmptyEvents()) return;
$pending_event = $this->pending_event->getEvent();
if (!$this->schedule->isEmptyEvents()) {
foreach ($this->schedule->getEvents() as $event) {
if ($event->hasConflict($pending_event)) {
$this->conflicts->assignEvent($event);
}
}
}
if (!$this->schedule->isEmptyUnitDefault()) {
$default_hours = $this->schedule->getUnitDefault();
if ($default_hours->hasConflict($pending_event)) {
$this->conflicts->assignEvent($default_hours);
}
}
if (!$this->schedule->isEmptyException()) {
$exception = $this->schedule->getException();
if ($exception->hasConflict($pending_event)) {
$this->conflicts->assignEvent($exception);
}
}
}
/**
* Assigns event the to EventContainer schedule
* @param EventInterface $event
*/
public function assignEvent(EventInterface $event)
{
$this->schedule->assignEvent($event);
}
public function assignPendingEvent(EventInterface $event)
{
$this->pending_event->assignEvent($event);
}
/**
* Basic idea behind the algorithm:
* ********************************
* Open Period(START)
* |open -> between opening and first event
* Closed Period(START)
* Closed Period(END)
* |open -> between events
* Closed Period(START)
* Closed Period(END)
* |open -> between closing and last event
* Open Period(END)
* ********************************
*
* @return TimePeriod[]
* returns empty array if closed
*/
public function getAvailableHours() //todo:make sure data is valid before this algorithm happens
{
$available_hours = array();
$open_hours = $this->getOpenTimePeriod();
//means its closed this day (cases: closed all day exception, closed on that day in unit default)
if ($open_hours->isEmpty()) return $available_hours; //which is an empty array
$closed_time_periods = $this->getClosedTimePeriods($open_hours);
$events_size = count($closed_time_periods);
$hours_start = $open_hours->getStartTimeStamp();
$hours_end = $open_hours->getEndTimeStamp();
//if events are empty, the available hours are the open hours
if (empty($closed_time_periods)) {
$available = new TimePeriod();
$available->setStartTimeStamp($hours_start);
$available->setEndTimeStamp($hours_end);
array_push($available_hours, $available);
return $available_hours;
} else {
//takes care of available hours between opening hour and first event
$first_event = $closed_time_periods[0];
$first_available = new TimePeriod();
$first_available->setStartTimeStamp($hours_start);
$first_available->setEndTimeStamp($first_event->getStartTimeStamp());
array_push($available_hours, $first_available);
//takes care of available hours in between events if more then one
if ($events_size > 1) {
for ($i = 0; $i setStartTimeStamp($event->getEndTimeStamp());
$available->setEndTimeStamp($next_event->getStartTimeStamp());
array_push($available_hours, $available);
}
}
//takes care of available hours between closing hour and last event
$last_event = $closed_time_periods[$events_size - 1];
$last_available = new TimePeriod();
$last_available->setStartTimeStamp($last_event->getEndTimeStamp());
$last_available->setEndTimeStamp($hours_end);
array_push($available_hours, $last_available);
}
//filter out available hours less then 30m //todo: ask slava if its 30m
foreach ($available_hours as $i => $available) {
$minutes = ($available->getEndTimeStamp() - $available->getStartTimeStamp()) / 60;
if ($minutes schedule->isEmptyUnitDefault()) {
$unit_default = $this->schedule->getUnitDefault();
$open_hours->setStartTimeStamp($unit_default->getStartTimeStamp());
$open_hours->setEndTimeStamp($unit_default->getEndTimeStamp());
$open_hours->setSource($unit_default);
}
//if there is an exception for open hours, replace default hours
else if (!$this->schedule->isEmptyException()) {
$exception = $this->schedule->getException();
if ($exception->is_open) {
$open_hours->setStartTimeStamp($exception->getStartTimeStamp());
$open_hours->setEndTimeStamp($exception->getEndTimeStamp());
$open_hours->setSource($exception);
}
}
return $open_hours;
}
/**
* @param TimePeriod $open_hours
* @return TimePeriod[]
*/
public function getClosedTimePeriods(TimePeriod $open_hours)
{
$closed_hours = array();
//first take care of events and treat them as closed time periods
$events = $this->schedule->getEvents();
foreach ($events as $event) {
$closed_period = new TimePeriod();
//add 10 minutes before and after for events //todo:: possibly account for events that go right after each other to avoid 20m gap instead of 10m
$padding = 0;
if ($event->event_status != 'blocked') { $padding = 10 * 60; }
$closed_period->setSource($event);
//don't need the date
$closed_period->setStartTimeStamp($event->getStartTimeStamp() - $padding);
$closed_period->setEndTimeStamp($event->getEndTimeStamp() + $padding);
array_push($closed_hours, $closed_period);
}
//take care of closed exception if any and treat it as closed time period
if (!$this->schedule->isEmptyException()) {
$exception = $this->schedule->getException();
if (!$exception->is_open) {
$closed_period = new TimePeriod();
$closed_period->setSource($exception);
$closed_period_start = $exception->getStartTimeStamp();
$closed_period_end = $exception->getEndTimeStamp();
//open hours generally don't have date as part of time stamp so we need to add it for future comparisons with exception
$open_period_start = $open_hours->getStartTimeStamp();
$open_period_end = $open_hours->getEndTimeStamp();
//now we need to take care of cases when closed exception starts before or after opening hours.
//why? to simplify further algorithms so that all closed time period fall within open time period.
//if closed exception starts before open -> trunkate
if ($closed_period_start $open_period_end) {
$closed_period_end = $open_period_end;
}
$closed_period->setStartTimeStamp($closed_period_start);
$closed_period->setEndTimeStamp($closed_period_end);
array_push($closed_hours, $closed_period);
}
if (!function_exists('cmp')) {
function cmp($a, $b)
{
if ($a->getStartTimeStamp() == $b->getStartTimeStamp()) {
return 0;
}
return ($a->getStartTimeStamp() getStartTimeStamp()) ? -1 : 1;
}
}
usort($closed_hours, 'cmp');
}
return $closed_hours;
}
public function selectCorrectUnitDefault()
{
if (!$this->schedule->isEmptyException()) {
$exception = $this->schedule->getException();
if ($exception->is_open) { //if we have exception for open hours, get rid of defaults
$this->schedule->emptyUnitDefaults();
return;
}
if (!$exception->is_open) { //check for closed exception that last all day //todo:: think about how to do this in available_periods section
if ($exception->getStartTimeStamp('only_time') == strtotime('12:00:00 AM') && $exception->getEndTimeStamp('only_time') == strtotime('11:59:00 PM')) {
$this->schedule->emptyUnitDefaults();
return;
}
}
}
$unit_defaults = $this->schedule->getUnitDefaults();
//discard library hours if there are multiple unit defaults b/c we are going to use a different one.
if (count($unit_defaults) > 1) {
foreach ($unit_defaults as $i => $unit_default) {
if ($unit_default->getName() == 'Library') unset($unit_defaults[$i]);
}
$unit_defaults = array_values($unit_defaults);
//take the more restrictive unit default
$last_unit_default = $unit_defaults[0];
foreach ($unit_defaults as $unit_default) {
if ($unit_default->getStartTimeStamp() > $last_unit_default->getStartTimeStamp()) $last_unit_default = $unit_default;
}
$unit_defaults[0] = $last_unit_default;
}
$unit_default = $unit_defaults[0]; //take whatever remains as unit default
$this->schedule->emptyUnitDefaults();
$this->schedule->assignEvent($unit_default);
}
/**
* Sets date to correspond to hash table date key for convinience
* @param string $date
*/
public function setDate($date)
{
$this->date = $date;
$this->schedule->setDate($date);
$this->conflicts->setDate($date);
$this->pending_event->setDate($date);
}
public function getDate($format = "Y-m-d")
{
return date($format, strtotime($this->date));
}
public function getAllConflicts()
{
return $this->conflicts->getAllEvents();
}
public function getConflictCount()
{
return count($this->getAllConflicts());
}
public function emptyPendingEvent()
{
$this->pending_event->emptyEvents();
}
public function emptyConflicts()
{
$this->conflicts->emptyAll();
}
public function isEmptyPendingEvent()
{
return $this->pending_event->isEmptyEvents();
}
public function getPendingEvent()
{
return $this->pending_event->getEvent();
}
}
Контейнер CalendarDay использует для хранения.
class EventContainer {
/**
* @var Hours_UnitDefaultInterfaceAdopter[]
*/
private $unit_defaults = array();
/**
* @var Hours_HoursException
*/
private $exception;
/**
* @var Event[]
*/
private $events = array();
/**
* Assigns event type to the appropriate variable by checking its class.
* UnitDefault are saved as UnitDefaultEventInterfaceAdopter for code reuse.
* @param EventInterface|Hours_UnitDefaultEventInterfaceAdopter|Hours_HoursException|Event $event
*/
public function assignEvent(EventInterface $event) {
// echo "assigning: ".get_class($event) . "<br/>";
switch (get_class($event)) {
case 'Event':
array_push($this->events, $event);
break;
case 'Hours_HoursException':
$this->exception = $event;
break;
case 'Hours_UnitDefaultEventInterfaceAdopter':
array_push($this->unit_defaults, $event);
break;
default:
die('Invalid class type argument supplied to the DaySchedule->assign() function');
}
}
/**
* Checks whether all of the event types arrays are empty.
* @return bool
*/
public function isEmpty(){
if (empty($this->unit_defaults) && empty($this->exception) && empty($this->events)) return true;
else return false;
}
/**
*
* @return bool
*/
public function isEmptyException(){
return empty($this->exception);
}
/**
*
* @return bool
*/
public function isEmptyEvents(){
return empty($this->events);
}
/**
*
* @return bool
*/
public function isEmptyUnitDefault(){
return empty($this->unit_defaults);
}
/**
* @return Hours_HoursException
*/
public function getException(){
return $this->exception;
}
/*
* @return Hours_UnitDefaultInterfaceAdopter
*/
public function getUnitDefault(){
return $this->unit_defaults[0];
}
/*
* @return Hours_UnitDefaultInterfaceAdopter[]
*/
public function getUnitDefaults(){
return $this->unit_defaults;
}
/*
* @return Event[]
*/
public function getEvents(){
return $this->events;
}
public function getEvent(){
return $this->events[0];
}
public function setDate($date){
$this->date = $date;
}
public function getAllEvents(){
$events = array();
if(!$this->isEmptyEvents()){
$events = $this->events;
}
if(!$this->isEmptyException()){
array_push($events, $this->exception);
}
if(!$this->isEmptyUnitDefault()){
array_push($events, $this->getUnitDefault());
}
return $events;
}
public function emptyEvents(){
$this->events = array();
}
public function emptyUnitDefaults(){
$this->unit_defaults = array();
}
public function emptyException(){
$this->exception = null;
}
public function emptyAll(){
$this->emptyEvents();
$this->emptyException();
$this->emptyUnitDefaults();
}
}
Интерфейс, который используется везде.
interface EventInterface
{
public function getStartTimeStamp();
public function getEndTimeStamp();
public function getStartDate();
public function getEndDate();
public function hasConflict(EventInterface $pending_event);
public function getName();
public function getDetails();
}
Теперь проблема заключается в обнаружении конфликтов и т. Д. ... Требуйте, чтобы я разделил объекты EventContainer, составленные в CalendarDay, для хранения ожидающих событий и т. Д. ... По мере роста возможностей я обнаруживаю, что добавляю в него слишком много вещей.
Любая критика приветствуется.
Я здесь, чтобы учиться.Любой, кто нашел время, чтобы прочитать: спасибо заранее!