1

Our Sarbanes-Oxley compliance auditors asked for a process to verify which files were changing in our system, so I wrote a quick VBA script in Excel to recurse through a directory tree, get the path, last modified datetime stamp, and size. I saved this all to a CSV a month ago, and ran it again this month. I then compare last month's snapshot to this month's snapshot by creating two dictionaries. The key to the dictionaries is the file path and filename, and the value is an array of DateTime and Size. Then I loop through the dictionaries to compare them. If a file exists in the old snapshot but not the new, I know it was deleted. If it exists in the new snapshot but not the old, it was created, and if the file exists in both but the DateTime or Size values are different I know it was modified.

The issue is that Daylight Savings went into effect between the two runs, and now all the DateTime stamps visually differ on the worksheet by an hour. I stress visually because my code says the two files are the same, and I know the two files are the same, but Excel is displaying the DateTime stamps as being different by 1 hour. I know this is not an Excel issue. It is a well documented "feature" of the Win32 API.

My question is how do I (or should I) deal with a non-computer savvy SOX auditor comparing a past file listing to a present file listing and seeing all the DateTimes of supposedly unchanged files being different by one hour? This tool is being used by several sister companies and will continue to be used for an indeterminate length of time. It is possible it may still be in use when the clocks move ahead an hour next Spring. If I intend to programatically adjust DateTime stamps so that the files appear to have the same Date an Time on the worksheet (and not just the same UTC time in the compared file data), how would I detect if or when DST is in effect?

I don't know that its really needed for my question, but here is the current code. The first bit of code goes in a VBA Worksheet with 2 ActiveX buttons. Put the second set of code in a module and make sure your workbook has 3 worksheets: Current Snapshot, Old Snapshot, and Changes.

Sheet1

Option Explicit

Private Sub cmdTakeSnapshot_Click()
    Dim strStartFolder As String
    Dim SaveChoice As Long

    strStartFolder = selectFolder
    If strStartFolder <> "" Then
        Application.Cursor = xlWait
        Application.ScreenUpdating = False
        takeSnapshot strStartFolder
        Application.ScreenUpdating = True
        Application.Cursor = xlDefault
        SaveChoice = MsgBox("Snapshot complete." & vbNewLine & "Click OK to Save.", vbOKCancel, "Finished")
        If SaveChoice = 1 Then saveSnapshot

        ThisWorkbook.Worksheets("Current Snapshot").Activate
    Else
        MsgBox "No folder selected...exiting", vbOKOnly, "Cancelled"
    End If
End Sub

Private Sub cmdCompareSnapshots_Click()
    Dim FSO As Object
    Dim strStartFolder As String
    Dim strOldSnapshot As String
    Dim SaveChoice As Long

    strOldSnapshot = selectFile
    If strOldSnapshot <> "" Then
        Application.Cursor = xlWait
        Application.ScreenUpdating = False
        loadSnapshot strOldSnapshot
        Application.ScreenUpdating = True
        Application.Cursor = xlDefault
    End If

    strStartFolder = selectFolder
    If strStartFolder <> "" Then
        Application.Cursor = xlWait
        Application.ScreenUpdating = False
        takeSnapshot strStartFolder
        Application.ScreenUpdating = True
        Application.Cursor = xlDefault
        SaveChoice = MsgBox("New snapshot complete." & vbNewLine & "Click OK to Save.", vbOKCancel, "Finished")
        If SaveChoice = 1 Then saveSnapshot
    End If

    ThisWorkbook.Worksheets("Changes").Activate

    Application.Cursor = xlWait
    Application.ScreenUpdating = False
    compareSnapshots
    Application.ScreenUpdating = True
    Application.Cursor = xlDefault
    ThisWorkbook.Worksheets("Changes").Activate
End Sub

Module1

Option Explicit

Public Sub takeSnapshot(sFolder As String)
    Dim FSO As Object 'FileSystemObject
    Dim oFolder As Object 'Folder

    Set FSO = CreateObject("Scripting.FileSystemObject")
    Set oFolder = FSO.GetFolder(sFolder)
    ThisWorkbook.Worksheets("Current Snapshot").UsedRange.Delete
    ThisWorkbook.Worksheets("Current Snapshot").Range("A1:D1") = Array("Last Modified", "Size", "File Name", "Folder")
    ThisWorkbook.Worksheets("Current Snapshot").Range("A1:D1").Font.Bold = True
    ThisWorkbook.Worksheets("Current Snapshot").Columns(1).NumberFormat = "mm/dd/yyyy h:mm:ss"
    listFolders oFolder
    ThisWorkbook.Worksheets("Current Snapshot").Columns.AutoFit

    Set FSO = Nothing
    Set oFolder = Nothing
End Sub

Public Sub loadSnapshot(sFile As String)
    Dim sh As Worksheet
    Dim qt As QueryTable

    Set sh = ThisWorkbook.Sheets("Old Snapshot")
    For Each qt In sh.QueryTables
        qt.SaveData = False
        qt.Delete
    Next
    sh.UsedRange.Delete
    With sh.QueryTables.Add(Connection:="TEXT;" & sFile, Destination:=sh.Range("A1"))
        .TextFileParseType = xlDelimited
        .TextFileCommaDelimiter = True
        .Refresh
    End With
    For Each qt In sh.QueryTables
        qt.SaveData = False
        qt.Delete
    Next
    sh.Range("A1:D1").Font.Bold = True
    sh.Columns(1).NumberFormat = "mm/dd/yyyy h:mm:ss"
    sh.Columns.AutoFit
End Sub

Public Sub compareSnapshots()
    Dim sh As Worksheet
    Dim objOld As Object, objNew As Object, objChanged As Object
    Dim r As Long, lastRow As Long
    Dim sKey As String, aValue(1) As String, sPath As String, sFilename As String
    Dim vItem As Variant

    Set objOld = CreateObject("Scripting.Dictionary")
    Set objNew = CreateObject("Scripting.Dictionary")
    Set objChanged = CreateObject("Scripting.Dictionary")

    Set sh = ThisWorkbook.Sheets("Old Snapshot")
    lastRow = sh.Cells(sh.Rows.Count, 1).End(xlUp).Row
    For r = 2 To lastRow
        If sh.Cells(r, 4) <> "" Then
            sKey = sh.Cells(r, 4).value & sh.Cells(r, 3).value
            aValue(0) = sh.Cells(r, 1).value
            aValue(1) = sh.Cells(r, 2).value
            If Not objOld.Exists(sKey) Then
                objOld.Add sKey, aValue
            End If
        End If
    Next

    Set sh = ThisWorkbook.Sheets("Current Snapshot")
    lastRow = sh.Cells(sh.Rows.Count, 1).End(xlUp).Row
    For r = 2 To lastRow
        If sh.Cells(r, 4) <> "" Then
            sKey = sh.Cells(r, 4).value & sh.Cells(r, 3).value
            aValue(0) = sh.Cells(r, 1).value
            aValue(1) = sh.Cells(r, 2).value
            If Not objNew.Exists(sKey) Then
                objNew.Add sKey, aValue
            End If
        End If
    Next

    Set sh = ThisWorkbook.Sheets("Changes")
    sh.UsedRange.Delete
    r = 2
    For Each vItem In objNew.Keys
        If objOld.Exists(vItem) Then
            Dim vTemp As Variant
            If objOld(vItem)(0) <> objNew(vItem)(0) Then
                vTemp = Split(vItem, "\", -1, vbBinaryCompare)
                sFilename = vTemp(UBound(vTemp))
                sPath = Replace(vItem, sFilename, "", 1, -1, vbBinaryCompare)
                sh.Cells(1, 1) = "Changed Files:"
                sh.Cells(r, 2) = sPath
                sh.Cells(r, 3) = sFilename
                sh.Cells(r, 4) = objOld(vItem)(0)
                sh.Cells(r, 5) = objOld(vItem)(1)
                sh.Cells(r + 1, 4) = objNew(vItem)(0)
                sh.Cells(r + 1, 5) = objNew(vItem)(1)
                r = r + 2
            ElseIf objOld(vItem)(1) <> objNew(vItem)(1) Then
                vTemp = Split(vItem, "\", -1, vbBinaryCompare)
                sFilename = vTemp(UBound(vTemp))
                sPath = Replace(vItem, sFilename, "", 1, -1, vbBinaryCompare)
                sh.Cells(1, 1) = "Changed Files:"
                sh.Cells(r, 2) = sPath
                sh.Cells(r, 3) = sFilename
                sh.Cells(r, 4) = objOld(vItem)(0)
                sh.Cells(r, 5) = objOld(vItem)(1)
                sh.Cells(r + 1, 4) = objNew(vItem)(0)
                sh.Cells(r + 1, 5) = objNew(vItem)(1)
                r = r + 2
            End If

            objOld.Remove vItem
            objNew.Remove vItem
        End If
    Next

    If objOld.Count > 0 Then
        sh.Cells(r, 1) = "Deleted Files:"
        r = r + 1
        For Each vItem In objOld.Keys
            Dim vTempArray As Variant
            vTempArray = Split(vItem, "\", -1, vbBinaryCompare)
            sFilename = vTempArray(UBound(vTempArray))
            sPath = Replace(vItem, sFilename, "", 1, -1, vbBinaryCompare)
            sh.Cells(r, 2) = sPath
            sh.Cells(r, 3) = sFilename
            sh.Cells(r, 4) = objOld(vItem)(0)
            sh.Cells(r, 5) = objOld(vItem)(1)
            r = r + 1
        Next
    End If

    If objNew.Count > 0 Then
        sh.Cells(r, 1) = "Added Files:"
        r = r + 1

        For Each vItem In objNew.Keys
            Dim vTempArray2 As Variant
            vTempArray2 = Split(vItem, "\", -1, vbBinaryCompare)
            sFilename = vTempArray2(UBound(vTempArray2))
            sPath = Replace(vItem, sFilename, "", 1, -1, vbBinaryCompare)
            sh.Cells(1, 1) = "Changed Files:"
            sh.Cells(r, 2) = sPath
            sh.Cells(r, 3) = sFilename
            sh.Cells(r + 1, 4) = objNew(vItem)(0)
            sh.Cells(r + 1, 5) = objNew(vItem)(1)
            r = r + 1
        Next
    End If
    sh.Columns(4).NumberFormat = "mm/dd/yyyy h:mm:ss"
    sh.Columns.AutoFit
End Sub

Public Function listFolders(fldStart As Object)
    Dim oFolder As Object 'Folder
    Dim sh As Worksheet
    Dim r As Long

    Set sh = ThisWorkbook.Worksheets("Current Snapshot")
    For Each oFolder In fldStart.SubFolders
        r = sh.Cells(sh.Rows.Count, 1).End(xlUp).Row
        r = r + 1
        sh.Cells(r, 1) = oFolder.DateLastModified
        sh.Cells(r, 2) = "<DIR>"
        sh.Cells(r, 3) = oFolder.Name
        listFiles oFolder
        listFolders oFolder
        DoEvents
    Next

End Function

Private Function listFiles(oFolder As Object)
    Dim oFile As Object 'File
    Dim sh As Worksheet
    Dim DirSize As Double, Filecount As Double
    Dim r As Long

    On Error GoTo PermissionDenied
    Set sh = ThisWorkbook.Worksheets("Current Snapshot")
    r = sh.Cells(sh.Rows.Count, 1).End(xlUp).Row
    For Each oFile In oFolder.Files
        r = r + 1
        sh.Cells(r, 1) = oFile.DateLastModified
        sh.Cells(r, 2) = oFile.Size
        sh.Cells(r, 3) = oFile.Name
        sh.Cells(r, 4) = oFolder.Path
        DoEvents
    Next
    Exit Function

PermissionDenied:
        sh.Cells(r, 1) = Now()
        sh.Cells(r, 2) = "#N/A"
        sh.Cells(r, 3) = "Permission Denied on Folder:"
        sh.Cells(r, 4) = oFolder.Path
End Function

Public Function selectFolder() As String
    Dim fldr As FileDialog
    Dim sItem As String
    Set fldr = Application.FileDialog(msoFileDialogFolderPicker)
    With fldr
        .Title = "Select a Folder"
        .AllowMultiSelect = False
        .InitialFileName = Application.DefaultFilePath
        If .Show <> -1 Then GoTo NextCode
        sItem = .SelectedItems(1)
    End With
NextCode:
    selectFolder = sItem
    Set fldr = Nothing
End Function

Public Function selectFile() As String
    Dim fldr As FileDialog
    Dim sItem As String
    Set fldr = Application.FileDialog(msoFileDialogFilePicker)
    With fldr
        .Title = "Select a Snapshot"
        .AllowMultiSelect = False
        .InitialFileName = Application.DefaultFilePath
        If .Show <> -1 Then GoTo NextCode
        sItem = .SelectedItems(1)
    End With
NextCode:
    selectFile = sItem
    Set fldr = Nothing
End Function

Public Function saveSnapshot()
    Dim sFolderPath As String

    sFolderPath = selectFolder
    sFolderPath = sFolderPath & "\"
    Application.DisplayAlerts = False
    ThisWorkbook.Worksheets("Current Snapshot").Copy
    ActiveWorkbook.SaveAs Filename:=sFolderPath & Format(Date, "MM-dd-yyyy") & " Snapshot", FileFormat:=xlCSV, CreateBackup:=True
    ActiveWorkbook.Close
    Application.DisplayAlerts = True
End Function
Tim
  • 2,701
  • 3
  • 26
  • 47
  • 1
    Have you explained the issue to the Auditor? What was their response when you pointed out the hour difference is due to DST? – BruceWayne Dec 04 '18 at 15:20
  • If you store all times as (eg) GMT then this would not be an issue. See http://www.excelfox.com/forum/showthread.php/542-Get-standard-GMT-time-from-the-system-using-vba for example – Tim Williams Dec 04 '18 at 16:03
  • I emailed them this explanation...we'll see what they say. A couple of the "IT" guys at some sister companies have noticed it as well so I've had to explain this a few more times. I'm just wondering if it is worthwhile coding a fix for humans (and auditors...just kidding!), and if I do code a fix, how I end up detecting DST or even time zone changes. I'm not absolutely set on VBA, but I have to explain methodology to the auditors. – Tim Dec 04 '18 at 16:03
  • @TimWilliams I think that's exactly what I'm looking for! You can post it as an answer (or I'll just find another of your posts to upviote :P) – Tim Dec 04 '18 at 16:06
  • 3
    *Always* store UTC. If you don't, you *absolute*, *positively* must store timezone information along with the datetime. And do use an unambiguous display format. YYYY-MM-dd is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) compliant. – IInspectable Dec 04 '18 at 17:02

1 Answers1

1

If you store all times as (eg) GMT then this would not be an issue.

See

http://www.excelfox.com/forum/showthread.php/542-Get-standard-GMT-time-from-the-system-using-vba

for example.

EDIT: you might also consider adding an MD5 hash for each file to your sheet.

Tim Williams
  • 154,628
  • 8
  • 97
  • 125