Edit :
Maturity date, what is it ?
I'm not native english speaker. Sorry about that. I think the best we can do, is to define Maturity Date.
A maturity date is a date that indicates the deadline for the payment
of an invoice. In BtoB, the maturity date indicates when the customer
wants to pay us, defined during the contract it is usually a later date between 1 and 3 months
after the publishing of the bill. It's a calculated data, and
implemented in my application with the following code.
I have this code in my Invoice module I extracted it for you can tests :
<?php
class Config
{
protected $amountDelayedDays;
protected $paymentDay;
protected $paymentCondition;
public function getAmountDelayedDays()
{
return $this->amountDelayedDays;
}
public function getPaymentCondition()
{
return $this->paymentCondition;
}
public function getPaymentDay()
{
return $this->paymentDay;
}
public function setAmountDelayedDays($days)
{
$this->amountDelayedDays = $days;
return $this;
}
public function setPaymentDay($days)
{
$this->paymentDay = $days;
return $this;
}
public function setPaymentCondition($condition)
{
$this->paymentCondition = $days;
return $this;
}
}
class Test {
/**
* #param DateTime $dateInvoice
* #param Config $config
* #return DateTime
*/
public function calcMaturityDate(\DateTime $dateInvoice, $config)
{
if ($config->getPaymentCondition() == 'delayed') {
$dateMaturity = clone $dateInvoice;
$startDay = $dateMaturity->format('j');
$dateMaturity->modify("+{$config->getAmountDelayedDays()} days");
$endDay = $dateMaturity->format('j');
if ($startDay != $endDay && $endDay < 30) {
$dateMaturity->modify('last day of last month');
} else {
$dateMaturity->modify('last day of this month');
}
if ($config->getPaymentDay() != 0) {
$dateMaturity->modify('+' . $config->getPaymentDay() . 'days');
}
return $dateMaturity;
} else {
return $dateInvoice;
}
}
}
$config = new Config;
$config->setPaymentDay(15);
$config->setAmountDelayedDays(60);
$config->setPaymentCondition('delayed');
$test = new Test;
$date = $test->calcMaturityDate(new DateTime('2015-01-31'), $config);
var_dump($date);
?>
I have to calculate a maturity date from the date of invoice.
If my invoice is dated at 2014-11-30 and my client is configured to be charged 2 month later & on the 15'(=60days + 15 ), I have to produce a maturity date like this :
'2015-02-15'
For doing this I have to variables in my Config class :
$config->getAmountDelayedDays() and $config->getPaymentDay()
My code is not perfect to handle all problems. February changing years, custom value of days... Jumped month...
I think the problem is in
if ($startDay != $endDay && $endDay < 30) {
$dateMaturity->modify('last day of last month');
} else {
$dateMaturity->modify('last day of this month');
}
It's too simple to handle all cases, maybe it's wrong. I can't make my mind clear about this...
Tests case
I have units test testing this function I'm not passing
/**
* Tests MaturityDate
*
*/
public function testCanGiveCorrectMaturityDate()
{
$config = $this->parser->setConfig();
$config->setAmountDelayedDays(60);
$config->setPaymentDay(15);
$config->setPaymentCondition('delayed');
// From February Ok ?
$dateInvoice = new \Datetime('2015-02-28');
$maturityDate = $this->mock->calcMaturityDate($dateInvoice , $config);
$this->assertEquals('15-05-2015', $maturityDate->format('d-m-Y'));
// From February ok ?
$dateInvoice = new \Datetime('2015-02-28');
$config->setAmountDelayedDays(30);
$config->setPaymentDay(0);
$maturityDate = $this->mock->calcMaturityDate($dateInvoice , $config);
$this->assertEquals('31-03-2015', $maturityDate->format('d-m-Y'));
// New years and pass february
$config->setAmountDelayedDays(90);
$config->setPaymentDay(15);
$dateInvoice = new \Datetime('2014-11-30');
$maturityDate = $this->mock->calcMaturityDate($dateInvoice , $config);
$this->assertEquals('15-03-2015', $maturityDate->format('d-m-Y'));
// No delayed
$config->setPaymentCondition('standard');
$dateInvoice = new \Datetime('2014-11-30');
$maturityDate = $this->mock->calcMaturityDate($dateInvoice , $config);
$this->assertEquals('30-11-2014', $maturityDate->format('d-m-Y'));
}
If I get it right, you expect the paymentDay to be always 15th day of month of maturity, or next month, when date of maturity is later than 15th.
With this assumption your class would be:
public function calcMaturityDate(\DateTime $dateInvoice, $config)
{
$dateMaturity = clone $dateInvoice;
$dateMaturity->add(new \DateInterval("P{$config->getAmountDelayedDays()}D"));
$payDay = $config->getPaymentDay();
// patch 0 payDay to last day of month
if (0 == $payDay) {
$payDay = $dateMaturity->format('t');
}
if ($dateMaturity->format('j') > $payDay) {
$dateMaturity->modify('next month');
}
$dateMaturity->setDate(
$dateMaturity->format('Y'),
$dateMaturity->format('m'),
$payDay
);
return $dateMaturity;
}
Few notes:
the class name is a bit misleading, as it returns payment date, not maturity date;
maturity interval is 60 days, not 2 months;
For the last point I would recommend to refactor your config class to return \DateInterval for maturity interval instead of integer, so you will have flexibility to define 2 month interval as "P2M" or 60 days as "P60D" depending on business requirements:
If instead of
public function getAmountDelayedDays()
{
return $this->amountDelayedDays;
}
you have
/**
* #return \DateInterval
*/
public function getDelayInterval()
{
return $this->delayInterval;
}
the ugly line from above
$dateMaturity->add(new \DateInterval("P{$config->getAmountDelayedDays()}D"));
turns to elegant
$dateMaturity->add($config->getDelayInterval());
In these cases a always convert the "friendly date" to a timestamp; do the math; then convert back to a friendly date.
Something like this:
$invoice_date = "2014-11-30";
$ts_invoice_date = time($invoice_date);
$ts_maturity_month = $ts_invoice_date + (60*24*60*60); //60 days*24hrs*60mins*60secs
$maturity_date = date("Y-m-15 00:00:00", $ts_maturity_month);
From documentation: Date and Time
date('Y-m-d', strtotime('+1 week'))
strtotime documentation
Your question:
I have to calculate a maturity date from the date of invoice.
You can use date('Y-m-d', strtotime('+60 days')) and that will handle odd/even number of days in month, as well as fix to hours (if you need that precision)..
A date 2014-11-30 with date('Y-m-d', strtotime('+ $amountDelayedDays days')) yelds the correct date.
I think i get what your trying to do and I have had to do something similar. You basically want something to determine if the current day of the month is less than 15 and if it is it remains the 15th of this month else if its say the 20th of the same month then date is 15th of next month?. I think this is how I am reading this. If this is the case then you need to compare the day of the invoice to the 15th and if less than its the same month or if its greater than then its the next month. It then gets interesting as are you allowing a leverage period of say 5 days before the due date. A factor on what I was working on I used. This could be the case if the date of the invoice was the 14th and you set payment date on the 15th. With the logic the client would only have a day to pay the invoice. I think we went with the 15th minus 5 days.
Related
I'm working on a client scheduler and my employer wants it to automatically reschedule clients each year. He wants them to keep the same day of the same week every year.
For example, a client is scheduled for May 23rd 2014. This is the fourth friday of may. Once May 23rd 2014 has passed, a appointment for the fourth friday of may in 2015 should be booked (in this case the 22nd).
I've tried various things to get this to work (such as using DateTime to advance by a year and find "previous" of whatever day of the week it was). But every model I've tried breaks down a bit after just a few years. They'll end up on like...the second Friday of the month.
Does anyone have a way to get this to work? My employer is very specific about wanting the scheduler to work this way. x.x I'd really appreciate the help if someone knows how.
Thanks for reading this!
As has already been pointed out, there may not be a 5th Friday in June, for example, so there needs to be some standard way to decide which week next year is the same week as the current one.
As it happens there is already a standard for week numbering in ISO 8601 and PHP's DateTime class has built in functionality for handling them.
My suggestion would be to schedule the next meeting for the same day in the same ISO 8601 week number the following year. The following function will do that for you:-
/**
* #param \DateTime $date Date of the original meeting
* #return \DateTime Date of the next meeting
*/
function getSameDayNextYear(\DateTime $date = null)
{
if(!$date){
$date = new \DateTime();
}
return (new \DateTime())->setISODate((int)$date->format('o') + 1, (int)$date->format('W'), (int)$date->format('N'));
}
See it working with some test code.
I'm sure you'll agree that this is the simplest way of doing it and it should see you right for the next 100 years or more :)
References DateTime and Date for formats.
I think this will do what you need. You'll need to thoroughly test it out to be sure, though.
$event = new DateTime('2014-05-18');
$dayOfWeek = $event->format('l');
if ($dayOfWeek !== 'Sunday') {
$event->modify('previous Sunday');
}
else {
$event->modify('-1 day');
}
$event->modify('+1 year');
if ($dayOfWeek !== 'Sunday' && $dayOfWeek !== $event->format('l')) {
$event->modify('next ' . $dayOfWeek);
}
echo $event->format('Y-m-d');
Demo
So changing your requirements to "add a year and find the next matching day of the week" I've come up with this:
function nextDate($date = false){
if(!$date){ $date = new DateTime(); }
$oneYear = new DateInterval('P1Y');
$dayOfWeek = $date->format('w');
$nextDate = clone $date;
$nextDate->add($oneYear);
$nextYearDayOfWeek = $nextDate->format('w');
while($nextYearDayOfWeek != $dayOfWeek){
// add a day and check again
$nextDate->add(new DateInterval('P1D'));
$nextYearDayOfWeek = $nextDate->format('w');
}
return $nextDate;
}
And my tests:
$test1 = new DateTime('5/23/2014');
$test2 = new DateTime('5/24/2014');
$test3 = new DateTime('5/25/2014');
$test4 = new DateTime('5/26/2014');
$test5 = new DateTime('5/27/2014');
$test6 = new DateTime('5/28/2014');
$test7 = new DateTime('5/29/2014');
$test8 = new DateTime('1/1/2014');
$test9 = new DateTime('12/31/2014');
$test10 = new DateTime('5/5/2040');
$nextDate1 = nextDate($test1);
$nextDate2 = nextDate($test2);
$nextDate3 = nextDate($test3);
$nextDate4 = nextDate($test4);
$nextDate5 = nextDate($test5);
$nextDate6 = nextDate($test6);
$nextDate7 = nextDate($test7);
$nextDate8 = nextDate($test8);
$nextDate9 = nextDate($test9);
$nextDate10 = nextDate($test10);
print($nextDate1->format('m/d/y'));
print('<br />');
print($nextDate2->format('m/d/y'));
print('<br />');
print($nextDate3->format('m/d/y'));
print('<br />');
print($nextDate4->format('m/d/y'));
print('<br />');
print($nextDate5->format('m/d/y'));
print('<br />');
print($nextDate6->format('m/d/y'));
print('<br />');
print($nextDate7->format('m/d/y'));
print('<br />');
print($nextDate8->format('m/d/y'));
print('<br />');
print($nextDate9->format('m/d/y'));
print('<br />');
print($nextDate10->format('m/d/y'));
Results:
05/29/15
05/30/15
05/31/15
06/01/15
06/02/15
06/03/15
06/04/15
01/07/15
01/06/16
05/11/41
Edit
I've modified the function below to find the closest day of the week instead of the next one (though it tends to just be the same as the previous one except subtracting).
function nextDate($date = false){
if(!$date){ $date = new DateTime(); }
$oneYear = new DateInterval('P1Y');
$dayOfWeek = $date->format('w');
$nextDate = clone $date;
$nextDate->add($oneYear);
$nextYearDayOfWeek = $nextDate->format('w');
$diff = $dayOfWeek-$nextYearDayOfWeek;
// if $diff is more than 3, it's faster to go the other way
if(abs($diff) > 3){
if($diff > 0){
$diff = $diff-7;
}else{
$diff = 7+$diff;
}
}
if($diff != 0){
if($diff < 0){
$nextDate->sub(new DateInterval('P'.abs($diff).'D'));
}else{
$nextDate->add(new DateInterval('P'.$diff.'D'));
}
}
return $nextDate;
}
Test results this time:
05/22/15
05/23/15
05/24/15
05/25/15
05/26/15
05/27/15
05/28/15
12/31/14
12/30/15
05/04/41
We have a service that has people paid out in a rolling two week block timeframe:
03-31-13 to 04-13-13
04-14-13 to 04-27-13
04-28-13 to 05-11-13
etc
We want to pay people out on dates that would reflect the pay period that ended a week prior:
04-20-13 would be the payout date for the time period 03-31-13 to 04-13-13
05-04-13 would be the payout date for the time period 04-14-13 to 04-27-13
And so forth
What would be the best way to calculate these time periods and somewhat 'rolling' intervals in PHP? I am no stranger to the DateTime class, but I am not quite sure how to approach this.
Basically, the idea is that when a user lands on their billing page, we can tell them what time period they are in, and when they can expect to be paid for the previously ended time period.
update
After giving it further thought, it almost seems as though I would need to give it some kind of reference point, to know where to start from. Any ideas?
Thank you.
UPDATE #2 - My Solution
class PayoutDate {
const PERIOD_LENGTH = 14;
public static $now;
public static $refStart;
public static $currentMonth;
public static $currentYear;
public static $currPeriodStart;
public static $currPeriodEnd;
public static $prevPeriodStart;
public static $prevPeriodEnd;
public function initialize(Controller $controller) {
self::$now = new DateTime();
self::$currentMonth = self::$now->format('m');
self::$currentYear = self::$now->format('Y');
self::$refStart = new DateTime("10/20/2013");
}
public function getPreviousPeriodStart() {
$daysIntoCurrentPeriod = ((int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH);
self::$prevPeriodStart = new DateTime('2 weeks ago');
self::$prevPeriodStart->sub(new DateInterval('P'.$daysIntoCurrentPeriod.'D'));
return self::$prevPeriodStart;
}
public function getPreviousPeriodEnd() {
$daysLeftCurrentPeriod = self::PERIOD_LENGTH - ((int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH) - 1;
self::$prevPeriodStart = new DateTime('2 weeks ago');
self::$prevPeriodStart->add(new DateInterval('P'.$daysLeftCurrentPeriod.'D'));
return (self::$prevPeriodStart);
}
public function getCurrentPeriodStart() {
$daysIntoCurrentPeriod = (int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH;
self::$currPeriodStart = clone self::$now;
self::$currPeriodStart->sub(new DateInterval('P'.$daysIntoCurrentPeriod.'D'));
return (self::$currPeriodStart);
}
public function getCurrentPeriodEnd() {
$daysLeftCurrentPeriod = self::PERIOD_LENGTH - ((int)self::$now->diff(self::$refStart)->format('%a') % self::PERIOD_LENGTH) - 1;
self::$currPeriodEnd = clone self::$now;
self::$currPeriodEnd->add(new DateInterval('P'.$daysLeftCurrentPeriod.'D'));
return (self::$currPeriodEnd);
}
public function getPreviousPeriodPayout() {
$prevEnd = new DateTime(self::getPreviousPeriodEnd());
return ($prevEnd->modify('next friday'));
}
public function getCurrentPeriodPayout() {
$currentEnd = new DateTime(self::getCurrentPeriodEnd());
return ($currentEnd->modify('next friday'));
}
}
I welcome feedback or improvements to this solution :)
Well, I think you're right about needing some sort of reference point. I would pick an arbitrary start time of one your pay periods. It doesn't matter if it's in the past or future. You're also going to need a variable representing the length of a pay period. Then it's just a matter of doing some math.
$refStart = new DateTime('2013-03-31');
$periodLength = 14;
$now = new DateTime();
// do some modular arithmetic :)
$daysIntoCurrentPeriod = (int)$now->diff($refStart)->format('%a') % $periodLength;
$currentPeriodStart = clone $now;
$currentPeriodStart->sub(new DateInterval('P'.$daysIntoCurrentPeriod.'D'));
So after you run that code, $currentPeriodStart will store the date of the start of the current pay period. (It won't be accurate down to the second, but the date will be correct.) Then you can just subtract 14 days from it to get the start of the previous period, and add/subtract whatever you need to get the pay day.
This solution has the added benefit of working with daylight savings (which can be annoying) since I'm pretty sure DateTime accounts for that.
This works for me using some rules:
Determinate next Saturday (pay day) of given date and add 1 week
You can do some adjustments for your requirements, hope it helps:
<?php
/*
* Using:
* http://stackoverflow.com/a/1485512/496176
* http://www.php.net/manual/es/datetime.add.php
*/
date_default_timezone_set('Europe/London');
$interval = new DateInterval('P1W');
$payPeriod1 = new DateTime("2013-04-13");
$payPeriod1
->setISODate($payPeriod1->format("Y"), $payPeriod1->format("W"), 6)
->add($interval);
print "Pay day: " . $payPeriod1->format('Y-m-d') . "\n"; // Pay day: 2013-04-20
$payPeriod2 = new DateTime("2013-04-27");
$payPeriod2
->setISODate($payPeriod2->format("Y"), $payPeriod2->format("W"), 6)
->add($interval);
print "Pay day: " . $payPeriod2->format('Y-m-d') . "\n"; // Pay day: 2013-05-04
?>
I want all next dates with day from specified date for sending mail within cron job file.
right now I m checking for whether its week by calculating like this
$event_from_date = strtotime(date('Y-m-d',strtotime($rs->from_date)));
$today_date = strtotime(date('Y-m-d'));
$event_expire_on = strtotime($rs->to_date);
if($rs->time_zone == "CST")
$current_date=strtotime($cst_date);
elseif($rs->time_zone == "PST")
$current_date=strtotime($pst_date);
elseif($rs->time_zone == "MST")
$current_date=strtotime($est_date);
elseif($rs->time_zone == "EST")
$current_date=strtotime($mst_date);
if($current_date <= $event_expire_on && $current_date >= $event_from_date)
{
$diff=$today_date-$event_from_date;
if($diff%604800 == 0 && $rs->report_cycle=="Weekly")
$flag = 1;
else
$flag = 0;
}
can any body tell me . how can I get all next 7 days for weekly,15 days for biweekly,30 days for monthly with its day like Monday,Tuesday..
I'm not 100% sure I have understood your question. The DateTime classes offer simple ways of manipulating dates and times in all manner of ways.
I think, in your position I would be looking for DatePeriods to represent the various erm, periods I need. A function like this may suit:-
/**
* #param Int $days
* #return DatePeriod
*/
function getNumDays($days)
{
$today = new \DateTime('UTC');
$oneDay = new \DateInterval('P1D');
return new \DatePeriod($today, $oneDay, $days);
}
foreach(getNumDays(7) as $day){
//Do what ever you want with the DateTime instance
//We'll just var_dump() it for now.
var_dump($day);
}
See it working.
From October 1st to March 31 the fee is $1 (season 1). From April 1st to September 30 the fee is $2 (season 2).
How can I calculate the total fee of a given date range (user input) depending on how many days of this date range fall into season 1 and season 2?
The following gives me the number of days of the userĀ“s date range, but I have no idea how to test against season 1 or season 2:
$user_input_start_date = getdate( $a );
$user_input_end_date = getdate( $b );
$start_date_new = mktime( 12, 0, 0, $user_input_start_date['mon'], $user_input_start_date['mday'], $user_input_start_date['year'] );
$end_date_new = mktime( 12, 0, 0, $user_input_end_date['mon'], $user_input_end_date['mday'], $user_input_end_date['year'] );
return round( abs( $start_date_new - $end_date_new ) / 86400 );
Given that a date range starts and ends in 2012 or starts in 2012 and ends in 2013 alone gives me 10 different possibilities of in which season a date range can start and where it can end.
There must be a better solution than iterating if/else and comparing dates over and over again for the following conditions:
Date range is completely within season 1
Date range starts in season 1 and ends in season 2
Date range starts in season 1, spans across season 2 and ends in the second part of season 1
... and so forth with "Starts in season 2", etc
This not a duplicate of How many days until X-Y-Z date? as that only deals with counting the number of days. It does not address the issue of comparing one date range with another.
The key to this problem is to simplify it as much as possible. I think using an array as a lookup table for the cost of each day of the year is the way to go. The first thing to do then, is to generate the array. The array just represents each day of the year and doesn't represent any particular year. I chose to use 2012 to generate the lookup array as it is a leap year and so has every possible day in it.
function getSeasonArray()
{
/**
* I have chosen 2012 as it was a leap year. All we want to do is
* generate an array which has avery day of the year in it.
*/
$startDate = new DateTime('1st January 2012');
//DatePeriod always drops the last day.
$endDate = new DateTime('1st January 2013');
$season2Start = new DateTime('1st April 2012');
$season2End = new DateTime('1st October 2012');
$allDays = new DatePeriod($startDate, new DateInterval('P1D'), $endDate);
$season2Days = new DatePeriod($season2Start, new DateInterval('P1D'), $season2End);
$seasonArray = array();
foreach($allDays as $day){
$seasonArray[] = $day->format('d-M');
$seasonArray[$day->format('d-M')]['season'] = 1;
}
foreach($season2Days as $day){
$seasonArray[$day->format('d-M')]['season'] = 2;
}
return $seasonArray;
}
Once that is done you just need the period over which to calculate:-
$bookingStartDate = new DateTime();//Or wherever you get this from
$bookingEndDate = new DateTime();
$bookingEndDate->setTimestamp(strtotime('+ 7 month'));//Or wherever you get this from
$bookingPeriod = new DatePeriod($bookingStartDate, new DateInterval('P1D'), $bookingEndDate);
Then we can do the calculation:-
$seasons = getSeasonArray();
$totalCost = 0;
foreach($bookingPeriod as $day){
$totalCost += $seasons[$day->format('d-M')]['season'];
var_dump($day->format('d-M') . ' = $' . $seasons[$day->format('d-M')]['season']);
}
var_dump($totalCost);
I have chosen a long booking period, so that you can scan through the var_dump() output and verify the correct price for each day of the year.
This is a quick stab done between distractions at work and I'm sure that with a bit of thought you can mould it into a more elegant solution. I'd like to get rid of the double iteration for example, unfortunately, work pressures prevent me from spending further time on this.
See the PHP DateTime man page for further information on these useful classes.
At first I suggested using the DateTime class that PHP provides, naively assuming that it has some kind of thought-out API that one could use. It turns out that it does not. While it features very basic DateTime functionality, it is mostly unusable because, for most operations, it relies on the DateInterval class. In combination, those classes represent another masterpiece of bad API design.
An interval should be defined like so:
An interval in Joda-Time represents an interval of time from one millisecond instant to another instant. Both instants are fully specified instants in the datetime continuum, complete with time zone.
In PHP, however, an Interval is just a duration:
A date interval stores either a fixed amount of time (in years, months, days, hours etc) or a relative time string [such as "2 days"].
Unfortunately, PHP's DateInterval definition does not allow for intersection/overlap calculation (which the OP needs) because PHP's Intervals have no specific position in the datetime continuum. Therefore, I've implemented a (very rudimentary) class that adheres to JodaTime's definition of an interval. It is not extensively tested, but it should get the work done:
class ProperDateInterval {
private $start = null;
private $end = null;
public function __construct(DateTime $start, DateTime $end) {
$this->start = $start;
$this->end = $end;
}
/**
* Does this time interval overlap the specified time interval.
*/
public function overlaps(ProperDateInterval $other) {
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
return $start < $oEnd && $oStart < $end;
}
/**
* Gets the overlap between this interval and another interval.
*/
public function overlap(ProperDateInterval $other) {
if(!$this->overlaps($other)) {
// I haven't decided what should happen here yet.
// Returning "null" doesn't seem like a good solution.
// Maybe ProperDateInterval::EMPTY?
throw new Exception("No intersection.");
}
$start = $this->getStart()->getTimestamp();
$end = $this->getEnd()->getTimestamp();
$oStart = $other->getStart()->getTimestamp();
$oEnd = $other->getEnd()->getTimestamp();
$overlapStart = NULL;
$overlapEnd = NULL;
if($start === $oStart || $start > $oStart) {
$overlapStart = $this->getStart();
} else {
$overlapStart = $other->getStart();
}
if($end === $oEnd || $end < $oEnd) {
$overlapEnd = $this->getEnd();
} else {
$overlapEnd = $other->getEnd();
}
return new ProperDateInterval($overlapStart, $overlapEnd);
}
/**
* #return long The duration of this interval in seconds.
*/
public function getDuration() {
return $this->getEnd()->getTimestamp() - $this->getStart()->getTimestamp();
}
public function getStart() {
return $this->start;
}
public function getEnd() {
return $this->end;
}
}
It may be used like so:
$seasonStart = DateTime::createFromFormat('j-M-Y', '01-Apr-2012');
$seasonEnd = DateTime::createFromFormat('j-M-Y', '30-Sep-2012');
$userStart = DateTime::createFromFormat('j-M-Y', '01-Jan-2012');
$userEnd = DateTime::createFromFormat('j-M-Y', '02-Apr-2012');
$i1 = new ProperDateInterval($seasonStart, $seasonEnd);
$i2 = new ProperDateInterval($userStart, $userEnd);
$overlap = $i1->overlap($i2);
var_dump($overlap->getDuration());
How do you actually perform datetime operations such as adding date, finding difference, find out how many days excluding weekends in an interval? I personally started to pass some of these operations to my postgresql dbms as typically I would only need to issue one sql statement to obtain an answer, however, to do it in PHP way I would have to write a lot more code that means more chances for errors to occur...
Are there any libraries in PHP that does datetime operation in a way that don't require a lot of code? that beats sql in a situation where 'Given two dates, how many workdays are there between the two dates? Implement in either SQL, or $pet_lang' that is solved by making this query?
SELECT COUNT(*) AS total_days
FROM (SELECT date '2008-8-26' + generate_series(0,
(date '2008-9-1' - date '2008-8-26')) AS all_days) AS calendar
WHERE EXTRACT(isodow FROM all_days) < 6;
While for most datetime operations I would normally convert to Unixtime and perform addition subtraction etc. on the Unixtime integer, you may want to look at the Zend framework Zend_Date class.
This has a lot of the functionality you describe. Although Zend is billed as a "framework" it works exceptionally well as a class library to pick and chose elements from. We routinely include it in projects and then just pull in bits as and when we need them.
PHP5+'s DateTime object is useful because it is leap time and
daylight savings aware, but it needs some extension to really
solve the problem. I wrote the following to solve a similar problem.
The find_WeekdaysFromThisTo() method is brute-force, but it works reasonably quickly if your time span is less than 2 years.
$tryme = new Extended_DateTime('2007-8-26');
$newer = new Extended_DateTime('2008-9-1');
print 'Weekdays From '.$tryme->format('Y-m-d').' To '.$newer->format('Y-m-d').': '.$tryme -> find_WeekdaysFromThisTo($newer) ."\n";
/* Output: Weekdays From 2007-08-26 To 2008-09-01: 265 */
print 'All Days From '.$tryme->format('Y-m-d').' To '.$newer->format('Y-m-d').': '.$tryme -> find_AllDaysFromThisTo($newer) ."\n";
/* Output: All Days From 2007-08-26 To 2008-09-01: 371 */
$timefrom = $tryme->find_TimeFromThisTo($newer);
print 'Between '.$tryme->format('Y-m-d').' and '.$newer->format('Y-m-d').' there are '.
$timefrom['years'].' years, '.$timefrom['months'].' months, and '.$timefrom['days'].
' days.'."\n";
/* Output: Between 2007-08-26 and 2008-09-01 there are 1 years, 0 months, and 5 days. */
class Extended_DateTime extends DateTime {
public function find_TimeFromThisTo($newer) {
$timefrom = array('years'=>0,'months'=>0,'days'=>0);
// Clone because we're using modify(), which will destroy the object that was passed in by reference
$testnewer = clone $newer;
$timefrom['years'] = $this->find_YearsFromThisTo($testnewer);
$mod = '-'.$timefrom['years'].' years';
$testnewer -> modify($mod);
$timefrom['months'] = $this->find_MonthsFromThisTo($testnewer);
$mod = '-'.$timefrom['months'].' months';
$testnewer -> modify($mod);
$timefrom['days'] = $this->find_AllDaysFromThisTo($testnewer);
return $timefrom;
} // end function find_TimeFromThisTo
public function find_YearsFromThisTo($newer) {
/*
If the passed is:
not an object, not of class DateTime or one of its children,
or not larger (after) $this
return false
*/
if (!is_object($newer) || !($newer instanceof DateTime) || $newer->format('U') < $this->format('U'))
return FALSE;
$count = 0;
// Clone because we're using modify(), which will destroy the object that was passed in by reference
$testnewer = clone $newer;
$testnewer -> modify ('-1 year');
while ( $this->format('U') < $testnewer->format('U')) {
$count ++;
$testnewer -> modify ('-1 year');
}
return $count;
} // end function find_YearsFromThisTo
public function find_MonthsFromThisTo($newer) {
/*
If the passed is:
not an object, not of class DateTime or one of its children,
or not larger (after) $this
return false
*/
if (!is_object($newer) || !($newer instanceof DateTime) || $newer->format('U') < $this->format('U'))
return FALSE;
$count = 0;
// Clone because we're using modify(), which will destroy the object that was passed in by reference
$testnewer = clone $newer;
$testnewer -> modify ('-1 month');
while ( $this->format('U') < $testnewer->format('U')) {
$count ++;
$testnewer -> modify ('-1 month');
}
return $count;
} // end function find_MonthsFromThisTo
public function find_AllDaysFromThisTo($newer) {
/*
If the passed is:
not an object, not of class DateTime or one of its children,
or not larger (after) $this
return false
*/
if (!is_object($newer) || !($newer instanceof DateTime) || $newer->format('U') < $this->format('U'))
return FALSE;
$count = 0;
// Clone because we're using modify(), which will destroy the object that was passed in by reference
$testnewer = clone $newer;
$testnewer -> modify ('-1 day');
while ( $this->format('U') < $testnewer->format('U')) {
$count ++;
$testnewer -> modify ('-1 day');
}
return $count;
} // end function find_AllDaysFromThisTo
public function find_WeekdaysFromThisTo($newer) {
/*
If the passed is:
not an object, not of class DateTime or one of its children,
or not larger (after) $this
return false
*/
if (!is_object($newer) || !($newer instanceof DateTime) || $newer->format('U') < $this->format('U'))
return FALSE;
$count = 0;
// Clone because we're using modify(), which will destroy the object that was passed in by reference
$testnewer = clone $newer;
$testnewer -> modify ('-1 day');
while ( $this->format('U') < $testnewer->format('U')) {
// If the calculated day is not Sunday or Saturday, count this day
if ($testnewer->format('w') != '0' && $testnewer->format('w') != '6')
$count ++;
$testnewer -> modify ('-1 day');
}
return $count;
} // end function find_WeekdaysFromThisTo
public function set_Day($newday) {
if (is_int($newday) && $newday > 0 && $newday < 32 && checkdate($this->format('m'),$newday,$this->format('Y')))
$this->setDate($this->format('Y'),$this->format('m'),$newday);
} // end function set_Day
public function set_Month($newmonth) {
if (is_int($newmonth) && $newmonth > 0 && $newmonth < 13)
$this->setDate($this->format('Y'),$newmonth,$this->format('d'));
} // end function set_Month
public function set_Year($newyear) {
if (is_int($newyear) && $newyear > 0)
$this->setDate($newyear,$this->format('m'),$this->format('d'));
} // end function set_Year
} // end class Extended_DateTime
strtotime() is useful but it does have some odd behaviors that can pop-up from time to time if you are not just using it to convert a formatted date/time string.
things like "+1 month" or "-3 days" can sometimes not give you what you expect it to output.
For adding a date, you can use the method DateTime::add (Adds an amount of days, months, years, hours, minutes and seconds to a DateTime object), available from php 5.3.0 onwards.
To find the difference between two dates, there's the DateTime::diff method; but there doesn't seem to be a method for counting the working days between two dates.
PEAR::Date looks like it might have some useful functionality.
PEAR::Calendar might also be useful.
The easiest method is to use a timestamp, representing the number of seconds since January 1, 2008. With a timestamp type, you can do things like...
now = time();
tomorrow = now + 24 * 60 * 60; // 24 hours * 60 minutes * 60 seconds
Check out the documentation for time(), date() and mktime() on the php web pages. Those are the three methods that I tend to use the most frequently.
You can use a combination of strtotime, mktime and date todo the arithmetic
Here is an example which uses a combo todo some arithmetic http://rushi.wordpress.com/2008/04/13/php-print-out-age-of-date-in-words/ I'll reproduce the code here for simplicity
if ($timestamp_diff < (60*60*24*7)) {
echo floor($timestamp_diff/60/60/24)." Days";
} elseif ($timestamp_diff > (60*60*24*7*4)) {
echo floor($timestamp_diff/60/60/24/7)." Weeks";
} else {
$total_months = $months = floor($timestamp_diff/60/60/24/30);
if($months >= 12) {
$months = ($total_months % 12);
$years = ($total_months - $months)/12;
echo $years . " Years ";
}
if($months > 0)
echo $months . " Months";
}
?>
#Rushi I don't like strtotime() personally.. i don't know why but i discovered this morning that passing a string like this '2008-09-11 9:5 AM' to strtotime returns a false...
I don't think the code you provided solve the example problem 'Given two dates, how many workdays are there between the two dates? Implement in either SQL, or $pet_lang' and I haven't consider if I have a list of public holiday...
You can get number of days between two dates like this:
$days = (strtotime("2008-09-10") - strtotime("2008-09-12")) / (60 * 60 * 24);
And you can make function something like that (I don't have php installed in my work computer so i can't guarantee syntax is 100% correct)
function isWorkDay($date)
{
// check if workday and return true if so
}
function numberOfWorkDays($startdate, $enddate)
{
$workdays = 0;
$tmp = strtotime($startdate);
$end = strtotime($enddate);
while($tmp <= $end)
{
if ( isWorkDay( date("Y-m-d",$tmp) ) ) $workdays++;
$tmp += 60*60*24;
}
return $workdays;
}
If you don't like strtotime and you always have date in same format you can use explode function like
list($year, $month, day) = explode("-", $date);
I would strongly recommend using PHP 5.2's DateTime objects, rather than using UNIX timestamps, when doing date calculations. When you use the PHP date functions that return UNIX timestamps, you have a very limited range to work with (e.g. nothing before 1970).
If you have a look at http://php.net/date , you will find some examples of using mktime() to perform operations.
A simple example would be to workout what tomorrows date would be. You can do that by simply adding 1, to the day value in mktime() as follows:
$tomorrow = date("Y-m-d", mktime(0, 0, 0, date("m"), date("d") + 1, date("Y")));
So here, you will receive a date in the form of YYYY-MM-DD containing tomorrows date. You can also subtract days by simply replacing '+' with '-'. mktime() makes life a lot easier, and saves you from having to do nested if statements and other such troublesome coding.
to get working days/holidays, postgresql CTE ftw -- see http://osssmb.wordpress.com/2009/12/02/business-days-working-days-sql-for-postgres-2/