How to calculate the difference between two days as a formatted string?

大兔子大兔子 提交于 2019-12-23 10:27:06

问题


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.


回答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.




回答2:


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.




回答3:


$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.




回答4:


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



回答5:


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.



来源:https://stackoverflow.com/questions/8993067/how-to-calculate-the-difference-between-two-days-as-a-formatted-string

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!