Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > PHP: Общие вопросы > Посчитать количество рабочих дней


Автор: Gold Dragon 12.1.2014, 16:48
Есть дата договора. Есть количество дней (рабочих) на исполнение, т.е. без субботы и воскресенья.

Как минимальными затратами вычислить дату окончания договора?

Автор: baldina 12.1.2014, 20:31
в предположении, что дата договора попадает на рабочий день недели
<остаток>=<рабочих дней>%5
<дней всего>=7*<рабочих дней>/5+<остаток>+max(0,<остаток>?<день недели даты договора>-3:0)

Автор: Gold Dragon 12.1.2014, 20:49
если честно, то ничего не понял. И мне не нужны проценты вероятности. мне нужна точная дата (государственные праздники не в счёт).

Запросов должно быть много, так что код должен быть оптимизированный... 

ps
может есть какая готовая функция? что-то я не нашёл

Автор: ksnk 12.1.2014, 21:12
Разумнее, imho, вести базу нерабочих дней. То есть каждая запись в базе выглядит {день:XXX}. В базу попадаю все выходные и праздничные дни. Из базы выкидываются все рабочие дни и дни переноса "праздников". Заполняются вручную + полуавтомат про выходные. Сама табличка не особенно большая, к тому же ее можно чистить от устаревших данных.
Для небольшого временнОго периода (неделя?), можно вычислять рабочие дни перебирая дни в цикле. Проверка - есть ли день в базе, если нету, значит рабочий. 
Для длинного диапазона - считаем, что от начальной даты должно пройти 7/5 дней от количества рабочих. Получаем диапазон. Выкидываем оттуда количество всех праздничных дней, из базы. получаем количество рабочих дней. Если рабочих дней нужно меньше или больше - отнимаем или прибавляем рабочие дни, как в первом случае.

Автор: Gold Dragon 12.1.2014, 21:37
с базой не самый оптимальный вариант, но зато можно предусмотреть все праздники.. Нужно прикинуть...

я так понимаю, мне нужно посчитать количество недель и добавить по 2 дня для каждой недели. Плюсь сделать поправку на полную неделю, т.е. начало и конец

Автор: baldina 12.1.2014, 22:21
Цитата(Gold Dragon @  12.1.2014,  21:37 Найти цитируемый пост)
я так понимаю, мне нужно посчитать количество недель и добавить по 2 дня для каждой недели. Плюсь сделать поправку на полную неделю, т.е. начало и конец 

я тебе так и написал - как рассчитать число календарных дней. надеюсь по дате договора и числу дней ты в состоянии посчитать конечную дату. а ты про какие-то вероятности...

но вообще ksnk прав, если это реальная задача, праздники тоже надо учитывать. и база - самый разумный вариант. её размер будет невелик, так что скорость достаточно высока.

Добавлено @ 22:35
Код

function end_date ($start_date, $working_days) {
  $modulo = $working_days%5;
  $weekday = date ('N', $start_date);
  $days=7*(int)($working_days/5)+$modulo+max(0,$modulo?$weekday-3:0);
  return // $start_date + $days
}

разжевал. в рот сам положишь?  smile 

Автор: Gold Dragon 12.1.2014, 23:17
baldina, так если бы ты кодом писал сразу, а то алгоритмами.. вот и гадай что такое %5  smile 
а за разжевал +1  smile 

а вот по базе вопрос.. Мне необходимо "пробежаться" по базе договоров, допустим их порядка 500-1000 и высчитать просрочку.. Не сильно ли это подвисать будет? Хотя если кеш умный сделать...

Автор: baldina 12.1.2014, 23:26
Цитата(Gold Dragon @  12.1.2014,  23:17 Найти цитируемый пост)
Мне необходимо "пробежаться" по базе договоров, допустим их порядка 500-1000 и высчитать просрочку

1000 это очень, очень немного...
Да и не нужна по 1000 договоров такая инфа, максимум по одному. Либо это некий отчет, а отчеты могут генериться не мгновенно, это нормально.

Добавлено через 3 минуты и 4 секунды
кстати. те же подсчеты можно прямо в запросе к базе проводить. и что-то мне подсказывает, что они будут быстрее php... и медленнее join таблицы праздников.

Автор: Gold Dragon 12.1.2014, 23:44
Цитата(baldina @  13.1.2014,  00:26 Найти цитируемый пост)
Либо это некий отчет, а отчеты могут генериться не мгновенно, это нормально.
Это типа корпоративный сайт, и это в том числе отчёты smile

В общем нужно скорость проверять в реальных условиях...

Автор: maxipub 13.1.2014, 14:04
Получаем timestamp начальной даты, и в цикле прибавляем к нему 86400, пока не дойдем до конечной даты, инкрементируя счетчик рабочих дней, где date('w', $timestamp) не 0, и не 6 (вс=0, сб=6). Может быть не минимальные затраты, но все же 5-6 строй кода. Да и корректней будет считать.

ЗЫ: лет 10 сижу на форуме, всегда только спрашивал, что это сегодня решил ответить? smile  smile  smile

Добавлено через 2 минуты и 23 секунды
Цитата(Gold Dragon @  12.1.2014,  23:17 Найти цитируемый пост)
Мне необходимо "пробежаться" по базе договоров, допустим их порядка 500-1000 и высчитать просрочку..

0.05 сек, не более

Автор: Fortop 13.1.2014, 15:03
Цитата(maxipub @  13.1.2014,  14:04 Найти цитируемый пост)
 что это сегодня решил ответить? 


Цитата(maxipub @  13.1.2014,  14:04 Найти цитируемый пост)
Получаем timestamp начальной даты, и в цикле прибавляем к нему 86400, пока не дойдем до конечной даты

В таком духе лучше не отвечать smile

Автор: Gold Dragon 23.1.2014, 21:42
baldina, что-то твой код не хочет рабоать.. уже устал его ломать smile 
например, задаю даты с шагом один день, получаю в два. Или задаю нчало договора в выходной, вообще как-то странно считает

Автор: baldina 24.1.2014, 13:34
Цитата(Gold Dragon @  23.1.2014,  21:42 Найти цитируемый пост)
задаю нчало договора в выходной


Цитата(baldina @  12.1.2014,  20:31 Найти цитируемый пост)
в предположении, что дата договора попадает на рабочий день недели


Цитата(Gold Dragon @  23.1.2014,  21:42 Найти цитируемый пост)
задаю даты с шагом один день, получаю в два

дата договора за день считается: договор может закончиться в тот же день, но не раньше. если надо можешь вычесть 1
http://ideone.com/Ao5zCk

Автор: Gold Dragon 24.1.2014, 13:43
а если так
Код

$start_date = date_timestamp_get(date_create ('2014-01-01'));
for ($i=10; $i < 15; ++$i) {
echo date ('d/m/Y',end_date($start_date,$i)), PHP_EOL;
}

то дата окончания 
Цитата
...
18/01/2014
19/01/2014

А это выходные


PS
выходные - рассматриваем только субботу и воскресенье

Добавлено через 3 минуты и 22 секунды
а если так
Код

$start_date = date_timestamp_get(date_create ('2014-01-04'));
for ($i=10; $i < 15; ++$i) {
echo date ('d/m/Y',end_date($start_date,$i)), PHP_EOL;
}


то имеем
Цитата

18/01/2014 - сб
22/01/2014 - ср
23/01/2014 - чт
24/01/2014 - пт
25/01/2014 - сб

Автор: capitan 24.1.2014, 14:35
Была подобная задача. Показывать дату доставки учитывая выходные, праздники + смещение.

Код

// Исходные данные
    define ('DELIV_SHIFT', 2) ; //смещение доставки ч + дней
    
    // Массив праздников. Ключ - месяц
    $holiday_days = array();
    $holiday_days[1] = array(1,2,3,4,5,6,7,8);
    $holiday_days[2] = array(23);
    $holiday_days[3] = array(8,10);
    $holiday_days[5] = array(1,9);
    $holiday_days[6] = array(12);
    $holiday_days[11] = array(3,4);
    
    
    $date=date("d.m.Y"); // Сегодня
    echo CheckHoliday($date, 0);
        
    function CheckHoliday($date, $count) {    
        global $holiday_days;
        
        $dayName = date('N', strtotime($date));
        $new_data = date("d.m.Y", strtotime($date ."+1 day"));    
                        
        if($dayName == 6 || $dayName == 7) {            
            return CheckHoliday($new_data, $count);
        } else {
            $check_day = date('j', strtotime($new_data));
            $check_month = date('n', strtotime($new_data));
            if(in_array($check_day, $holiday_days[$check_month])) {                    
                return CheckHoliday($new_data, $count);
            } else {
                if($count <= DELIV_SHIFT) {
                    $count +=1;
                    return CheckHoliday($new_data, $count);
                } else {
                    return $date;
                }
                
            }
        }        
    }


Автор: Gold Dragon 24.1.2014, 14:38
так это перебор получается?

Автор: capitan 24.1.2014, 14:43
Цитата(Gold Dragon @ 24.1.2014,  14:38)
так это перебор получается?

А чем не устраивает? У вас где данные лежат по датам договоров и сроках? Если в базе, тогда просто в таблицу добавляем поле, где пишем дату окончания договора. При заполнении, считаем и пишем. Какие проблемы? Зато считаем все точно, учитывая все, что угодно.

Автор: Gold Dragon 24.1.2014, 15:03
capitan, так проблем нет  smile просто я хотел что-то типа функции без перебора.. Я просто думаю что перебор всё же будет медленнее..

с перебором я написал для себя это: (вырезал из кода, т.ч. три метода)
Код

class LibDateTime
{
    /**
     * Возвращает конечную дату без учёта выходных
     *
     * @param string $date   : начальная дата
     * @param int    $days   : количество дней
     * @param string $format : формат возвращаемой даты
     *
     * @return string: дата окончания
     */
    public static function getEndDateWorking($date, $days, $format = 'd.m.Y')
    {
        while ($days) {
            $date = self::getDateAdd($date, 'P1D');
            if (self::formatDate($date, 'N') < 6) {
                $days--;
            }
        }
        return self::formatDate($date, $format);
    }

    /**
     * Прибавляет интервал к дате
     *
     * @param string $date     - дата
     * @param string $interval - интервал в формате ISO 8601, например, P5В (5 дней) или P3Y (3 года)
     *                         Y - Количество лет
     *                         M - Количество месяцев
     *                         D - Количество дней
     * @param string $format   - возвращаемый формат (по умолчанию  d.m.Y)
     *
     * @return string - дата в формате $format
     */
    public static function getDateAdd($date, $interval, $format = 'd.m.Y')
    {
        $d1 = new DateTime($date);
        $result = $d1->add(new DateInterval($interval))->format($format);
        return $result;
    }

    /**
     * Преобразует дату в нужный формат
     *
     * @param string $date   - дата
     * @param string $format - возвращаемый формат (по умолчанию  d.m.Y)
     *
     * @return string
     */
    public static function formatDate($date = '00.00.0000', $format = 'd.m.Y')
    {
        $date_obj = new DateTime($date);
        return $date_obj->format($format);
    }
}

 в условие можно и праздники впихнуть смело. Гонял код, вроде всё корректно отдаёт.

PS
Код скорее всего не будет работать PHP 5 < 5.3.0

Автор: capitan 24.1.2014, 15:18
Каждую задачу нужно рассматривать с позиции целесообразности smile Если потом заказчик скажет, давай будем учитывать ещё и праздники, код полетит к чертям собачим. Я бы это учел сразу. Без учета праздников, учитывая только  сб и вск сделать можно и без перебора.

Автор: Gold Dragon 24.1.2014, 15:20
Цитата(capitan @  24.1.2014,  16:18 Найти цитируемый пост)
Без учета праздников, учитывая только  сб и вск сделать можно и без перебора. 
да вот не смог.. или дни вылетают или конечная дата приходится на выходной..

Автор: capitan 24.1.2014, 16:52
Нужно тестить, поидее должно работать:
Код

    $date=date("d.m.Y"); // Сегодня
    echo CheckHolidayBeta($date, 33)."<br/>";

    function CheckHolidayBeta($date, $count) {

        $dayShift = date('N', strtotime($date));
        $count += $dayShift;

        $week_count = floor($count/5);
        $lastWeekDays = $week_count*7 - $dayShift + ($count % 5) + 1;

        $dateEnd = date("d.m.Y", strtotime($date ."+{$lastWeekDays} day"));
        $dateEndName = date('N', strtotime($dateEnd));
        $shiftEnd = ($dateEndName > 5) ? 7 - $dateEndName + 1 : 0;

        if(!empty($shiftEnd)) {
            echo date("d.m.Y", strtotime($dateEnd ."+{$shiftEnd} day"));
        } else {
            echo  $dateEnd;
        }
     }

Автор: Gold Dragon 24.1.2014, 19:43
Цитата(capitan @  24.1.2014,  17:52 Найти цитируемый пост)
Нужно тестить, поидее должно работать:

Это 
Код

echo CheckHolidayBeta('03.01.2014', 10);

выдаёт 20.01.2014, хотя должно 17-е

Автор: ksnk 24.1.2014, 21:25
а так?
Код

<?php
/**
 * class to hold all-around-calendar function
 */
class calendar
{

    /**
     * just a source; key - d/m/Y ; true==holiday, false==workdaay.
     * if absent - use weekday to consider
     * http://www.garant.ru/calendar/buhpravo/
     */
    private static $workdays = array(
//2013
        '01/01/2013' => true,
        '02/01/2013' => true,
        '03/01/2013' => true,
        '04/01/2013' => true,
        '07/01/2013' => true,
        '08/01/2013' => true,
        '08/03/2013' => true,
        '01/05/2013' => true,
        '02/05/2013' => true,
        '03/05/2013' => true,
        '09/05/2013' => true,
        '10/05/2013' => true,
        '12/06/2013' => true,
        '04/11/2013' => true,
// 2014
        '01/01/2014' => true,
        '02/01/2014' => true,
        '03/01/2014' => true,
        '06/01/2014' => true,
        '07/01/2014' => true,
        '08/01/2014' => true,
        '10/03/2014' => true,
        '01/05/2014' => true,
        '02/05/2014' => true,
        '09/05/2014' => true,
        '12/06/2014' => true,
        '03/11/2014' => true,
        '04/11/2014' => true,
    );

    /**
     * return day $workdays days after $day
     * @param string|int $day
     * @param int $workdays
     * @return int|string
     */
    static function addWorkDays($day, $workdays)
    {
        if (is_string($day)) {
            $day = strtotime($day);
        }
        while ($workdays > 0) {
            $day = strtotime('+1 day', $day);
            $key = date('d/m/Y', $day);
            if (isset(self::$workdays[$key])) {
                if (!self::$workdays[$key]) {
                    $workdays--;
                }
            } else {
                switch (date('w', $day)) {
                    case '0':
                    case '6':
                        break;
                    default:
                        $workdays--;
                }
            }
        }
        return $day;
    }
}

/**
 *  so let's test it
 */

echo date('d/m/Y', calendar::addWorkDays('01.01.2014', 10)) . "\n";
echo date('d/m/Y', calendar::addWorkDays('08.01.2014', 10)) . "\n";
echo date('d/m/Y', calendar::addWorkDays('01.01.2014', 8)) . "\n";
echo date('d/m/Y', calendar::addWorkDays('03.01.2014', 7)) . "\n";


P.S. слегка отформатировал и добавил 2013 год

Автор: ksnk 25.1.2014, 11:43
Если в моем коде заменить strtotime('+1 day'... на +=60*60*24, и  поменять ключики массива праздников на их числовые значения с выкидыванием формирования ключа, то работать будет в 4 раза быстрее.

Вообще, о чем такая задача? 
На своей машинке - ноутбук с i3, 1000 200-дневных периодов посчиталось за 7 сек (исходно опубликованный - 30 сек). Как показывает практика - сервера (в моей доступности) решают такие задачи в 5-10 раз быстрее. Может быть и хватит скорости?

Автор: capitan 25.1.2014, 16:10
Цитата(Gold Dragon @ 24.1.2014,  19:43)
Цитата(capitan @  24.1.2014,  17:52 Найти цитируемый пост)
Нужно тестить, поидее должно работать:

Это 
Код

echo CheckHolidayBeta('03.01.2014', 10);

выдаёт 20.01.2014, хотя должно 17-е

не учел последние выхи на неделе.
Делаем так:
строку:
Код

       $lastWeekDays = $week_count*7 - $dayShift + ($count % 5) + 1;


меняем на:
Код

        // Fix на последние выходные на неделе
        $fix_end = ( $count % 5 > 0 ) ? 0 : 2;
        $lastWeekDays = $week_count*7 - $dayShift + ($count % 5) - $fix_end;


Теперь должно работать.

Автор: baldina 28.1.2014, 19:53
Gold Dragon, если учитывать все обстоятельства, то как-то так
http://ideone.com/mIyRNv

Добавлено через 1 минуту и 3 секунды
с базой попроще будет ;-)

Автор: Gold Dragon 29.1.2014, 08:19
протестировал малость..

Тест:
Код

$sysstart = microtime(true);
for($i = 0; $i < 250; $i++){
    LibDateTime::getEndDateWorking('12.01.2014', 50);
}
echo round((microtime(true) - $sysstart), 5). '<hr>';



мой код:

250 запросов 50 дней   - 3.78722
250 запросов 100 дней - 7.81845
500 запросов 50 дней   - 7.61444
500 запросов 100 дней - 15.58989

код baldina:

250 запросов 50 дней   - 0.016
250 запросов 100 дней - 0.011
500 запросов 50 дней   - 0.022
500 запросов 100 дней - 0.022

 smile 

Вот вам и проблемы перебора.. Осталось проверить правильность

Добавлено через 12 минут и 41 секунду
baldina, 2014-01-04 10 дней всё таки 17.01 а не 20.01

Автор: baldina 29.1.2014, 09:46
Цитата(Gold Dragon @  29.1.2014,  08:19 Найти цитируемый пост)
baldina, 2014-01-04 10 дней всё таки 17.01 а не 20.01 

ну ты уж определись, считать или нет день даты договора

Автор: Gold Dragon 29.1.2014, 09:56
ну вообще-то нет. "Шесть частей, но пять распилов"

   1    2    3   4    5
3---4---5---6---7---8


Автор: baldina 29.1.2014, 12:08
ну тады вычти один день. сможешь? ;-)

Автор: Gold Dragon 29.1.2014, 12:12
что-то совсем запутался..  smile 
в других вариантах правильно считает.. видимо перекос если начало суббота или воскресенье..

Автор: baldina 29.1.2014, 12:13
Код

  if ($weekday > 4) {
    $start_date -= ONE_DAY*(6-$weekday);
    $weekday=4;
  }


Добавлено @ 12:13
http://ideone.com/IHqHPV

Добавлено @ 12:14
т.е. двигаем не на пн а на предыдущую пт

Автор: baldina 29.1.2014, 12:58
наврал
Код

$start_date -= ONE_DAY*($weekday-4);

http://ideone.com/cSskNE

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)