将excel中的两个考勤表与VBA合并,返回错误的输出

combining two attendance tables in excel with VBA returning wrong output

提问人:kamal_hamad 提问时间:10/22/2023 最后编辑:kamal_hamad 更新时间:10/23/2023 访问量:75

问:

我分别在表1和表2中有两个表。

工作表 1 中的表格包含员工签入记录,与工作表 2 相同,只是它用于签出。

我一直在尝试将它们合并到一个表格中,并列出每个员工每天的总工作时间。

我将举例说明我的输入和预期输出与我得到的输出。

希望有人能帮助我解决代码问题。

输入 1(打卡表):

enter image description here

输入 2(打孔表):

enter image description here

预期输出:

enter image description here

我从代码中得到的输出:

enter image description here

我的代码:

Sub CombineAttendanceData()
Dim wsA As Worksheet, wsB As Worksheet, wsResult As Worksheet
Dim LastRowA As Long, LastRowB As Long
Dim EmployeeData As Object
Dim EmployeeID As Long
Dim CurrentRowA As Long, CurrentRowB As Long
Dim MaxPunches As Long
Dim i As Long, j As Long
Dim TotalHours As Double

' Set your worksheets by index
Set wsA = ThisWorkbook.Sheets(1)
Set wsB = ThisWorkbook.Sheets(2)

' Create a new worksheet for the result
Set wsResult = ThisWorkbook.Sheets.Add
wsResult.Name = "Attendance Result"

' Initialize the data structure
Set EmployeeData = CreateObject("Scripting.Dictionary")

' Find the last row in both sheets
LastRowA = wsA.Cells(wsA.Rows.Count, "A").End(xlUp).Row
LastRowB = wsB.Cells(wsB.Rows.Count, "A").End(xlUp).Row

' Loop through Sheet A to gather punch-in data
For CurrentRowA = 2 To LastRowA ' Assuming headers in row 1
EmployeeID = wsA.Cells(CurrentRowA, 2).Value
If Not EmployeeData.Exists(EmployeeID) Then
Set EmployeeData(EmployeeID) = CreateObject("System.Collections.ArrayList")
End If
EmployeeData(EmployeeID).Add Array(wsA.Cells(CurrentRowA, 1).Value, wsA.Cells(CurrentRowA, 3).Value)
Next CurrentRowA

' Loop through Sheet B to gather punch-out data
For CurrentRowB = 2 To LastRowB
EmployeeID = wsB.Cells(CurrentRowB, 2).Value
If EmployeeData.Exists(EmployeeID) Then
EmployeeData(EmployeeID)(EmployeeData(EmployeeID).Count - 1) = _
Array(EmployeeData(EmployeeID)(EmployeeData(EmployeeID).Count - 1)(0), _
EmployeeData(EmployeeID)(EmployeeData(EmployeeID).Count - 1)(1), _
wsB.Cells(CurrentRowB, 3).Value)
End If
Next CurrentRowB

' Find the maximum number of punches
For Each Key In EmployeeData.Keys
If EmployeeData(Key).Count > MaxPunches Then
MaxPunches = EmployeeData(Key).Count
End If
Next Key

' Add headers to the result worksheet

wsResult.Cells(1, 1).Value = "Employee ID"
wsResult.Cells(1, 2).Value = "Date"
For i = 1 To MaxPunches
wsResult.Cells(1, 2 * i + 1).Value = "Check In " & i
wsResult.Cells(1, 2 * i + 2).Value = "Check Out " & i
Next i
wsResult.Cells(1, 2 * MaxPunches + 3).Value = "Total Hours"

' Set the format for check-in and check-out columns
For i = 1 To MaxPunches
wsResult.Cells(1, 2 * i + 1).NumberFormat = "h:mm:ss"
wsResult.Cells(1, 2 * i + 2).NumberFormat = "h:mm:ss"
Next i


' keyword for missing data
Dim missingKeyword As String
missingKeyword = "MISSING_PUNCH"

' Populate the result worksheet
CurrentRowB = 2
For Each Key In EmployeeData.Keys
For Each punch In EmployeeData(Key)
wsResult.Cells(CurrentRowB, 1).Value = Key
wsResult.Cells(CurrentRowB, 2).Value = punch(0)
Dim totalCheckIn As Double
Dim totalCheckOut As Double

For i = 1 To MaxPunches
If i <= EmployeeData(Key).Count Then
Dim subarray As Variant
If i <= UBound(punch) Then
subarray = punch(i)
If IsArray(subarray) Then
If UBound(subarray) >= 1 Then
' Check if the value can be interpreted as a date and time
If IsDate(subarray(1)) Then
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = TimeValue(subarray(1))
Else
' Replace missing or invalid data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = missingKeyword
End If
Else
' Replace missing or invalid data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = missingKeyword
End If
If UBound(subarray) >= 2 Then
' Check if the value can be interpreted as a date and time
If IsDate(subarray(2)) Then
wsResult.Cells(CurrentRowB, 2 * i + 2).Value = TimeValue(subarray(2))
Else
' Replace missing or invalid data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 2).Value = missingKeyword
End If
Else
' Replace missing or invalid data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 2).Value = missingKeyword
End If
Else
' If subarray is not an array, check if it can be interpreted as a date and time
If IsDate(subarray) Then
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = TimeValue(subarray)
Else
' Replace missing or invalid data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = missingKeyword
End If
End If
Else
' Replace missing data with the keyword
wsResult.Cells(CurrentRowB, 2 * i + 1).Value = missingKeyword
wsResult.Cells(CurrentRowB, 2 * i + 2).Value = missingKeyword
End If
End If
' Calculate Total Hours

On Error Resume Next

If Not IsEmpty(wsResult.Cells(CurrentRowB, 2 * i + 1).Value) Then
totalCheckIn = totalCheckIn + CDbl(wsResult.Cells(CurrentRowB, 2 * i + 1).Value)
End If
If Not IsEmpty(wsResult.Cells(CurrentRowB, 2 * i + 2).Value) Then
totalCheckOut = totalCheckOut + CDbl(wsResult.Cells(CurrentRowB, 2 * i + 2).Value)
End If
Next i
TotalHours = (totalCheckOut - totalCheckIn) * 24 ' Convert to hours
wsResult.Cells(CurrentRowB, 2 * MaxPunches + 3).Value = TotalHours
TotalHours = 0

CurrentRowB = CurrentRowB + 1
Next punch
Next Key


MsgBox "Done"

End Sub
数组 Excel VBA

评论

0赞 Black cat 10/22/2023
晚入住晚退房应该如何解释?
0赞 kamal_hamad 10/22/2023
@Blackcat请进一步解释,我没有得到你的要求。
0赞 Black cat 10/22/2023
第 12 排 E 和 F 列稍后入住然后退房
0赞 kamal_hamad 10/22/2023
@Blackcat谢谢你,亲爱的,我只是用正确的图片替换了图片。现在它制作了更多,并准确地显示了我正在寻找的东西。
0赞 Black cat 10/22/2023
零件有问题。所有打孔时间在 IF 循环中相互覆盖。时间应存储到相关打卡记录中。Loop through Sheet B to gather punch-out data

答:

0赞 vbakim 10/22/2023 #1

我编写了代码,将特定任务分离为函数。因此,代码更加不言自明和可读性。

Sub CombineAttendanceData()
    Dim wsA As Worksheet, wsB As Worksheet, wsResult As Worksheet
    
    Set wsA = ThisWorkbook.Sheets("Sheet1")
    Set wsB = ThisWorkbook.Sheets("Sheet2")
    Set wsResult = CreateOrGetResultSheet()
    
    Dim AttendanceData As Object
    Set AttendanceData = CreateObject("Scripting.Dictionary")
    
    PopulateDictionaryFromSheet wsA, AttendanceData, "_In"
    PopulateDictionaryFromSheet wsB, AttendanceData, "_Out"
    
    PrintDataToResultSheet wsResult, AttendanceData
    FormatResultSheet wsResult
End Sub

Private Function CreateOrGetResultSheet() As Worksheet
    Dim ws As Worksheet
    
    ' Check if "Attendance Result" worksheet already exists. If it does, delete it.
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name = "Attendance Result" Then
            Application.DisplayAlerts = False
            ws.Delete
            Application.DisplayAlerts = True
            Exit For
        End If
    Next ws

    ' Add a new worksheet for the attendance results
    Set CreateOrGetResultSheet = ThisWorkbook.Sheets.Add
    CreateOrGetResultSheet.Name = "Attendance Result"
    
    ' Set the header for the new columns
    With CreateOrGetResultSheet
        .Cells(1, 1).Value = "Employee ID"
        .Cells(1, 2).Value = "Date"
        .Cells(1, 3).Value = "1st in"
        .Cells(1, 4).Value = "1st out"
        .Cells(1, 5).Value = "2nd in"
        .Cells(1, 6).Value = "2nd out"
        .Cells(1, 7).Value = "Total Hours"
    End With
End Function


Private Sub PopulateDictionaryFromSheet(ByRef ws As Worksheet, ByRef AttendanceData As Object, suffix As String)
    Dim LastRow As Long, EmployeeID As Long, EmployeeDate As Date, EmployeeDateKey As String, CurrentSuffix As String
    LastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row

    For i = 2 To LastRow
        EmployeeID = ws.Range("B" & i).Value
        EmployeeDate = ws.Range("A" & i).Value
        EmployeeDateKey = Format(EmployeeDate, "YYYY-MM-DD")
        
        If IsTimeValue(ws.Range("C" & i)) Then
            ' Determine if this is the first or second entry for this type (In or Out)
            CurrentSuffix = DetermineSuffix(AttendanceData, CStr(EmployeeID) & "_" & EmployeeDateKey, suffix)
            
            If Not AttendanceData.Exists(CStr(EmployeeID) & "_" & EmployeeDateKey & CurrentSuffix) Then
                AttendanceData(CStr(EmployeeID) & "_" & EmployeeDateKey & CurrentSuffix) = ws.Range("C" & i).Value
            End If
        End If
    Next i
End Sub

Private Function DetermineSuffix(ByRef AttendanceData As Object, baseKey As String, originalSuffix As String) As String
    ' Check if the original key (like "_In" or "_Out") exists, if it does then this is a second entry so append "2"
    If AttendanceData.Exists(baseKey & originalSuffix) Then
        DetermineSuffix = originalSuffix & "2"
    Else
        DetermineSuffix = originalSuffix
    End If
End Function

Private Sub PrintDataToResultSheet(ByRef wsResult As Worksheet, ByRef AttendanceData As Object)
    Dim CurrentRowB As Long, key As Variant, parts() As String
    
    ' Populate the result worksheet
    CurrentRowB = 2
    Dim uniqueKeys As Object
    Set uniqueKeys = CreateObject("Scripting.Dictionary")

    For Each key In AttendanceData.Keys
        parts = Split(key, "_")
        If Not uniqueKeys.Exists(parts(0) & "_" & parts(1)) Then
            uniqueKeys(parts(0) & "_" & parts(1)) = CurrentRowB
            wsResult.Cells(CurrentRowB, 1).Value = parts(0)
            wsResult.Cells(CurrentRowB, 2).Value = DateValue(parts(1))
            CurrentRowB = CurrentRowB + 1
        End If
        
        Select Case parts(2)
            Case "In"
                wsResult.Cells(uniqueKeys(parts(0) & "_" & parts(1)), 3).Value = AttendanceData(key)
            Case "Out"
                wsResult.Cells(uniqueKeys(parts(0) & "_" & parts(1)), 4).Value = AttendanceData(key)
            Case "In2"
                wsResult.Cells(uniqueKeys(parts(0) & "_" & parts(1)), 5).Value = AttendanceData(key)
            Case "Out2"
                wsResult.Cells(uniqueKeys(parts(0) & "_" & parts(1)), 6).Value = AttendanceData(key)
        End Select
    Next key

    ' Add formula to calculate total hours. Assumes a 24-hour format.
    For i = 2 To wsResult.Cells(wsResult.Rows.Count, 1).End(xlUp).Row
        wsResult.Cells(i, 7).Formula = "=IF(ISNUMBER(D" & i & "),D" & i & "-C" & i & ",0) + IF(ISNUMBER(F" & i & ") * ISNUMBER(E" & i & "),F" & i & "-E" & i & ",0)"
    Next i
End Sub

Private Sub FormatResultSheet(ByRef ws As Worksheet)
    ws.Range("C:F").NumberFormat = "h:mm:ss AM/PM"
    ws.Range("G:G").NumberFormat = "[h]:mm:ss" ' Format for total hours to handle times greater than 24 hours if needed
    ' Optional: AutoFit columns for better visibility
    ws.Columns("A:G").AutoFit
End Sub

Private Function IsTimeValue(targetCell As Range) As Boolean
    IsTimeValue = False
    If IsNumeric(targetCell.Value) And Int(targetCell.Value) = 0 And targetCell.Value <> 0 Then
        IsTimeValue = True
    End If
End Function

评论

0赞 Black cat 10/23/2023
您的代码不会在同一天处理第二个输入输出对。如果你解决了这个问题,它就可以正常工作了。
0赞 vbakim 10/23/2023
应用第二进/出。
0赞 taller 10/23/2023
原始海报 (OP) 似乎试图动态支持多对输入/输出(在现实生活中可能不止两对)并解决丢失的打孔数据。这只是我的猜测。
0赞 kamal_hamad 10/23/2023
完全@taller
0赞 kamal_hamad 10/23/2023
所以基本上,如果员工 X 在同一天休息了两次,输出应该是>>>>> 1 进 1 出 |break|二进二出|break|3进3出--->合计=出3进3+出2进2+出1进1
0赞 taller 10/23/2023 #2
  • 将两个表合并为一个表并对工作表上的数据进行排序将简化左侧部分的管理。
Option Explicit
Sub Demo()
    Const MISSING_DT = "Missing"
    Dim objDic As Object, rngData As Range
    Dim i As Long, j As Long
    Dim arrData, arrRes(), arrTotal(), sKey
    Dim oSht As Worksheet, srcSheet As Worksheet
    Set objDic = CreateObject("scripting.dictionary")
    Set srcSheet = Sheets("Sheet1") ' Modify as needed
    ' Consolidate two tables
    Set oSht = Sheets.Add
    srcSheet.ListObjects("Table_In").Range.Copy oSht.Cells(1, 1)
    oSht.Range("D1") = "Flag"
    ' Add a colum named "Flag" which is filled w/ In or Out
    oSht.Range(oSht.ListObjects(1).Name & "[Flag]").Value = "In"
    srcSheet.ListObjects("Table_Out").DataBodyRange.Copy oSht.Cells(1, 1).End(xlDown).Offset(1)
    oSht.Range(oSht.ListObjects(1).Name & "[Flag]").SpecialCells(xlCellTypeBlanks).Value = "Out"
    ' Sort table
    oSht.ListObjects(1).Range.Sort key1:="ID Number", Order1:=xlAscending, key2:="Date", _
        Order2:=xlAscending, key3:="Time", Order3:=xlAscending, Header:=xlYes
    arrData = oSht.ListObjects(1).DataBodyRange.Value
    oSht.ListObjects(1).Range.Clear
    Dim pair_cnt As Integer
    ReDim arrRes(UBound(arrData), 1 To 2)
    ReDim arrTotal(UBound(arrData), 0)
    ' Output table header
    arrRes(0, 1) = "Date"
    arrRes(0, 2) = "ID Number"
    arrTotal(0, 0) = "Total/Day"
    j = 0: pair_cnt = 0
    For i = LBound(arrData) To UBound(arrData)
        ' Date + ID as key
        sKey = arrData(i, 1) & "|" & arrData(i, 2)
        If objDic.exists(sKey) Then
            objDic(sKey) = objDic(sKey) + 1
        Else
            ' Collect Date, ID
            j = j + 1
            arrRes(j, 1) = arrData(i, 1)
            arrRes(j, 2) = arrData(i, 2)
            objDic(sKey) = 1
        End If
        If objDic(sKey) > pair_cnt Then
            ' Extend columns
            pair_cnt = objDic(sKey)
            ReDim Preserve arrRes(UBound(arrData), 1 To pair_cnt * 2 + 2)
            ' Column header
            arrRes(0, pair_cnt * 2 + 1) = "In_" & pair_cnt
            arrRes(0, pair_cnt * 2 + 2) = "Out_" & pair_cnt
        End If
        ' Loading punch data
        If arrData(i, 4) = "In" Then
            arrRes(j, objDic(sKey) * 2 + 1) = arrData(i, 3)
            If arrData(i + 1, 4) = "Out" Then
                arrRes(j, objDic(sKey) * 2 + 2) = arrData(i + 1, 3)
                arrTotal(j, 0) = arrTotal(j, 0) + arrData(i + 1, 3) - arrData(i, 3)
                i = i + 1
            Else
                arrRes(j, objDic(sKey) * 2 + 2) = MISSING_DT
            End If
        Else
            arrRes(j, objDic(sKey) * 2 + 1) = MISSING_DT
            arrRes(j, objDic(sKey) * 2 + 2) = arrData(i, 3)
        End If
    Next i
    ' Write data to work sheet
    With oSht.Range("A3")
        .Resize(UBound(arrRes), UBound(arrRes, 2)).Value = arrRes
        .Offset(0, 2).Resize(, pair_cnt * 2 + 1).EntireColumn.NumberFormat = "h:mm:ss"
        .End(xlToRight).Offset(0, 1).Resize(UBound(arrRes), 1) = arrTotal
    End With
End Sub

enter image description here