| 1 |
<?php |
| 2 |
/** |
| 3 |
* @package VikBooking |
| 4 |
* @subpackage core |
| 5 |
* @author E4J s.r.l. |
| 6 |
* @copyright Copyright (C) 2026 E4J s.r.l. All Rights Reserved. |
| 7 |
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL |
| 8 |
* @link https://vikwp.com |
| 9 |
*/ |
| 10 |
|
| 11 |
// No direct access |
| 12 |
defined('ABSPATH') or die('No script kiddies please!'); |
| 13 |
|
| 14 |
/** |
| 15 |
* Room-Booking sub-unit matrix implementation. |
| 16 |
* |
| 17 |
* @since 1.18.7 (J) - 1.8.7 (WP) |
| 18 |
*/ |
| 19 |
final class VBOBookingSubunitMatrix |
| 20 |
{ |
| 21 |
/** |
| 22 |
* @var array |
| 23 |
*/ |
| 24 |
private array $options = []; |
| 25 |
|
| 26 |
/** |
| 27 |
* @var ?VBOBookingRegistry |
| 28 |
*/ |
| 29 |
private ?VBOBookingRegistry $registry = null; |
| 30 |
|
| 31 |
/** |
| 32 |
* @var array |
| 33 |
*/ |
| 34 |
private array $daysUnitsMap = []; |
| 35 |
|
| 36 |
/** |
| 37 |
* @var array |
| 38 |
*/ |
| 39 |
private array $records = []; |
| 40 |
|
| 41 |
/** |
| 42 |
* @var ?VBOBookingSubunitRecord |
| 43 |
*/ |
| 44 |
private ?VBOBookingSubunitRecord $relocateRecord = null; |
| 45 |
|
| 46 |
/** |
| 47 |
* @var ?DatePeriod |
| 48 |
*/ |
| 49 |
private ?DatePeriod $recordsDatePeriod = null; |
| 50 |
|
| 51 |
/** |
| 52 |
* @var int |
| 53 |
*/ |
| 54 |
private int $fittingSolutionsCount = 0; |
| 55 |
|
| 56 |
/** |
| 57 |
* Generates all possible combinations to be assigned to a list of items, from the minimum index to the |
| 58 |
* maximum index. Calculated values for each matrix container are applied over the provided items list. |
| 59 |
* Returns an iterable (Generator) object that will provide a container with the items assigned values. |
| 60 |
* For every item in the list, the Base-N is calculated to count the possible combinations. |
| 61 |
* Base-N = maxIndex - minIndex + 1. Possible combinations = Base-N^itemsCount. |
| 62 |
* |
| 63 |
* @param array $items List of item objects/arrays for which combinations will be calculated. |
| 64 |
* @param int $minIndex The minimum combination value to assign to each item. |
| 65 |
* @param int $maxIndex The maximum combination value to assign to each item. |
| 66 |
* @param ?callable $valueCallback Optional callback for setting the matrix iteration value on each item. |
| 67 |
* |
| 68 |
* @return Generator |
| 69 |
* |
| 70 |
* @throws InvalidArgumentException |
| 71 |
*/ |
| 72 |
public static function testYieldingMatrix(array $items, int $minIndex, int $maxIndex, ?callable $valueCallback = null) |
| 73 |
{ |
| 74 |
if ($minIndex > $maxIndex) { |
| 75 |
throw new InvalidArgumentException('Minimum index must be lower than maximum.', 400); |
| 76 |
} |
| 77 |
|
| 78 |
// count total items |
| 79 |
$itemsCount = count($items); |
| 80 |
|
| 81 |
// calculate the Base-N value |
| 82 |
$baseN = $maxIndex - $minIndex + 1; |
| 83 |
|
| 84 |
// count total combinations |
| 85 |
$combinationsCount = pow($baseN, $itemsCount); |
| 86 |
|
| 87 |
// loop over the total combinations count |
| 88 |
for ($index = 0; $index < $combinationsCount; $index++) { |
| 89 |
// start current matrix container |
| 90 |
$container = []; |
| 91 |
|
| 92 |
// get initial value |
| 93 |
$value = $index; |
| 94 |
|
| 95 |
// shift combination values for all items |
| 96 |
for ($j = 0; $j < $itemsCount; $j++) { |
| 97 |
// get digit |
| 98 |
$digit = $value % $baseN; |
| 99 |
|
| 100 |
// get value |
| 101 |
$value = intdiv($value, $baseN); |
| 102 |
|
| 103 |
// calculate matrix container item value |
| 104 |
$itemValue = $digit + $minIndex; |
| 105 |
|
| 106 |
// obtain matrix container element |
| 107 |
if ($valueCallback) { |
| 108 |
// call provided function |
| 109 |
$element = call_user_func_array($valueCallback, [$items[$j], $itemValue]) ?: $items[$j]; |
| 110 |
} else { |
| 111 |
// set item value |
| 112 |
$element = $itemValue; |
| 113 |
} |
| 114 |
|
| 115 |
// push container item |
| 116 |
$container[] = $element; |
| 117 |
} |
| 118 |
|
| 119 |
// yield current container with the current index as key |
| 120 |
yield $index => $container; |
| 121 |
} |
| 122 |
} |
| 123 |
|
| 124 |
/** |
| 125 |
* Class constructor will bind the registry and days-units map. |
| 126 |
* |
| 127 |
* @param VBOBookingRegistry $registry The involved booking registry. |
| 128 |
* @param array $daysUnitsMap Associative list of days and related data. |
| 129 |
* |
| 130 |
* @throws InvalidArgumentException |
| 131 |
*/ |
| 132 |
public function __construct(VBOBookingRegistry $registry, array $daysUnitsMap) |
| 133 |
{ |
| 134 |
if (!$daysUnitsMap) { |
| 135 |
// missing days-units mapping |
| 136 |
throw new InvalidArgumentException('Missing days-units mapping', 400); |
| 137 |
} |
| 138 |
|
| 139 |
// bind booking registry |
| 140 |
$this->registry = $registry; |
| 141 |
|
| 142 |
// bind days-units map |
| 143 |
$this->daysUnitsMap = $daysUnitsMap; |
| 144 |
} |
| 145 |
|
| 146 |
/** |
| 147 |
* Generates a matrix with all possible moves to apply to the room booking records. |
| 148 |
* For each moveset within the matrix, checks if room records can be re-assigned to |
| 149 |
* fit the minimum check-in and maximum check-out of the involved reservations. |
| 150 |
* |
| 151 |
* @return VBOBookingSubunitMoveset First fitting moveset of room booking records. |
| 152 |
* |
| 153 |
* @throws OverflowException|Exception |
| 154 |
*/ |
| 155 |
public function relocateRoomRecords() |
| 156 |
{ |
| 157 |
if (!$this->relocateRecord) { |
| 158 |
throw new Exception('Missing room record to relocate.', 500); |
| 159 |
} |
| 160 |
|
| 161 |
// access default script execution time |
| 162 |
$defaultMaxExecTime = @ini_get('max_execution_time'); |
| 163 |
$defaultMaxExecTime = is_numeric($defaultMaxExecTime) ? (int) $defaultMaxExecTime : 0; |
| 164 |
|
| 165 |
// determine the script max execution time and cycle lifetime |
| 166 |
$maxExecTime = intval(($this->options['max_exec_time'] ?? 0) ?: 180); |
| 167 |
$maxExecTime = $maxExecTime < 10 ? 10 : $maxExecTime; |
| 168 |
$maxExecTime = $defaultMaxExecTime > $maxExecTime ? $defaultMaxExecTime : $maxExecTime; |
| 169 |
$cycleLifetime = $maxExecTime - 20; |
| 170 |
$cycleLifetime = $cycleLifetime > 0 ? $cycleLifetime : ($maxExecTime - 1); |
| 171 |
|
| 172 |
// try to give the script a higher execution time |
| 173 |
@set_time_limit($maxExecTime); |
| 174 |
@ini_set('max_execution_time', $maxExecTime); |
| 175 |
|
| 176 |
// start timer |
| 177 |
$timerStart = time(); |
| 178 |
|
| 179 |
// always reset fitting solutions counter |
| 180 |
$this->fittingSolutionsCount = 0; |
| 181 |
|
| 182 |
// count listing total inventory units |
| 183 |
$totalUnits = $this->registry->getRoomDetails($this->registry->getCurrentRoomID())['units'] ?? 0; |
| 184 |
|
| 185 |
if ($totalUnits < 2) { |
| 186 |
throw new Exception('Listing total inventory units is less than 2.', 500); |
| 187 |
} |
| 188 |
|
| 189 |
// sort records by dates closer to relocation target |
| 190 |
$this->sortRecords(); |
| 191 |
|
| 192 |
// build room record objects list |
| 193 |
$objectsList = $this->getRecords(); |
| 194 |
|
| 195 |
// prepend room record to relocate to the list |
| 196 |
array_unshift($objectsList, $this->getRelocateRecord()); |
| 197 |
|
| 198 |
// count total number of possible moves (iterations = Base-N^totalBookings) |
| 199 |
$totalMoves = pow(($totalUnits + 1), count($objectsList)); |
| 200 |
|
| 201 |
// generate all possible room booking record moves matrix |
| 202 |
$possbileMovesGenerator = $this->generateMovesetMatrix($objectsList, $totalUnits); |
| 203 |
|
| 204 |
// the Generator object is iterable, but only once |
| 205 |
foreach ($possbileMovesGenerator as $comboCount => $moveset) { |
| 206 |
// parse the combination moveset |
| 207 |
|
| 208 |
// check first if the cycle lifetime is over to prevent an un-handled script termination |
| 209 |
// perform the check every 100k iterations, which should take approximately 2 seconds |
| 210 |
if (($comboCount % 100000) === 0 && (time() - $timerStart) >= $cycleLifetime) { |
| 211 |
// terminate the iterations to prevent the server from collapsing |
| 212 |
throw new OverflowException( |
| 213 |
'The operation was taking too long to complete. Matrix size is too large for the maximum script execution time (' . $maxExecTime . 's).', |
| 214 |
508 |
| 215 |
); |
| 216 |
} |
| 217 |
|
| 218 |
// check if the calculated moveset fits |
| 219 |
if ($this->relocationFits($moveset, $totalUnits) === true) { |
| 220 |
// increase fitting solutions counter |
| 221 |
$this->fittingSolutionsCount++; |
| 222 |
|
| 223 |
// wrap the fitting moveset into a registry |
| 224 |
$movesetRegistry = (new VBOBookingSubunitMoveset($moveset, $this->registry)) |
| 225 |
->setIterationNumber($comboCount + 1) |
| 226 |
->setTotalMoves($totalMoves) |
| 227 |
->setSolutionsCount($this->fittingSolutionsCount) |
| 228 |
->setVerboseRelocation($this->relocationFits($moveset, $totalUnits, $verbose = true)); |
| 229 |
|
| 230 |
if ($this->options['count_all'] ?? null) { |
| 231 |
// all eligible movesets should be identified and counted |
| 232 |
continue; |
| 233 |
} |
| 234 |
|
| 235 |
if ($this->options['skip_moveset_signatures'] ?? null) { |
| 236 |
// some moveset should be skipped |
| 237 |
if (in_array($movesetRegistry->getSignature(), (array) $this->options['skip_moveset_signatures'])) { |
| 238 |
// we don't want this moveset |
| 239 |
continue; |
| 240 |
} |
| 241 |
} |
| 242 |
|
| 243 |
if ($this->options['skip_booking_ids'] ?? null) { |
| 244 |
// some bookings should not be moved |
| 245 |
if (array_intersect($movesetRegistry->getBookingIDs(), (array) $this->options['skip_booking_ids'])) { |
| 246 |
// the moveset includes some bookings that should not be moved |
| 247 |
continue; |
| 248 |
} |
| 249 |
} |
| 250 |
|
| 251 |
// abort and return the fitting relocation moveset registry |
| 252 |
return $movesetRegistry; |
| 253 |
} |
| 254 |
} |
| 255 |
|
| 256 |
if (($this->options['count_all'] ?? null) && $this->countFittingSolutions() && isset($movesetRegistry)) { |
| 257 |
// return the last fitting relocation moveset registry found |
| 258 |
return $movesetRegistry; |
| 259 |
} |
| 260 |
|
| 261 |
// no valid combinations found after exhausting the whole matrix |
| 262 |
throw new Exception( |
| 263 |
sprintf( |
| 264 |
'Could not relocate room reservation after going through all possible moves (%d). Total fitting solutions: %d.', |
| 265 |
($comboCount + 1), |
| 266 |
$this->countFittingSolutions() |
| 267 |
), |
| 268 |
404 |
| 269 |
); |
| 270 |
} |
| 271 |
|
| 272 |
/** |
| 273 |
* Returns the current room booking records. |
| 274 |
* |
| 275 |
* @return array |
| 276 |
*/ |
| 277 |
public function getRecords() |
| 278 |
{ |
| 279 |
return $this->records; |
| 280 |
} |
| 281 |
|
| 282 |
/** |
| 283 |
* Returns the current room booking record to relocate. |
| 284 |
* |
| 285 |
* @return ?VBOBookingSubunitRecord |
| 286 |
*/ |
| 287 |
public function getRelocateRecord() |
| 288 |
{ |
| 289 |
return $this->relocateRecord; |
| 290 |
} |
| 291 |
|
| 292 |
/** |
| 293 |
* Sets the room booking record to relocate. |
| 294 |
* |
| 295 |
* @param ?VBOBookingSubunitRecord $record The record to set. |
| 296 |
* |
| 297 |
* @return static |
| 298 |
*/ |
| 299 |
public function setRelocateRecord(?VBOBookingSubunitRecord $record) |
| 300 |
{ |
| 301 |
$this->relocateRecord = $record; |
| 302 |
|
| 303 |
return $this; |
| 304 |
} |
| 305 |
|
| 306 |
/** |
| 307 |
* Resets the room booking records to relocate. |
| 308 |
* |
| 309 |
* @param bool $main True to also reset the main record to relocate. |
| 310 |
* |
| 311 |
* @return void |
| 312 |
*/ |
| 313 |
public function resetRecords(bool $main = false) |
| 314 |
{ |
| 315 |
// empty sub-unit records |
| 316 |
$this->records = []; |
| 317 |
|
| 318 |
if ($main) { |
| 319 |
// reset main record to relocate |
| 320 |
$this->relocateRecord = null; |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
/** |
| 325 |
* Injects the matrix options. |
| 326 |
* |
| 327 |
* @param array $options Options to bind. |
| 328 |
* |
| 329 |
* @return static |
| 330 |
*/ |
| 331 |
public function setOptions(array $options) |
| 332 |
{ |
| 333 |
$this->options = $options; |
| 334 |
|
| 335 |
return $this; |
| 336 |
} |
| 337 |
|
| 338 |
/** |
| 339 |
* Returns the number of fitting solutions found so far. |
| 340 |
* |
| 341 |
* @return int |
| 342 |
*/ |
| 343 |
public function countFittingSolutions() |
| 344 |
{ |
| 345 |
return $this->fittingSolutionsCount; |
| 346 |
} |
| 347 |
|
| 348 |
/** |
| 349 |
* Adds a room-booking record wrapper to the pool, or sets it as main record. |
| 350 |
* |
| 351 |
* @param VBOBookingSubunitRecord $record The record to push. |
| 352 |
* |
| 353 |
* @return void |
| 354 |
* |
| 355 |
* @throws Exception |
| 356 |
*/ |
| 357 |
public function pushRecord(VBOBookingSubunitRecord $record) |
| 358 |
{ |
| 359 |
if ($record->isRelocating()) { |
| 360 |
// ensure the record to relocate is not a closure |
| 361 |
if ($record->isClosure()) { |
| 362 |
throw new Exception('Booking closures do not support sub-unit relocation.', 500); |
| 363 |
} |
| 364 |
|
| 365 |
// ensure we only get one record to relocate |
| 366 |
if ($this->relocateRecord) { |
| 367 |
throw new Exception('Matrix can only relocate one room booking record per time.', 500); |
| 368 |
} |
| 369 |
|
| 370 |
// set record as main record, without pushing it to the queue |
| 371 |
$this->relocateRecord = $record; |
| 372 |
} else { |
| 373 |
// push room-booking record wrapper |
| 374 |
$this->records[] = $record; |
| 375 |
} |
| 376 |
} |
| 377 |
|
| 378 |
/** |
| 379 |
* Builds and returns the iterable date period for all |
| 380 |
* room booking records (min check-in to max check-out). |
| 381 |
* |
| 382 |
* @return DatePriod |
| 383 |
* |
| 384 |
* @throws Exception |
| 385 |
*/ |
| 386 |
public function getRecordsDatePeriod() |
| 387 |
{ |
| 388 |
if ($this->recordsDatePeriod) { |
| 389 |
// date period already available |
| 390 |
return $this->recordsDatePeriod; |
| 391 |
} |
| 392 |
|
| 393 |
if (!$this->records && !$this->relocateRecord) { |
| 394 |
throw new Exception('No room booking records for calculating the iterable date period.', 500); |
| 395 |
} |
| 396 |
|
| 397 |
// gather all check-in and check-out timestamps |
| 398 |
$checkins = []; |
| 399 |
$checkouts = []; |
| 400 |
|
| 401 |
if ($this->relocateRecord) { |
| 402 |
// push relocate record details |
| 403 |
$checkins[] = $this->relocateRecord->getCheckin(); |
| 404 |
$checkouts[] = $this->relocateRecord->getCheckout(); |
| 405 |
} |
| 406 |
|
| 407 |
foreach ($this->records as $roomRecord) { |
| 408 |
// push room booking record details |
| 409 |
$checkins[] = $roomRecord->getCheckin(); |
| 410 |
$checkouts[] = $roomRecord->getCheckout(); |
| 411 |
} |
| 412 |
|
| 413 |
// local timezone |
| 414 |
$tz = new DateTimezone(date_default_timezone_get()); |
| 415 |
|
| 416 |
// get date bounds |
| 417 |
$from_bound = new DateTime(date('Y-m-d H:i:s', min($checkins)), $tz); |
| 418 |
$to_bound = new DateTime(date('Y-m-d H:i:s', max($checkouts)), $tz); |
| 419 |
|
| 420 |
// set iterable dates interval (period) |
| 421 |
$this->recordsDatePeriod = new DatePeriod( |
| 422 |
// start date included by default in the result set |
| 423 |
$from_bound, |
| 424 |
// interval between recurrences within the period |
| 425 |
new DateInterval('P1D'), |
| 426 |
// end date (check-out) excluded by default from the result set |
| 427 |
$to_bound |
| 428 |
); |
| 429 |
|
| 430 |
// return the iterable date period |
| 431 |
return $this->recordsDatePeriod; |
| 432 |
} |
| 433 |
|
| 434 |
/** |
| 435 |
* Generates all possible moves for the room booking records. Returns an iterator, more precisely |
| 436 |
* a Generator object containing a list of room booking record moveset for every combination. |
| 437 |
* For every room booking record the Base-N is calculated to count the possible combinations. |
| 438 |
* Base-N = maxIndex - minIndex + 1. Possible combinations = Base-N^countRoomBookingRecords. |
| 439 |
* The minimum index is 0, meaning no moves, while the maximum index is the room inventory count. |
| 440 |
* If we had 9 room booking records plus one room booking record to relocate, and if the room |
| 441 |
* had 5 units in total, then the total count of possible moves would be: Base-6^10 = 60.466.176. |
| 442 |
* |
| 443 |
* @param array $records List of room booking record objects, inclusive of the one to relocate (0th). |
| 444 |
* @param int $maxIndex Max combination value (maximum room index = total inventory count). |
| 445 |
* |
| 446 |
* @return Generator For every iteration, list of room booking record moveset. |
| 447 |
*/ |
| 448 |
private function generateMovesetMatrix(array $records, int $maxIndex) |
| 449 |
{ |
| 450 |
// count total room booking records (inclusive of the one to relocate) |
| 451 |
$objectsCount = count($records); |
| 452 |
|
| 453 |
// default minimum index (0 = no moves) |
| 454 |
$minIndex = 0; |
| 455 |
|
| 456 |
// calculate the Base-N value |
| 457 |
$baseN = $maxIndex - $minIndex + 1; |
| 458 |
|
| 459 |
// count total combinations |
| 460 |
$combinationsCount = pow($baseN, $objectsCount); |
| 461 |
|
| 462 |
// loop over the total combinations count |
| 463 |
for ($index = 0; $index < $combinationsCount; $index++) { |
| 464 |
// start current moveset list |
| 465 |
$moveset = []; |
| 466 |
|
| 467 |
// get initial value |
| 468 |
$value = $index; |
| 469 |
|
| 470 |
// shift combination values for all elements |
| 471 |
for ($j = 0; $j < $objectsCount; $j++) { |
| 472 |
// get digit |
| 473 |
$digit = $value % $baseN; |
| 474 |
|
| 475 |
// get value |
| 476 |
$value = intdiv($value, $baseN); |
| 477 |
|
| 478 |
// calculate room index for this combination |
| 479 |
$comboRoomIndex = $digit + $minIndex; |
| 480 |
|
| 481 |
// clone current room record object |
| 482 |
$roomObject = clone $records[$j]; |
| 483 |
|
| 484 |
// apply room index combination |
| 485 |
$roomObject->setRoomUnitIndex($comboRoomIndex); |
| 486 |
|
| 487 |
// set moveset object |
| 488 |
$moveset[$j] = $roomObject; |
| 489 |
} |
| 490 |
|
| 491 |
// yield current moveset with the current index as key |
| 492 |
yield $index => $moveset; |
| 493 |
} |
| 494 |
} |
| 495 |
|
| 496 |
/** |
| 497 |
* Tells if the given moveset fits without any overlapping room booking. |
| 498 |
* |
| 499 |
* @param VBOBookingSubunitRecord[] $moveset List of room booking record (cloned) objects. |
| 500 |
* @param ?int $maxIndex Maximum room index (total inventory count). |
| 501 |
* @param bool $verbose Whether to describe the relocation plan. |
| 502 |
* |
| 503 |
* @return bool|string String if successful and verbose, boolean otherwise. |
| 504 |
* |
| 505 |
* @throws InvalidArgumentException |
| 506 |
*/ |
| 507 |
private function relocationFits(array $moveset, ?int $maxIndex = null, bool $verbose = false) |
| 508 |
{ |
| 509 |
if (!$moveset) { |
| 510 |
throw new InvalidArgumentException('No room booking records in the moveset.', 500); |
| 511 |
} |
| 512 |
|
| 513 |
if (!$maxIndex) { |
| 514 |
// count listing total inventory units |
| 515 |
$maxIndex = $this->registry->getRoomDetails($this->registry->getCurrentRoomID())['units'] ?? 0; |
| 516 |
} |
| 517 |
|
| 518 |
// start verbose description |
| 519 |
$verboseTexts = []; |
| 520 |
|
| 521 |
// scan the date period for all room records to ensure we've got no duplicate indexes |
| 522 |
foreach ($this->getRecordsDatePeriod() as $date) { |
| 523 |
if ($verbose) { |
| 524 |
$verboseTexts[] = 'Analysing date-time ' . $date->format('Y-m-d H:i:s') . "\n"; |
| 525 |
} |
| 526 |
|
| 527 |
// access current calendar date timestamp |
| 528 |
$currentTs = $date->format('U'); |
| 529 |
|
| 530 |
// build the list of occupied room indexes |
| 531 |
$occupiedIndexes = []; |
| 532 |
|
| 533 |
// iterate all room record objects in the moveset |
| 534 |
foreach ($moveset as $roomRecord) { |
| 535 |
// get record room index occupied |
| 536 |
$occupiedIndex = $roomRecord->getRoomUnitIndex(); |
| 537 |
|
| 538 |
if (!$occupiedIndex) { |
| 539 |
// this record is not impacting the moveset |
| 540 |
if ($roomRecord->isRelocating()) { |
| 541 |
// room record to relocate always requires a move |
| 542 |
return false; |
| 543 |
} |
| 544 |
|
| 545 |
// process the next moveset |
| 546 |
continue; |
| 547 |
} |
| 548 |
|
| 549 |
// check if the current room record intersects the current calendar date |
| 550 |
if ($roomRecord->getCheckin() <= $currentTs && $roomRecord->getLastNight() >= $currentTs) { |
| 551 |
// push occupied index |
| 552 |
$occupiedIndexes[] = $occupiedIndex; |
| 553 |
|
| 554 |
if ($verbose) { |
| 555 |
$verboseTexts[] = 'Booking ID ' . $roomRecord->getBookingID() . ' is occupying the unit ' . (int) $occupiedIndex; |
| 556 |
} |
| 557 |
} |
| 558 |
} |
| 559 |
|
| 560 |
if ($verbose) { |
| 561 |
$verboseTexts[] = 'Occupied indexes: ' . implode(', ', $occupiedIndexes) . "\n\n"; |
| 562 |
} |
| 563 |
|
| 564 |
// count number of occupied indexes |
| 565 |
$totOccupiedSlots = count($occupiedIndexes); |
| 566 |
|
| 567 |
if (!$totOccupiedSlots) { |
| 568 |
// no bookings on this day |
| 569 |
continue; |
| 570 |
} |
| 571 |
|
| 572 |
if ($totOccupiedSlots > $maxIndex) { |
| 573 |
// the relocation does not fit as this day is overbooked |
| 574 |
return false; |
| 575 |
} |
| 576 |
|
| 577 |
if (count(array_unique($occupiedIndexes)) != $totOccupiedSlots) { |
| 578 |
// multiple room bookings were occupying the same index |
| 579 |
return false; |
| 580 |
} |
| 581 |
} |
| 582 |
|
| 583 |
// the relocation moveset does fit! |
| 584 |
return $verbose ? implode("\n", $verboseTexts) : true; |
| 585 |
} |
| 586 |
|
| 587 |
/** |
| 588 |
* Sorts the room booking records by dates closer to target. |
| 589 |
* |
| 590 |
* @return void |
| 591 |
*/ |
| 592 |
private function sortRecords() |
| 593 |
{ |
| 594 |
// access the target checkin and checkout date timestamps |
| 595 |
$targetCheckinTs = $this->relocateRecord->getCheckin(); |
| 596 |
$targetCheckoutTs = $this->relocateRecord->getCheckout(); |
| 597 |
|
| 598 |
// check if some bookings should be skipped |
| 599 |
$lowPriorityBids = (array) ($this->options['skip_booking_ids'] ?? null); |
| 600 |
|
| 601 |
// sort records |
| 602 |
usort($this->records, function($a, $b) use ($targetCheckinTs, $targetCheckoutTs, $lowPriorityBids) { |
| 603 |
// calculate timestamp distance for both comparison elements |
| 604 |
$aDistance = abs($targetCheckinTs - $a->getCheckin()) + abs($targetCheckoutTs - $a->getCheckout()); |
| 605 |
$bDistance = abs($targetCheckinTs - $b->getCheckin()) + abs($targetCheckoutTs - $b->getCheckout()); |
| 606 |
|
| 607 |
if ($lowPriorityBids) { |
| 608 |
if (in_array($a->getBookingID(), $lowPriorityBids)) { |
| 609 |
$aDistance = PHP_INT_MAX; |
| 610 |
} |
| 611 |
if (in_array($b->getBookingID(), $lowPriorityBids)) { |
| 612 |
$bDistance = PHP_INT_MAX; |
| 613 |
} |
| 614 |
} |
| 615 |
|
| 616 |
return $aDistance <=> $bDistance; |
| 617 |
}); |
| 618 |
} |
| 619 |
} |
| 620 |
|