5

Here's what I've got so far:

/**
 * Parse a duration between 2 date/times in seconds
 * and to convert that duration into a formatted string
 *
 * @param integer $time_start start time in seconds
 * @param integer $time_end   end time in seconds
 * @param string  $format     like the php strftime formatting uses %y %m %w %d %h or %i.
 * @param boolean $chop       chop off sections that have 0 values
 */
public static function FormatDateDiff($time_start = 0, $time_end = 0, $format = "%s", $chop = false) {

        if($time_start > $time_end) list($time_start, $time_end) = array($time_end, $time_start);

        list($year_start,$month_start,$day_start) = explode('-',date('Y-m-d',$time_start));
        list($year_end,$month_end,$day_end) = explode('-',date('Y-m-d',$time_end));

        $years = $year_end - $year_start;
        $months = $month_end - $month_start;
        $days = $day_start - $day_end;
        $weeks = 0;
        $hours = 0;
        $mins = 0;
        $secs = 0;

        if(mktime(0,0,0,$month_end,$day_end) < mktime(0,0,0,$month_start,$day_start)) {
            $years -= 1;
        }
        if($days < 0) {
            $months -= 1;
            $days += 30; // this is an approximation...not sure how to figure this out
        }
        if($months < 0) $months += 12;
        if(strpos($format, '%y')===false) {
            $months += $years * 12;
        }
        if(strpos($format, '%w')!==false) {
            $weeks = floor($days/7);
            $days %= 7;
        }
        echo date('Y-m-d',$time_start).' to '.date('Y-m-d',$time_end).": {$years}y {$months}m {$weeks}w {$days}d<br/>";
}

(It's incomplete and inaccurate)

I can't seem to get the math right. Naively dividing it out won't work because of leap years and differing lengths of months.

The logic also needs to change depending on the format string. For example, passing 04-Feb-2010 to 28-Jun-2011 (as unix timestamps) with format string %y year %m month %d day should output 1 year 4 month 24 day but if %y year is omitted then it needs to add 12 months to the month, i.e., output should be 16 month 24 day.

Should handle times too...but I haven't got to that yet.


None of these date_diff solutions handle weeks. And I don't know how I could hack it into date_diff, so that's not really a solution for me.

Furthermore, $diff->format doesn't do what I asked...to give the total months and days if "bigger units" are omitted. Example:

>>> $start = new DateTime('04-Feb-2010')
>>> $end = new DateTime('28-Jun-2011')
>>> $diff = $start->diff($end)
>>> $diff->format('%m months, %d days')
'4 months, 24 days'

Should be 16 months, 24 days, as I stated earlier. Please stop being so quick to close my question as a dupe before you understand it fully. If the solutions to other questions can be tweaked to solve this, fine, but please explain how, because I don't get it.

To be clear,

  • if %y is omitted, years should be rolled in the months
  • if %m is omitted, months should be rolled into the days
  • if %w is omitted, weeks should be rolled into the days
  • if %h is omitted, hours should be rolled into minutes
  • if %m is omitted, minutes should be rolled into seconds

If "smaller units" are omitted, the next biggest unit can be rounded or floored where it makes sense.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 1
    If you have PHP 5.3.0 or above, this might help you: [DateInterval::format](http://www.php.net/manual/en/dateinterval.format.php) – drew010 Jan 24 '12 at 19:47
  • 2
    duplicate of [this post](http://stackoverflow.com/questions/676824/how-to-calculate-the-difference-between-two-dates-using-php) – bowlerae Jan 24 '12 at 19:48
  • @drew010: No...that doesn't handle weeks. – mpen Jan 24 '12 at 21:22
  • @bowlerae: No it isn't. Please read my edit which should hopefully make it more clear. – mpen Jan 24 '12 at 21:29

5 Answers5

1
$absolute = false; //default, returns years, months, days, etc. True would return seconds
$difference = date_diff($dateTimeStart, $dateTimeEnd, $absolute);
echo $difference->format("%y Year(s) %m Month(s) ...");

I would try this first, if it does not work for your purposes then you could try to use the Calendar functions to fetch the correct number of days in each month.

I think you would need to decide upon your own logic for calculating weeks. Personally, I would say $weeks = $difference->d / 7; but there is no strictly correct way to count elapsed weeks in a portion of a month. Think of a calendar, we often speak of first, second, third week, but unless the month started on a Sunday (or Monday, or Saturday, depending on company, religion, etc.) then really we are not being absolute in these descriptions. You can say absolutely that there have been 3 Sundays (for instance) since the beginning of the month, but not three weeks. On the 28th, have four weeks passed? What if the month started on Wed, then is it five?

Also, if you want a custom format (35 Months) you can always access the members directly and concatenate a format string.

Tim
  • 1,011
  • 6
  • 12
  • `date_diff` doesn't handle weeks. You mean calculate the number of days in each month between the 2 dates and sum them or something like that? Or just the start and end month..? – mpen Jan 24 '12 at 21:24
  • 1
    You're right, it doesn't. I'm sorry I didn't notice that requirement. – Tim Jan 24 '12 at 21:27
  • It's okay. That requirement was kind of buried in the code; I should have made it more clear. It won't handle the "bigger units" requirement either though. – mpen Jan 24 '12 at 21:34
  • I see your edit...for the purposes of this, exactly 7 days is a week. It's the months and years that were throwing me off; my application has some requirements for ages 6 months and 6 years (which is loosely defined) but people get confused when it's their calendar birthday and their age isn't incremented (because it's off by a day or a few hours due to rounding and leap years and such). – mpen Jan 24 '12 at 23:02
1

I would use DateTime::diff() for this:

$start = new DateTime('2012-01-01 12:00:00');
$end   = new DateTime('2012-01-20 06:59:59');
$diff  = $start->diff($end);

echo $diff->format('%d days, %h hours, %m minutes, %s seconds');

See DateInterval::format() for more info about the formats.

FtDRbwLXw6
  • 27,774
  • 13
  • 70
  • 107
1

I don't expect an accepted answer, but here is how to get date_diff to do weeks.

<?php

$january = new DateTime('2010-01-01');
$february = new DateTime('2011-02-20 3:35:28');
$interval = $february->diff($january);

$parts = $interval->format('%y %m %d %h %i %s %a');

$weeks = 0;
list($years, $months, $days, $hours, $minutes, $seconds, $total_days) = explode(' ', $parts);

if ($days >= 7) {
    $weeks = (int)($days / 7);
    $days  %= 7;
}

echo "$years years, $months months, $weeks weeks, $days days, $hours hours, $minutes minutes $seconds seconds";
// 1 years, 1 months, 2 weeks, 5 days, 3 hours, 35 minutes 28 seconds

Maybe with that you can integrate it into your function to do the rolling over and handling the user given format.

If the bigger units aren't given, you can start from the largest unit and apply them back to the next smaller unit. (i.e. 1 year 1 month with no years should add 12 back to months). If "month" isn't included in the format, then you can use the total days to handle the fact that months have different numbers of days.

drew010
  • 68,777
  • 11
  • 134
  • 162
  • Nice! I'll try playing with this and see if I can get it to do what I want. Looks promising. – mpen Jan 24 '12 at 22:36
  • Hopefully you can get somewhere with it. I always try to rely on "built-in" date methods like that now because of so many issues when dealing with observing DST boundaries, leap years/seconds, and different days in each month and so on. – drew010 Jan 24 '12 at 22:50
  • Yeah....I was fiddling with `DateTime` before, but there were still some issues with it, so I said "Screw it! I'll write it myself!". Evidentially that was the wrong approach. Yours is nice and clean though. I'll accept this :) – mpen Jan 24 '12 at 22:56
1

I think I've got it:

if($time_start > $time_end) list($time_start, $time_end) = array($time_end, $time_start);

$start_dt = new DateTime();
$end_dt = new DateTime();
$start_dt->setTimestamp($time_start);
$end_dt->setTimestamp($time_end);
$has_time = preg_match('`%[his]`',$format) > 0;
if(!$has_time) {
    $start_dt->setTime(0,0,0);
    $end_dt->setTime(0,0,0);
}
$interval = $end_dt->diff($start_dt);
$parts = $interval->format('%y %m %d %h %i %s %a');
$weeks = 0;
list($years, $months, $days, $hours, $mins, $secs, $total_days) = explode(' ',$parts);
if(strpos($format,'%y')===false) {
    $months += $years * 12;
}
if(strpos($format,'%m')===false) {
    $days = $total_days;
    if(strpos($format,'%y')!==false) {
        $start_dt->add(new DateInterval('P'.$years.'Y'));
        $interval = $end_dt->diff($start_dt);
        $days = $interval->days;
    }
}
if(strpos($format,'%w')!==false) {
    $weeks = (int)($days/7);
    $days %= 7;
}
if(strpos($format,'%d')===false) {
    $hours += $days * 24;
}
if(strpos($format,'%h')===false) {
    $mins += $hours * 60;
}
if(strpos($format,'%i')===false) {
    $secs += $mins * 60;
}

(FYI, I just do some basic str_replacing at the end to throw it into my custom format string)

Edit: There's still 1 more scenario I don't know how to handle... when years and days are requested but not months, the output will be wrong. I'm wondering if I should just mod the total days with 365.... it's kind of a rare scenario.

Edit2: Solved it! We'll just add that many years to the start date and then recalculate the total days.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 1
    Nice solution Mark, clean and easy to read all the way through. Looks like you're almost there. – drew010 Jan 25 '12 at 03:28
1

Here's my attempt at an interesting problem. It's not quite working 100%, but it's very close.

The function begins by calculating the total number of seconds and months that the date difference requires. Then, using the $tokens array that is sorted in ascending order, it loops through those tokens and tries to match it with the input $format. If the $format is found, the appropriate value is subtracted from the total number of months or seconds.

However, there is a possibility of having a value greater than zero for months or years, but the $format string didn't specify either of those parameters. If so, the function rolls over the appropriate number of days in order to come up with the correct calculations.

I've briefly tested it, and it works for all of the examples in this thread. You can test it out on codepad to see if you can break it! :) I'd be interested in improvements upon this.

<?php

function calc( $start, $end, $format = '%s', $chop = false)
{
    $tokens = array( '%y', '%m', '%w', '%d', '%h', '%i', '%s');

    if( !is_a( $start, 'DateTime') || !is_a( $end, 'DateTime'))
    {
        return;
    }
    $diff = $start->diff( $end);

    $months = ($diff->y * 12) + $diff->m;
    $secs  = ($diff->d * 24 * 3600) + 
             ($diff->h * 3600) +
             ($diff->i * 60) +
             ($diff->s);

    $output = array();
    while( $token = array_shift( $tokens))
    {
        $token_present = !(strpos( $format, $token) === false);

        switch( $token)
        {
            case '%y':
                if( ($months / 12) > 0 && $token_present)
                {
                    $output[$token] = floor( $months / 12);
                    $months -= $output[$token] * 12;
                }
            break;
            case '%m':
                if( $months > 0 && $token_present)
                {
                    $output[$token] = $months;
                    $months = 0;
                }
            break;
            case '%w':
                // Rollover between (months or years) and seconds
                if( (!isset( $output['%y']) || !isset( $output['%m'])) && $months > 0)
                {
                    $days = $diff->format( '%a');
                // Need a fix for leap year probably.
                $days -= (isset( $output['%y'])) ? ($output['%y'] * 365) : 0;
                $days -= $diff->d;
                $secs += ($days * 24 * 60 * 60);
                }

                $val = (7 * 24 * 60 * 60);
                if( ($secs / $val) > 0 && $token_present)
                {
                    $output[$token] = floor( $secs / $val);
                    $secs -= $output[$token] * $val;
                }
            break;
            case '%d':
                $val = (24 * 60 * 60);
                if( ($secs / $val) > 0 && $token_present)
                {
                    $output[$token] = floor( $secs / $val);
                    $secs -= $output[$token] * $val;
                }
            break;
            case '%h':
                $val = (60 * 60);
                if( ($secs / $val) > 0 && $token_present)
                {
                    $output[$token] = floor( $secs / $val);
                    $secs -= $output[$token] * $val;
                }
            break;
            case '%i':
                $val = (60);
                if( ($secs / $val) > 0 && $token_present)
                {
                    $output[$token] = floor( $secs / $val);
                    $secs -= $output[$token] * $val;
                }
            break;
            case '%s':
                if( $secs > 0 && $token_present)
                {
                    $output[$token] = $secs;
                }
            break;
        }
    }

    // Filter out blank keys and replace their tokens in the $format string
    $filtered = $chop ? array_filter( $output) : $output;
    $format = str_replace( array_diff( array_keys($output), array_keys($filtered)), '', $format);

    return str_replace( array_keys( $filtered), array_values( $filtered), $format);
}

$start = new DateTime('04-Feb-2010');
$end = new DateTime('28-Jun-2011');
echo calc( $start, $end, "%m months %d days\n"); // 16 months 24 days

$january = new DateTime('2010-01-01');
$february = new DateTime('2011-02-20 3:35:28');
echo calc( $january, $february, '%y years %m months %w weeks %d days %h hours %i minutes %s seconds'); // 1 years 1 months 2 weeks 5 days 3 hours 35 minutes 28 seconds
nickb
  • 59,313
  • 13
  • 108
  • 143
  • @Mark I noticed that bug as I posted the answer... I think this fixes it: http://codepad.viper-7.com/T7qehe – nickb Jan 24 '12 at 23:47
  • Yeah..that's the same result I get now, however I think you've still got some rounding errors, particularly around leap years. I don't like that "365" in there ;) My solution is presently at the bottom of this page BTW. – mpen Jan 25 '12 at 00:02
  • 1
    @Mark - Yeah neither do I `:P` That's what I'm trying to remove as of right now. Besides that, I think everything else is pretty much working. I will check out your solution once I cannot fix mine to remove the 365. `:)` As a side note, it will never hit that 365, unless both 1) The input range is greater than a month and 2) The input format does not specify the `%m` or `%y` formats. – nickb Jan 25 '12 at 00:07