Skip to content

Instantly share code, notes, and snippets.

@jurchiks
Last active May 12, 2022 17:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jurchiks/62ee8c4267dde48a296a8c2ab18965df to your computer and use it in GitHub Desktop.
Save jurchiks/62ee8c4267dde48a296a8c2ab18965df to your computer and use it in GitHub Desktop.
PHP class to calculate workdays/holidays after a given date/between dates
$weekdayCalculator = new WeekdayCalculator();
$weekdayCalculator->addSpecialWorkday(new Date('2022-01-08')); // Override Saturday as a workday.
// Sunday stays a normal holiday as usual.
$weekdayCalculator->addSpecialHoliday(new Date('2022-01-10')); // Override the following Monday as a holiday.
// As a result, we've shifted the workday one day back.

var_dump(
	$weekdayCalculator->isWorkday(new Date('2022-01-08')), // Returns TRUE - we manually set this date as a workday.
	$weekdayCalculator->isHoliday(new Date('2022-01-09')), // Returns TRUE - normal holiday.
	$weekdayCalculator->getNextWorkday(new Date('2022-01-07')), // Returns Date(2022-01-08).
	$weekdayCalculator->getNextWorkday(new Date('2022-01-08')), // Returns Date(2022-01-11) - 9, 10 = holidays, 11 = normal workday.
	$weekdayCalculator->getNextHoliday(new Date('2022-01-08')), // Returns Date(2022-01-09).
	$weekdayCalculator->countDays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns int(31) - January has 31 days, 02-01 is excluded.
	$weekdayCalculator->countWorkdays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns int(21).
	$weekdayCalculator->countHolidays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns int(10).
	$weekdayCalculator->getDays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns a Generator instance for all days between specified dates.
	$weekdayCalculator->getWorkdays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns a Generator instance for all workdays.
	$weekdayCalculator->getHolidays(new Date('2022-01-01'), new Date('2022-02-01')), // Returns a Generator instance for all holidays.
	iterator_to_array($weekdayCalculator->getWorkdays(new Date('2022-01-01'), new Date('2022-02-01'))), // Returns an array of 21 Date instances.
);
<?php
use Cake\Chronos\ChronosInterval;
use Cake\Chronos\Date;
class WeekdayCalculator
{
private array $workdays;
private array $specialHolidays = [];
private array $specialWorkdays = [];
public function __construct(array $workdays = [Date::MONDAY, Date::TUESDAY, Date::WEDNESDAY, Date::THURSDAY, Date::FRIDAY])
{
$this->workdays = $workdays;
}
/**
* Set a date as a workday, overriding any holiday on that date.
*
* @param Date $workday
*/
public function addSpecialWorkday(Date $workday): void
{
$this->specialWorkdays[$workday->toDateString()] = true;
}
/**
* Set a date as a holiday, overriding normal work day on that date.
*
* @param Date $holiday
*/
public function addSpecialHoliday(Date $holiday): void
{
$this->specialHolidays[$holiday->toDateString()] = true;
}
public function isWorkday(Date $date): bool
{
$dateStr = $date->toDateString();
if (isset($this->specialWorkdays[$dateStr])) {
return true;
} else if (isset($this->specialHolidays[$dateStr])) {
return false;
} else if (in_array($date->dayOfWeek, $this->workdays, true)) {
return true;
} else {
return false;
}
}
public function isHoliday(Date $date): bool
{
return !$this->isWorkday($date);
}
public function getNextWorkday(Date $dateFrom): Date
{
do {
$dateFrom = $dateFrom->addDay();
if ($this->isWorkday($dateFrom)) {
break;
}
} while (true);
return $dateFrom;
}
public function getNextHoliday(Date $dateFrom): Date
{
do {
$dateFrom = $dateFrom->addDay();
if ($this->isHoliday($dateFrom)) {
break;
}
} while (true);
return $dateFrom;
}
/**
* @param Date $dateFromIncluding
* @param Date $dateTillExcluding
* @return Generator<Date> - use {@link iterator_to_array()} on the return value to get an array of {@link Date} objects.
*/
public function getWorkdays(Date $dateFromIncluding, Date $dateTillExcluding): Generator
{
/** @var Date $date */
foreach ($this->getDatePeriod($dateFromIncluding, $dateTillExcluding) as $date) {
if ($this->isWorkday($date)) {
yield $date;
}
}
}
public function countWorkdays(Date $dateFromIncluding, Date $dateTillExcluding): int
{
return iterator_count($this->getWorkdays($dateFromIncluding, $dateTillExcluding));
}
/**
* @param Date $dateFromIncluding
* @param Date $dateTillExcluding
* @return Generator<Date> - use {@link iterator_to_array()} on the return value to get an array of {@link Date} objects.
*/
public function getHolidays(Date $dateFromIncluding, Date $dateTillExcluding): Generator
{
/** @var Date $date */
foreach ($this->getDatePeriod($dateFromIncluding, $dateTillExcluding) as $date) {
if ($this->isHoliday($date)) {
yield $date;
}
}
}
public function countHolidays(Date $dateFromIncluding, Date $dateTillExcluding): int
{
return iterator_count($this->getHolidays($dateFromIncluding, $dateTillExcluding));
}
/**
* @param Date $dateFromIncluding
* @param Date $dateTillExcluding
* @return Generator<Date> - use {@link iterator_to_array()} on the return value to get an array of {@link Date} objects.
*/
public function getDays(Date $dateFromIncluding, Date $dateTillExcluding): Generator
{
foreach ($this->getDatePeriod($dateFromIncluding, $dateTillExcluding) as $date) {
yield $date;
}
}
public function countDays(Date $dateFromIncluding, Date $dateTillExcluding): int
{
return iterator_count($this->getDatePeriod($dateFromIncluding, $dateTillExcluding));
}
private function getDatePeriod(Date $dateFromIncluding, Date $dateTillExcluding): DatePeriod
{
return new DatePeriod($dateFromIncluding, ChronosInterval::day(), $dateTillExcluding);
}
}
@jurchiks
Copy link
Author

This class depends on cakephp/chronos for much simplified handling of dates.
This allows me to avoid validating input data myself, unlike most variations of this concept that try to do it with date strings or timestamps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment