Many attributes in Active Directory have a data type (syntax) called Integer8. These 64-bit numbers (8 bytes) often represent time in 100-nanosecond intervals. If the Integer8 attribute is a date, the value represents the number of 100-nanosecond intervals since 12:00 AM January 1, 1601. Any leap seconds are ignored.

In .NET Framework (and PowerShell) these 100-nanosecond intervals are called ticks, equal to one ten-millionth of a second. There are 10,000 ticks per millisecond. In addition, .NET Framework and PowerShell DateTime values represent dates as the number of ticks since 12:00 AM January 1, 0001.

ADSI automatically employs the IADsLargeInteger interface to deal with these 64-bit numbers. This interface has two property methods, HighPart and LowPart, which break the number up into two 32-bit numbers. The HighPart and LowPart property methods return values between -2^31 and 2^31 - 1. The standard method of handling these attributes is demonstrated by this VBScript program to retrieve the domain lockoutDuration value in minutes.

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
' Retrieve lockoutDuration with IADsLargeInteger interface.
Set objDuration = objDomain.lockoutDuration
' Calculate number of 100-nanosecond intervals.
lngDuration = (objDuration.HighPart * (2^32)) + objDuration.Lowpart
' Convert to minutes. The value retrieved is negative, so make positive.
lngDuration = -lngDuration / (60 * 10000000)
Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration

However, whenever the LowPart method returns a negative value, the calculation above is wrong by 7 minutes, 9.5 seconds. The work-around is to increase the value returned by the HighPart method by one whenever the value returned by the LowPart method is negative. The revised code below gives correct results in all cases.

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
' Retrieve lockoutDuration with IADsLargeInteger interface.
Set objDuration = objDomain.lockoutDuration
lngHigh = objDuration.HighPart
lngLow = objDuration.LowPart
' Adjust for error in IADsLargeInteger interface.
If (lngLow < 0) then
    lngHigh = lngHigh + 1
End If
' Calculate number of 100-nanosecond intervals.
lngDuration = (lngHigh * (2^32)) + lngLow
' Convert to minutes.
lngDuration = -lngDuration / (60 * 10000000)
Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration

The error introduced if this inaccuracy is not accounted for is not large. The error is always 2^32 100- nanosecond intervals, which is 7 minutes, 9.5 seconds. All the programs on this site that deal with Integer8 attributes have been revised as shown on this page to give accurate results.

The link on the left discusses the details of this problem and unsigned arithmetic.

The function linked below accounts for this problem and can be used to convert any Integer8 attribute value into a date in the local time zone:

Integer8Date.txt <<-- Click here to view or download the program

For completeness, here is a VBScript program that converts a date and time in the local time zone into the corresponding Integer8 value:

DateToInteger8.txt <<-- Click here to view or download the program

An alternative method to convert Integer8 values into dates uses the Windows time service tool w32tm.exe. This is included with Windows XP and Windows Server 2003 default installations (and newer operating systems). This tool can be used to convert 64-bit values to dates in the local time zone. The program must still use the IADsLargeInteger property methods to convert the Integer8 value to a 64-bit number. We must also account for the inaccuracy described above when the LowPart method returns a negative value. However, w32tm.exe takes the local time zone bias into account and converts a 64-bit value into a date and time in the local time zone. The program linked below also uses the Exec method of the wshShell object, so it requires WSH 5.6 as well as w32tm.exe. The example program demonstrating a function using w32tm.exe is linked here:

Integer8Date2.txt <<-- Click here to view or download the program

In PowerShell (and .NET Framework) DateTime values are represented internally as the number of Ticks since 12:00 AM January 1, 0001. Ticks due to leap seconds are ignored (as are the days lost when the switch was made from the Julian to the Gregorian calendar in 1582). A PowerShell script to convert an Integer8 value into the corresponding date in both the local time zone and UTC (Coordinated Universal Time) is linked here:

PSInteger8ToDate.txt <<-- Click here to view or download the program

And a PowerShell script to convert a DateTime value in the local time zone into the corresponding Integer8 value is linked here:

PSDateToInteger8.txt <<-- Click here to view or download the program

If you use ADO in a VBScript program to retrieve Integer8 attribute values, the following code will not invoke the IADsLargeInteger interface and will raise an error:

Do Until adoRecordset.EOF
    ' This does not invoke the IADsLargeInteger interface.
    Set objDate = adoRecordset.Fields("pwdLastSet")
    ' This statement raises an error.
    lngHigh = objDate.HighPart
    ' Likewise, the Intger8Date function, documented above,
    ' raises an error.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

You must either specify the Value property of the Field object and use the Set keyword:

Do Until adoRecordset.EOF
    ' Specify the Value property of the Field object.
    Set objDate = adoRecordset.Fields("pwdLastSet").Value
    ' Invoke methods of the IADsLargeInteger interface directly.
    lngHigh = objDate.HighPart
    ' Or use the Integer8Date function documented in the link above.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

Or, you must assign the value to a variant, and then use the Set keyword to invoke the IADsLargeInteger interface:

Do Until adoRecordset.EOF
    ' Assign the value to a variant.
    lngDate = adoRecordset.Fields("pwdLastSet")
    ' Use the Set keyword to invoke the IADsLargeInteger interface.
    Set objDate = lngDate
    ' Invoke methods of the IADsLargeInteger interface directly.
    lngHigh = objDate.HighPart
    ' Or use the Integer8Date function documented in the link above.
    dtmDate = Integer8Date(objDate, lngTZBias)
    adoRecordset.MoveNext
Loop

A complication arises if the Integer8 attribute does not have a value. If you attempt to retrieve the value directly from the Active Directory object, the IADs interface returns data type "Empty", instead of "Object". If you use ADO to retrieve the attribute value, the data type is "Null" when the Integer8 attribute has no value. In both cases, the Set statement used to invoke the IADsLargeInteger interface raises an error. One way to handle this is to trap the possible error. For example:

Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")
On Error Resume Next
Set objDate = objUser.lockoutTime
If (Err.Number <> 0) Then
    On Error GoTo 0
    dtmDate = "Never"
Else
    On Error GoTo 0
    ' Use the Integer8Date function documented in the link above.
    dtmDate = Integer8Date(objDate, lngTZBias)
End If

An alternative way to handle the possibility that an Integer8 attribute does not have a value is to use the VBscript TypeName or VarType function. For example:

Do Until adoRecordset.EOF
    strType = TypeName(adoRecordset.Fields("lockoutTime").Value)
    If (strType = "Object") Then
        Set objDate = adoRecordset.Fields("lockoutTime").Value
        ' Use the Integer8Date function documented in the link above.
        dtmDate = Integer8Date(objDate, lngTZBias)
    Else
        dtmDate = "Never"
    End If
    adoRecordset.MoveNext
Loop

Another complication arises if the Integer8 value corresponds to a date so far in the future that an error is raised when the 64-bit value is converted into a date. The accountExpires attribute is the only one where this has been seen. If a user object has never had an expiration date, Active Directory assigns the value 2^63 - 1 to the accountExpires attribute. This is the largest number that can be saved as a 64-bit value. It really means "never". If you attempt to convert the value to a date using the CDate function, an error is raised. The Integer8Date and Integer8Date2 functions linked above account for this and trap the error. The following table documents the possible values that have been observed for several Integer8 attributes that represent dates.

attribute No Value 0 2^63 - 1 Date
lastLogon  Yes Yes   Yes
lastLogonTimeStamp Yes Yes   Yes
pwdLastSet   Yes   Yes
accountExpires    Yes Yes Yes
lockoutTime Yes Yes   Yes
badPasswordTime Yes Yes   Yes

Finally, it should be noted that a few Integer8 attributes can be modified with the IADsLargeInteger interface. So far the only Integer8 attributes found that can be modified in code (and assigned values other than 0 and -1) are maxStorage, accountExpires, maxPwdAge, minPwdAge, lockoutDuration, and lockoutObservationWindow. For example, the following VBScript program assigns the account expiration date of November 21, 2009, 4:02:18 PM UTC:

Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")
Set objDate = objUser.Get("accountExpires")
objDate.HighPart = 30042820
objDate.LowPart = 1500000
objUser.Put "accountExpires", objDate
objUser.SetInfo

Because the accountExpires attribute seems to always have a value (see the table above), we do not expect an error to be raised by the "Set objDate" statement. If an error could be raised, the solution would be to first assign a value that does not require the IADsLargeInteger interface, such as 0 (zero), so that we can then retrieve the object reference required to assign values with the HighPart and LowPart methods.

And the following VBScript program assigns the value -9,000,000,000 (corresponding to 15 minutes) to the domain minPwdAge attribute:

Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
Set objDate = objDomain.Get("minPwdAge")
objDomain.HighPart = -3
objDomain.LowPart = -410065408
objDomain.Put "minPwdAge", objDate
objDomain.SetInfo