AXForum  
Вернуться   AXForum > Блоги > Gustav'ово бложище, или Записки DAX-дилетанта-III
All
Забыли пароль?
Зарегистрироваться Правила Справка Пользователи Сообщения за день Поиск

Стараюсь писать про Аксапту, хотя частенько тянет в Офис
Рейтинг: 4.00. Голосов: 2.

Функция определения рабочего статуса дня

Запись от Gustav размещена 28.08.2009 в 15:18
Обновил(-а) Gustav 28.08.2009 в 16:08

В системах, с которыми мне довелось работать довольно близко, включая Аксапту, БОСС-Кадровик, некоторые бухгалтерские и биллинговые самописки и т.п., как правило, имеется специальная таблица-календарь, из которой черпается информация о рабочих и выходных днях. Одна запись в такой таблице соответствует одному календарному дню.

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

Каждый раз сталкиваясь близко с таким календарем, я невольно задумывался: почему таблица? почему не функция? Ведь существует же, например, в том же Excel функция WORKDAY (РАБДЕНЬ), рассчитывающая дату, отстоящую от исходной на заданное количество рабочих дней. Эта функция даже учтёт праздники, если ей их передать параметром в виде массива дат, правда, не совсем точно для российской реальности - на переносе праздников, попадающих на субботу и воскресенье, на ближайший следующий рабочий день, который предусмотрен статьей 112 нашего Трудового кодекса, функция WORKDAY сдается ("Ааа, блин!" - сказали суровые сибирские мужики ) Однако, сама попытка создания подобной универсальной функции, вызывает заслуженное уважение в адрес команды разработчиков Excel.

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

Казалось бы, смирись, используй таблицу-календарь и не мучайся! Тем не менее, смутное желание создать функцию, учитывающую российские нюансы переносов праздников, периодически теребило меня несколько лет, но каждый раз начавшийся было процесс раздумий прерывался чем-то более насущным и не оставлял после себя никаких набросков, которые можно было бы развить дальше при следующем приступе "заболевания".

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

Лично мне она понравилась своей компактностью - мне почему-то представлялось, что в подобной функции всяких IF-ов должно быть на порядок больше, чем оно оказалось на самом деле. Может быть, конечно, это из-за того, что в Аксапте есть такие достойнейшие средства, как Map и Set, и без оных как раз куча IF-ов бы и получилась?

Во всяком случае я пока не думал о портации функции в другие языки. Может, через несколько лет...

В рамках демонстрационного примера функция (dayWorkingStatus) вложена в нижеследующий джоб, результатом работы которого будет вывод в окно infolog всех дат годов 2007,2008, 2009 с указанием дня недели и рабочего статуса дня:
X++:
#macrolib.HolidayExceptions // глобальный макрос (см. ниже)

static void Job_RussianHolidays(Args _args)
{
    date    currDate;
    Map     holidayExceptions = new Map(Types::Date, Types::Integer);

    //------------------------------------------------------------------------
    int dayWorkingStatus(date   _currDate,
                         Map    _exceptions = null,
                         Set    _holidays   = null)
    {
        Set holidaysBefore      = new Set(Types::Date);
        Set holidaysTransfer    = new Set(Types::Date);

        SetEnumerator enumr;
        int holiYear, holiMonth, holiDay;
        date holidayToMove;
        ;

        // самыми первыми проверяем исключения
        // если искомый день среди них, то заканчиваем и возвращаем статус
        if (_exceptions)
        {
            if (_exceptions.exists(_currDate))
                return _exceptions.lookup(_currDate);
        }

        if (!_holidays)
        {
            _holidays = new Set(Types::Container);
            // Russian holidays
            _holidays.add([ 1, 1]); // Новогодние каникулы
            _holidays.add([ 1, 2]); // Новогодние каникулы
            _holidays.add([ 1, 3]); // Новогодние каникулы
            _holidays.add([ 1, 4]); // Новогодние каникулы
            _holidays.add([ 1, 5]); // Новогодние каникулы
            _holidays.add([ 1, 7]); // Рождество Христово
            _holidays.add([ 2,23]); // День защитника Отечества
            _holidays.add([ 3, 8]); // Международный женский день
            _holidays.add([ 5, 1]); // Праздник Весны и Труда
            _holidays.add([ 5, 9]); // День Победы
            _holidays.add([ 6,12]); // День России
            _holidays.add([11, 4]); // День народного единства
        }

        if ( dayOfWk(_currDate)==6 || dayOfWk(_currDate)==7 ||
            _holidays.in([mthOfYr(_currDate), dayOfMth(_currDate)]))
            return 0; // текущий - выходной (сб,вс или празд)

        // если уж дошли сюда, то проверям нет ли праздничного переноса

        // для этого пробегаем по всему множеству "безгодовых" праздников (_holidays)
        // и формируем последовательность всех праздничных дат до текущей даты (holidaysBefore),
        // т.е. генерим реальные праздничные даты за годовой период, предшествующий текущей дате
        enumr = _holidays.getEnumerator();
        while (enumr.moveNext())
        {
            [holiMonth, holiDay] = enumr.current();
            holiYear = ([mthOfYr(_currDate), dayOfMth(_currDate)] > [holiMonth, holiDay]) ?
                            (year(_currDate)) :
                            (year(_currDate)-1);
            holidaysBefore.add(mkDate(holiDay, holiMonth, holiYear));
        }

        // обрабатываем сдвиги выходных дней, вызванных попаданием праздника на субботу или воскрсенье (на 6 или 7)
        enumr = holidaysBefore.getEnumerator();
        while (enumr.moveNext())
        {
            holidayToMove = enumr.current();

            // смещение происходит только в том случае, если выходной попадает на 6 или 7
            while (dayOfWk(holidayToMove)==6 || dayOfWk(holidayToMove)==7)
            {
                if      (dayOfWk(holidayToMove)==6) holidayToMove += 2;
                else if (dayOfWk(holidayToMove)==7) holidayToMove += 1;

                // если перемещаясь попали на другой  праздничный день из НЕСМЕЩЕННЫХ (before)
                // или из УЖЕ СМЕЩЕННЫХ (transfer), то двигаемся дальше
                while ( holidaysBefore  .in(holidayToMove)  ||
                        holidaysTransfer.in(holidayToMove)  )
                    holidayToMove += 1;
            }
            // только если было смещение, добавляем день в множество переносов
            if (! holidaysBefore.in(holidayToMove))
                holidaysTransfer.add(holidayToMove);
        }
        // в результате имеем множество дат с (годами), которые стали выходными из-за предшествующих праздников

        // окончательно определяем рабочийСтатусДня
        if (holidaysTransfer.in(_currDate))
            return 0; // выходной = день, на который перенесли праздник, попавший на 6 или 7
        else
        {
            if (_holidays.in([mthOfYr(_currDate+1), dayOfMth(_currDate+1)]))
                return 2; // рабочий ПРЕДпраздничный
            else
                return 1; // обычный рабочий;
        }
    }
    //------------------------------------------------------------------------
    ;

    #holidayExceptionsInsert(holidayExceptions) // передаем map макросу 

    for (currDate=01\01\2007; currDate<=31\12\2009; currDate++)
    {
        info(strFmt('%1 -- %2 -- %3',
            currDate, dayOfWk(currDate), dayWorkingStatus(currDate, holidayExceptions)));
    }
}
Возвращаемые int-значения ("статусы рабочего дня"):
  • 0 - выходной день,
  • 1 - рабочий день,
  • 2 - предпраздничный рабочий день.
Параметры:
  • _currDate - дата, для которой определяется статус,
  • _exceptions - список переносов-исключений, которые не поддаются алгоритму статьи 112 российского Трудового кодекса; если опущен, то работаем без исключений.
  • _holidays - список общегосударственных праздников (только день и месяц); если опущен, то подразумеваются праздники России (прописаны в коде).
Способ формирования списка переносов-исключений - по желанию разработчика: или читать из специальной таблицы; или использовать макрос, который по мере продвижения вперед по годам следует дополнять новыми строками - на основании информации из очередного Постановления.

(to be continued...)
Размещено в Без категории
Просмотров 156364 Комментарии 5
Всего комментариев 5

Комментарии

  1. Старый комментарий
    Аватар для Gustav
    Собственно я прервал предыдущий пост, потому что напоролся на ограничение в 10 тысяч символов!! Поэтому продолжаю уже в этом комментарии

    Способ с макросом я использовал для демонстрационного примера. Каждое исключение характеризуется двумя датами: день, который переносят, и день, НА который переносят. Глобальный макрос HolidayExceptions наполнен информацией о реальных переносах-исключениях в 2007-2009 годах:
    X++:
    /* %1 holiday exceptions map  */
    
    #LOCALMACRO.holidayExceptionsInsert
        // ФОРМИРОВАНИЕ СПИСКА ИСКЛЮЧЕНИЙ - на основании Постановлений Правительства РФ
    
        // В соответствии c Постановлением Правительства РФ от 11 ноября 2006 года N 661
        // переносятся следующие выходные дни 2007 года
    
        // с субботы 28 апреля на понедельник 30 апреля
        %1.insert( 28\04\2007, 2);  // 2 - т.к. перенос с предпраздничного дня
        %1.insert( 30\04\2007, 0);
    
        // с субботы 9 июня на понедельник 11 июня
        %1.insert( 09\06\2007, 2); // 2 - т.к. перенос с предпраздничного дня
        %1.insert( 11\06\2007, 0);
    
        // с субботы 29 декабря на понедельник 31 декабря
        %1.insert( 29\12\2007, 2);  // 2 - т.к. перенос с предпраздничного дня
        %1.insert( 31\12\2007, 0);
    
    
        // В соответствии с Постановлением Правительства РФ от 11.08.2007 № 512
        // переносятся следующие выходные дни 2008 года:
    
        // с воскресенья 4 мая на пятницу 2 мая
        %1.insert( 04\05\2008, 1); //  4 мая 2008 стал рабочим днем
        %1.insert( 02\05\2008, 0); //  2 мая 2008 стал выходным днем
    
        // с субботы 7 июня на пятницу 13 июня
        %1.insert( 07\06\2008, 1); //  7 июня 2008 стал рабочим днем
        %1.insert( 13\06\2008, 0); // 13 июня 2008 стал выходным днем
    
        // с субботы 1 ноября на понедельник 3 ноября
        %1.insert( 01\11\2008, 2); //  // 2 - т.к. перенос с предпраздничного дня
        %1.insert( 03\11\2008, 0); //
    
    
        // В соответствии c Постановлением Правительства от 26.11.2008 № 877
        // переносятся следующие выходные дни 2009 года:
    
        // с воскресенья 11 января на пятницу 9 января.
        %1.insert( 11\01\2009, 1);
        %1.insert( 09\01\2009, 0);
    #ENDMACRO
    Запись от Gustav размещена 28.08.2009 в 15:19 Gustav is offline
    Обновил(-а) Gustav 28.08.2009 в 16:11
  2. Старый комментарий
    Запись от mazzy размещена 08.09.2009 в 14:35 mazzy is offline
  3. Старый комментарий
    Упражнение в программировании, не более того. Определенный интерес представляла бы функция, которая генерировала бы календарь по этой схеме, а "исключения" заносились бы руками в готовый календарь.
    Запись от EVGL размещена 29.09.2009 в 20:59 EVGL is offline
  4. Старый комментарий
    Аватар для Gustav
    Евгений, ну так... запустить ее в цикле за год без блока "исключений" и сгенерить календарь, нет?

    Руками потом, да - информация о новых исключениях все равно обычно появляется тогда, когда календари на след. год в системах уже в основном заготовлены.

    Кстати, в Австрии тоже есть такие сдвиги выходных из-за праздников, попавших на уик-энд? Или это исключительно российская особенность?
    Запись от Gustav размещена 01.10.2009 в 18:53 Gustav is offline
  5. Старый комментарий
    Да, это - исключительно советское извращение. (Я называю это извращением потому, что заранее со 100%-ой вероятностью не знаешь, работаешь ли в выбранный день в будущем, или нет.) Если в Австрии праздник попадает на выходной, то считай, не повезло. А алгоритм пришлось бы расширять на вычисление Пасхи и Троицы.
    Запись от EVGL размещена 02.10.2009 в 16:51 EVGL is offline
    Обновил(-а) EVGL 02.10.2009 в 16:54
 


Рейтинг@Mail.ru
Часовой пояс GMT +3, время: 18:28.