Дата и рабочие дни, дающие противоречивые результаты

#vba #function #ms-access

Вопрос:

Я использую DateAddWorkdays в своей базе данных access, в которой есть серверная часть SQL, которая большую часть времени отлично работает. Но у меня возникают некоторые странные несоответствия, и я не знаю, почему. Я просматривал сценарий в течение последних нескольких часов и не вижу, откуда берется проблема. У меня есть таблица дат, которая включает 25 октября 2021 года в качестве праздничного дня: введите описание изображения здесь

Я использую поле Дата праздника. Я вызываю функцию с помощью

 Me.ProductionDate = DateAddWorkdays(-Me.Lag, Me.DelDate)
 

Это отрицательное число, так как мне нужно отсчитать количество дней задержки, чтобы знать, когда начать производство товаров вовремя к дате доставки (я. DelDate).
Как показано ниже, если есть только 2 дня, он отлично отсчитывается до 21 октября, так как 25-е число считается праздничным, а 23-е и 24-е-выходными днями. Но если задержка составляет 6 дней, он где-то пропускает день и возвращает 18-е число, где я ожидал бы, что он должен вернуться в предыдущую пятницу 15-го числа (чтобы проверить эту теорию, если я изменю дату доставки на 27 октября, в результате он также вернет 18-е число).

введите описание изображения здесь

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

     Option Compare Database
Option Explicit
    
Public Function DateAddWorkdays( _
    ByVal lngNumber As Long, _
    ByVal datDate As Date, _
    Optional ByVal booWorkOnHolidays As Boolean) _
    As Date

'   Adds lngNumber of workdays to datDate.
'   2014-10-03. Cactus Data ApS, CPH

    ' Calendar days per week.
    Const clngWeekdayCount  As Long = 7
    ' Workdays per week.
    Const clngWeekWorkdays  As Long = 5
    ' Average count of holidays per week maximum.
    Const clngWeekHolidays  As Long = 1
    ' Maximum valid date value.
    Const cdatDateRangeMax  As Date = #12/31/9999#
    ' Minimum valid date value.
    Const cdatDateRangeMin  As Date = #1/1/100#

    Dim aHolidays() As Date

    Dim lngDays     As Long
    Dim lngDiff     As Long
    Dim lngDiffMax  As Long
    Dim lngSign     As Long
    Dim datDate1    As Date
    Dim datDate2    As Date
    Dim datLimit    As Date
    Dim lngHoliday  As Long


    lngSign = Sgn(lngNumber)
    datDate2 = datDate

    If lngSign <> 0 Then
        If booWorkOnHolidays = True Then
            ' Holidays are workdays.
        Else
            ' Retrieve array with holidays between datDate and datDate   lngDiffMax.
            ' Calculate the maximum calendar days per workweek.
            lngDiffMax = lngNumber * clngWeekdayCount / (clngWeekWorkdays - clngWeekHolidays)
            ' Add one week to cover cases where a week contains multiple holidays.
            lngDiffMax = lngDiffMax   Sgn(lngDiffMax) * clngWeekdayCount
            datDate1 = DateAdd("d", lngDiffMax, datDate)
            aHolidays = GetHolidays(datDate, datDate1)
        End If
        Do Until lngDays = lngNumber
            If lngSign = 1 Then
                datLimit = cdatDateRangeMax
            Else
                datLimit = cdatDateRangeMin
            End If
            If DateDiff("d", DateAdd("d", lngDiff, datDate), datLimit) = 0 Then
                ' Limit of date range has been reached.
                Exit Do
            End If

            lngDiff = lngDiff   lngSign
            datDate2 = DateAdd("d", lngDiff, datDate)
            Select Case Weekday(datDate2)
                Case vbSaturday, vbSunday
                    ' Skip weekend.
                Case Else
                    ' Check for holidays to skip.
                    ' Ignore error when using LBound and UBound on an unassigned array.
                    On Error Resume Next
                    For lngHoliday = LBound(aHolidays) To UBound(aHolidays)
                        If Err.Number > 0 Then
                            ' No holidays between datDate and datDate1.
                        ElseIf DateDiff("d", datDate2, aHolidays(lngHoliday)) = 0 Then
                            ' This datDate2 hits a holiday.
                            ' Subtract one day before adding one after the loop.
                            lngDays = lngDays - lngSign
                            Exit For
                        End If
                    Next
                    On Error GoTo 0
                    lngDays = lngDays   lngSign
            End Select
        Loop
    End If

    DateAddWorkdays = datDate2
    End Function

    Public Function GetHolidays( _
    ByVal datDate1 As Date, _
    ByVal datDate2 As Date, _
    Optional ByVal booDesc As Boolean) _
    As Date()

'   Finds the count of holidays between datDate1 and datDate2.
'   The holidays are returned as an array of dates.
'   DAO objects are declared static to speed up repeated calls with identical date parameters.
'   2014-10-03. Cactus Data ApS, CPH

    ' The table that holds the holidays.
    Const cstrTable             As String = "tblHoliday"
    ' The field of the table that holds the dates of the holidays.
    Const cstrField             As String = "HolidayDate"
    ' Constants for the arrays.
    Const clngDimRecordCount    As Long = 2
    Const clngDimFieldOne       As Long = 0

    Static dbs              As DAO.Database
    Static rst              As DAO.Recordset

    Static datDate1Last     As Date
    Static datDate2Last     As Date

    Dim adatDays()  As Date
    Dim avarDays    As Variant

    Dim strSQL      As String
    Dim strDate1    As String
    Dim strDate2    As String
    Dim strOrder    As String
    Dim lngDays     As Long

    If DateDiff("d", datDate1, datDate1Last) <> 0 Or DateDiff("d", datDate2, datDate2Last) <> 0 Then
        ' datDate1 or datDate2 has changed since the last call.
        strDate1 = Format(datDate1, "#yyyy/mm/dd#")
        strDate2 = Format(datDate2, "#yyyy/mm/dd#")
        strOrder = Format(booDesc, "Asc;Desc")

        strSQL = "Select " amp; cstrField amp; " From " amp; cstrTable amp; " " amp; _
            "Where " amp; cstrField amp; " Between " amp; strDate1 amp; " And " amp; strDate2 amp; " " amp; _
            "Order By 1 " amp; strOrder

        Set dbs = CurrentDb
        Set rst = dbs.OpenRecordset(strSQL, dbOpenSnapshot)

        ' Save the current set of date parameters.
        datDate1Last = datDate1
        datDate2Last = datDate2
    End If

    lngDays = rst.RecordCount
    If lngDays = 0 Then
        ' Leave adatDays() as an unassigned array.
    Else
        ReDim adatDays(lngDays - 1)
        ' As repeated calls may happen, do a movefirst.
        rst.MoveFirst
        avarDays = rst.GetRows(lngDays)
        ' rst is now positioned at the last record.
        For lngDays = LBound(avarDays, clngDimRecordCount) To UBound(avarDays, clngDimRecordCount)
            adatDays(lngDays) = avarDays(clngDimFieldOne, lngDays)
        Next
    End If

    ' DAO objects are static.
    ' Set rst = Nothing
    ' Set dbs = Nothing

    GetHolidays = adatDays()

End Function
 

введите описание изображения здесь

В дополнение к моему вопросу в комментариях ниже я наткнулся на другой пример, в котором расчет не согласуется. Выше указана дата доставки 11/01/2022, которая приходится на 11 января 2022 года, и с задержкой в 2 дня она правильно рассчитывается, включая праздничные дни, из таблицы ниже. Но отставание где-то между 9 и 14, и, похоже, оно игнорирует праздники, но до тех пор, пока отставание меньше 9 или больше 14, оно вычисляется правильно.

введите описание изображения здесь

введите описание изображения здесь

Комментарии:

1. Я запустил функцию с параметрами 25 октября 2021 года в качестве праздника, 26 октября 2021 года в качестве даты доставки, задержка -6. Это возвращает 15 октября 2021 года. 27 октября 2021 года дата доставки возвращается 18 октября 2021 года. Не удается воспроизвести проблему.

Ответ №1:

Ошибка заключается в том, что расширенный диапазон возможных праздников не соответствовал знаку интервала дат, что в вашем случае привело к исключению праздника:

             ' Retrieve array with holidays between datDate and datDate   lngDiffMax.
            ' Calculate the maximum calendar days per workweek.
            lngDiffMax = lngNumber * clngWeekdayCount / (clngWeekWorkdays - clngWeekHolidays)

            ' Add one week to cover cases where a week contains multiple holidays.
'' Missing sign.
''            lngDiffMax = lngDiffMax   clngWeekdayCount
'' Corrected to follow the sign of the date interval.
            lngDiffMax = lngDiffMax   Sgn(lngDiffMax) * clngWeekdayCount

            datDate1 = DateAdd("d", lngDiffMax, datDate)
            aHolidays = GetHolidays(datDate, datDate1)
 

Теперь вы можете запустить этот быстрый тест:

 For n = 0 To 7 : ? d, -n, DateAddWorkdays(-n, d) : Next
26-10-2021     0            26-10-2021 
26-10-2021    -1            22-10-2021 
26-10-2021    -2            21-10-2021 
26-10-2021    -3            20-10-2021 
26-10-2021    -4            19-10-2021 
26-10-2021    -5            18-10-2021 
26-10-2021    -6            15-10-2021 
26-10-2021    -7            14-10-2021 
 

Функция была доработана и включена в мой проект VBA.Дата в модуле DateWork.bas . (Отказ от ответственности: Проект содержит обширный код, написанный мной).

Продленные каникулы

Увеличьте максимальное количество возможных праздничных дней в неделю до количества рабочих дней в неделю минус один:

 ' Common constants.
    
    ' Workdays per week.
    Public Const WorkDaysPerWeek    As Long = 5
    ' Average count of holidays per week maximum.
    Public Const HolidaysPerWeek    As Long = 4 '1
 

Теперь ваш образец будет работать следующим образом:

 Lag  Production Date 
---  ---------------
  1  10-01-2022 
  2  21-12-2021 
  3  20-12-2021 
  4  17-12-2021 
  5  16-12-2021 
  6  15-12-2021 
  7  14-12-2021 
  8  13-12-2021 
  9  10-12-2021 
 10  09-12-2021 
 11  08-12-2021 
 12  07-12-2021 
 13  06-12-2021 
 14  03-12-2021 
 15  02-12-2021 
 16  01-12-2021 
 

Комментарии:

1. Почему код работал правильно без изменений для меня?

2. Но это уже видно в коде вопроса, который я скопировал/вставил. Я не вижу никаких правок, которые можно было бы подвергнуть сомнению. Я провел тестирование перед тем, как вы опубликовали ответ.

3. Да, это тоже работает. Так что проблема операции все еще остается загадкой.

4. Даты ваших праздников отображаются выровненными по левому краю, поэтому, вероятно, они имеют тип данных Текст, а не Дата. Измените поле на тип данных Дата .

5. Это было вызвано вашей серверной частью SQL Server , так как T-SQL ведет себя немного иначе, чем Access SQL . MoveLast Для подсчета записей необходимо вызвать A, а в SQL Between Value1 And Value2 значение 1 должно быть меньше значения 2. Этого не было в случае с вашим отрицательным лагом (интервалом) для добавления. Модуль DateWork на VBA.Date был полностью обновлен, чтобы исправить эти проблемы.