File Dates and Times


The official Visual Basic FileDateTime function still believes that the last file modification time is the only time available. The new and improved FileAny­DateTime function knows not only about the last modification time but also about the creation time and the last access time. What it doesn’t know is which file system you have. Some file systems save only part of the file times, and you can’t get more than the system provides.


Windows 95 supports one file system on each local drive. It will be either VFAT or FAT32, depending on when the operating system was purchased and other factors that don’t concern us because you use the same code to program either system. VFAT and FAT32 are enhancements of the FAT (file allocation table) system, known and despised by MS-DOS and Windows 3.x programmers. Windows NT supports FAT32 or any other installable file system you choose. The default is the Windows NT file system (NTFS). Windows 95 can access NTFS files through networks.


Under MS-DOS, file times were accurate to the nearest two seconds since 1980. Under Win32, they’re accurate to the nearest micromillisecond (or some such) back to sometime just after Columbus logged onto America with his Z80 computer (or whatever they had back then). Or, to be explicit, they’re supposed to be accurate to the number of 100-nanosecond intervals since January 1, 1601, and, under NTFS, they are. But in an effort to be compatible with the old FAT system, VFAT and FAT32 had to compromise. The file creation date is indeed fully accurate to the precision mentioned. The last modification time is accurate to two seconds (the same as on FAT). The last access time is accurate to one day—in other words, you only get the date, not the time.


My better FileAnyDateTime function works like FileDateTime except that it takes additional optional arguments for the last access and creation times. The function returns the modification time directly and the other two times by reference:

Dim datModified As Date, datCreated As Date, datAccessed As Date
datModified = FileAnyDateTime(sFile, datCreated, datAccessed)
Debug.Print "Last modified: " & datModified
Debug.Print "Last accessed: " & datAccessed
Debug.Print "Created: " & datCreated

Under NTFS, the output looks like this:

Last modified: 7/12/96 12:48:52 PM
Last accessed: 7/12/96 1:31:35 PM
Created: 7/6/96 3:49:24 PM

Under VFAT or FAT32, the output looks like this:

Last modified: 7/12/96 12:48:52 PM
Last accessed: 7/12/96
Created: 7/6/96 3:49:24 PM

Here’s the code that returns all those dates:

Function FileAnyDateTime(sPath As String, _
Optional datCreation As Date = datMin, _
Optional datAccess As Date = datMin) As Date
' Take the easy way if no optional arguments
If datCreation = datMin And datAccess = datMin Then
FileAnyDateTime = VBA.FileDateTime(sPath)
Exit Function
End If

Dim fnd As WIN32_FIND_DATA
Dim ftCreate As FILETIME, ftAccess As FILETIME, ftModify As FILETIME
Dim hFind As Long, f As Boolean, stime As SYSTEMTIME
' Get all three times in UDT
hFind = FindFirstFile(sPath, fnd)
If hFind = hInvalid Then ApiRaise Err.LastDllError
FindClose hFind
' Convert them to Visual Basic format
datCreation = Win32ToVbTime(fnd.ftCreationTime)
datAccess = Win32ToVbTime(fnd.ftLastAccessTime)
FileAnyDateTime = Win32ToVbTime(fnd.ftLastWriteTime)
End Function

The key function called from this code is Win32ToVbTime. In earlier versions, I did time conversion the hard way—from FindFirstFile to Win32ToVbTime, which then went from FileTimeToLocalFileTime to FileTimeToSystemTime to DateSerial plus TimeSerial. It was a long, slow conversion involving two API functions and two Visual Basic functions. Here’s how I do it now:

' Difference between day zero for VB dates and Win32 dates
' (or #12-30-1899# - #01-01-1601#)
Const rDayZeroBias As Double = 109205# ' Abs(CDbl(#01-01-1601#))

' 10000000 nanoseconds * 60 seconds * 60 minutes * 24 hours / 10000
' comes to 86400000 (the 10000 adjusts for fixed point in Currency)
Const rMillisecondPerDay As Double = 10000000# * 60# * 60# * 24# / 10000#

Function Win32ToVbTime(ft As Currency) As Date
Dim ftl As Currency
' Call API to convert from UTC time to local time
If FileTimeToLocalFileTime(ft, ftl) Then
' Local time is nanoseconds since 01-01-1601
' In Currency that comes out as milliseconds
' Divide by milliseconds per day to get days since 1601
' Subtract days from 1601 to 1899 to get VB Date equivalent
Win32ToVbTime = CDate((ftl / rMillisecondPerDay) - rDayZeroBias)
Else
ApiRaise Err.LastDllError
End If
End Function

Math is a lot faster than function calls, and this technique avoids one API call and two Visual Basic calls that, behind the scenes, did what you see here out in the open. I wish I had thought of it, but the code actually comes from an
article by Jim Mack in Windows Developer’s Journal (May, 1997). Notice that Win32To­VbTime takes a Currency parameter. Now what in the world could Currency have to do with dates? Nothing really. This is just a continuation of the concept introduced in “Large Integers and Currency” in Chapter 2. The Windows API passes dates around in FILETIME structure variables. A FILETIME is a crude way of representing a 64-bit integer in two 32-bit integer fields called dwLowDateTime and dwHighDateTime. You never mess with these fields individually. FILETIME is just a package for passing a variable of undefined format from one API function to another. In Visual Basic, it’s a lot easier to pass those packages in a Currency type. The advantage is that Currency, unlike FILETIME, is a legal COM Automation type that can be passed in public properties or parameters.


Many Windows API functions pass time parameters as pointers to FILETIME structures, but the Windows API type library redefines these as LPVOID, the type library equivalent of As Any. You can pass a real FILETIME structure to them if you prefer. Unfortunately, there’s no way to be ambiguous about UDT fields. The type library uses the Currency type for structures such as WIN32_FIND­_DATA that would normally have FILETIME fields. For those who don’t like shortcuts, I provide an equivalent WIN32_FIND_DATAO structure with the FILETIME fields. API functions that use these structures take As Any parameters so that you can pass the one you prefer.