Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 88
WorkDayCalculator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 13
1892.00
0.00% covered (danger)
0.00%
0 / 88
 __construct
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 4
 roundDate
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 setTime
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 hourToWorkableHour
n/a
0 / 0
1
n/a
0 / 0
 add
n/a
0 / 0
1
n/a
0 / 0
 sub
n/a
0 / 0
1
n/a
0 / 0
 addWorkDay
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 11
 nextWorkDay
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 4
 subWorkDay
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 11
 prevWorkDay
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 4
 numberOfWorkDays
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 9
 workDaysArray
0.00% covered (danger)
0.00%
0 / 1
110.00
0.00% covered (danger)
0.00%
0 / 22
 is_nonWorkDay
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 _is_nonWorkDay
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 set_workHoursByWeekDay
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 workHoursByDayCacheCommon
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 17
 toJson
n/a
0 / 0
1
n/a
0 / 0
<?php
namespace ia\Work\WorkDay;
// https://period.thephpleague.com/
class WorkDayCalculator {
    protected $byHour = false;
    protected $dayCalculator;
    /** @var array [dayOfWeek=>[[8,14],[15, 18]], ...] 0=Sun,...,6=Sat. */
    public static $workHoursByWeekDayDefault = [
                1=>[[9,17],],
                2=>[[9,17],],
                3=>[[9,17],],
                4=>[[9,17],],
                5=>[[9,17],],
            ];
    /** @var array [dayOfWeek=>[[8,14],[15, 18]], ...] 0=Sun,...,6=Sat. */
    protected $workHoursByWeekDay;
    /** @var array [dayOfWeek=>[[15,18],[8, 14]], ...] 0=Sun,...,6=Sat. */
    protected $workHoursByWeekDayReversed;
    /** @var array [dayOfWeek=>int, ...]  */
    protected $totalWorkHourByWeekDay;
    /** @var array [dayOfWeek=>int, ...]
    */
    protected $firstWorkHourByWeekDay;
    /** @var array [dayOfWeek=>int, ...]
     *  simulate array_key_first by caching first key, for previous to php 7.3
    */
    protected $lastWorkHourByWeekDay;
    /** @var DateInterval 1 day interval
     *  simulate array_key_last by caching last key, for previous to php 7.3
    */
    protected $dateIntervalOneDay;
    public function __construct($dayCalculator) {
        $this->dateIntervalOneDay = new \DateInterval("P1D");
        $this->dayCalculator = $dayCalculator;
        $this->set_workHoursByWeekDay($dayCalculator->get_weekDaySchedule());
    }
// OVERRIDE _______________________________________
    protected function roundDate($anyDate) {
        return self::toDateImmutable($anyDate); // DAY
        // return self::toDateHourImmutable($anyDate); // Hour
    }
    protected function setTime(\DateTimeImmutable $dateTime) {
        return $dateTime->setTime(0, 0, 0); // day
        //return $dateTime->setTime($this->firstWorkHourByWeekDay[$dateTime->format('w')], 0, 0); // por hour
    }
    public function hourToWorkableHour(\DateTimeImmutable $dateTime) { return $dateTime->setTime(0, 0, 0); }
    /**
     * Add units (days in days, overrided for hours in WorkHourCalculator)
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDate
     * @param integer|string $numDays
     * @return DateTimeImmutable with $numDays workable units days added, ie after
     */
    public function add($anyDate, $numDays) { return $this->addWorkDay($anyDate, $numDays); }
    /**
     * Substract units (days in days, overrided for hours in WorkHourCalculator)
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDate
     * @param integer|string $numDays
     * @return DateTimeImmutable with $numDays workable units days/hours subsctracted, ie before
     */
    public function sub($anyDate, $numDays) { return $this->subWorkDay($anyDate, $numDays); }
// BY DAY ________________
    /**
     * Add workable days to a date
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDate
     * @param integer|string $numDays
     * @return DateTimeImmutable with $numDays workable days added, ie after
     */
    public function addWorkDay($anyDate, $numDays = 1) {
        $numDays = (int)$numDays;
        if($numDays < 0) {
            return $this->subWorkDay($anyDate, -$numDays);
        }
        $dateTime = $this->hourToWorkableHour(self::toDateHourImmutable($anyDate));
        if($this->_is_nonWorkDay($dateTime)) {
            $dateTime = $this->nextWorkDay($dateTime);
        }
        if($numDays == 0) {
            return $dateTime;
        }
        for($i=1; $i<=$numDays; $i++) {
            $dateTime = $this->nextWorkDay($dateTime);
        }
        return $this->hourToWorkableHour($dateTime);
    }
    /**
     * Get next or following workable day
     *
     * @param DateTimeImmutable $dateTime
     * @return DateTimeImmutable next or following workable day
     */
    protected function nextWorkDay(\DateTimeImmutable $dateTime) {
        $preventEternalLoop = 0;
        do {
            $dateTime = $dateTime->add($this->dateIntervalOneDay);
        } while($this->_is_nonWorkDay($dateTime) && ++$preventEternalLoop < 367);
        return $this->setTime($dateTime);
        //return $dateTime->setTime($this->firstWorkHourByWeekDay[$dateTime->format('w')], 0, 0);
    }
    /**
     * Substract workable days to a date
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDate
     * @param integer|string $numDays
     * @return DateTimeImmutable with $numDays workable days subsctracted, ie before
     */
    public function subWorkDay($anyDate, $numDays = 1) {
        $numDays = (int)$numDays;
        if($numDays < 0) {
            return $this->addWorkDay($anyDate, -$numDays);
        }
        $dateTime = $this->hourToWorkableHour($this->roundDate($anyDate));
        if( $this->_is_nonWorkDay($dateTime)  ) {
           $dateTime = $this->prevWorkDay($dateTime);
        }
        if($numDays === 0) {
            return $dateTime;
        }
        for($i = 1; $i <= $numDays; $i++) {
            $dateTime = $this->prevWorkDay($dateTime);
        }
        return $dateTime;
    }
    /**
     * Get previous, or the day before, workable day
     *
     * @param DateTimeImmutable $dateTime
     * @return DateTimeImmutable previous, or the day before, workable day
     */
    protected function prevWorkDay(\DateTimeImmutable $dateTime) {
        $preventEternalLoop = 0;
        do {
            $dateTime = $dateTime->sub($this->dateIntervalOneDay);
        } while($this->_is_nonWorkDay($dateTime) && ++$preventEternalLoop < 367);
        return $this->setTime($dateTime);
        //return $dateTime->setTime($this->firstWorkHourByWeekDay[$dateTime->format('w')], 0, 0);
    }
    /**
     * Number of workable days between dates $anyDateFrom and $anyDateTo
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDateFrom
     * @param string|integer|DateTime|DateTimeImmutable $anyDateTo
     * @return int Number of days workable between dates $anyDateFrom and $anyDateTo
     */
    public function numberOfWorkDays($anyDateFrom, $anyDateTo) {
        $start = $this->roundDate($anyDateFrom);
        $end = $this->roundDate($anyDateTo);
        $swaped = self::orderd($start, $end);
        $days = 0;
        while($end >= $start) {
            if(!$this->_is_nonWorkDay($start)) {
                $days++;
            }
            $start = $start->add($this->dateIntervalOneDay);
        }
        return $swaped * $days;
    }
    /**
     * Array of workable days between dateTimes $anyDateFrom to $anyDateTo
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDateFrom
     * @param string|integer|DateTime|DateTimeImmutable $anyDateTo
     * @return array ['Y-m-d'=>Number_WorkableHours, ...] orderd from $anyDateFrom to $anyDateTo
     */
    public function workDaysArray($anyDateFrom, $anyDateTo = null) {
        $firstDate = $this->roundDate($anyDateFrom);
        if($anyDateTo == null) {
            $anyDateTo = $anyDateFrom;
        }
        $lastDate = $this->roundDate($anyDateTo);
        $swaped = self::orderd($firstDate, $lastDate);
        $lastDayIsWorkDay = !$this->_is_nonWorkDay($lastDate);
        if($firstDate->format('Y-m-d') === $lastDate->format('Y-m-d')) {
            if(!$lastDayIsWorkDay) {
                return [];
            }
            if($this->byHour && $firstDate == $lastDate) {
                return [$firstDate->format('Y-m-d') => 0];
            }
            return [ $firstDate->format('Y-m-d') => $this->totalWorkHourByWeekDay[$firstDate->format('w')] ];
        }
        $workDays = [];
        while($lastDate > $firstDate) {
            if(!$this->_is_nonWorkDay($firstDate)) {
                $workDays[$firstDate->format('Y-m-d')] = $this->totalWorkHourByWeekDay[$firstDate->format('w')];
            }
            $firstDate = $firstDate->add($this->dateIntervalOneDay);
        }
        if($lastDayIsWorkDay) {
            $workDays[$lastDate->format('Y-m-d')] =  $this->totalWorkHourByWeekDay[$lastDate->format('w')];
        }
        if($swaped === -1) {
            return \array_reverse($workDays, true);
        }
        return $workDays;
    }
// ___________________________________________________________________________________________
    /**
     * Is day not workable
     *
     * @param string|integer|DateTime|DateTimeImmutable $anyDate
     * @return boolean true for non workable day, true is a work day
     */
    public function is_nonWorkDay($anyDate) {
        return $this->_is_nonWorkDay(self::toDateImmutable($anyDate));
    }
    /**
     * Is day not workable
     *
     * @param DateTimeImmutable $date
     * @return boolean true for non workable day, true is a work day
     */
    protected function _is_nonWorkDay(\DateTimeImmutable $date) {
        return !$this->dayCalculator->is_workable($date);
    }
// ___________________________________________________________________________________________
    /**
     * Set work hours per day of week
     *
     * @param array $workHoursByWeekDay [dayOfWeek=>[[8,14],[15, 18]], ...] 0=Sun,...,6=Sat.
     * @return void
     */
    protected function set_workHoursByWeekDay(array $workHoursByWeekDay) {
        //$workHoursByWeekDay = self::validateWorkHoursByWeekDay($workHoursByWeekDay);
        $this->workHoursByDayCacheCommon($workHoursByWeekDay);
    }
    /**
     * Calculate and save common used values
     *
     * @param array $workHoursByWeekDay [dayOfWeek=>[[8,14],[15, 18]], ...] 0=Sun,...,6=Sat.
     * @return void
     */
    protected function workHoursByDayCacheCommon(array $workHoursByWeekDay) {
        $firstWorkHourByWeekDay = [];
        $lastWorkHourByWeekDay = [];
        $totalWorkHourByWeekDay = [];
        foreach($workHoursByWeekDay as $dayOfWeek => &$horarios) {
            $totalHours = 0;
            foreach($horarios as $rango) {
                $totalHours += (int)$rango[1] - (int)$rango[0];
            }
            if($totalHours > 0) {
                $totalWorkHourByWeekDay[$dayOfWeek] = $totalHours;
            }
            $lastWorkHourByWeekDay[$dayOfWeek] = \end($horarios)[1];
            $firstWorkHourByWeekDay[$dayOfWeek] = \reset($horarios)[0];
            $this->workHoursByWeekDayReversed[$dayOfWeek] = \array_reverse($horarios);
        }
        $this->workHoursByWeekDay = $workHoursByWeekDay;
        $this->firstWorkHourByWeekDay = $firstWorkHourByWeekDay;
        $this->lastWorkHourByWeekDay = $lastWorkHourByWeekDay;
        $this->totalWorkHourByWeekDay = $totalWorkHourByWeekDay;
    }
    /**
     * Get json encoded workday Data
     *
     * @return string|bool json encoded workday Data false on error
     *
     * @codeCoverageIgnore
     */
    public function toJson() {
        return \json_encode([
            'workHoursByWeekDay' => $this->workHoursByWeekDay,
            'workHoursByWeekDayReversed' => $this->workHoursByWeekDayReversed,
            'firstWorkHourByWeekDay' => $this->firstWorkHourByWeekDay,
            'lastWorkHourByWeekDay' => $this->lastWorkHourByWeekDay,
            'totalWorkHourByWeekDay' => $this->totalWorkHourByWeekDay,
            'holidays' => $this->dayCalculator->get_AllMarkedDays()
        ]);
    }
}