Tuesday, September 15, 2015

Period: Time range API for php

Date/time programming is one of the tricky aspects of software development. Although inherently not complex in itself, coding date/time algorithms can be a subtle source of bugs. Especially in web development a feature such as payment subscription processing that ranges from days to weeks to months can get complex quickly. Also such kind of scenarios require additional features like auto renewal, scheduled email alerts to subscribers etc. Such kind of features require good date/time handling algorithms and libraries that handle such chores are always welcome.

One such library I encountered recently is Period. The library helps resolve many recurrent issues around time range selection and usage.
Period lets you easily works with time ranges – creating a time range, comparing between ranges, checking if a range falls withing another range etc.

Installation

You need PHP >= 5.3.0 to use Period but the latest stable version of PHP is recommended. Period is available on Packagist and can be installed using Composer.
composer require league/period
You can also install the library as a standalone by downloading the zip an including it in your project.

Creating a Period

To start using the Period class the first task you have to do is create a Period object. The default way to instantiate a new Period is with the Period constructor –
Period::__construct($start, $end)
Both $start and $end parameters represent the period endpoints as DateTime objects. The $start represents the starting endpoint. The $end value represents the ending endpoint. $end must be greater or equal to $start or the instantiation will throw a LogicException error. $start point is included in the period while the $end point is excluded from the period.
You can instantiate the Period object like the following.
$period = new Period('2014-10-01 00:00:00', '2014-10-04 12:30:10');
OR
$start  = new Datetime('2014-10-01 00:00:00');
$end    = new Datetime('2014-10-04 12:30:10');
 
$period = new Period($start, $end);
You can either pass a time string as shown in the example above or pass a DateTime object. When you pass a string they are automatically converted to a DateTime object before processing.
Once you have instantiated Period object you can access its properties using various getter methods.
Period::getStart();
$startp = $period->getStart();
This will return the following DateTime object.
DateTime Object
(
    [date] => 2014-10-01 00:00:00
    [timezone_type] => 3
    [timezone] => UTC
)
Period::getEnd();
$endp = $period->getEnd();
This will return the following DateTime object.
DateTime Object
(
    [date] => 2014-10-04 12:30:10
    [timezone_type] => 3
    [timezone] => UTC
)
Period::getDuration($get_as_seconds = false);
$duration  = $period->getDuration();
This will return the following Period duration as a DateInterval object.
DateInterval Object
(
    [y] => 0
    [m] => 0
    [d] => 3
    [h] => 12
    [i] => 30
    [s] => 10
    [invert] => 0
    [days] => 3
)
Passing a ‘true’ parameter to the above method returns the period in seconds.
$duration  = $period->getDuration(true);
This will return the following (in seconds).
304210

Creating different period bocks

In the above examples we have created a period using 2 parameters, a start and end endpoints. However you can also quickly create various periods using some pre-built named constructors.
Period::createFromWeek($year, $week)
Returns a Period object with a duration of 1 week for a given year and week.
The $year parameter is a valid year;
The $week parameter is a selected week (between 1 and 53) according to the ISO8601 date and time standard;
//this period represents the 3rd week of 2014
$period = Period::createFromWeek(2014, 3);
You can now use the various methods on this period as done in the previous section, a example using ‘getStart’ is given below.
$start = $period->getStart();
This will return the following.
DateTime Object
(
    [date] => 2014-01-13 00:00:00
    [timezone_type] => 3
    [timezone] => UTC
)
Period::createFromMonth($year, $month);
Returns a Period object with a duration of 1 month for a given year and month.
The $year parameter is a valid year;
The $month parameter is a selected month (between 1 and 12);
//this period represents the month of October 2014
$period = Period::createFromMonth(2014, 10);
One important point should be mentioned here. When we use the ‘getEnd’ method on any period object, it returns the end point of the period, which as mentioned at the start of the post, is excluded from the period. The actual endpoint is ‘one less’ than the returned value. or example for the above ‘createFromMonth’ method the actual last day is ’31 Oct 2014′, but ‘getEnd’ returns ‘1st Nov 2014′. So keep this in mind while programming. This can lead to subtle bugs.
// returns DateTime 2014-11-01 00:00:00
$end = $period->getEnd();
Period::createFromQuarter($year, $quarter);
Returns a Period object with a duration of 3 months for a given year and quarter.
The $year parameter is a valid year;
The $quarter parameter is a selected quarter (between 1 and 4);
//this period represents the second quarter of 2013
$period = Period::createFromQuarter(2013, 2);
Period::createFromSemester($year, $semester);
Returns a Period object with a duration of 6 months for a given year and semester.
The $year parameter is a valid year;
The $semester parameter is a selected semester (between 1 and 2);
//this period represents the first semester of 2013
$period = Period::createFromSemester(2013, 1);
Period::createFromYear($year);
Returns a Period object with a duration of 1 year for a given year.
The $year parameter is a valid year;
//this period represents the year 1973
$period = Period::createFromYear(1973);
Period::createFromDuration($start, $duration);
Returns a Period object which starts at $start with a duration equals to $duration. This a generic way to create periods.
The $start represents the starting included endpoint expressed as DateTime object.
The $duration parameter is a DateInterval object;
$period = Period::createFromDuration('2012-04-01 08:30:25', '1 DAY');
$alt    = Period::createFromDuration('2012-04-01 08:30:25', new DateInterval('P1D'));
$other  = Period::createFromDuration(new DateTime('2012-04-01 08:30:25'), 86400);

Comparing time periods

An important element in creating time periods is the ability to compare them. The following methods help you compare different Period objects according to their endpoints or durations. Let us see how to compare using endpoints. The following tells whether two Period objects shares the same endpoints.
$orig  = Period::createFromMonth(2014, 2); // Feb 2014
$alt   = Period::createFromMonth(2014, 4); // Apr 2014
$other = Period::createFromDuration('2014-02-01', '1 MONTH');
 
$orig->sameValueAs($alt);   //return false
$orig->sameValueAs($other); //return true
In the above example both $orig and $other have the same endpoint – 2014-03-01 00:00:00.
The following example calculates whether two Period objects overlap each other or not.
$orig  = Period::createFromMonth(2014, 3);
$alt   = Period::createFromMonth(2014, 4);
$other = Period::createFromDuration('2014-03-15', '3 WEEKS');
 
$orig->overlaps($alt);   //return false
$orig->overlaps($other); //return true
$alt->overlaps($other);  //return true
In the above example the $orig Period overlaps the $other Period, although the 3 week of $other spills in the 4th month. A graphical representation is given below. A Period does not have to 100% overlap or cover another Period, a partial overlap qualifies.
period1
The following example tells if a Period contains a particular date.
$period = Period::createFromMonth(2014, 4);
$period->contains('2014-04-15');   //returns true;
Also the following code make it explicityle clear as mentioned above that a Period endpoint is not included the Period.
$period = Period::createFromMonth(2014, 4);
$period->contains($period->getEnd());  //returns false;
Another important method is to find differences between 2 Periods. The durationDiff method returns a difference between 2 Period objects, returning a DateInterval object.
$period    = Period::createFromMonth(2014, 1);
$altPeriod = Period::createFromMonth(2014, 3);
$diff = $period->durationDiff($altPeriod); // returns a DateInterval object
You can also return the difference in seconds.
$period    = Period::createFromMonth(2014, 1); // Jan 2014
$altPeriod = Period::createFromMonth(2014, 3); // Mar 2014
$diff_as_seconds = $period->durationDiff($altPeriod, true);
//$diff_as_seconds represents the interval expressed in seconds
The above example returns a difference of ‘0’ seconds as the month of January and March contains the same number of days and hence the difference will be ‘0’. However with the next example the difference will be ‘86400’ (1 day) seconds as April has 1 day less than January.
$period    = Period::createFromMonth(2014, 1); // Jan 2014
$altPeriod = Period::createFromMonth(2014, 4); // April 2014
$diff_as_seconds = $period->durationDiff($altPeriod, true);
//$diff_as_seconds represents the interval expressed in seconds
The final few methods that aid in comparing duration are given below. The first iscompareDuration. Compare two Period objects according to their duration.
Period::compareDuration(Period $period)
– Return 1 if the current object duration is greater than the submitted $period duration;
– Return -1 if the current object duration is less than the submitted $period duration;
– Return 0 if the current object duration is equal to the submitted $period duration;
$orig  = Period::createFromDuration('2014-01-01', '1 MONTH');
$alt   = Period::createFromDuration('2014-01-01', '1 WEEK');
$orig->compareDuration($alt);  //return 1
$alt->compareDuration($orig);  //return -1
To ease the method usage you can rely on the following alias methods which return boolean values:
Period::durationGreaterThan(Period $period)
Period::durationLessThan(Period $period)
Period::sameDurationAs(Period $period)
$orig  = Period::createFromDuration('2014-01-01', '1 MONTH');
$alt   = Period::createFromDuration('2014-01-01', '1 WEEK');
$other = Period::createFromDuration('2015-01-01', '1 MONTH');
 
$orig->durationGreaterThan($alt); //return true
$orig->durationLessThan($alt);    //return false
 
$alt->durationGreaterThan($other); //return false
$alt->durationLessThan($other);    //return true
 
$orig->sameDurationAs($other);    //return true
$orig->sameValueAs($other);       //return false
//the duration between $orig and $other are equals but not the endpoints!!

Modifying Periods

You can modify, add, merge Period objects. Note that Period object is an immutable value object so any change to its property returns a new Period object rather than changing the original.
Period::add($interval)
Returns a new Period object by adding an interval to the current ending excluded endpoint. The $interval parameter is expressed as a DateInterval object.
$period    = Period::createFromMonth(2014, 1);
$newPeriod = $period->add('2 WEEKS');
 
// $period->getStart() 2014-01-01 00:00:00
// $newPeriod->getStart() 2014-01-01 00:00:00
 
// $period->getEnd() 2014-02-01 00:00:00
// $newPeriod->getEnd() 2014-02-15 00:00:00
Period::sub($interval)
Returns a new Period object by substracting an interval to the current ending excluded endpoint. The $interval parameter is expressed as a DateInterval object.
$period    = Period::createFromMonth(2014, 3);
$newPeriod = $period->sub('2 WEEKS');
// $period->getStart() equals $newPeriod->getStart();
 
// $period->getStart() 2014-01-01 00:00:00
// $newPeriod->getStart() 2014-01-01 00:00:00
 
// $period->getEnd() 2014-02-01 00:00:00
// $newPeriod->getEnd() 2014-01-18 00:00:00
Period::merge(Period $period[, Period $…])
Merges two or more Period objects by returning a new Period object which encloses all the submitted objects.
$period = Period::createFromMonth(2014, 3); // March 2014
$alt    = Period::createFromMonth(2014, 5); // May 2014
$newPeriod = $period->merge($alt); // March 2014 to May 2014
 
// $period->getStart() 2014-03-01 00:00:00
// $newPeriod->getStart() 2014-03-01 00:00:00
 
// $period->getEnd() 2014-04-01 00:00:00
// $newPeriod->getEnd() 2014-06-01 00:00:00
 
// $newPeriod->getDuration(true) 7948800 ( 3months )
In the above example we have created 2 Periods, each of 1 month , March and May. When we merge the Periods the middle days are automatically added to the merged Period. In the above example the merged Period will also contain the month of April.
Period::intersect(Period $period)
Computes the intersection between two Period objects and returns a new Period object. Before getting the intersection, make sure the Period object at least overlaps.
$period    = Period::createFromDuration(2012-01-01, '2 MONTHS');
$altPeriod = Period::createFromDuration(2012-01-15, '3 MONTHS');
if ($period->overlaps($altPeriod)) {
    $newPeriod = $period->insersect($altPeriod);
    //$newPeriod is a Period object 
}
The following figure give a visual idea of the process.
period2

In summary

Rounding the discussion, the Period class can be used to easily develop subscription like solutions wherein we need to extensively process time ranges. http://www.codediesel.com/algorithms/period-time-range-api-for-php/