Skip to content

SubniC/slot-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

slot-engine

Time-slot algebra in pure PHP. An immutable, dependency-light toolkit that turns the messy interval math behind every booking system into a handful of well-tested operations:

availability = invert(closed_hours ∪ appointments ∪ blocks)

Compute free time, merge and invert busy periods, and place new appointments into the gaps — without a single off-by-one boundary or hand-rolled "free time" loop scattered across your codebase.

It is deliberately low level and framework-agnostic: the core depends only on Carbon and ramsey/uuid, so any scheduling, booking or capacity-planning layer can sit on top. An optional Laravel service provider + facade ship in src/Laravel/ for those who want it.

Highlights

  • Immutable by design. Every operation (pad(), merge(), cutWith(), invert, …) returns new instances. No hidden mutation of shared state, so results are easy to reason about.
  • O(log n) lookups. SlotBag keeps slots sorted and uses binary search for containment, overlap and insertion queries instead of linear scans — with a PHPBench suite to prove the hot paths stay fast as bags grow.
  • A real algebra of time. invert, merge and split over collections of slots, plus per-slot intersect, contains, touches, distanceTo, cutWith, fitIn and padding.
  • Placement algorithms. FirstFitAlgorithm and BestFitAlgorithm, the latter with AlignmentOption (LEFT / RIGHT / MIDDLE / NONE), behind a pluggable AlgorithmFactory.
  • A text DSL for ranges. TimeRanges parses and generates date / time / datetime ranges (2026-06-22...2026-06-24, 09:00:00...18:00:00, …) — explicit window required, no hidden now(), so behaviour is fully deterministic and testable.
  • Recurring schedules. WeeklySchedule is an immutable value object for weekly opening patterns (ISO 1=Mon … 7=Sun) that materializes straight into merged slot bags.
  • Tested and benchmarked. ~147 PHPUnit tests / 658 assertions, plus a PHPBench suite for the performance-critical paths.

Concepts

Type Responsibility
TimeSlot Immutable window [start, start + duration) with optional tags and payload.
SlotBag Ordered, non-overlapping collection of slots with O(log n) lookup.
MergedSlotBag Same, but adjacent/overlapping slots are auto-merged on insert.
SlotProcessor Functional operations across bags: invertSlotBags(), mergeSlotBags().
Algorithms\* FirstFitAlgorithm, BestFitAlgorithm (with AlignmentOption LEFT/RIGHT/MIDDLE/NONE).
Schedule\WeeklySchedule Immutable recurring weekly schedule (ISO days 1=Mon … 7=Sun).
TimeRanges\* A small text DSL to parse/generate ranges (dates, times, datetimes).
Laravel\* Optional service provider + facade for Laravel integration.

Requirements

Installation

composer require mdps/slot-engine

Quick start

Compute the free time in a working day given a set of busy periods, then place a 30-minute appointment into the first available gap:

use Carbon\CarbonImmutable;
use SlotEngine\TimeSlot;
use SlotEngine\SlotBag;
use SlotEngine\SlotProcessor;
use SlotEngine\Algorithms\FirstFitAlgorithm;

$day = CarbonImmutable::parse('2026-06-22 00:00:00');

// The window we reason within: a full day.
$window = new TimeSlot($day, 24 * 3600);

// Busy periods (closed hours + existing appointments).
$busy = new SlotBag($window);
$busy->addOne(new TimeSlot($day->setTime(0, 0),  9 * 3600));   // closed until 09:00
$busy->addOne(new TimeSlot($day->setTime(13, 0), 1 * 3600));   // lunch 13:00–14:00
$busy->addOne(new TimeSlot($day->setTime(18, 0), 6 * 3600));   // closed from 18:00

// Free time = invert(busy) within the window.
$free = (new SlotProcessor())->invertSlotBags($busy);
foreach ($free->getSlots() as $s) {
    echo $s->getStart()->format('H:i').''.$s->getEnd()->format('H:i')."\n";
}
// 09:00–13:00
// 14:00–18:00

// Place the first 30-minute slot that fits, given the busy periods and the window.
$slot = (new FirstFitAlgorithm())->getNextSlot($busy->getSlots(), $window, 30 * 60);

echo $slot->getStart()->format('H:i'); // 09:00

Tests

composer install
vendor/bin/phpunit

The unit suite is pure (no Laravel required). The Laravel-specific tests under tests/Unit/Laravel/ are excluded from the package runner by phpunit.xml.dist; they are meant to run inside a consuming application.

Benchmarks

vendor/bin/phpbench run tests/Benchmark --report=default

Design notes

  • Immutability everywhere. TimeSlot operations (pad(), etc.) return new instances; bags derive new bags. No hidden mutation of shared state.
  • Ordered storage, O(log n) lookups. SlotBag keeps slots sorted and uses binary search for containment/overlap queries instead of linear scans.
  • Explicit over implicit. The TimeRanges parser requires an explicit window — there is no hidden now() fallback — so behaviour is deterministic and testable.
  • Framework-optional. The core depends only on Carbon + ramsey/uuid; the Laravel layer is a thin, opt-in adapter.

License

MIT © 2026 mdps


Un proyecto de mdps · 2026 · desarrollado en Murcia.

About

Pure-PHP time-slot algebra: immutable slots, O(log n) bag operations, availability inversion, and FirstFit/BestFit placement.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages