Алгоритм подсчёта количества рабочих дней

При разработке программного обеспечения, направленного на использование в каких-либо производственных процессах, частой является задача расчёта количества рабочих дней в заданном диапазоне дат. Также могут применяться и другие задачи, например, прибавить (или отнять) определенное количество рабочих дней от заданной даты и так далее. В большинстве случаев, рабочими считаются дни с понедельника по пятницу, исключая национальные праздники. При этом, например, в Российской Федерации может использоваться как шести-, так и пятидневная рабочая неделя, а праздничные и рабочие дни могут переноситься. В этой статье мы попытаемся разработать свой собственный класс, реализующий подсчет рабочих дней в заданном диапазоне, а также, при необходимости добавление и вычитание рабочих дней из определенной даты.

Входные данные

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

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

Во-вторых, для определения праздничных дней и переносов нам необходим производственный календарь, например, воспользуемся производственным календарем на 2024 год от Консультант+. Этот календарь мы будем использовать как для тестирования алгоритма, так и для составления необходимых коллекций дат.

Базовый алгоритм расчёта рабочих дней в диапазоне дат

В самом общем случае, когда учитываются только выходные дни (суббота и воскресенье), но не учитываются праздники, переносы выходных и другие национальные особенности определения рабочих дней, алгоритм расчёта рабочих дней можно представить следующим образом.

1. Скорректировать диапазон дат так, чтобы началом и концом диапазона были рабочие дни

Например, возьмем две даты:  01.09.2024 (воскресенье) по 29.09.2024 (воскресенье).

Диапазон дат Первый день диапазона воскресенье — следовательно мы должны прибавить один день к началу диапазона (двигаемся вперед, чтобы не выйти за пределы диапазона) и получим начало диапазона — 02.09.2024.

Последний день диапазона также воскресенье — следовательно, чтобы получить рабочий день в диапазоне, мы должны отнять от даты два дня (двигаемся назад, чтобы не выйти за пределы диапазона) и получим 27.09.2024.

Аналогичные действия нам необходимо производить и в случае, если начало или конец диапазона выпадает на субботу — также двигаемся вперед или назад. чтобы получить ближайший рабочий день внутри заданного диапазона.

Таким образом, диапазон дат, с которыми мы будем дальше работать будет выглядеть следующим образом:

 

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

Для этого берем значения диапазона из предыдущего шага и просто отнимаем от конечной даты начальную. При этом, следует учитывать, что такой расчёт в C# не включает конечную дату, то есть количество дней будет меньше на 1. Например,

DateTime start = new(2024, 9, 2);
DateTime end = new(2024, 9, 27);

Console.WriteLine((end-start).TotalDays); //результат - 25, хотя с учётом конечной даты должно быть 26 дней
Этот момент учитывается в конце расчёта путем добавления единицы к полученному результату

3. Определить количество выходных дней в диапазоне 

Мы точно знаем, что в каждой неделе ровно два выходных — суббота и воскресенье. Для расчёта количества выходных дней мы должны выполнить следующие шаги:

  1. полученное на предыдущем шаге количество дней, разделить его на 7 и отбросить любую дробную часть результата. Таким образом, мы получим целое количество недель в диапазоне.
  2. если день недели конца диапазона предшествует дню недели начала диапазона, то количество недель необходимо увеличить на 1
  3. полученное количество недель умножить на два.

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

Рассмотрим вот такой диапазон: 02.01.2024 (вторник) и 15.01.2024 (понедельник), не учитывая праздничные дни — только субботы и воскресенья.

На первом шаге алгоритма мы получили значение 13. Делим это число на 7 и отбрасываем дробную часть:

13/7 => 1,85 => 1

В C# у структуры DateTime имеется свойство DayOfWeek, возвращающее день недели, которому соответствует определенная дата. Это свойство представлено одноименном перечислением, которое выглядит следующим образом:

public enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6
}

Теперь откроем календарь и посмотрим на дату начала и конца нашего диапазона:

Конец диапазона (понедельник- Monday) предшествует дню недели начала диапазона (вторник — Tuesday), следовательно, количество недель надо будет увеличить на 1. Таким образом, результат вычисления на этом шаге будет равен 2. И, наконец, умножаем значение на 2, чтобы получить количество выходных дней. Итоговое значение на этом шаге будет равно 4. В C# такой расчёт можно представить следующим образом:

DateTime start = new(2024, 9, 2);
DateTime end = new(2024, 9, 15);

int difference = (int)(end - start).TotalDays; //результат = 13
int weeks = difference / 7; //результат = 1
if (end.DayOfWeek < start.DayOfWeek)
    weeks++; //результат 2
int weekends = weeks * 2;
Console.WriteLine(weekends); //результат 4

Можно посмотреть на календарь на рисунке выше и увидеть, что количество выходных дней (суббот и воскресений) в заданном диапазоне действительно равно четырем.

4. Отнимаем количество выходных дней из количества дней в диапазоне дат

Таким образом мы получаем количество рабочих дней в диапазоне, исключая последний день диапазона (см. пункт 1 алгоритма)

5. Добавляем к результату, полученному в пункте 3 единицу

Полученное значение и будет количеством рабочих дней в диапазоне без учёта праздников и переносов рабочих дней. Таким образом, в C# такой алгоритм можно представить следующим образом:

DateTime start = new(2024, 9, 2);
DateTime end = new(2024, 2, 15);

int difference = (int)(end - start).TotalDays; //результат = 13
int weeks = difference / 7; //результат = 1
if (end.DayOfWeek < start.DayOfWeek)
    weeks++; //результат 2
int weekends = weeks * 2; //результат - 4 выходных дня

int workDays = difference - weekends; //результат - 9 дней

Console.WriteLine(workDays + 1); //результат - 10 рабочих дней

Итак, запоминаем базовый алгоритм расчёта количества рабочих дней в диапазоне дат:

  1. Скорректировать диапазон так, чтобы началом и концом диапазона были рабочие дни
  2. Рассчитываем количество дней между двумя датами. Полученное значение сохраняем в переменной, например, difference 
  3. Делим difference на 7 и отбрасываем дробную часть. Полученное значение сохраняем в переменной, например, weeks  
  4. Если день недели конца диапазона предшествует дню недели начала диапазона, weeks увеличиваем на 1
  5. От значения difference отнимаем удвоенное значение weeks
  6. Добавляем к результату единицу

Реализация алгоритма расчёта количества рабочих дней между двумя датами в C#

Реализуем представленный алгоритм в виде отдельного класса C#. Создадим новое консольное приложение и добавим в него класс BuisenessCalendar

public class BuisenessCalenadar
{
}

Добавим в класс два метода для корректировки диапазона дат:

   public class BuisenessCalenadar
   {
       private static DateTime FixStartDate(DateTime start)
       {
        //начальная дата - воскресенье. Значит надо добавить к дате 1 день
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Sunday) 
            return start.AddDays(1);
        //начальная дата - суббота. Значит надо добавить к дате 2 дня
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Saturday)
            return start.AddDays(2);
        return start;
    }

    private static DateTime FixEndDate(DateTime end)
    {
        //конечная дата - воскресенье. Значит надо отнять от даты 2 дня
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Sunday)
            return end.AddDays(-2);
        //конечная дата - суббота. Значит надо отнять от даты 1 день
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Saturday)
            return end.AddDays(-1);
        return end;
    }
}

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

private int CalculateDifference(DateTime startDate, DateTime endDate)
{
    int difference = (int)(endDate - startDate).TotalDays;
    int weeks = difference / 7;
    if (endDate.DayOfWeek < startDate.DayOfWeek) 
        weeks++;
    difference -= weeks * 2;
    return difference + 1;
}

В качестве входных параметров этот метод получает уже скорректированный диапазон дат. Здесь реализуется весь алгоритм расчёта рабочих дней (см. описание алгоритма выше). Теперь, всё, что нам остается — это создать новый публичный метод класса, который и будет рассчитывать итоговый результат:

public int CalculateWorkDays(DateTime startDate, DateTime endDate)
{
    DateTime _startDate = FixStartDate(startDate);
    DateTime _endDate = FixEndDate(endDate);
    if (_startDate > _endDate)
       return 0;
    else
       return CalculateDifference(_startDate, _endDate);
}

В этом методе после корректировки начала и окончания диапазона мы должны обязательно проверить корректность полученного диапазона (чтобы не получить отрицательное количество рабочих дней). Если диапазон некорректен, то мы просто возвращаем пользователю значение 0, иначе — запускаем метод расчёта количества рабочих дней в диапазоне.

Итого, весь наш класс на данный момент выглядит следующим образом:

 public class BuisenessCalenadar
 {
     private static DateTime FixStartDate(DateTime start)
     {
         //начальная дата - воскресенье. Значит надо добавить к дате 1 день
         //чтобы началом диапазона стал понедельник
     if (start.DayOfWeek == DayOfWeek.Sunday) 
             return start.AddDays(1);
     //начальная дата - суббота. Значит надо добавить к дате 2 дня
     //чтобы началом диапазона стал понедельник
     if (start.DayOfWeek == DayOfWeek.Saturday)
     return start.AddDays(2);
         return start;
 }

 private static DateTime FixEndDate(DateTime end)
 {
 //конечная дата - воскресенье. Значит надо отнять от даты 2 дня
 //чтобы окончанием диапазона стала пятница
     if (end.DayOfWeek == DayOfWeek.Sunday)
     return end.AddDays(-2);
 //конечная дата - суббота. Значит надо отнять от даты 1 день
 //чтобы окончанием диапазона стала пятница
     if (end.DayOfWeek == DayOfWeek.Saturday)
     return end.AddDays(-1);
     return end;
 }

     private int CalculateDifference(DateTime startDate, DateTime endDate)
     {
         int difference = (int)(endDate - startDate).TotalDays;
         int weeks = difference / 7;
         if (endDate.DayOfWeek < startDate.DayOfWeek) 
             weeks++;
         difference -= weeks * 2;
         return difference + 1;
     }

     public int CalculateWorkDays(DateTime startDate, DateTime endDate)
     {
         DateTime _startDate = FixStartDate(startDate);
         DateTime _endDate = FixEndDate(endDate);
         if (_startDate > _endDate)
            return 0;
         else
            return CalculateDifference(_startDate, _endDate);
     }
}

Проверим работу нашего класса. Для этого напишем следующий код метода Main (или непосредственно в файле Program.cs, если вы используете операторы верхнего уровня):

internal class Program
{
    static void Main(string[] args)
    {
        BuisenessCalenadar BuisenessCalenadar = new BuisenessCalenadar();

        DateTime start = new(2024, 9, 1); //начало диапазона
        DateTime end = new(2024, 9, 28); //конец диапазона

        int count = BuisenessCalenadar.CalculateWorkDays(start, end);

        Console.WriteLine($"Количество рабочих дней {count}");
        Console.WriteLine($"Количество календарных дней {(end.Date - start.Date).Days+1}");
    }
}

Результат расчёта будет следующий:

Количество рабочих дней 20
Количество календарных дней 28

Можете открыть производственный календарь на 2024 год от Консультант+ и убедиться, что расчёт произведен корректно. Теперь мы можем развивать наш класс, добавляя в базовый алгоритм расчёта количества рабочих дней дополнительную функциональность, связанную с нашими национальными особенностями.

Учёт праздничных дней и переносов выходных дней

С праздниками и их переносами дело обстоит несколько иначе, чем просто с выходными днями. Как мы знаем, в России ежегодно выпускается очередной производственный календарь, а Правительство выпускает постановление в котором какие-то праздники могут перенестись на другие дни недели. Например, в 2024 году можно встретить такой перенос выходных дней «с субботы 6 января на пятницу 10 мая» или «с субботы 27 апреля на понедельник 29 апреля». Здесь интересен второй перенос в котором суббота (выходной день при пятидневке) становится рабочим днем, а 29 апреля (понедельник — выходным). Каким-либо образом алгоритмизировать праздники и переносы выходных практически невозможно, особенно, если мы попытаемся сделать расчёт рабочих дней, например, для России и Испании — у каждой страны свои особенности, свои национальные праздники и т.д. Поэтому, наиболее оптимальным вариантом является сохранение дат праздников и переносов выходных где-либо, например, в базе данных и использовать эти коллекции для корректировки работы основного алгоритма.

Каким образом мы будем корректировать алгоритм? Если мы учитываем даты праздников, то после расчета рабочих дней в диапазоне мы должны определить все праздничные дни в этом диапазоне и отнять их количество от полученного значения. Соответственно, что касается переносов выходных, то мы должны будем произвести обратное вычисление — нам необходимо определить все рабочие субботы в диапазоне и прибавить их количество к рабочим дням. Реализуем эти моменты в нашем классе. Для этого, создадим два поля только для чтения:

    public class BuisenessCalenadar
    {
        private readonly DateTime[] _holidays = []; //праздничные дни в году
        private readonly DateTime[] _exceptions = [];

        public BuisenessCalenadar(DateTime[] holidays, DateTime[] exceptions) 
        {
            _holidays = holidays;
            _exceptions = exceptions;
        }
      //здесь остальной код класса
}

в _holidays мы будем хранить праздничные дни, а в _exceptions — рабочие субботы (переносы выходных). Теперь напишем метод с помощью которого мы будем определять входит ли определенный праздничный день в заданный диапазон. Сделать это довольно просто:

private bool HolidayInRange(DateTime startDate, DateTime endDate, DateTime holiday)
{
    return (holiday.Date >= startDate && holiday <= endDate
        && holiday.DayOfWeek != DayOfWeek.Saturday
        && holiday.DayOfWeek != DayOfWeek.Sunday);
}

Теперь подсчитаем все праздники в диапазоне дат. Сделать это можно несколькими способами. Первый способ — использовать обычный цикл:

private int HolidayCount(DateTime startDate, DateTime endDate)
{
    int holidayCount = 0;
    foreach (DateTime holiday in _holidays)
    {
        if (HolidayInRange(startDate, endDate, holiday)) holidayCount++;
    }
    return holidayCount;
}

здесь мы проходимся по всей коллекции праздничных дней и проверяем входит ли очередной праздник в заданный диапазон. Если входит, то результат метода увеличивается на 1. Второй способ — использовать для такого подсчета LINQ, например, так:

private int HolidayCount(DateTime startDate, DateTime endDate)
{
    return _holidays.Where(t => HolidayInRange(startDate, endDate, t))
        .Distinct()
        .Count();

    //int holidayCount = 0;
    //foreach (DateTime holiday in _holidays)
    // {
    //     if (HolidayInRange(startDate, endDate, holiday)) holidayCount++;
    // }
    //  return holidayCount;
}
В это случае нам даже нет необходимости создавать отдельный метод — мы можем включить вызовы LINQ непосредственно в метод CalculateDifference, что мы в итоге и сделаем ниже

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

private bool WorkingSaturdayInRange(DateTime startDate, DateTime endDate, DateTime date)
{
    return (date.Date >= startDate) 
        && (date.Date <= endDate)
        && (date.DayOfWeek == DayOfWeek.Saturday);
}

Теперь скорректируем основной алгоритм расчёта рабочих дней следующим образом:

private int CalculateDifference(DateTime startDate, DateTime endDate)
{
    int difference = (int)(endDate - startDate).TotalDays;
    int weeks = difference / 7;
    if (endDate.DayOfWeek < startDate.DayOfWeek) 
        weeks++;
    difference -= weeks * 2;
    //учитываем праздники и переносы выходных дней
    difference -= _holidays.Where(t => HolidayInRange(startDate, endDate, t)).Distinct().Count();
    difference += _exceptions.Where(t => WorkingSaturdayInRange(startDate, endDate, t)).Distinct().Count();
    return difference + 1;
}

В итоге, весь класс будет выглядеть следующим образом:

public class BuisenessCalenadar
{
    private readonly DateTime[] _holidays = []; //праздничные дни в году
    private readonly DateTime[] _exceptions = [];

    public BuisenessCalenadar(DateTime[] holidays, DateTime[] exceptions) 
    {
        _holidays = holidays;
        _exceptions = exceptions;
    }

    private static DateTime FixStartDate(DateTime start)
    {
        //начальная дата - воскресенье. Значит надо добавить к дате 1 день
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Sunday) 
            return start.AddDays(1);
        //начальная дата - суббота. Значит надо добавить к дате 2 дня
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Saturday)
            return start.AddDays(2);
        return start;
    }

    private static DateTime FixEndDate(DateTime end)
    {
        //конечная дата - воскресенье. Значит надо отнять от даты 2 дня
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Sunday)
            return end.AddDays(-2);
        //конечная дата - суббота. Значит надо отнять от даты 1 день
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Saturday)
            return end.AddDays(-1);
        return end;
    }


    private static bool HolidayInRange(DateTime startDate, DateTime endDate, DateTime holiday)
    {
        return (holiday.Date >= startDate && holiday <= endDate
            && holiday.DayOfWeek != DayOfWeek.Saturday
            && holiday.DayOfWeek != DayOfWeek.Sunday);
    }

    private static bool WorkingSaturdayInRange(DateTime startDate, DateTime endDate, DateTime date)
    {
        return (date.Date >= startDate) 
            && (date.Date <= endDate)
            && (date.DayOfWeek == DayOfWeek.Saturday);
    }

    private int CalculateDifference(DateTime startDate, DateTime endDate)
    {
        int difference = (int)(endDate - startDate).TotalDays;
        int weeks = difference / 7;
        if (endDate.DayOfWeek < startDate.DayOfWeek) 
            weeks++;
        difference -= weeks * 2;
        //учитываем праздники и переносы выходных дней
        difference -= _holidays.Where(t => HolidayInRange(startDate, endDate, t)).Distinct().Count();
        difference += _exceptions.Where(t => WorkingSaturdayInRange(startDate, endDate, t)).Distinct().Count();
        return difference + 1;
    }

    public int CalculateWorkDays(DateTime startDate, DateTime endDate)
    {
        DateTime _startDate = FixStartDate(startDate);
        DateTime _endDate = FixEndDate(endDate);
        if (_startDate > _endDate)
           return 0;
        else
           return CalculateDifference(_startDate, _endDate);
    }
}

Снова протестируем наш класс рассчитав количество рабочих дней, например, во всем 2024 году:

internal class Program
{
    static void Main(string[] args)
    {
        //праздничные дни в 2024 году
        DateTime[] holidays = [new DateTime(2024, 1, 1), 
                               new DateTime(2024, 1, 2), 
                               new DateTime(2024, 1, 3), 
                               new DateTime(2024, 1, 4), 
                               new DateTime(2024, 1, 5), 
                               new DateTime(2024, 1, 8),
                               new DateTime(2024, 2, 23),
                               new DateTime(2024, 3, 8),
                               new DateTime(2024, 4, 29),
                               new DateTime(2024, 4, 30), 
                               new DateTime(2024, 5, 1),
                               new DateTime(2024, 5, 9),
                               new DateTime(2024, 5, 10),
                               new DateTime(2024, 6, 12),
                               new DateTime(2024, 11, 4),
                               new DateTime(2024, 12, 30),
                               new DateTime(2024, 12, 31)];
        //переносы выходных дней в 2024 году
        DateTime[] exceptions = [new DateTime(2024, 4, 27), 
                                 new DateTime(2024, 11, 2), 
                                 new DateTime(2024, 12, 28)];

        BuisenessCalenadar BuisenessCalenadar = new BuisenessCalenadar(holidays, exceptions);

        DateTime start = new(2024, 1, 1);
        DateTime end = new(2024, 12, 31);

        int count = BuisenessCalenadar.CalculateWorkDays(start, end);

        Console.WriteLine($"Количество рабочих дней {count}");
        Console.WriteLine($"Количество календарных дней {(end.Date - start.Date).Days+1}");
    }
}

Результат расчёта:

Количество рабочих дней 248
Количество календарных дней 366

Расчёт полностью соответствует производственному календарю на 2024 год. Теперь мы можем задавать любые диапазоны дат и рассчитывать количество рабочих ней в нем.

Прибавляем/отнимаем определенное количество рабочих дней

Кроме расчёта количества рабочих дней в заданном интервале дат, также довольно часто требуется решение таких задач как прибавить определенное количество рабочих дней к дате или, наоборот — отнять рабочие дни от заданной даты. И здесь нам снова необходимо будет учитывать субботы, воскресенья, праздники и переносы выходных. День начала отсчёта может быть любым, включая праздники и выходные, но при этом, мы должны учитывать, что в конечном итоге мы должны также получить рабочий день. В первом приближении расчёт может выглядеть следующим образом (в случае прибавления рабочих дней к дате):

public DateTime CalculateFutureDate(DateTime fromDate, int numberofWorkDays)
{
    var futureDate = fromDate;
    int added = 0;
    while (added < numberofWorkDays) 
    {
        futureDate = futureDate.AddDays(1);
        //если день недели суббота и эта суббота не включена в переносы выходных
        if ((futureDate.DayOfWeek == DayOfWeek.Saturday) && (_exceptions.Contains(futureDate) == false))
            continue;
        //если день воскресенье или праздник
        if ((futureDate.DayOfWeek == DayOfWeek.Sunday) || _holidays.Contains(futureDate))
            continue;
        added++;
    }
    return futureDate.Date;
}

Здесь мы последовательно прибавляем к дате по одному дню и проверяем текущую дату. Если дата оказывается выходным или праздником, то счётчик добавленных рабочих дней не увеличивается. Нетрудно этот метод немного изменить, чтобы с помощью него можно было как прибавлять так и отнимать определенное количество рабочих дней из даты:

public DateTime CalculateDate(DateTime fromDate, int workDaysCount, bool addDays = true)
{
    int counter = 0;
    int i = addDays ? 1 : -1;

    var futureDate = fromDate;
    while (counter < workDaysCount)
    {
        futureDate = futureDate.AddDays(i);
        //если день недели суббота и эта суббота не включена в переносы выходных
        if ((futureDate.DayOfWeek == DayOfWeek.Saturday) && (_exceptions.Contains(futureDate) == false))
            continue;
        //если день воскресенье или праздник
        if ((futureDate.DayOfWeek == DayOfWeek.Sunday) || _holidays.Contains(futureDate))
            continue;
        counter++;
    }
    return futureDate.Date;
}

И последний момент, который нам необходимо учесть — это расчёт конечной даты с включением заданной даты в расчёт. На данный момент метод CalculateDate() проводит расчёт, не включая fromDate в количество рабочих дней. Например, если мы зададим дату 18.09.2024 (среда) и прибавим к ней один рабочий день, то результатом будет 19.09.2024 (четверг). Однако, иногда может потребоваться включить в расчёт и текущую дату, если эта дата является рабочим днем. То есть, так как 18.09.2024 — это рабочий день, то при включении текущей даты в расчёт и прибавлении к дате 1 дня мы должны также получить 18.09.2024. Такой подход к расчёту дат используется, например, в калькуляторе того же Консультант+. Чтобы реализовать такую возможность в нашем классе, добавим в него ещё один метод, который будет определять является ли заданный день рабочим:

public bool IsWorkDay(DateTime day)
{
    if ((day.DayOfWeek == DayOfWeek.Saturday)&&(_exceptions.Contains(day.Date)))
        return true;
    if ((day.DayOfWeek != DayOfWeek.Sunday)&&(_holidays.Contains(day)==false))
        return true;
    return false;
}

И теперь, используя его, изменим метод CalculateDate() следующим образом:

    public DateTime CalculateDate(DateTime fromDate, int workDaysCount, bool addDays = true, bool includeFromDate = false)
    {
        
        int i = addDays ? 1 : -1;

        var futureDate = fromDate;

        int counter = IsWorkDay(fromDate) && includeFromDate ? 1 : 0;


        while (counter < workDaysCount)
        {
            futureDate = futureDate.AddDays(i);
            //если день недели суббота и эта суббота не включена в переносы выходных
            if ((futureDate.DayOfWeek == DayOfWeek.Saturday) && (_exceptions.Contains(futureDate) == false))
                continue;
            //если день воскресенье или праздник
            if ((futureDate.DayOfWeek == DayOfWeek.Sunday) || _holidays.Contains(futureDate))
                continue;
            counter++;
        }
        return futureDate.Date;
    }
}

Здесь мы, в зависимости от условий расчёта определяем начальное значение счётчика counter. Если начальная дата — рабочий день и необходимо его включить в расчёт, то переменная counter сразу становится равной 1.

Класс для расчёта рабочих дней

Итого, окончательный вид класса для расчёта количества рабочих дней будет выглядеть следующим образом:

public class BuisenessCalenadar
{
    private readonly DateTime[] _holidays = []; //праздничные дни в году
    private readonly DateTime[] _exceptions = [];

    public BuisenessCalenadar(DateTime[] holidays, DateTime[] exceptions) 
    {
        _holidays = holidays;
        _exceptions = exceptions;
    }

    private static DateTime FixStartDate(DateTime start)
    {
        //начальная дата - воскресенье. Значит надо добавить к дате 1 день
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Sunday) 
            return start.AddDays(1);
        //начальная дата - суббота. Значит надо добавить к дате 2 дня
        //чтобы началом диапазона стал понедельник
        if (start.DayOfWeek == DayOfWeek.Saturday)
            return start.AddDays(2);
        return start;
    }

    private static DateTime FixEndDate(DateTime end)
    {
        //конечная дата - воскресенье. Значит надо отнять от даты 2 дня
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Sunday)
            return end.AddDays(-2);
        //конечная дата - суббота. Значит надо отнять от даты 1 день
        //чтобы окончанием диапазона стала пятница
        if (end.DayOfWeek == DayOfWeek.Saturday)
            return end.AddDays(-1);
        return end;
    }


    private static bool HolidayInRange(DateTime startDate, DateTime endDate, DateTime holiday)
    {
        return (holiday.Date >= startDate && holiday <= endDate
            && holiday.DayOfWeek != DayOfWeek.Saturday
            && holiday.DayOfWeek != DayOfWeek.Sunday);
    }

    private static bool WorkingSaturdayInRange(DateTime startDate, DateTime endDate, DateTime date)
    {
        return (date.Date >= startDate) 
            && (date.Date <= endDate)
            && (date.DayOfWeek == DayOfWeek.Saturday);
    }

    private int CalculateDifference(DateTime startDate, DateTime endDate)
    {
        int difference = (int)(endDate - startDate).TotalDays;
        int weeks = difference / 7;
        if (endDate.DayOfWeek < startDate.DayOfWeek) 
            weeks++;
        difference -= weeks * 2;
        //учитываем праздники и переносы выходных дней
        difference -= _holidays.Where(t => HolidayInRange(startDate, endDate, t)).Distinct().Count();
        difference += _exceptions.Where(t => WorkingSaturdayInRange(startDate, endDate, t)).Distinct().Count();
        return difference + 1;
    }

    public int CalculateWorkDays(DateTime startDate, DateTime endDate)
    {
        DateTime _startDate = FixStartDate(startDate);
        DateTime _endDate = FixEndDate(endDate);
        if (_startDate > _endDate)
           return 0;
        else
           return CalculateDifference(_startDate, _endDate);
    }


    public bool IsWorkDay(DateTime day)
    {
        if ((day.DayOfWeek == DayOfWeek.Saturday)&&(_exceptions.Contains(day.Date)))
            return true;
        if ((day.DayOfWeek != DayOfWeek.Sunday)&&(_holidays.Contains(day)==false))
            return true;
        return false;
    }


    public DateTime CalculateDate(DateTime fromDate, int workDaysCount, bool addDays = true, bool includeFromDate = false)
    {
        
        int i = addDays ? 1 : -1;

        var futureDate = fromDate;

        int counter = IsWorkDay(fromDate) && includeFromDate ? 1 : 0;


        while (counter < workDaysCount)
        {
            futureDate = futureDate.AddDays(i);
            //если день недели суббота и эта суббота не включена в переносы выходных
            if ((futureDate.DayOfWeek == DayOfWeek.Saturday) && (_exceptions.Contains(futureDate) == false))
                continue;
            //если день воскресенье или праздник
            if ((futureDate.DayOfWeek == DayOfWeek.Sunday) || _holidays.Contains(futureDate))
                continue;
            counter++;
        }
        return futureDate.Date;
    }
}

Наш класс умеет:

  1. Рассчитывать количество рабочих дней в заданном интервале дат
  2. Прибавлять к дате определенное количество рабочих дней
  3. Отнимать от даты определенное количество рабочих дней

Используя этот класс, можно производить различные расчёты, а при небольшой его модификации, можно также обеспечить работы с шестидневной рабочей неделей, добавить возможность работы с календарными днями и так далее.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии