centamiv / chronoset
The ultimate Time Period management toolkit for PHP/Laravel.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/centamiv/chronoset
Requires
- php: ^8.2
- illuminate/support: ^10.0 || ^11.0
- nesbot/carbon: ^2.66 || ^3.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0 || ^9.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-12-21 20:53:51 UTC
README
ChronoSet is a powerful, immutable Laravel/PHP library designed to treat time as a mathematical set.
Managing complex schedules, booking availabilities, and recurring time blocks can quickly become a headache with standard date libraries. ChronoSet solves this by allowing you to add, subtract, intersect, and normalize collections of dates effortlessly.
Key Features
- Period Normalization: Automatically merges overlapping or contiguous periods.
- Set Operations: Union, Intersection, Difference, and Symmetric Difference.
- Immutability: Built on
CarbonImmutable. - Infinite Bounds: Supports
nullas "infinity". - Gap Finding: Easily find free slots (availability).
- Precise: Second-level precision. Periods are inclusive (
[start, end]). ChronoSet distinguishes between overlapping and touching: a period ending at10:00:00does NOT conflict with one starting at10:00:00. This allows seamless back-to-back scheduling.
Installation
composer require centamiv/chronoset
Core Concepts & Usage
1. Time as a Mathematical Set
ChronoSet treats time not as a sequence of dates, but as a continuous mathematical line. "Sets" of time can be added, subtracted, and intersected just like Venn diagrams.
2. Period vs PeriodCollection
- Period: A single continuous span of time (e.g., "Meeting from 9am to 10am").
- PeriodCollection: A set containing zero, one, or multiple disjoint (non-overlapping) Periods. This allows you to represent complex schedules like "Mondays and Wednesdays from 9-5" as a single object.
3. Open/Closed Boundaries & Infinity
- Periods are inclusive of their start and end points (
[start, end]). nullrepresents Infinity.new Period(null, '2025-01-01')covers everything from the beginning of time up to Jan 1st 2025.
4. Normalization
This is the library's superpower. When you add periods to a PeriodCollection, it automatically merges overlaps and connects touching periods. You never have to manually check for conflicting times.
Input:
Period A: [=======]
Period B: [=======]
Period C: [===]
Normalized:
Result: [================]
$schedule = new PeriodCollection([ new Period('09:00', '10:00'), new Period('09:30', '11:00'), new Period('11:00', '12:00'), ]); $clean = $schedule->normalize(); // Result: One period from 09:00 -> 12:00
5. Set Operations
Since time is a set, you can perform standard set operations on PeriodCollections:
- Union: Combine two schedules (
$a->union($b)). - Intersection: Find common free time (
$a->intersect($b)). - Difference: Remove booked time from a schedule (
$a->diff($b)).
6. Finding Availability (Gaps)
Find free slots by "subtracting" your booked periods from a "boundary" period (like a work day).
Boundary: [=========================]
Booked: [=====] [=====]
Gaps: [=] [=====] [=]
$workDay = new Period('09:00', '18:00'); $booked = new PeriodCollection([ new Period('10:00', '11:00'), new Period('14:00', '15:00') ]); // "Work Day" minus "Booked" = "Free Time" $free = $booked->gaps($workDay); // Result: [09:00-10:00], [11:00-14:00], [15:00-18:00]
7. Immutability
All classes are immutable. Operations like merge or subtract return a new instance, leaving the original unchanged. This prevents side-effects and makes the code easier to reason about.
API Reference
Period
overlaps(Period $other): booloverlapsOrTouches(Period $other): boolcontainsDate(Carbon $date): boolcontainsPeriod(Period $other): boolsubtract(Period $other): Period[]intersect(Period $other): ?Periodmerge(Period $other): Periodduration(): ?CarbonIntervalsplitByDays(): PeriodCollection
PeriodCollection
normalize(): self- Merges overlapping/adjacent periods.subtractPeriod(Period $p): selfunion(PeriodCollection $others): selfdiff(PeriodCollection $others): selfintersect(PeriodCollection $others): selfsymmetricDifference(PeriodCollection $others): selfgaps(Period $boundary): selftotalDuration(): ?CarbonInterval
Detailed API Reference
ChronoSet\Period
new Period($start, $end)
Creates a new period instance. null represents infinity.
$p = new Period('2023-01-01', '2023-01-02'); $forever = new Period(null, null); // Infinite duration
static make($start, $end): self
Static factory method to create a new period.
$p = Period::make('2023-01-01', '2023-02-01');
durationIn(string $unit = 'hours'): float
Calculates the duration of the period in the specified unit (seconds, minutes, hours, days). Returns INF if infinite.
$p = new Period('09:00', '10:30'); echo $p->durationIn('minutes'); // 90.0 echo $p->durationIn('hours'); // 1.5
duration(): ?CarbonInterval
Returns the duration as a CarbonInterval object, or null if infinite.
$p = new Period('09:00', '10:30'); echo $p->duration()->forHumans(); // "1 hour 30 minutes"
overlaps(Period $other): bool
Determines if this period strictly overlaps with another. Touching boundaries (e.g., end == start) is NOT considered an overlap.
Case 1 (True):
A: [=========]
B: [=========]
Case 2 (False - Touching):
A: [=========]
B: [=========]
$a = new Period('09:00', '10:00'); $b = new Period('09:30', '10:30'); $c = new Period('10:00', '11:00'); $a->overlaps($b); // true $a->overlaps($c); // false (just touches)
overlapsOrTouches(Period $other): bool
Determines if periods overlap OR if they just touch boundaries. Useful for finding periods that can be merged.
$a->overlapsOrTouches($c); // true
containsDate($date): bool
Checks if a specific date/time falls within the period (inclusive).
$p = new Period('09:00', '10:00'); $p->containsDate('09:30'); // true $p->containsDate('10:00'); // true
containsPeriod(Period $other): bool
Checks if this period completely encloses another period.
$parent = new Period('09:00', '12:00'); $child = new Period('10:00', '11:00'); $parent->containsPeriod($child); // true
intersect(Period $other): ?Period
Returns a new Period representing the shared time interval, or null if no overlap exists.
A: [=========]
B: [=========]
Result: [=]
$a = new Period('09:00', '11:00'); $b = new Period('10:00', '12:00'); $intersection = $a->intersect($b); // Result: Period('10:00', '11:00')
merge(Period $other): Period
Combines two overlapping or touching periods into a single continuous period. Throws detailed exception if they are disjoint.
A: [=========]
B: [=========]
Result: [=============]
$a = new Period('09:00', '10:00'); $b = new Period('10:00', '11:00'); $merged = $a->merge($b); // Result: Period('09:00', '11:00')
subtract(Period $other): Period[]
Removes a period from the current one. Returns an array containing 0, 1, or 2 resulting periods.
Case 1: Punching a hole
A: [==================]
B: [======]
Result: [===] [===]
$base = new Period('09:00', '12:00'); $remove = new Period('10:00', '11:00'); $result = $base->subtract($remove); // Result: [Period('09:00', '10:00'), Period('11:00', '12:00')]
splitByDays(): PeriodCollection
Splits a multi-day period into daily 00:00-23:59 chunks.
$trip = new Period('2023-01-01 10:00', '2023-01-03 15:00'); $days = $trip->splitByDays(); // Result: // 1. 10:00 -> 23:59:59 (Day 1) // 2. 00:00 -> 23:59:59 (Day 2) // 3. 00:00 -> 15:00:00 (Day 3)
ChronoSet\PeriodCollection
normalize(): self
Merges all overlapping or contiguous periods in the collection into the minimal set of disjoint periods.
Input:
Period A: [=======]
Period B: [=======]
Period C: [===]
Result: [================]
$col = new PeriodCollection([ new Period('09:00', '10:00'), new Period('09:30', '11:00') ]); $norm = $col->normalize(); // Result: [Period('09:00', '11:00')]
subtractPeriod(Period $p): self
Subtracts a single Period from every period in the collection.
$col = new PeriodCollection([new Period('09:00', '12:00')]); $lunch = new Period('12:00', '13:00'); // No overlap example $break = new Period('10:00', '10:15'); $col->subtractPeriod($break); // Result: [09:00->10:00, 10:15->12:00]
union($others): self
Adds another PeriodCollection (or array of periods) to this one and normalizes the result.
Coll A: [=====] [=====]
Coll B: [=====]
Result: [===================]
$morning = new PeriodCollection([new Period('09:00', '12:00')]); $afternoon = new PeriodCollection([new Period('13:00', '17:00')]); $workDay = $morning->union($afternoon); // Result: [09:00->12:00, 13:00->17:00]
diff($others): self
Calculates A - B. Preserves time in A that is NOT in B.
Coll A: [===================]
Coll B: [=====]
Result: [=====] [=====]
$available = new PeriodCollection([new Period('09:00', '17:00')]); $meetings = new PeriodCollection([new Period('10:00', '11:00')]); $freeTime = $available->diff($meetings); // Result: [09:00->10:00, 11:00->17:00]
intersect($others): self
Calculates A AND B. Keeps only time present in BOTH collections.
Coll A: [=====] [=====]
Coll B: [=========]
Result: [=] [=]
$alice = new PeriodCollection([new Period('09:00', '12:00')]); $bob = new PeriodCollection([new Period('11:00', '15:00')]); $common = $alice->intersect($bob); // Result: [11:00->12:00]
symmetricDifference($others): self
Calculates A XOR B. Keeps time present in A OR B, but NOT both.
Coll A: [=======]
Coll B: [=======]
Result: [===] [===]
$a = new PeriodCollection([new Period('09:00', '11:00')]); $b = new PeriodCollection([new Period('10:00', '12:00')]); $xor = $a->symmetricDifference($b); // Result: [09:00->10:00, 11:00->12:00] // The overlap (10-11) is removed.
gaps(Period $boundary): self
Finds availability within a specific boundary period. Validates which parts of $boundary are NOT covered by this collection.
$schedule = new PeriodCollection([new Period('10:00', '12:00')]); $day = new Period('08:00', '18:00'); $free = $schedule->gaps($day); // Result: [08:00->10:00, 12:00->18:00]
totalDuration(): ?CarbonInterval
Sums the length of all periods in the collection. Returns null if any period is infinite.
$col = new PeriodCollection([ new Period('09:00', '10:00'), // 1h new Period('14:00', '16:00') // 2h ]); echo $col->totalDuration()->forHumans(); // "3 hours"
Contributing
Contributions are welcome! Please submit a Pull Request.
License
The MIT License (MIT). Please see License File for more information.