thephpleague / period

PHP's time range API

Home Page:https://period.thephpleague.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Introduce high-level "timeline" abstraction

slavafomin opened this issue · comments

Hello!

Thank you for this great library! It looks very promising.
I was using my own custom solution for this exact purpose, but it looks like the better idea wood be to switch to this library instead.

However, it's lacking some extra functionality that I (and probably other developers) require. Please consider the following use case:

There are users in the system who can purchase subscription for the service. Each subscription can be represented as a Period instance with start and end dates. Subscription starts on the date of purchase or exacly when previous subscription ends (overlapping or "abuts" periods). Sometimes user can purchase subscription to use in the future (there is a gap between subscriptions). So in the end we have a Timeline entity that consists of overlapping and isolated periods and we have to answer this question: when the current continous period starts/ends?. What if we will need to merge all overlapping periods on a timeline to get only continous periods for better visualization? What if we need to sort all periods on a timeline?

I think we can introduce a Timeline class that will allow us to work with such abstraction.

timeline

The API could be the following:

$timeline = new Timeline([$periodA, $periodB, ..., $periodN]);

$timeline->addPeriod($periodC);

// Timeline will sort all periods added to it internally to always maintain a chronological order.

// Timeline instance can be easilly iterated in chronological order
foreach ($timeline as $period) {
    // output a period for visualization
}

// Will return a new Timeline with all overlapping periods merged together.
$mergedTimeline = $timeline->merge();

// Merge method will support a tolerance as a DateInerval instance
$mergedTimeline = $timeline->merge(new \DateInterval('P1D'));
// If two periods are close enough (the gap between them is less than tolerance) they will be also merged together

// Will return periods from the Timeline that includes the specified date.
$periods = $timeline->getPeriodsForDate(new \DateTime());

// Will merge the periods around the specified date (with respect to tolerance) and return a merged period.
// Actually this can be achieved by calling merge() and getPeriodsForDate() inernally.
// Or a better optimized method can be used.
$period = $timeline->getContinousPeriodForDate(new \DateTime, $tolerance);

Does it make sense?

Thank you!


mergePeriods

Here's the function that can be used to merge periods on the timeline with respect to tolerance. All periods MUST be chronologically ordered first. It implements a linear algorithm.

    /**
     * Merges intersecting time periods inside the collection.
     *
     * @param \DateInterval $tolerance
     *
     * @return TimePeriodCollection
     */
    public function mergePeriods(\DateInterval $tolerance = null)
    {
        // Making a shallow copy of the list.
        $list = $this->list;

        $count = count($list);

        for ($i = 0; $i < ($count - 1); $i++) {

            $timePeriodA = $list[$i];
            $timePeriodB = $list[$i + 1];

            $firstDateEnd = $timePeriodA->getDateEnd();

            // Adding tolerance to the right border of the first period
            // if it's specified.
            if ($tolerance) {
                // We have to clone the date in order to
                // preserve original instance's value.
                $firstDateEnd = clone $firstDateEnd;
                $firstDateEnd->add($tolerance);
            }

            // Periods are intersecting.
            if ($timePeriodB->getDateStart() <= $firstDateEnd) {

                // Finding an end date for a merged period.
                // Second period can be inside of the first one.
                $newDateEnd = max(
                    $timePeriodA->getDateEnd(),
                    $timePeriodB->getDateEnd()
                );

                $mergedPeriod = new TimePeriod(
                    $timePeriodA->getDateStart(),
                    $newDateEnd
                );

                // Removing first period from the list.
                unset($list[$i]);

                // Adding merged period as a second one.
                // It will become first period in the next iteration.
                $list[$i + 1] = $mergedPeriod;
            }
        }

        // Returning a new instance of the collection with the merged list.
        return new self($list);
    }

I've tried to consider all possible use cases and make it as ellegant as possible.

Hi,

thanks for using the Period package. I've read you features request and I do understand and share part of your concern however I think that this is out of scope for the package. The reason is that what you are suggesting in currently achievable using the composition pattern.

While reading your suggestion I had already thought of 2 possible ways to creaet your class:

1°) What your call a timeline can be seen as a Calendar. So nothing prevents you from using one of the currently available calendar package on Github (granted some don't use the Period package but I think any good Calendar package ought to provide the feature you are requesting).

2°) If the Calendar solution does not correctly respond to your need then you can think of your class as a Collection of Period object to which you add various filters to manipulate. In the perspective I would recommend using a adapter class around Laravel Collection class for instance. By simply doing that you would get your Timeline class up and running in a couple of minute.

Both solution could use the currently available class without having to create a TimeLine/Calender class.

The Bottom line is that the Period package provide low level usefulness to manipulate Time range as value objects. It is a starting block from which you can build more complex and powerful classes and/or packages. That is why I would avoid adding more sugar to the package so that anyone can use it freely in his/her codebase.

Hope my explanation makes sens

Hello @nyamsprod! Thank you for your reply.

Probably you are right — this functionality belongs to another package.

I would've created it myself, but I'm migrating from PHP to Node.js and my time is very limitted. Maybe you or someone else from PHP league will be able to implement it in the future. I think this functionality is useful in many projects.

Thanks again )