844 lines
23 KiB
PHP
844 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* Simply Schedule Appointments Availability Schedule.
|
|
*
|
|
* @since 3.6.10
|
|
* @package Simply_Schedule_Appointments
|
|
*/
|
|
|
|
use League\Period\Period;
|
|
|
|
/**
|
|
* Simply Schedule Appointments Availability Schedule.
|
|
*
|
|
* @since 3.6.10
|
|
*/
|
|
class SSA_Availability_Schedule implements Countable, Iterator {
|
|
/**
|
|
* Parent plugin class.
|
|
*
|
|
* @since 3.6.10
|
|
*
|
|
* @var Simply_Schedule_Appointments
|
|
*/
|
|
protected $plugin = null;
|
|
|
|
protected $blocks = array();
|
|
protected $is_sorted = true;
|
|
|
|
protected $new_blocks = array();
|
|
protected $count = 0;
|
|
protected $position = 0;
|
|
protected $_queried_appointments = null;
|
|
|
|
// Dynamic property - can be set by factory
|
|
public $capacity_available;
|
|
|
|
const MERGE_MODE_MIN = -1;
|
|
const MERGE_MODE_MAX = 1;
|
|
protected $merge_mode = self::MERGE_MODE_MIN;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 3.6.10
|
|
*
|
|
* @param Simply_Schedule_Appointments $plugin Main plugin object.
|
|
*/
|
|
public function __construct() {
|
|
|
|
}
|
|
|
|
public function get_keyed_blocks_array( $blocks ) {
|
|
// check if the blocks are already keyed
|
|
if ( ! empty( $blocks ) ) {
|
|
$first_block = reset( $blocks );
|
|
$first_start_date = $first_block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' );
|
|
if ( ! empty( $blocks[$first_start_date] ) ) {
|
|
return $blocks;
|
|
}
|
|
}
|
|
|
|
// build the keyed blocks array
|
|
$keyed_blocks = array();
|
|
foreach ($blocks as $block) {
|
|
$keyed_blocks[$block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' )] = $block;
|
|
}
|
|
|
|
return $keyed_blocks;
|
|
}
|
|
|
|
public function set_blocks( $blocks, $is_clean = false ) {
|
|
$clone = $this->get_clone();
|
|
// get the first block and see if its array key matches that block's period start date
|
|
$blocks = $this->get_keyed_blocks_array( $blocks );
|
|
|
|
$clone->blocks = $blocks;
|
|
$clone->is_sorted = $is_clean;
|
|
|
|
return $clone;
|
|
// $this->is_clean = $is_clean; // sorted, gapless, overlapless
|
|
}
|
|
|
|
public function get_blocks() {
|
|
if ( $this->is_sorted() ) {
|
|
return $this->blocks;
|
|
}
|
|
|
|
$this->blocks = $this->sort()->blocks;
|
|
$this->is_sorted = true;
|
|
|
|
return $this->blocks;
|
|
}
|
|
|
|
public function overlaps( SSA_Availability_Schedule $schedule ) {
|
|
$this_boundaries = $this->boundaries();
|
|
if ( empty( $this_boundaries ) ) {
|
|
return false;
|
|
}
|
|
|
|
$schedule_boundaries = $schedule->boundaries();
|
|
if ( empty( $schedule_boundaries ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $this_boundaries->overlaps( $schedule_boundaries );
|
|
}
|
|
|
|
public function subrange( Period $period, $exact = true ) {
|
|
if ( $this->boundaries() == $period ) {
|
|
return $this;
|
|
}
|
|
|
|
$filter_function = function ( $value ) use ( $period ) {
|
|
if ( $value->get_period()->overlaps( $period ) ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
$overlapping_schedule = $this->filter( $filter_function );
|
|
if ( empty( $exact ) ) {
|
|
return $overlapping_schedule;
|
|
}
|
|
|
|
$blocks = $overlapping_schedule->get_blocks();
|
|
while ( ! empty( $blocks ) && reset( $blocks )->get_period()->getStartDate() < $period->getStartDate() ) {
|
|
$block = array_shift( $blocks );
|
|
if ( $block->get_period()->getEndDate() <= $period->getStartDate() ) {
|
|
continue; // this block begins and ends before the desired time, so we should just toss it rather than modify it
|
|
}
|
|
|
|
$block = $block->set_period( new Period(
|
|
$period->getStartDate(),
|
|
$block->get_period()->getEndDate()
|
|
) );
|
|
array_unshift( $blocks, $block );
|
|
}
|
|
|
|
while ( ! empty( $blocks ) && end( $blocks )->get_period()->getEndDate() > $period->getEndDate() ) {
|
|
$block = array_pop( $blocks );
|
|
if ( $block->get_period()->getStartDate() >= $period->getEndDate() ) {
|
|
continue; // this block begins and ends after the desired time, so we should just toss it rather than modify it
|
|
}
|
|
|
|
$block = $block->set_period( new Period(
|
|
$block->get_period()->getStartDate(),
|
|
$period->getEndDate()
|
|
) );
|
|
$blocks[] = $block;
|
|
}
|
|
|
|
$exact_schedule = $this->set_blocks( $blocks, true );
|
|
return $exact_schedule;
|
|
}
|
|
|
|
public function binarize( $gte_threshold = 1 ) {
|
|
$schedule = new SSA_Availability_Schedule();
|
|
foreach ($this->get_blocks() as $block) {
|
|
$schedule = $schedule->pushmerge( $block->binarize( $gte_threshold ) );
|
|
}
|
|
|
|
return $schedule;
|
|
}
|
|
|
|
// public function blackout_by_capacity_reserved_gte( int $capacity_reserved ) {
|
|
// $schedule = $this->set_blocks(array(), true);
|
|
// foreach ($this->get_blocks() as $block) {
|
|
// echo '<pre>'.print_r($block, true).'</pre>';
|
|
// if ( $block->capacity_reserved >= $capacity_reserved ) {
|
|
// $block->capacity_available = 0;
|
|
// $schedule = $schedule->pushmerge( $block );
|
|
// }
|
|
// }
|
|
|
|
// return $schedule;
|
|
// }
|
|
|
|
/**
|
|
* Filters the sequence according to the given predicate.
|
|
*
|
|
* This method MUST retain the state of the current instance, and return
|
|
* an instance that contains the interval which validate the predicate.
|
|
*/
|
|
public function map( callable $predicate ) {
|
|
$blocks = $this->get_blocks();
|
|
$mapped_blocks = array_map( $predicate, $blocks );
|
|
|
|
$mapped_schedule = new SSA_Availability_Schedule();
|
|
foreach ($mapped_blocks as $mapped_block) {
|
|
$mapped_schedule = $mapped_schedule->pushmerge( $mapped_block );
|
|
}
|
|
|
|
return $mapped_schedule;
|
|
}
|
|
|
|
/**
|
|
* Filters the sequence according to the given predicate.
|
|
*
|
|
* This method MUST retain the state of the current instance, and return
|
|
* an instance that contains the interval which validate the predicate.
|
|
*/
|
|
public function filter( callable $predicate ) {
|
|
$blocks = $this->get_blocks();
|
|
$filtered_blocks = array_filter( $blocks, $predicate, ARRAY_FILTER_USE_BOTH );
|
|
if ( $filtered_blocks === $blocks ) {
|
|
return $this;
|
|
}
|
|
|
|
return $this->set_blocks( $filtered_blocks );
|
|
}
|
|
|
|
/**
|
|
* Returns an instance sorted according to the given comparison callable
|
|
* but does not maintain index association.
|
|
*
|
|
* This method DOES NOT retain the state of the current instance, it sorts the original
|
|
*/
|
|
public function sort( callable $compare = null ) {
|
|
if ( $this->is_sorted() ) {
|
|
return $this;
|
|
}
|
|
|
|
$blocks = $this->blocks;
|
|
ksort( $blocks );
|
|
$new_instance = $this->set_blocks( $blocks, true );
|
|
return $new_instance;
|
|
}
|
|
|
|
private function get_clone() {
|
|
$instance = clone $this;
|
|
$instance->_queried_appointments = null;
|
|
|
|
return $instance;
|
|
}
|
|
|
|
public function boundaries() {
|
|
if ( $this->is_empty() ) {
|
|
return null;
|
|
}
|
|
|
|
$blocks = $this->get_blocks();
|
|
if ( 1 === count( $blocks ) ) {
|
|
$first_block = reset( $blocks );
|
|
return $first_block->get_period();
|
|
}
|
|
|
|
|
|
$start_date = array_shift( $blocks )->get_period()->getStartDate();
|
|
$end_date = array_pop( $blocks )->get_period()->getEndDate();
|
|
|
|
return new Period( $start_date, $end_date );
|
|
}
|
|
|
|
/**
|
|
* Sorts two Interval instance using their start datepoint.
|
|
*/
|
|
private function sort_by_start_date(SSA_Availability_Block $block1, SSA_Availability_Block $block2) {
|
|
$a = $block1->get_period()->getStartDate();
|
|
$b = $block2->get_period()->getStartDate();
|
|
|
|
if ( $a == $b ) {
|
|
return 0;
|
|
} else if ( $a < $b ) {
|
|
return -1;
|
|
} else if ( $a > $b ) {
|
|
return 1;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function reconcile_new_blocks() {
|
|
if ( empty( $this->new_blocks ) || ! is_array( $this->new_blocks ) ) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->new_blocks as $key => $block) {
|
|
$schedule = $this->add_block( $block );
|
|
}
|
|
}
|
|
|
|
public function add_block( SSA_Availability_Block $new_block ) {
|
|
$boundaries = $this->boundaries();
|
|
|
|
// Let's try to handle the simple (non-overlapping) cases first
|
|
if ( $this->is_empty() ) {
|
|
$reconciled_schedule = $this->push( $new_block );
|
|
|
|
return $reconciled_schedule;
|
|
} else if ( $new_block->is_after_period( $boundaries ) ) {
|
|
$reconciled_schedule = $this->pushmerge( $new_block );
|
|
return $reconciled_schedule;
|
|
} else if ( $new_block->is_before_period( $boundaries ) ) {
|
|
$reconciled_schedule = $this->unshiftmerge( $new_block );
|
|
return $reconciled_schedule;
|
|
} else if ( ! $new_block->overlaps_period( $boundaries ) ) {
|
|
throw new Exception("unhandled case in add_block()", 1);
|
|
}
|
|
|
|
// we must be dealing with an overlapping block (the complex case)
|
|
$reconciled_schedule = $this->add_overlapping_block( $new_block );
|
|
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
private function add_overlapping_block( SSA_Availability_Block $new_block ) {
|
|
$reconciled_schedule = $this->set_blocks( array() );
|
|
$has_added_reconciled_blocks = false;
|
|
|
|
foreach ($this->get_blocks() as $key => $original_block) {
|
|
if ( $has_added_reconciled_blocks ) {
|
|
$reconciled_schedule = $reconciled_schedule->add_block( $original_block );
|
|
continue;
|
|
}
|
|
|
|
if ( ! $has_added_reconciled_blocks &&
|
|
! $original_block->get_period()->overlaps( $new_block->get_period() )
|
|
) {
|
|
$reconciled_schedule = $reconciled_schedule->push( $original_block );
|
|
continue;
|
|
}
|
|
|
|
$reconciled_blocks = array();
|
|
|
|
if ( $original_block->contains( $new_block ) ) {
|
|
// Add Contained Block
|
|
$left_block = $original_block;
|
|
$right_block = $original_block;
|
|
} else if ( $new_block->contains( $original_block ) ) {
|
|
// Add Container Block
|
|
$left_block = $new_block;
|
|
$right_block = $new_block;
|
|
} else if ( $new_block->overlaps( $original_block ) ) {
|
|
if ( $new_block->get_period()->getStartDate() < $original_block->get_period()->getStartDate() ) {
|
|
// Add overlapping earlier block
|
|
$left_block = $new_block;
|
|
$right_block = $original_block;
|
|
} else if ( $new_block->get_period()->getStartDate() >= $original_block->get_period()->getStartDate() ) {
|
|
// Add overlapping later block
|
|
$left_block = $original_block;
|
|
$right_block = $new_block;
|
|
}
|
|
} else {
|
|
// we shouldn't even be in this function unless the blocks overlap
|
|
throw new Exception("unhandled case in add_overlapping_block()", 1);
|
|
}
|
|
|
|
$middle_block = null;
|
|
|
|
$diff_array = $original_block->get_period()->diff( $new_block->get_period() );
|
|
|
|
$intersect_period = $original_block->get_period()->intersect( $new_block->get_period() );
|
|
|
|
|
|
if ( ! empty( $diff_array[0] ) ) {
|
|
$reconciled_blocks[] = $left_block->set_period( $diff_array[0] );
|
|
}
|
|
|
|
if ( ! empty( $intersect_period ) ) {
|
|
if ( self::MERGE_MODE_MIN === $this->merge_mode ) {
|
|
$reconciled_blocks[] = $this->min_reconcile_overlapping_blocks_intersection(
|
|
$original_block,
|
|
$new_block,
|
|
$intersect_period
|
|
);
|
|
} else if ( self::MERGE_MODE_MAX === $this->merge_mode ) {
|
|
$reconciled_blocks[] = $this->max_reconcile_overlapping_blocks_intersection(
|
|
$original_block,
|
|
$new_block,
|
|
$intersect_period
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $diff_array[1] ) ) {
|
|
$reconciled_blocks[] = $right_block->set_period( $diff_array[1] );
|
|
}
|
|
|
|
$schedule_to_merge = $this->set_blocks( $reconciled_blocks )->cleaned();
|
|
|
|
$reconciled_schedule = $reconciled_schedule->merge( $schedule_to_merge );
|
|
|
|
$has_added_reconciled_blocks = true;
|
|
}
|
|
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
public function min_reconcile_overlapping_blocks_intersection( SSA_Availability_Block $original_block, SSA_Availability_Block $new_block, Period $intersect_period ) {
|
|
|
|
$intersect_block = new SSA_Availability_Block();
|
|
$intersect_block->period = $intersect_period;
|
|
|
|
$intersect_block->capacity_reserved = max(
|
|
$original_block->capacity_reserved,
|
|
$new_block->capacity_reserved
|
|
);
|
|
$intersect_block->capacity_available = min(
|
|
$original_block->capacity_available,
|
|
$new_block->capacity_available
|
|
);
|
|
|
|
$capacity_reserved_delta = $original_block->capacity_reserved_delta + $new_block->capacity_reserved_delta;
|
|
|
|
|
|
|
|
$intersect_block->buffer_reserved = max(
|
|
$original_block->buffer_reserved,
|
|
$new_block->buffer_reserved
|
|
);
|
|
$intersect_block->buffer_available = min(
|
|
$original_block->buffer_available,
|
|
$new_block->buffer_available
|
|
);
|
|
|
|
$buffer_reserved_delta = $original_block->buffer_reserved_delta + $new_block->buffer_reserved_delta;
|
|
|
|
if ( $intersect_block->capacity_available < SSA_Constants::CAPACITY_MAX ) {
|
|
$intersect_block->capacity_available -= $capacity_reserved_delta;
|
|
$intersect_block->capacity_reserved += $capacity_reserved_delta;
|
|
|
|
}
|
|
|
|
if ( $intersect_block->buffer_available < SSA_Constants::CAPACITY_MAX ) {
|
|
$intersect_block->buffer_available -= $buffer_reserved_delta;
|
|
$intersect_block->buffer_reserved += $buffer_reserved_delta;
|
|
}
|
|
|
|
return $intersect_block;
|
|
}
|
|
|
|
public function max_reconcile_overlapping_blocks_intersection( SSA_Availability_Block $original_block, SSA_Availability_Block $new_block, Period $intersect_period ) {
|
|
|
|
$intersect_block = new SSA_Availability_Block();
|
|
$intersect_block->period = $intersect_period;
|
|
|
|
$intersect_block->capacity_reserved = min(
|
|
$original_block->capacity_reserved,
|
|
$new_block->capacity_reserved
|
|
);
|
|
$intersect_block->capacity_available = max(
|
|
$original_block->capacity_available,
|
|
$new_block->capacity_available
|
|
);
|
|
|
|
$capacity_reserved_delta = $original_block->capacity_reserved_delta + $new_block->capacity_reserved_delta;
|
|
|
|
|
|
|
|
$intersect_block->buffer_reserved = max(
|
|
$original_block->buffer_reserved,
|
|
$new_block->buffer_reserved
|
|
);
|
|
$intersect_block->buffer_available = max(
|
|
$original_block->buffer_available,
|
|
$new_block->buffer_available
|
|
);
|
|
|
|
$buffer_reserved_delta = $original_block->buffer_reserved_delta + $new_block->buffer_reserved_delta;
|
|
|
|
if ( $intersect_block->capacity_available < SSA_Constants::CAPACITY_MAX ) {
|
|
$intersect_block->capacity_available -= $capacity_reserved_delta;
|
|
$intersect_block->capacity_reserved += $capacity_reserved_delta;
|
|
|
|
}
|
|
|
|
if ( $intersect_block->buffer_available < SSA_Constants::CAPACITY_MAX ) {
|
|
$intersect_block->buffer_available -= $buffer_reserved_delta;
|
|
$intersect_block->buffer_reserved += $buffer_reserved_delta;
|
|
}
|
|
|
|
// extra computation and not actually helpful?:
|
|
// if ( $intersect_block->capacity_reserved > 0 && $intersect_block->capacity_available <= 0 ) {
|
|
// $intersect_block->buffer_available = 0;
|
|
// }
|
|
|
|
return $intersect_block;
|
|
}
|
|
|
|
public function get_criteria() {
|
|
|
|
}
|
|
|
|
public function is_empty() {
|
|
return array() === $this->blocks;
|
|
}
|
|
|
|
public function is_sorted() {
|
|
return $this->is_sorted;
|
|
}
|
|
|
|
public function is_continuous() {
|
|
if ( empty( $this->blocks ) ) {
|
|
return false;
|
|
}
|
|
|
|
$last_block = null;
|
|
foreach ($this->get_blocks() as $key => $block) {
|
|
if ( ! empty( $last_block ) ) {
|
|
if ( ! $block->abuts( $last_block ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$last_block = $block;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Adds new blocks at the end of the sequence.
|
|
*
|
|
* @param array of SSA_Availability_Block $blocks
|
|
*/
|
|
public function push( $blocks ) {
|
|
if ( ! is_array( $blocks ) ) {
|
|
$blocks = array( $blocks );
|
|
}
|
|
|
|
$clone = $this->get_clone();
|
|
foreach ( $blocks as $block ) {
|
|
$start_date = $block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' );
|
|
if ( ! empty( $clone->blocks[$start_date] ) ) {
|
|
ssa_debug_log( 'DUPLICATE BLOCK! ' . $start_date, 10 );
|
|
}
|
|
$clone->blocks[$start_date] = $block;
|
|
}
|
|
$clone->is_sorted = false;
|
|
|
|
return $clone;
|
|
}
|
|
|
|
/**
|
|
* Adds new blocks at the end of the sequence.
|
|
*
|
|
* @param array of SSA_Availability_Block $blocks
|
|
*/
|
|
public function pushmerge( $new_blocks ) {
|
|
if ( ! is_array( $new_blocks ) ) {
|
|
$new_blocks = array( $new_blocks );
|
|
}
|
|
|
|
$remaining_new_blocks = $new_blocks;
|
|
$first_new_block = array_shift( $remaining_new_blocks );
|
|
if ( empty( $first_new_block ) ) {
|
|
return $this;
|
|
}
|
|
|
|
$reconciled_schedule_blocks = $this->get_blocks();
|
|
$last_block = array_pop( $reconciled_schedule_blocks );
|
|
if ( empty( $last_block ) ) {
|
|
return $this->push( $new_blocks );
|
|
}
|
|
|
|
if ( ! $last_block->can_merge( $first_new_block ) ) {
|
|
return $this->push( $new_blocks );
|
|
}
|
|
|
|
$reconciled_schedule = $this->set_blocks( $reconciled_schedule_blocks );
|
|
$reconciled_schedule = $reconciled_schedule->push( $last_block->merge( $first_new_block ) );
|
|
if ( empty( $remaining_new_blocks ) ) {
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
$reconciled_schedule = $reconciled_schedule->push( $remaining_new_blocks );
|
|
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
/**
|
|
* Adds new blocks at the beginning of the sequence (and does the extra work to prevent abutting mergeable blocks)
|
|
*
|
|
* @param array of SSA_Availability_Block $blocks
|
|
*/
|
|
public function unshift( $blocks ) {
|
|
if ( ! is_array( $blocks ) ) {
|
|
$blocks = array( $blocks );
|
|
}
|
|
|
|
$clone = $this->get_clone();
|
|
foreach ( $blocks as $block ) {
|
|
$start_date = $block->get_period()->getStartDate()->format( 'Y-m-d H:i:s' );
|
|
if ( ! empty( $clone->blocks[$start_date] ) ) {
|
|
ssa_debug_log( 'DUPLICATE BLOCK! ' . $start_date, 10 );
|
|
}
|
|
$clone->blocks[$start_date] = $block;
|
|
}
|
|
|
|
$clone->is_sorted = false;
|
|
|
|
return $clone;
|
|
}
|
|
|
|
/**
|
|
* Adds new blocks at the beginning of the sequence (and does the extra work to prevent abutting mergeable blocks)
|
|
*
|
|
* @param array of SSA_Availability_Block $blocks
|
|
*/
|
|
public function unshiftmerge( $new_blocks ) {
|
|
if ( ! is_array( $new_blocks ) ) {
|
|
$new_blocks = array( $new_blocks );
|
|
}
|
|
|
|
$remaining_new_blocks = $new_blocks;
|
|
$last_new_block = array_shift( $remaining_new_blocks );
|
|
if ( empty( $last_new_block ) ) {
|
|
return $this;
|
|
}
|
|
|
|
$reconciled_schedule_blocks = $this->get_blocks();
|
|
$first_old_block = array_shift( $reconciled_schedule_blocks );
|
|
if ( empty( $first_old_block ) ) {
|
|
return $this->unshift( $new_blocks );
|
|
}
|
|
|
|
if ( ! $first_old_block->can_merge( $last_new_block ) ) {
|
|
return $this->unshift( $new_blocks );
|
|
}
|
|
|
|
$reconciled_schedule = $this->set_blocks( $reconciled_schedule_blocks );
|
|
$reconciled_schedule = $reconciled_schedule->unshift( $first_old_block->merge( $last_new_block ) );
|
|
if ( empty( $remaining_new_blocks ) ) {
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
$reconciled_schedule = $reconciled_schedule->unshift( $remaining_new_blocks );
|
|
|
|
return $reconciled_schedule;
|
|
}
|
|
|
|
public function merge_min( SSA_Availability_Schedule $another_schedule ) {
|
|
$this->merge_mode = self::MERGE_MODE_MIN;
|
|
$merged_schedule = $this->merge( $another_schedule );
|
|
return $merged_schedule;
|
|
}
|
|
|
|
public function merge_max( SSA_Availability_Schedule $another_schedule ) {
|
|
$this->merge_mode = self::MERGE_MODE_MAX;
|
|
$merged_schedule = $this->merge( $another_schedule );
|
|
$this->merge_mode = self::MERGE_MODE_MIN;
|
|
return $merged_schedule;
|
|
}
|
|
|
|
private function merge( SSA_Availability_Schedule $another_schedule ) {
|
|
if ( null === $another_schedule ) {
|
|
return $this;
|
|
}
|
|
|
|
$another_schedule_blocks = $another_schedule->get_blocks();
|
|
if ( empty( $another_schedule_blocks ) ) {
|
|
return $this;
|
|
}
|
|
|
|
if ( empty( $this->get_blocks() ) ) {
|
|
return $another_schedule;
|
|
}
|
|
|
|
|
|
$merged_schedule = $this->get_clone();
|
|
foreach ( $another_schedule_blocks as $block) {
|
|
$merged_schedule = $merged_schedule->add_block( $block );
|
|
}
|
|
|
|
return $merged_schedule;
|
|
}
|
|
|
|
public function cleaned() {
|
|
$last_block = null;
|
|
$reduced_blocks = array();
|
|
|
|
foreach ($this->get_blocks() as $block) {
|
|
$last_block = array_pop( $reduced_blocks );
|
|
|
|
if ( empty( $last_block ) ) {
|
|
$reduced_blocks[] = $block;
|
|
continue;
|
|
}
|
|
|
|
if ( ! $last_block->can_merge( $block ) ) {
|
|
$reduced_blocks[] = $last_block;
|
|
$reduced_blocks[] = $block;
|
|
continue;
|
|
}
|
|
|
|
$reduced_blocks[] = $last_block->merge( $block );
|
|
}
|
|
|
|
return $this->set_blocks( $reduced_blocks );
|
|
}
|
|
|
|
public function get_blocks_for_period( Period $period ) {
|
|
$overlapping_blocks = array();
|
|
foreach ($this->get_blocks() as $key => $block) {
|
|
if ( $block->get_period()->overlaps( $period ) ) {
|
|
$overlapping_blocks[] = $block;
|
|
}
|
|
}
|
|
|
|
return $overlapping_blocks;
|
|
}
|
|
|
|
public function get_block_for_date( $date ) {
|
|
$boundary_period = $this->boundaries();
|
|
if ( empty( $boundary_period ) || ! $boundary_period->contains( $date ) ) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($this->get_blocks() as $key => $block) {
|
|
if ( $block->get_period()->contains( $date ) ) {
|
|
return $block;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function get_free_busy_schedule( SSA_Appointment_Type_Object $appointment_type = null, $minimum_free_capacity = 1 ) {
|
|
$schedule = new SSA_Availability_Schedule();
|
|
|
|
foreach ( $this->get_blocks() as $block ) {
|
|
$block = $block->set_capacity_available( min( $block->capacity_available, $minimum_free_capacity ) );
|
|
$block = $block->set_capacity_reserved( min( $block->capacity_reserved, $minimum_free_capacity ) );
|
|
|
|
$block = $block->set_buffer_available( min( $block->buffer_available, $minimum_free_capacity ) );
|
|
$block = $block->set_buffer_reserved( min( $block->buffer_reserved, $minimum_free_capacity ) );
|
|
|
|
if ( $block->capacity_available >= $minimum_free_capacity && ( ! empty( $appointment_type ) ) ) {
|
|
if ( 'group' === $appointment_type->capacity_type ) {
|
|
if ( $block->capacity_reserved > 0 ) {
|
|
$block = $block->set_capacity_available( 0 );
|
|
}
|
|
}
|
|
}
|
|
|
|
$schedule = $schedule->pushmerge( $block );
|
|
}
|
|
|
|
return $schedule;
|
|
}
|
|
|
|
public function is_appointment_period_available( SSA_Appointment_Object $appointment, SSA_Appointment_Type_Object $appointment_type = null ) {
|
|
$appointment_buffered_period = $appointment->get_buffered_period();
|
|
$blocks = $this->get_blocks_for_period( $appointment_buffered_period );
|
|
if ( empty( $blocks ) ) {
|
|
return false;
|
|
}
|
|
|
|
if ( empty( $appointment_type ) ) {
|
|
$appointment_type = $appointment->get_appointment_type();
|
|
// We should use the appointment type that we're querying for if it's set (faster than getting appointment type dynamically each run)
|
|
}
|
|
|
|
/* Check Buffered Period conflicts */
|
|
if ( $appointment_type->get_buffer_capacity_multiplier() ) {
|
|
foreach ($blocks as $block) {
|
|
if ( $block->buffer_available <= 0 ) {
|
|
return false;
|
|
}
|
|
|
|
if ( $block->capacity_available <= 0 && $block->capacity_reserved > 0 ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Check raw appointment period - less common case, deferred from initial loop for performance reasons */
|
|
$appointment_period = $appointment->get_appointment_period();
|
|
$appointment_period_blocks = $this->get_blocks_for_period( $appointment_period );
|
|
if ( empty( $appointment_period_blocks ) ) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($blocks as $block) {
|
|
if ( ! $block->get_period()->overlaps( $appointment_period ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( $block->capacity_available <= 0 ) {
|
|
return false;
|
|
}
|
|
|
|
if ( 2 == $appointment_type->get_buffer_capacity_multiplier() ) {
|
|
if ( $block->buffer_available <= 1 ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Iteratively reduces the sequence to a single value using a callback.
|
|
*
|
|
* @param callable $func Accepts the carry, the current value, and
|
|
* returns an updated carry value.
|
|
*
|
|
* @param mixed|null $carry Optional initial carry value.
|
|
*
|
|
* @return mixed The carry value of the final iteration, or the initial
|
|
* value if the sequence was empty.
|
|
*/
|
|
public function reduce(callable $func, $carry = null) {
|
|
foreach ($this->intervals as $offset => $interval) {
|
|
$carry = $func($carry, $interval, $offset);
|
|
}
|
|
|
|
return $carry;
|
|
}
|
|
|
|
|
|
|
|
/* Countable */
|
|
public function count() : int {
|
|
return count( $this->blocks );
|
|
}
|
|
|
|
|
|
/* Iterator */
|
|
public function rewind() : void {
|
|
$this->position = 0;
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function current() {
|
|
return $this->blocks[$this->position];
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function key() {
|
|
return $this->position;
|
|
}
|
|
|
|
public function next() : void {
|
|
++$this->position;
|
|
}
|
|
|
|
public function valid() : bool {
|
|
return isset($this->blocks[$this->position]);
|
|
}
|
|
}
|