Расчет рабочих дней на C#


Обычной задачей является вычисление количества рабочих дней в пределе диапазона дат. В большинстве западных культур это дни с понедельника по пятницу, исключая субботу и воскресенье. Полученное число также должно исключать национальные праздники.

Рабочие дни

Программное обеспечение, которое записывает или вычисляет продолжительность, часто нуждается в определении количества дней в диапазоне дат. Это простая задача при использовании структуры даты и времени. Однако, когда программное обеспечение предназначено для использования предприятиями, иногда необходимо выполнить такой расчет с использованием рабочих дней. Для большинства западных культур это только дни между понедельником и пятницей. Суббота и воскресенье являются выходными и не считаются рабочими днями. Кроме того, некоторые даты являются национальными праздниками, которые также не являются рабочими днями.

В этой статье мы создадим класс, содержащий один общедоступный метод, который вычисляет количество рабочих дней в диапазоне дат. Алгоритм сначала определит количество дней в диапазоне, а затем вычтет количество выходных и праздничных дней. Предполагается, что стандартная рабочая неделя начинается в понедельник и заканчивается в пятницу, хотя было бы легко изменить расчет для других культур. Эта версия алгоритма будет включительной, с учетом даты начала и окончания. Диапазон дат с понедельника по пятницу, при условии отсутствия праздничных дней, должен возвращать результат в пять дней.

Расчет количества рабочих дней между двумя датами

Для начала создайте новый проект нового консольного приложения. Добавьте новый класс в проект с именем "BusinessDaysCalculator". Этот класс будет содержать код алгоритма.

Создание метода расчета

Единственным общедоступным методом в классе BusinessDaysCalculator является тот, который выполняет вычисления. Этот метод требует трех входных параметров. Первые два параметра принимают даты начала и окончания диапазона. Третий параметр используется для предоставления массива дат, которые являются праздничными днями. Массив может включать даты, которые не входят в основной диапазон дат. Однако это не повлияет на конечный результат. Расчет всегда будет возвращать целое значение, содержащее количество рабочих дней в заданном диапазоне.

Чтобы создать основу для нового метода, добавьте следующее объявление в класс BusinessDaysCalculator:

public int Calculate(DateTime startDate, DateTime endDate, DateTime[] holidays)
{
}
Подготовка диапазона дат

При работе с диапазоном дат нас интересует только часть даты начала, окончания и праздничных дат. Временная часть даты и времени должна быть установлена на полночь. Для даты начала и окончания мы также должны иметь возможность предположить, что каждая дата приходится на будний день. Вместо того, чтобы предполагать, что значения, предоставленные методу, соответствуют этим критериям, мы создадим частные методы, которые будут применять их, изменяя диапазон дат по мере необходимости.

Первый из этих методов устранит любые проблемы с датой начала. Сначала будет проверен день недели. Если дата начала приходится на выходные, она будет перенесена на следующий понедельник. Во-вторых, часть времени в DateTime будет удалена. Чтобы создать этот метод, добавьте в класс следующее:

private DateTime FixStartDate(DateTime date)
{
    if (date.DayOfWeek == DayOfWeek.Sunday)
        date = date.AddDays(1);
    else if (date.DayOfWeek == DayOfWeek.Saturday)
        date = date.AddDays(2);
    return date.Date;
}
Аналогичный метод устранит проблемы с датой окончания. Разница здесь в том, что если дата приходится на субботу или воскресенье, она будет изменена на предыдущую пятницу.

private DateTime FixEndDate(DateTime date)
{
    if (date.DayOfWeek == DayOfWeek.Sunday)
        date = date.AddDays(-2);
    else if (date.DayOfWeek == DayOfWeek.Saturday)
        date = date.AddDays(-1);
    return date.Date;
}
Чтобы убедиться, что любые проблемы с датами ввода будут устранены до выполнения расчета, добавьте вызовы двух новых методов, добавив следующий код в метод расчета:

startDate = FixStartDate(startDate);
endDate = FixEndDate(endDate.Date);
Проверка на наличие недопустимых диапазонов дат

Приведенный выше код гарантирует, что как дата начала, так и дата окончания допустимы для использования в алгоритме. Однако это не гарантирует, что сам диапазон действителен. Возможно, что дата начала наступает после даты окончания или что исходные даты, указанные в методе, приходятся на одни и те же выходные. В любом случае диапазон следует считать недопустимым. Если диапазон неверен, мы просто вернем ноль. Чтобы включить эту окончательную проверку, добавьте следующий код в конец метода расчета. Код проверяет диапазон дат и возвращает ноль, если он неверен. Если он действителен, вызывается другой частный метод, который вычисляет количество рабочих дней в диапазоне.

if (startDate > endDate)
    return 0;
else
    return CalculateDifference(startDate, endDate, holidays);
Проверка, находится ли отпуск в пределах диапазона дат

Следующий частный метод, который мы добавим в класс, используется для определения того, влияет ли один отпуск на расчет. Этот метод возвращает логический результат, значение true указывает, что праздник находится в пределах диапазона дат и не приходится на выходные.

Добавьте следующий метод в класс BusinessDaysCalculator:

private bool HolidayInRange(DateTime startDate, DateTime endDate, DateTime holiday)
{
    return (holiday.Date >= startDate && holiday <= endDate
        && holiday.DayOfWeek != DayOfWeek.Saturday
        && holiday.DayOfWeek != DayOfWeek.Sunday);
}
Подсчет количества праздничных дней в диапазоне дат

Метод HolidayInRange неоднократно используется другим частным методом. Метод подсчета отпусков определяет общее количество отпусков, влияющих на расчет. Для этого метод перебирает все праздники в массиве и добавляет один к счетчику каждый раз, когда вызов HolidayInRange возвращает значение true. У этого метода есть ограничение. Если одна и та же дата появляется дважды или более в массиве праздников, она будет учитываться более одного раза. Это можно исправить, убедившись, что массив содержит только уникальные значения.

private int HolidayCount(DateTime startDate, DateTime endDate, DateTime[] holidays)
{
    int holidayCount = 0;
    foreach (DateTime holiday in holidays)
    {
        if (HolidayInRange(startDate, endDate, holiday)) holidayCount++;
    }
    return holidayCount;
}
Если вы используете версию .NET framework, которая поддерживает запрос, интегрированный с языком (LINQ), вы можете заменить код в методе HolidayCount выражением LINQ. Приведенное ниже выражение подсчитывает количество праздничных дней, влияющих на общий расчет. Использование Distinct также гарантирует, что любые повторяющиеся праздники учитываются только один раз.

return (from h in holidays
        where HolidayInRange(startDate, endDate, h)
        select h).Distinct().Count();
Окончательный расчет

Заключительной задачей для завершения алгоритма является добавление метода CalculateDifference. Этот метод выполняет несколько шагов для определения количества рабочих дней в указанном диапазоне дат. Шаги следующие:

  1. Рассчитайте разницу в днях между двумя датами. Это достигается путем вычитания даты начала из даты окончания. Результатом вычитания является значение промежутка времени, содержащее количество дней в свойстве TotalDays. Значение будет включать начало, но не конец диапазона. Это будет исправлено позже, чтобы дать всеобъемлющий результат.
  2. Определите количество выходных дней, которые находятся в диапазоне, разделив количество дней на семь, отбросив любую дробь и сохранив только целое число. Если день недели даты окончания предшествует дню недели даты начала, значение недель увеличивается по мере наличия дополнительных выходных.
  3. Уменьшите значение разницы, чтобы удалить выходные. Разница уменьшается на количество недель, умноженное на два.
  4. Уменьшите значение разницы на количество праздничных дней в диапазоне.
  5. Добавьте единицу к результату, прежде чем возвращать его, чтобы сделать значение включающим.


private int CalculateDifference(DateTime startDate, DateTime endDate, DateTime[] holidays)
{
    int difference = (int)(endDate - startDate).TotalDays;
    int weeks = difference / 7;
    if (endDate.DayOfWeek < startDate.DayOfWeek) weeks++;
    difference -= weeks * 2;
    difference -= HolidayCount(startDate, endDate, holidays);
    return difference + 1;
}
Использование калькулятора

Для тестирования алгоритма мы можем использовать метод Main. Добавьте код ниже, чтобы рассчитать количество рабочих дней в феврале 2023 года, при условии отсутствия праздников,

DateTime start = new DateTime(2023,2,1);
DateTime end = new DateTime(2023, 2, 28);
BusinessDaysCalculator calc = new BusinessDaysCalculator();
int days = calc.Calculate(start, end, new DateTime[0]);
 
Console.WriteLine(days);   
Чтобы использовать алгоритм и включить праздники в расчет, попробуйте выполнить следующее:

DateTime[] holidays = new DateTime[1];
holidays[0] = new DateTime(2023, 2, 23);
 
DateTime start = new DateTime(2023,2,1);
DateTime end = new DateTime(2023, 2, 28);

BusinessDaysCalculator calc = new BusinessDaysCalculator();
int days = calc.Calculate(start, end, holidays);
 
Console.WriteLine(days);    
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегизаметки, си шарп, математика, алгоритмы




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Роман "Работа во сне": Глава №1
Урок 36. Коллекция Hashtable C#
Магия закончилась: часть 1