Giter Site home page Giter Site logo

period's Introduction

Complex period comparisons

Latest Version on Packagist Quality Score Total Downloads

This package adds support for comparing multiple dates with each other. You can calculate the overlaps and differences between n-amount of periods, as well as some more basic comparisons between two periods.

Periods can be constructed from any type of DateTime implementation, making this package compatible with custom DateTime implementations like Carbon (see cmixin/enhanced-period to convert directly from and to CarbonPeriod).

Periods are always immutable, there's never the worry about your input dates being changed.

Support us

We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.

Installation

You can install the package via composer:

composer require spatie/period

Usage

Creating periods

You're encouraged to create periods using their static constructor:

$period = Period::make('2021-01-01', '2021-01-31');

You can manually construct a period, but you'll need to manually provide its precision and boundaries. Using Period::make, the default precision (Precision::DAY()) and default boundaries (Boundaries::EXCLUDE_NONE()) are used.

Before discussing the API provided by this package, it's important to understand both how precision and boundaries are used.

Precision

Date precision is of utmost importance if you want to reliably compare two periods. The following example:

Given two periods: [2021-01-01, 2021-01-15] and [2021-01-15, 2021-01-31]; do they overlap?

At first glance the answer is "yes": they overlap on 2021-01-15. But what if the first period ends at 2021-01-15 10:00:00, while the second starts at 2021-01-15 15:00:00? Now they don't anymore!

This is why this package requires you to specify a precision with each period. Only periods with the same precision can be compared.

A more in-depth explanation on why precision is so important can be found here. A period's precision can be specified when constructing that period:

Period::make('2021-01-01', '2021-02-01', Precision::DAY());

The default precision is set on days. These are the available precision options:

Precision::YEAR()
Precision::MONTH()
Precision::DAY()
Precision::HOUR()
Precision::MINUTE()
Precision::SECOND()

Boundaries

By default, period comparisons are done with included boundaries. This means that these two periods overlap:

$a = Period::make('2021-01-01', '2021-02-01');
$b = Period::make('2021-02-01', '2021-02-28');

$a->overlapsWith($b); // true

The length of a period will also include both boundaries:

$a = Period::make('2021-01-01', '2021-01-31');

$a->length(); // 31

It's possible to override the boundary behaviour:

$a = Period::make('2021-01-01', '2021-02-01', boundaries: Boundaries::EXCLUDE_END());
$b = Period::make('2021-02-01', '2021-02-28', boundaries: Boundaries::EXCLUDE_END());

$a->overlapsWith($b); // false

There are four types of boundary exclusion:

Boundaries::EXCLUDE_NONE();
Boundaries::EXCLUDE_START();
Boundaries::EXCLUDE_END();
Boundaries::EXCLUDE_ALL();

Reference

The Period class offers a rich API to interact and compare with other periods and collections of periods. Take into account that only periods with the same precision can be compared:

  • startsBefore(DateTimeInterface $date): bool: whether a period starts before a given date.
  • startsBeforeOrAt(DateTimeInterface $date): bool: whether a period starts before or at a given date.
  • startsAfter(DateTimeInterface $date): bool: whether a period starts after a given date.
  • startsAfterOrAt(DateTimeInterface $date): bool: whether a period starts after or at a given date.
  • startsAt(DateTimeInterface $date): bool: whether a period starts at a given date.
  • endsBefore(DateTimeInterface $date): bool: whether a period ends before a given date.
  • endsBeforeOrAt(DateTimeInterface $date): bool: whether a period end before or at a given date.
  • endsAfter(DateTimeInterface $date): bool: whether a period ends after a given date.
  • endsAfterOrAt(DateTimeInterface $date): bool: whether a period end after or at a given date.
  • endsAt(DateTimeInterface $date): bool: whether a period starts ends at a given date.
  • overlapsWith(Period $period): bool: whether a period overlaps with another period.
  • touchesWith(Period $other): bool: whether a period touches with another period.
  • contains(DateTimeInterface|Period $other): bool: whether a period contains another period or a single date.
  • equals(Period $period): bool: whether a period equals another period.

On top of comparisons, the Period class also offers a bunch of operations:

overlap(Period ...$others): ?static

Overlaps two or more periods on each other. The resulting period will be the union of all other periods combined.

overlapAny(Period ...$others): PeriodCollection

Overlaps two or more periods on each other. Whenever two or more periods overlap, that overlapping period is added to a collection which will be returned as the final result.

subtract(Period ...$others): PeriodCollection

Subtracts one or more periods from the main period. This is the inverse operation of overlap.

gap(Period $period): ?static

Gets the gap between two periods, or 0 if the periods overlap.

diffSymmetric(Period $other): PeriodCollection

Performs a symmetric diff between two periods.

renew(): static

Renew the current period, creating a new period with the same length that happens after the current period.


Next, the Period class also has some getters:

  • isStartIncluded(): bool
  • isStartExcluded(): bool
  • isEndIncluded(): bool
  • isEndExcluded(): bool
  • start(): DateTimeImmutable
  • includedStart(): DateTimeImmutable
  • end(): DateTimeImmutable
  • includedEnd(): DateTimeImmutable
  • ceilingEnd(Precision::SECOND): DateTimeImmutable
  • length(): int
  • duration(): PeriodDuration
  • precision(): Precision
  • boundaries(): Boundaries

The PeriodCollection class represents a collection of periods and has some useful methods on its own:

overlapAll(PeriodCollection ...$others): PeriodCollection

Overlaps all collection periods on each other.

subtract(PeriodCollection|Period ...$others): PeriodCollection

Subtracts a period or a collection of periods from a period collection.

boundaries(): ?Period

Creates a new period representing the outer boundaries of the collection.

gaps(): static

Gives the gaps for all periods within this collection.

intersect(Period $intersection): static

Intersects given period with every period within a collection. The result is a new collection of overlapping periods between given period and every period in the collection. When there's no overlap, the original period is discarded.

union(): static

Merges all periods in collection with overlapping ranges.


Finally, there are a few utility methods available on PeriodCollection as well:

  • add(Period ...$periods): static
  • map(Closure $closure): static:
  • reduce(Closure $closure, $initial = null): mixed:
  • filter(Closure $closure): static:
  • isEmpty(): bool:

Compatibility

You can construct a Period from any type of DateTime object such as Carbon:

Period::make(Carbon::make('2021-01-01'), Carbon::make('2021-01-02'));

Note that as soon as a period is constructed, all further operations on it are immutable. There's never the danger of changing the input dates.

You can iterate a Period like a regular DatePeriod with the precision specified on creation:

$datePeriod = Period::make(Carbon::make('2021-01-01'), Carbon::make('2021-01-31'));

foreach ($datePeriod as $date) {
    /** @var DateTimeImmutable $date */
    // 2021-01-01
    // 2021-01-02
    // ...
    // (31 iterations)
}

$timePeriod = Period::make(Carbon::make('2021-01-01 00:00:00'), Carbon::make('2021-01-01 23:59:59'), Precision::HOUR());

foreach ($timePeriod as $time) {
    /** @var DateTimeImmutable $time */
    // 2021-01-01 00:00:00
    // 2021-01-01 01:00:00
    // ...
    // (24 iterations)
}

Visualizing periods

You can visualize one or more Period objects as well as PeriodCollection objects to see how they related to one another:

$visualizer = new Visualizer(["width" => 27]);

$visualizer->visualize([
    "A" => Period::make('2021-01-01', '2021-01-31'),
    "B" => Period::make('2021-02-10', '2021-02-20'),
    "C" => Period::make('2021-03-01', '2021-03-31'),
    "D" => Period::make('2021-01-20', '2021-03-10'),
    "OVERLAP" => new PeriodCollection(
        Period::make('2021-01-20', '2021-01-31'),
        Period::make('2021-02-10', '2021-02-20'),
        Period::make('2021-03-01', '2021-03-10')
    ),
]);

And visualize will return the following string:

A          [========]
B                      [==]
C                           [========]
D               [==============]
OVERLAP         [===]  [==] [==]

The visualizer has a configurable width provided upon creation which will control the bounds of the displayed periods:

$visualizer = new Visualizer(["width" => 10]);

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you've found a bug regarding security please mail [email protected] instead of using the issue tracker.

Postcardware

You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.

Our address is: Spatie, Kruikstraat 22, 2021 Antwerp, Belgium.

We publish all received postcards on our company website.

Credits

License

The MIT License (MIT). Please see License File for more information.

period's People

Contributors

adrianmrn avatar ahallhognason avatar aliowacom avatar amitmerchant1990 avatar andreybolonin avatar ayoobmh avatar brendt avatar dependabot[bot] avatar drmerk avatar endelwar avatar freekmurze avatar github-actions[bot] avatar jeromegamez avatar jrmessias avatar kylekatarnls avatar kyryl-bogach avatar masterix21 avatar mhyeganeh avatar morrislaptop avatar nielsvanpach avatar patinthehat avatar riasvdv avatar rubenvanassche avatar savamarkovic avatar sergiy-petrov avatar svenluijten avatar thecrypticace avatar winkbrace avatar wlius-support3 avatar yusufonur avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

period's Issues

Get all overlapsed dates from collection

Hello,
I need to get a range of overlapping dates from a collection.

Maybe I missed something in the docs but I can not find something like:

$collection = new PeriodCollection(
    Period::make('2018-01-01', '2018-01-05'),
    Period::make('2018-01-10', '2018-01-15'),
    Period::make('2018-01-20', '2018-01-25'),
    Period::make('2018-01-30', '2018-01-31')
);

$gaps = $collection->gaps();

But getting:
$overlaps = $collection->overlaps();
instead of gaps

Would that be possible ?

Best regards

Scheduling "things"

I'm using https://github.com/spatie/period for complex calendar operations. It's great, but it would be better if I could attach "things". For example, schedule a "vacation" and then be able to retrieve that vacation or vacations from Period or PeriodCollection. That way I would see which of my vacations are overlapping. I realize the inverse is possible: my vacation hasa Period. However that doesn't flow forward into the PeriodCollection of Overlaps... Which vacations are overlapping?

Add periods to a PeriodCollection?

Hello,

Probably stupid, but I can't. How to fill the PeriodCollection via an array of data:

                $collection = new PeriodCollection();
                foreach ($this->array_events as $event) {
                    $collection->Period::make($event['event_start'], $event['event_ended'], Precision::MINUTE());
                }
                $gaps = $collection->gaps();

It doesn't work, error:
Undefined property: Spatie\Period\PeriodCollection::$Period

thank you

Overlap all (n^2)

/*
 * A       [========]
 * B             [==]
 * C                                         [=====]
 * D                                         [===============]
 *
 * OVERLAP       [==]                        [=====]
 */

how would one combine the given overlap,diff,and gap functions to get
such collection as described in the graph above?

Improve PHP 8.1.0 support...

There is a warning if use strings for dates:

$period = Period::make('2021-01-01', '2021-01-31');


PHP Deprecated:  str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated in ****/vendor/spatie/period/src/PeriodFactory.php on line 122

Add a Period->coversDate method

I have use cases for this library but what prevents me from using it is a convenient Period->coversDate method that receives a DateTime object and two mode information for whether the start/end of the period are included or excluded. The mode may also be be combined to a single parameter.

Would this be possible and within the scope of this library?

[2.0] minimum PHP requirement

I'd like to bump the PHP requirement with the next major release. Ideally I'd want it to be 7.3. 7.2 should in any case be the minimum.

How do we feel about this?

Check if Period overlaps any Period's within a PeriodCollection

Is there a way to check if a single Period overlaps any other Period within a PeriodCollection, without having to manually loop over each collection item?

Sorry for asking this question as a issue, is there a discord chat or anything similar regarding this package?

Overlap percentage

Is it possible to calculate a percentage of coverage for one period with second?

Documentation typo

The first example:
$period = Period::make('2021-01-01', '2020-01-31');

Should be:
$period = Period::make('2021-01-01', '2021-01-31');

Ie - the end date was before the start date

touchesWith() does not take precision into consideration

Seems that touchesWith() will always return true if the compared periods are in the same day, even if the times are not really touched and the precision is SECOND.

Example:

$a = Period::make('2018-01-01 06:30:00', '2018-01-01 07:30:00');
$b = Period::make('2018-01-01 09:00:00', '2018-01-01 10:00:00');
$overlap = $a->touchesWith($b);
(true)

This leads to gap() function not detecting the gap for such cases.

Why does diff sometimes return a gap?

I interpreted the Period::diff function to be a "subtraction". So I can remove, say, a week from a month (whether or not they become 2 split periods). However, I noticed a special case in code that confused me and seems to do something else entirely: it returns a gap.

Right at the beginning of the function it checks whether the input was a single, non-overlapping period. In that case it returns the gap between them! But why is that? It's not consistent with the way diffSingle works (which is called in every other scenario). It seems to be an inconsistency.

In fact, diffSingle has a (conflicting) special case: it returns both of the periods. That makes no sense to me whatsoever. Here's some code:

// Period for entire month of January.
$january = new Period(
    new DateTimeImmutable('2021-01-01'),
    new DateTimeImmutable('2021-02-01'),
    Precision::SECOND,
    Boundaries::EXCLUDE_END
);

// Remove something (in this case nothing needs to be removed).
$firstPartOfJanuary = $january->diff(new Period(
    new DateTimeImmutable('2021-03-01'),
    new DateTimeImmutable('2021-04-01'),
    Precision::SECOND,
    Boundaries::EXCLUDE_END
))[0];

// Expected to be January. It turns out to be Februari!
$firstPartOfJanuary->getStart()->format('c'); // 2021-02-01T00:00:00+01:00
$firstPartOfJanuary->getEnd()->format('c');   // 2021-02-28T23:59:59+01:00

/** Doing the exact same thing with diffSingle. */

$partsOfJanuary = $january->diffSingle(new Period(
    new DateTimeImmutable('2021-03-01'),
    new DateTimeImmutable('2021-04-01'),
    Precision::SECOND,
    Boundaries::EXCLUDE_END
));

// Expected to be January, and it is. But it also includes March!
$partsOfJanuary[0]->getStart()->format('c'); // 2021-01-01T00:00:00+01:00
$partsOfJanuary[0]->getEnd()->format('c');   // 2021-02-01T00:00:00+01:00
$partsOfJanuary[1]->getStart()->format('c'); // 2021-03-01T00:00:00+01:00
$partsOfJanuary[1]->getEnd()->format('c');   // 2021-04-01T00:00:00+01:00

/** Doing the exact same thing, but remove a second, bogus, period as well. */

$partsOfJanuary = $january->diff(
    new Period(
        new DateTimeImmutable('2021-03-01'),
        new DateTimeImmutable('2021-04-01'),
        Precision::SECOND,
        Boundaries::EXCLUDE_END
    ),
    new Period(
        new DateTimeImmutable('9998-01-01'),
        new DateTimeImmutable('9999-01-01'),
        Precision::SECOND,
        Boundaries::EXCLUDE_END
    )
)[0];

// Expected to be January, and January *only*. And it is!
$partsOfJanuary[0]->getStart()->format('c'); // 2021-01-01T00:00:00+01:00
$partsOfJanuary[0]->getEnd()->format('c');   // 2021-02-01T00:00:00+01:00
count($partsOfJanuary);                      // 1

Calculate total length of Collection

Following on from Length with precision and the 2.0 discussion I think it would be useful to be able to call ->length() on a PeriodCollection (with an optional precision) to return the total duration of all the periods contained within, combined. This could be aliased to duration.

Proposed

$collection = new PeriodCollection(
    Period::make('2019-01-01', '2019-01-08'),
    Period::make('2019-02-01', '2019-02-08'),
    Period::make('2019-03-08', '2019-03-15')
);

$collection->length(); // 24 (as the precision of the periods' is DAY
$collection->duration; // 24 as this is another name for length
$collection->length(Precision::HOUR); // 576 (24 days of 24 hours)

Current

This can be achieved by using a Laravel Collection and passing a closure to the sum method but I think this should be part of this package.

$collection = collect([
    Period::make('2019-01-01', '2019-01-08', Precision::HOUR),
    Period::make('2019-02-01', '2019-02-08', Precision::HOUR),
    Period::make('2019-03-08', '2019-03-15', Precision::HOUR)
]);

$collection->sum(function ($p) {
    return $p->length();
}); // 24 (as this does not yet take into account the precision)

[2.0] discussion

With the change made to length() in #21, we're required to tag a new major version. So let's discuss what else we want to add.

Possible bug in PeriodCollection gaps()

Given two equal periods with boundaries:

$collection = new PeriodCollection(
    Period::make('2018-01-01', '2018-01-03', null, Boundaries::EXCLUDE_END),
    Period::make('2018-01-01', '2018-01-03', null, Boundaries::EXCLUDE_END)
);

$gaps = $collection->gaps();

I expected gaps to return 0 periods, but I get 1 period instead:

// $gaps->count() == 1
// $period->getStart() == '2018-01-03'
// $period->getEnd() == '2018-01-03'

Is this expected?

Compare multiple periods with precision

Hello. I get Exception\CannotComparePeriods when I use precision parameter with comparing multiple periods. Examples:

// Diff between multiple periods
$a = Period::make('2018-01-05 00:00:00', '2018-01-10 00:00:00', Precision::MINUTE);
$b = Period::make('2018-01-15 00:00:00', '2018-03-01 00:00:00', Precision::MINUTE);
$c = Period::make('2018-01-01 00:00:00', '2018-01-31 00:00:00', Precision::MINUTE);
$diff = $c->diff($a, $b);

or

// Overlap with all periods
$a = Period::make('2018-01-01 00:00:00', '2018-01-31 00:00:00', Precision::MINUTE);
$b = Period::make('2018-01-10 00:00:00', '2018-01-15 00:00:00', Precision::MINUTE);
$c = Period::make('2018-01-10 00:00:00', '2018-01-31 00:00:00', Precision::MINUTE);
$overlap = $a->overlapAll($b, $c);

Created a PeriodCollection, tried ->overlapAll() but it returns the same collection of periods, not overlaps

I've created the following PeriodCollection:

$dayPeriods = new PeriodCollection();

//period one

$period = Period::make('2022-11-16 21:00:00.0 UTC (+00:00)', '2022-11-16 23:00:00.0 UTC (+00:00)', Precision::MINUTE()); (the start and ends are actually carbon times, but ive made them strings for simplification)
 $dayPeriods = $dayPeriods->add($period);

//period two

$period = Period::make('2022-11-16 22:00:00.0 UTC (+00:00)', '2022-11-16 23:00:00.0 UTC (+00:00)', Precision::MINUTE());
 $dayPeriods = $dayPeriods->add($period);

//period three

$period = Period::make('2022-11-16 22:00:00.0 UTC (+00:00)', '2022-11-16 23:00:00.0 UTC (+00:00)', Precision::MINUTE());
 $dayPeriods = $dayPeriods->add($period);

when I run $overlap = $dayPeriods->overlapAll();

The $overlap is simply the same period collection with exactly the same 3 periods I've defined, it doesn't return the overlaps between the 3 periods inside the period collection.

Any idea why this is happening?

Carry over exclusion

At first I thought this was an off by one error, but it's not quite. It's just a nasty gotcha because the boundary exclusions are not carried over. Here's code with the problem.

// Period for entire month of January.
$january = new Period(
    new DateTimeImmutable('2021-01-01'),
    new DateTimeImmutable('2021-02-01'),
    Precision::SECOND,
    Boundaries::EXCLUDE_END
);

// Remove something.
$firstPartOfJanuary = $january->diff(new Period(
    new DateTimeImmutable('2021-01-22'),
    new DateTimeImmutable('2021-02-01'),
    Precision::SECOND,
    Boundaries::EXCLUDE_END
))[0];

// Now I'm expecting end to be 2021-01-22 0:00. It isn't:
$firstPartOfJanuary->getEnd()->format('c'); // 2021-01-21T23:59:59+01:00

// That is because:
$firstPartOfJanuary->endExcluded(); // false

I'm expecting the end to be excluded, as both input periods have EXCLUDE_END...

Exception thrown when getting gaps for a 0-length period with excluded boundaries

Description

If you try and get the gaps for a period collection containing a single 0-width period, an exception is thrown if any boundaries are excluded - start, end, or all

Replication

$period = Period::make(
   start: '2023-07-26 07:00:00',
   end: '2023-07-26 07:00:00',
   boundaries: Boundaries::EXCLUDE_END()
);

PeriodCollection::make($period)->gaps();

Trace

The end time `2023-07-25 00:00:00` is before the start time `2023-07-26 00:00:00`. {"userId":72,"exception":"[object] (Spatie\\Period\\Exceptions\\InvalidPeriod(code: 0): The end time `2023-07-25 00:00:00` is before the start time `2023-07-26 00:00:00`. at C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\Exceptions\\InvalidPeriod.php:12)
[stacktrace]
#0 C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\Period.php(37): Spatie\\Period\\Exceptions\\InvalidPeriod::endBeforeStart(Object(DateTimeImmutable), Object(DateTimeImmutable))
#1 C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\PeriodFactory.php(53): Spatie\\Period\\Period->__construct(Object(DateTimeImmutable), Object(DateTimeImmutable), Object(Spatie\\Period\\Precision), Object(Spatie\\Period\\Boundaries))
#2 C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\Period.php(54): Spatie\\Period\\PeriodFactory::make('Spatie\\\\Period\\\\P...', Object(DateTimeImmutable), Object(DateTimeImmutable), Object(Spatie\\Period\\Precision), Object(Spatie\\Period\\Boundaries), NULL)
#3 C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\PeriodCollection.php(68): Spatie\\Period\\Period::make(Object(DateTimeImmutable), Object(DateTimeImmutable), Object(Spatie\\Period\\Precision), Object(Spatie\\Period\\Boundaries))
#4 C:\\inetpub\\wwwroot\\xxxx\\vendor\\spatie\\period\\src\\PeriodCollection.php(74): Spatie\\Period\\PeriodCollection->boundaries()
#5 C:\\inetpub\\wwwroot\\xxxx\\app\\Http\\Controllers\\yyyy.php(66): Spatie\\Period\\PeriodCollection->gaps()

Visualizer cause error when it have one period

PHP 8.1
When $periods contains one period

        $visualizer = new Visualizer(["width" => 120]);
        $visualizer->visualize($periods);

This cause error in the selected line, because max function argument is integer.
In this line not need spread operator ..., because that function can works with arrays.


class Visualizer {
     ...
    public function visualize(array $blocks): string
    {
        $matrix = $this->matrix($blocks);

>>>>>>> $nameLength = max(...array_map('strlen', array_keys($matrix)));

        $lines = [];

        foreach ($matrix as $name => $row) {
            $lines[] = vsprintf('%s    %s', [
                str_pad($name, $nameLength, ' '),
                $this->toBars($row),
            ]);
        }

        return implode("\n", $lines);
    }
    ...
}

Question: Check per 5 minute interval

Hi,

First of all this is a great package! I've been playing with it and I was wondering if it's possible to check per 5 or 10 minute interval instead of per minute. This limits the numbers of checks for a certain time period if it spans a longer time frame.

Kind regards,

Rogier

Grab properties of Period object

Hi,

Why are all the properties of the Period object protected?

For example, I want to grab the start & end of a Period object.

$period = Period::make('2021-11-01 08:00:00', '2021-11-01 18:00:00', Precision::MINUTE(), Boundaries::EXCLUDE_ALL());

$period->start returns:

Uncaught Error: Cannot access protected property Spatie\Period\Period::$start 

$period->end returns:

Uncaught Error: Cannot access protected property Spatie\Period\Period::$end 

I tried $period->getStart() as well, which returns:

Uncaught Error: Call to undefined method Spatie\Period\Period::getStart()

Why?

Contains period

Might be just brain-fart, but what is the correct way to see if a period is fully contained within a given other period?

A     [===]
B      [=]

Result: true

A     [===]
B    [==]

Result false

A     [===]
B      [==]

Result true

A     [===]
B     [===]

Result true


A     [===]
B        [===]

Result false

I just see a contains function that accepts a DateTimeInterface. Is it

$c = $a->overlapSingle($b);
return $b->equals($c);

or am I missing something with the precision that could go wrong?

Precision::fromString incompatible to Boundaries::fromString

When trying to create a new period, I get a fatal error:

Fatal error: Declaration of Spatie\Period\Precision::fromString(string $string): Spatie\Period\Precision must be compatible with Spatie\Period\Boundaries::fromString(string $startBoundary, string $endBoundary)

The Period creation is done with the following code:
Period::make(new \DateTimeImmutable('now'), new \DateTimeImmutable('now +1 day'), Precision::SECOND());

Am i missing something here?

Get available time slots between periods

Is this package sufficient enough to calculate timeslots between certain periods?

Let's say my business opens from 08:00 to 18:00 on a specific day, with a work break at 13:00 to 14:00.

This makes the following periods:

Period::make('2020-07-27 08:00', '2020-07-27 13:00', Precision::MINUTE);
Period::make('2020-07-27 14:00', '2020-07-27 18:00', Precision::MINUTE);

Now having those periods, how would I be able to generate an array of (e.g.) 15 minutes timeslots, that are available/gasps?

Add/subtract collections..

I've just picked this library up and it's proving really useful. Could you tell me if/how it's possible to join periods? As in if I add a period to a period/collection, it would create a flattened/normalized collection of the two.

/*
 * A              [======]
 * B                   [=======]
 *
 * JOIN           [============]
 *
 * C         [===]
 * D                          [===]
 *
 * JOIN      [===]            [===]
 */

$a = Period::make('2018-01-01', '2018-01-15');
$b = Period::make('2018-01-10', '2018-01-25');

$joinCollection = $a->join($b);

$c = Period::make('2018-01-01', '2018-01-05');
$d = Period::make('2018-01-10', '2018-01-15');

$joinCollection = $c->join($d);

If I add a period to a collection it just holds the period even it they overlap.

I suspect it's a technique that I haven't figured out. A pointer would be very welcome.

Many thanks,

Jerry

Support for Eloquent

Hello, thank you for this package, really useful! I wonder if you are willing to support eloquent scopes to perform similar results? My use case would be to check if a booking is overlapping a given period.

return new class extends Migration 
{
  public function up(): void
  {
    Schema::create("bookings", function (Blueprint $table) {
      $table->id();
      $table->dateTime("starts_at");
      $table->dateTime("ends_at");
      $table->timestamps();
    });
  }
};
use App\Models\Booking;
use Spatie\Period\Period;

$period = Period::make("2024-01-24 10:00:00", "2024-01-24 19:00:00");

$bookings = Booking::overlapping($period)->get();

Hours, minutes and seconds are reset from provided period

Hello,

When creating a Period like this:

Period::make('10:00', '11:00', Precision::MINUTE(), Boundaries::EXCLUDE_END(), 'H:i');

Hours, minutes and seconds are cleared in the PeriodFactory.php lines 107-109:

if (! str_contains($format, ' ')) {
    $dateTime = $dateTime->setTime(0, 0, 0);
}

So this is wrong:

$period1 = Period::make('10:00', '11:00', Precision::MINUTE(), Boundaries::EXCLUDE_END(), 'H:i');
$period2 = Period::make('10:00', '11:00', Precision::MINUTE(), Boundaries::EXCLUDE_END(), 'H:i');

$period1->overlapsWith($period2); // false

Is there a reason why times are being cleared? Could we not do it depending on the precision instead of expecting a date format + time format?

Temporal (or not) fix:

Period::make(Date::now()->setTimeFromTimeString('10:00'), Date::now()->setTimeFromTimeString('11:00'), Precision::MINUTE(), Boundaries::EXCLUDE_END());

Multiple gaps support

We want to calculate multiple gaps like so:

/*
 * A                   [====]
 * B                               [========]
 * C         [=====]
 * D                                             [====]
 *
 * GAPS             [=]      [====]          [==]
 */

The method should be called gaps, accept n-amount of periods and calculate all the gaps.

This can be done by creating a period with the most left and right boundaries, and diffing that period against all others.

Visualization bugs


ℹ️ Apologies for the brief "issue dump", but my time is currently limited and I wanted to report this already.
Maybe I'll find time to provide details and/or PRs.

Also, thanks for this great package!


I found some bugs in the visualizer. My setup is the following:

use Spatie\Period\Visualizer;
use Spatie\Period\Period;

$v = new Visualizer(['width' => 10]);

$periods = [ /* ... */ ];

echo "\n" . $v->visualize($periods) . "\n";

1. Visualization of less than 2 periods

⚠️ max(): When only one parameter is given, it must be an array

// CASE: No periods
$periods = [];

// CASE: Only one period
$periods = [
  Period::make(now(), now()->addDay())
];

2. Visualization of period spanning no more than one day

⚠️ Division by zero

$periods = [
  Period::make(now(), now()),
  Period::make(now(), now())
];

3. Single-day periods sometimes not rendered

Single-day periods not renderd

$periods = [
     \Spatie\Period\Period::make(now(), now()),
     \Spatie\Period\Period::make(now(), now()->addDay()),
     \Spatie\Period\Period::make(now(), now()),
];
0              
1    [========]
2  

Single-day periods rendered depending on the width

Varying the visualization width seems to affect the outcome.

Assume the following periods:

$periods = [
    Period::make(now(), now()),
    Period::make(now()->addDay(), now()->addDay()),
    Period::make(now()->subDays(38), now()->addDays(2)),
];

Width: 50 (all periods rendered)

$v = new Visualizer(['width' => 50]);
0                                                   =  
1                                                    = 
2    [================================================]

Width: 60 (one period "gone")

$v = new Visualizer(['width' => 60]);
0                                                                
1                                                              = 
2    [==========================================================]

ISO-8601 time interval support

It'd be nice to be able to translate to/from the ISO-8601 time interval standard.

I'm surprised how little support there is in PHP for this as it's a nice, concise way of expressing a period of time. thephpleague's package has partial support but doesn't allow for using durations in the start (P1M/2022-02-01T00:00:00Z) or end (2022-01-01T00:00:00Z/P1M) nor for using a condensed datetime for the end such as 2022-01-01T00:00:00Z/15 which expands to 2022-01-01T00:00:00Z/2022-01-15T00:00:00Z.

The standard doesn't allow for transferring bounds but they could be defined in a parameter.

Including or excluding boundaries

Right now, the boundaries of a period are always included in calculations. We should have a discussion about what the desired default behaviour should be, whether we want to make this behaviour configurable, and how.

Add debug helper

According to the first example of the doc :

$a = Period::make('2018-01-01', '2018-01-31');
$b = Period::make('2018-02-10', '2018-02-20');
$c = Period::make('2018-03-01', '2018-03-31');
$current = Period::make('2018-01-20', '2018-03-10');
$overlaps = $current->overlap($a, $b, $c);

Why not implementing __string() that would make a nice representation of the result like in your doc :

echo $overlaps; // ouput:

A       [========]
B                    [==]
C                            [=====]
CURRENT        [===============]
OVERLAP        [=]   [==]    [=]

It could be a very convenient way for debugging.

v1 planned ?

Hi ! Your package looks really cool, thanks for it !

This package is still a work in progress.

I'd need something like it in my company's projects. Is there any plan for a first stable release ? If you open some tickets, we could help you for releasing it faster.

[2.0] Period::overlap methods

Period has three ways of overlapping:

Period::overlapSingle(Period $period): ?Period
Period::overlap(Period ...$periods): PeriodCollection
Period::overlapAll(Period ...$periods): ?Period

The naming here is very confusing, and I'd like this to improved with v2.

Period::overlapSingle will determine the overlap between two periods, and return a single Period or null.

A        [===========]
B            [============]

OVERLAP      [=======]

The more obvious name would simply be overlap.

Period::overlap allows for overlapping multiple periods on each other:

A       [========]
B                   [==]
C                           [=====]
D              [===============]

OVERLAP        [=]   [==]   [==]

This name is confusing, I can think of two improvements:

  • Period::overlapAny(Period ...$periods)
  • PeriodCollection::overlapAny()

They would have a subtle difference: Period::overlapAny(Period ...$periods) would compare each period to the "master period", while PeriodCollection::overlapAny() would compare all periods in the collection to each other.

Period::overlapAll is also confusing, it does the following:

A              [============]
B                   [==]
C                   [=======]

OVERLAP             [==]

I believe the correct solution would be to move this functionality to PeriodCollection::overlapAll() — although I think we should come up with a better name.

Again we could keep the shorthand on Period.

Another issue with Period::overlapAll is that it can only return one period, while this could also be possible with collection

COLLECTION A              [===============================]
COLLECTION B                   [==]             [=======]
COLLECTION C                   [=======]      [======]

COLLECTION OVERLAP             [==]             [====]

Is this package suitable for appointment systems?

Hi,

I had some questions regarding the Spatie/Period composer package.

Is this package suitable to use for an appointment system?

I know you can generate a list of periods using the PeriodCollection class, so if I add all the existing appointments of a single day to a PeriodCollection, is there a way for me to search for available periods/time slots, using gaps I guess?

Really looking forward to your response.

Thanks in advance,

Amir

Help about adding new period in a range

I have a problem, I tried to achieve this with this package, but I need help:

These are periods of dates:

$periods = new PeriodCollection(
    Period::make('2019-01-01', '2019-01-02', null, Boundaries::EXCLUDE_NONE),
    Period::make('2019-01-03', '2019-01-05', null, Boundaries::EXCLUDE_NONE),
    Period::make('2019-01-06', '2019-01-13', null, Boundaries::EXCLUDE_NONE),
    Period::make('2019-01-14', '2019-01-20', null, Boundaries::EXCLUDE_NONE),
    Period::make('2019-01-21', '2019-01-25', null, Boundaries::EXCLUDE_NONE),
    Period::make('2019-01-26', '2019-02-10', null, Boundaries::EXCLUDE_NONE)
)

And I want to add this new period:

$c = Period::make('2019-01-03', '2019-01-15', null,Boundaries::EXCLUDE_NONE )

But after calculation was made I want to get these results:

[
        ['start_time' => '2019-01-01', 'end_time' => '2019-01-02'],
        ['start_time' => '2019-01-03', 'end_time' => '2019-01-15'],
        ['start_time' => '2019-01-16', 'end_time' => '2019-01-20'],
        ['start_time' => '2019-01-16', 'end_time' => '2019-01-20'],
        ['start_time' => '2019-01-21', 'end_time' => '2019-01-25'],
        ['start_time' => '2019-01-26', 'end_time' => '2019-02-10'],
]

Could you help to achieve this?

I tried to do this with:

$clearPeriods = $periods->boundaries()->diff($c);
$clearPeriods = $clearPeriods->add($c);

And this is fine when you have only 3 periods, but with multiple, I will have to calculate range of periods, so is there a better way to do this?

PeriodCollection with null element after diff

Hi, if I calculate diff between two not intersected periods and try to loop:
Return value of Spatie\Period\PeriodCollection::current() must be an instance of Spatie\Period\Period, null returned

$dayPeriod = Period::make('2019-02-01 15:00:00', '2019-02-01 20:00:00', Precision::MINUTE);
$events = new PeriodCollection(
    // $dayPeriod does not contain this event
    Period::make('2019-02-01 10:00:00', '2019-02-01 14:00:00', Precision::MINUTE)
);
$periods = $dayPeriod->diff(...$events);

foreach ($periods as $period) { //throw TypeError
    var_dump($period);
}

Getting the periods that are not overlapped

Hi there,

I've just picked up this package. It seems pretty good. I've ran into an issue where I want the opposite of overlap.

overlap rightly returns what time out of a list of periods overlap with the current period. I want to be able to get what time is not overlapped.

See visual examples below 👇

Overlap
/*
 * A       [========]
 * B                    [==]
 * C                            [=====]
 * CURRENT        [===============]
 *
 * OVERLAP        [=]   [==]    [=]
 */
Clear(?)
/*
 * A       [========]
 * B                    [==]
 * C                            [=====]
 * CURRENT        [===============]
 *
 * CLEAR  [=======]                [===]
 */

What would be the best way to do this, if already implemented?

Periods without end

This might seem like an edge case, but it's one we have to deal with often: periods that never end. It would be nice if this package supported them.

Returns PeriodCollection with null element

When I try to use PeriodCollection's function isEmpty this return false, but when i dump

screen shot 2019-01-08 at 12 20 40 pm

I have that:

screen shot 2019-01-08 at 12 20 32 pm

So method isEmpty works not right, because array only with one null element.

Here in my code:
screen shot 2019-01-08 at 12 25 25 pm

Can you fix this issue?

Period Collection to accept array of periods

I think it would be really nice if we could pass an array or collection of periods into a Period Collection.

Rather than having to do

$collection = new PeriodCollection(
    Period::make('2018-01-01', '2018-01-05'),
    Period::make('2018-01-10', '2018-01-15'),
    Period::make('2018-01-20', '2018-01-25'),
    Period::make('2018-01-30', '2018-01-31')
);

We could do something like this:

$event_periods = $events->map(function($event) {
     return Period::make($event->start, $event->end);
});
$event_period_collection = new PeriodCollection($event_periods);

Let me know if that doesn't make sense.

[2.0] Configure Date class to use

Hi guys,

Periods are always immutable, there's never the worry about your input dates being changed.

This seems a bit too much opinionated to me for a Period library.

I think it should be an agnostic DateTimeInterface in typings (so stay flexible with inheritance ing PHP < 7.4) as in https://github.com/spatie/opening-hours

And I can propose a PR to have a class customization like in CarbonPeriod:
https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/CarbonPeriod.php#L228

It means you would have DateTimeImmutable as the default class, but other classes could be set as long as they implements DateTimeInterface so users could choose if getStart() and similar methods would return DateTimeImmutable, DateTime, Carbon, Chronos etc. objects.

CarbonPeriod has both an IMMUTABLE flag option to switch Carbon/CarbonImmutable and a ->setDateClass() method to switch to any DateTimeInterface sub-class. And any class that extends CarbonPeriod can easily change the default class by overriding the property default value.

Are you interested in a similar way to choose the date class for Spatie\Period\Period?

Length with precision

The length method always returns the length in days, instead of the precision of the period. An oversight by me. We're going to fix it and release a new major version.

My suggestion is the following:

  • Change length to return the length with the precision of that period,
  • Add helper methods lengthInDays, lengthInYears,...

One more thing I'm wondering about: should length return a simple integer, or should it return some kind of PeriodLength object, which also contains the precision?

Does this package only compares the dates, or does it also use the timestamp?

Not sure based on the documentation. Does this package also work with time stamps, or does it only compare the dates? The examples only show dates, but since the period accepts DateTime objects, it might also work with time.

For example, would this work?

$a = Period::make('2020-01-01 12:00:00', '2020-01-01 14:00:00');
$a = Period::make('2020-01-01 16:00:00', '2020-01-01 18:00:00');

$overlap = $a->gap($b); // Period('2020-01-01 14:00:00', '2020-01-01 16:00:00')

Thanks!

Period roundDate() does not keep date's timezone

When creating a new Period, with timezoned start, end, the period object ends up with start, end in UTC always.

  $start = new \DateTimeImmutable('2000-01-01', new \DateTimeZone('Europe/London'));
  $end = new \DateTimeImmutable('2000-02-01', new \DateTimeZone('Europe/London'));
  $period = new \Spatie\Period\Period($start, $end);
  var_dump($period->getStart());

result:

object(DateTimeImmutable)#3257 (3) { ["date"]=> string(26) "2000-01-01 00:00:00.000000" ["timezone_type"]=> int(3) ["timezone"]=> string(3) "UTC" }

Suggestion solution to change this:

implode(' ', [$year, $month, $day, $hour, $minute, $second])

to this:

 implode(' ', [$year, $month, $day, $hour, $minute, $second]), $date->getTimezone()

Precision - Overlap by X minutes

Would a overlap by X minutes be a good idea?

So this

2020-01-01 15:15:00 would overlap 2020-01-01 15:00:00

if Precision::MINUTE(15) was set, great for calendars I think.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.