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

dev-master 2025-12-21 20:53 UTC

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 null as "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 at 10:00:00 does NOT conflict with one starting at 10: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]).
  • null represents 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): bool
  • overlapsOrTouches(Period $other): bool
  • containsDate(Carbon $date): bool
  • containsPeriod(Period $other): bool
  • subtract(Period $other): Period[]
  • intersect(Period $other): ?Period
  • merge(Period $other): Period
  • duration(): ?CarbonInterval
  • splitByDays(): PeriodCollection

PeriodCollection

  • normalize(): self - Merges overlapping/adjacent periods.
  • subtractPeriod(Period $p): self
  • union(PeriodCollection $others): self
  • diff(PeriodCollection $others): self
  • intersect(PeriodCollection $others): self
  • symmetricDifference(PeriodCollection $others): self
  • gaps(Period $boundary): self
  • totalDuration(): ?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.