Почему моя функция CalcWorkingDays VBA дает мне два разных результата за один и тот же период? - PullRequest
0 голосов
/ 17 июня 2019

Прежде всего, я новичок и все еще изучаю VBA, спасибо за внимание.

У меня есть функция CalcWorkingDays, которая вычисляет рабочие дни в течение определенного периода (периода, определяемогопараметр запроса).

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

Я думаю, проблема в этихСтроки:

If Format(DateCnt, "w") <> "7" And _
    Format(DateCnt, "w") <> "6" Then

Спасибо!

Public Function CalcWorkingDays(BegDate As Variant, EndDate As Variant) As Integer

 Dim WholeWeeks As Variant
 Dim DateCnt As Variant
 Dim EndDays As Integer

 On Error GoTo Err_Work_Days

 BegDate = DateValue(BegDate)
 EndDate = DateValue(EndDate)
 WholeWeeks = DateDiff("w", BegDate, EndDate)
 DateCnt = DateAdd("ww", WholeWeeks, BegDate)
 EndDays = 0

 Do While DateCnt <= EndDate
 If Format(DateCnt, "w") <> "7" And _
 Format(DateCnt, "w") <> "6" Then
 EndDays = EndDays + 1
 End If
 DateCnt = DateAdd("d", 1, DateCnt)
 Loop

 CalcWorkingDays = WholeWeeks * 5 + EndDays

Exit Function

[...]
End Function`

Например, на март 2019. В общей сложности 21 рабочий день.У нас есть оба сотрудника A и BA: он работает над проектом с 01.01.2009 г. по 31.12.2009 г. Функция дает мне 21 рабочий день на март, и это правильно. B: Он был назначен проекту с 01.03.03 г.С 2019 по 08/03/2019, это дает мне 5, что неверно, это должно дать мне 6 (8 дней всего - 2 дня на выходные

Ответы [ 4 ]

1 голос
/ 17 июня 2019

Harassed Dad прав - если вы используете Format(DateCnt, "w"), воскресенье будет "1", понедельник "2" ... Но вы не должны использовать Format, чтобы получить деньнеделя - Format для форматирования данных в строки, и нет необходимости задействовать строки.Вместо этого используйте функцию Weekday.

Поведение по умолчанию для Weekday состоит в том, что воскресенье будет 1 (как число, а не строка), но вы можете изменить это с помощью второго параметра (FirstDayOfWeek).Это определяет, какой день вы хотите иметь в качестве первого дня недели.

Таким образом, вы можете изменить свою логику, например, на

If Weekday(DateCnt, vbMonday) < 6 Then
0 голосов
/ 17 июня 2019

Функция дает мне 21 рабочий день для марта, который является правильным. B

Он был назначен на проект с 01/03/2019 по 08/03/2019, он дает мне 5, чтоневерно, это должно дать мне 6.

Функция сравнения никогда не будет включать в себя последнюю дату.Если вы хотите включить эту последнюю дату, добавьте один день к последней дате перед вычислением:

? DateDiffWorkDays(#2019/03/01#, #2019/03/31#)
 21 
? DateDiffWorkDays(#2019/03/01#, #2019/04/01#)
 21 

? DateDiffWorkDays(#2019/03/01#, #2019/03/08#)
 5 
? DateDiffWorkDays(#2019/03/01#, #2019/03/09#)
 6

Также, как уже отмечалось, укажите Понедельник в качестве первого дня недели.Кроме того, не используйте формат;День недели - это «прямой» метод.Таким образом:

If Weekday(DateCnt, vbMonday) < 6 Then
    EndDays = EndDays + 1
End If

Для расширенного метода, который учитывает праздники, изучите мои функции:

Option Compare Database
Option Explicit

' Returns the count of full workdays between Date1 and Date2.
' The date difference can be positive, zero, or negative.
' Optionally, if WorkOnHolidays is True, holidays are regarded as workdays.
'
' Note that if one date is in a weekend and the other is not, the reverse
' count will differ by one, because the first date never is included in the count:
'
'   Mo  Tu  We  Th  Fr  Sa  Su      Su  Sa  Fr  Th  We  Tu  Mo
'    0   1   2   3   4   4   4       0   0  -1  -2  -3  -4  -5
'
'   Su  Mo  Tu  We  Th  Fr  Sa      Sa  Fr  Th  We  Tu  Mo  Su
'    0   1   2   3   4   5   5       0  -1  -2  -3  -4  -5  -5
'
'   Sa  Su  Mo  Tu  We  Th  Fr      Fr  Th  We  Tu  Mo  Su  Sa
'    0   0   1   2   3   4   5       0  -1  -2  -3  -4  -4  -4
'
'   Fr  Sa  Su  Mo  Tu  We  Th      Th  We  Tu  Mo  Su  Sa  Fr
'    0   0   0   1   2   3   4       0  -1  -2  -3  -3  -3  -4
'
' Execution time for finding working days of three years is about 4 ms.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-19. Gustav Brock. Cactus Data ApS, CPH.
'
Public Function DateDiffWorkdays( _
    ByVal Date1 As Date, _
    ByVal Date2 As Date, _
    Optional ByVal WorkOnHolidays As Boolean) _
    As Long

    Dim Holidays()      As Date

    Dim Diff            As Long
    Dim Sign            As Long
    Dim NextHoliday     As Long
    Dim LastHoliday     As Long

    Sign = Sgn(DateDiff("d", Date1, Date2))
    If Sign <> 0 Then
        If WorkOnHolidays = True Then
            ' Holidays are workdays.
        Else
            ' Retrieve array with holidays between Date1 and Date2.
            Holidays = GetHolidays(Date1, Date2, False) 'CBool(Sign < 0))
            ' Ignore error when using LBound and UBound on an unassigned array.
            On Error Resume Next
            NextHoliday = LBound(Holidays)
            LastHoliday = UBound(Holidays)
            ' If Err.Number > 0 there are no holidays between Date1 and Date2.
            If Err.Number > 0 Then
                WorkOnHolidays = True
            End If
            On Error GoTo 0
        End If

        ' Loop to sum up workdays.
        Do Until DateDiff("d", Date1, Date2) = 0
            Select Case Weekday(Date1)
                Case vbSaturday, vbSunday
                    ' Skip weekend.
                Case Else
                    If WorkOnHolidays = False Then
                        ' Check for holidays to skip.
                        If NextHoliday <= LastHoliday Then
                            ' First, check if NextHoliday hasn't been advanced.
                            If NextHoliday < LastHoliday Then
                                If Sgn(DateDiff("d", Date1, Holidays(NextHoliday))) = -Sign Then
                                    ' Weekend hasn't advanced NextHoliday.
                                    NextHoliday = NextHoliday + 1
                                End If
                            End If
                            ' Then, check if Date1 has reached a holiday.
                            If DateDiff("d", Date1, Holidays(NextHoliday)) = 0 Then
                                ' This Date1 hits a holiday.
                                ' Subtract one day to neutralize the one
                                ' being added at the end of the loop.
                                Diff = Diff - Sign
                                ' Adjust to the next holiday to check.
                                NextHoliday = NextHoliday + 1
                            End If
                        End If
                    End If
                    Diff = Diff + Sign
            End Select
            ' Advance Date1.
            Date1 = DateAdd("d", Sign, Date1)
        Loop
    End If

    DateDiffWorkdays = Diff

End Function

' Returns the holidays between Date1 and Date2.
' The holidays are returned as an array with the
' dates ordered ascending, optionally descending.
'
' The array is declared static to speed up
' repeated calls with identical date parameters.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-18. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function GetHolidays( _
    ByVal Date1 As Date, _
    ByVal Date2 As Date, _
    Optional ByVal OrderDesc As Boolean) _
    As Date()

    ' Constants for the arrays.
    Const DimRecordCount    As Long = 2
    Const DimFieldOne       As Long = 0

    Static Date1Last        As Date
    Static Date2Last        As Date
    Static OrderLast        As Boolean
    Static DayRows          As Variant
    Static Days             As Long

    Dim rs                  As DAO.Recordset

    ' Cannot be declared Static.
    Dim Holidays()          As Date

    If DateDiff("d", Date1, Date1Last) <> 0 Or _
        DateDiff("d", Date2, Date2Last) <> 0 Or _
        OrderDesc <> OrderLast Then

        ' Retrieve new range of holidays.
        Set rs = DatesHoliday(Date1, Date2, OrderDesc)

        ' Save the current set of date parameters.
        Date1Last = Date1
        Date2Last = Date2
        OrderLast = OrderDesc

        Days = rs.RecordCount
            If Days > 0 Then
                ' As repeated calls may happen, do a movefirst.
                rs.MoveFirst
                DayRows = rs.GetRows(Days)
                ' rs is now positioned at the last record.
            End If
        rs.Close
    End If

    If Days = 0 Then
        ' Leave Holidays() as an unassigned array.
        Erase Holidays
    Else
        ' Fill array to return.
        ReDim Holidays(Days - 1)
        For Days = LBound(DayRows, DimRecordCount) To UBound(DayRows, DimRecordCount)
            Holidays(Days) = DayRows(DimFieldOne, Days)
        Next
    End If

    Set rs = Nothing

    GetHolidays = Holidays()

End Function

' Returns the holidays between Date1 and Date2.
' The holidays are returned as a recordset with the
' dates ordered ascending, optionally descending.
'
' Requires table Holiday with list of holidays.
'
' 2015-12-18. Gustav Brock, Cactus Data ApS, CPH.
'
Public Function DatesHoliday( _
    ByVal Date1 As Date, _
    ByVal Date2 As Date, _
    Optional ByVal ReverseOrder As Boolean) _
    As DAO.Recordset

    ' The table that holds the holidays.
    Const Table         As String = "Holiday"
    ' The field of the table that holds the dates of the holidays.
    Const Field         As String = "Date"

    Dim rs              As DAO.Recordset

    Dim SQL             As String
    Dim SqlDate1        As String
    Dim SqlDate2        As String
    Dim Order           As String

    SqlDate1 = Format(Date1, "\#yyyy\/mm\/dd\#")
    SqlDate2 = Format(Date2, "\#yyyy\/mm\/dd\#")
    ReverseOrder = ReverseOrder Xor (DateDiff("d", Date1, Date2) < 0)
    Order = IIf(ReverseOrder, "Desc", "Asc")

    SQL = "Select " & Field & " From " & Table & " " & _
        "Where " & Field & " Between " & SqlDate1 & " And " & SqlDate2 & " " & _
        "Order By 1 " & Order

    Set rs = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)

    Set DatesHoliday = rs

End Function

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

0 голосов
/ 17 июня 2019

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

Public Function CalcWorkingDays(BegDate As Variant, EndDate As Variant) As Integer
    CalcWorkingDays = 0
    For i = begdate To enddate
        If Weekday(i, vbMonday) <= 5 Then
            CalcWorkingDays = CalcWorkingDays + 1
        End If
    Next
End Function

Не особенно элегантно, но эффективно, легко понять и легко изменить.

0 голосов
/ 17 июня 2019

Возможно, вы попытаетесь использовать функцию сетевые дни

=NETWORKDAYS(start_date,end_date,holidays)

праздничные дни необязательны

Например, если у вас есть дата 4 января 2016 года (понедельник)в ячейке B4 и 11 января 2016 г. (также в понедельник) в ячейке C4 эта формула вернет 6:

=NETWORKDAYS(B4,C4)

для VBA в ACCESS

Sub test()
    Dim xl As Object
    Set xl = CreateObject("Excel.Application")
        BegDate = #4/11/2019#
        EndDate = #6/11/2019#
        result = xl.WorksheetFunction.NetworkDays(BegDate, EndDate) ' 44
    Set xl = Nothing
End Sub

ИЛИ

этот

...