
Microsoft Year 2000 Readiness Disclosure
& Resource Center |
 |
 |
 |
Preparing Office Solutions for the Year
2000 |
 |
How
Microsoft Products Assume Centuries
The most
important of handling the Year 2000 problem for desktop development
is to ensure that you understand how your development environment
interprets short years. If you type 12/31/97 into your application,
what year is actually stored? What about 12/31/00? How do you figure
out the rules? The better understand this issue, you need to take a
brief look at some historical issues.
In the
world of 16-bit programming, most applications have their own
internal routines for working with dates. For the most part, a
two-digit year is always assumed to be in the 20th century. So if
you enter a date of 12/31/97, the application stores the value of
12/31/1997. The obvious problem with this approach is that when your
application meets the new millennium, a date entered as 12/31/00
defaults to 12/31/1900, not to 12/31/2000.
Obviously, a better solution was obviously neededùwhy have
each application define its own rules about how dates are managed?
And was automatic conversion of short years to the 20th century the
best approach? The new solution was to make the rules involving
dates to be handled by the operating system and to default to the
current century. This solution was developed during the development
of Microsoft Visual Basic version 4, and Microsoft Office 95
(including Microsoft Access 95, version 7) and involved two things:
- Moving the handling of date rules from the control of
applications to the domain of the operating system (through the
OLE Automation file OLEAUT32.DLL).
- A change in the assumption rule: two-digit years would default
to the current century as defined by the system's clock.
Later,
it was decided that defaulting to the current century was not the
best approach, so the OLE Automation library was updated to
introduce a new rule called the "sliding window". Two-digit years
between 00 and 29 are interpreted as belong in the 21st century, all
others default to the 20th century.
As you
can see, there are now three separate rules governing the
interpretation of short years. The problem is further compounded by
the fact that even within the same version of an application
development tool, you cannot assume that you automatically know
which rule will be used. This is because the rule is defined in the
OLE Automation libraries for 32-bit operating systems.
For
example, Microsoft Access 95 ships with the older OLE Automation
library that uses the rule of always defaulting to the current
century for two digit years. After Microsoft Access 95 shipped, the
OLE Automation library was updated to use the new rule. Just by
installing a program that uses the new library, you change how your
application interprets years.
The new
sliding window rule was introduced into the OLE Automation library
that shipped with a multitude of products. These include Microsoft
Internet Explorer version 3.0 and later, Microsoft Windows NT 3.51
Service Pack 5, Microsoft Windows NT 4.0, Microsoft Windows 95 OSR
2, Microsoft Office 97, Microsoft Visual Basic 5.0 and other
products. The bottom line is the OLEAUT32.DLL versions 2.20.4054 and
later use the new sliding window technique, while previous versions
use the current century rule.
Microsoft Excel on the other hand, does not use the OLE
Automation Library in any version for dealing with dates--it has its
own rules that depend on the version you are using. Excel embeds
Visual Basic for Applications (VBA), however, which does use the OLE
Automation Library.
Determining Which Rule Your Application Is Using
So how
do all these rules and libraries affect Microsoft's desktop
products? And how do you determine which one is in effect for a
given instance of your application? The following table shows how
each version of these applications interpret 2-digit years:
Product |
Rule |
Comments |
- Microsoft Access, version 2
- Microsoft Visual Basic, version 3
|
All two-digit years
are automatically and unchangeably assigned to the 20th
century. |
The logic and rules
for dealing with dates is built into each program's private
core executables and dynamic link libraries. |
- Microsoft Access 95, version 7
|
Uses the OLE
Automation library. |
The rule used depends
on which version of the library you have installed. |
- Microsoft Excel, version 5
- Microsoft Excel 95, version 7
|
A two-digit year less
than 20 is assigned to the 21st century. A two-digit year
greater than or equal to 20 is assigned to the 20th century.
|
For example, a value
of "12/31/19" is stored as 12/31/1919, where a value of
"12/13/20" is stored as 12/13/2020. |
- Microsoft Excel 97, version 8
|
A two-digit year less
than 30 is assigned to the 21st century. A two-digit year
greater than or equal to 30 is assigned to the 20th century.
|
For example, a value
of "12/31/25" is stored as 12/31/2025, where a value of
"12/13/30" is stored as 12/13/1935. |
- Microsoft Visual Basic, version 4,
16-bit
|
VB 4/16 uses the
16-bit version of the OLE Automation library. This version has
never been updated, so the built-in sliding window solution is
not available to VB 4/16 applications. |
Assumes that all
two-digit years belong in the current century as defined by
the system clock. |
- Microsoft Access 97, version 8
- Microsoft Visual Basic, version, 4, 32-bit
- Microsoft Visual Basic, version 5
|
Uses the OLE
Automation library. |
The rule used depends
on which version of the library you have installed. |
|
When dealing with
dates in tasks, events, and meeting events, Outlook uses a
window that defined as 30 years back from the current date and
70 years forward. Birthdays recorded in the Contacts portion
of Outlook use a different rule because a greater bias towards
the past is needed. In this case, two-digit years are assumed
to belong in a window between ninety-five years back and five
years forward from the current date as defined by the system
clock. |
You must update your
OUTTLIB.DLL file to version 8.03 or events that span the
century boundary will not be recorded correctly. |
- Microsoft Word, version 6
- Microsoft Word 95, version 7
|
The two digit year
"00" is interpreted as the year 2000, all other two digit
years are interpreted as being in the 20th century. This rule
is used by Microsoft Word when performing a table sort, and
when using Date formatting (picture switch) of Quote Fields.
|
Issues may exist with
user-input dates when sorting tables. Additionally, data
imported from other programs is represented as text, and
should therefore be examined. |
- Microsoft Word 97, version 8
|
Two digit years from
00 to 29 are assumed to be in the 21st century, all others are
assumed to be in the 20th century. A date window of 1930-2029
is used when interpreting two digit year when using the Date
formatting (picture switch) of Quote fields. |
Issues may exist with
user-input dates when sorting tables. Additionally, data
imported from other programs is represented as text, and
should therefore be examined. | Table 1 Rules Used for Century
Assumption
If you
are using a product listed above as using the OLE Automation
Library, you need to check the version of the OLEAUT32.DLL file on
your system to determine which rule is in place. You can check this
version manually, or use the supplied code to programmatically check
the version.
To
verify the version manually, open Windows Explorer and go to the
Windows\System directory. Locate the file named OLEAUT32.DLL and
right-click on it. From the context menu, select Properties. When
the Properties dialog appears, select the Version tab and click on
'Product Version' to see the version number for the file. If it is
2.20.4049 or later, the sliding window algorithm is being used. If
the version is earlier than this, all two-digit years are assumed to
be in the current century as defined by the system clock.
Programmatically Checking for the Version
You can
also check the version of the OLE Automation Library using VBA
program code. Place the following VBA code in a new module and run
the IsOLELibNewer() procedure to see if the newer version (sliding
window behavior) of OLEAUT32.DLL is installed.
' API Calls
for getting a file's version information Private Type
FileVersion FileVersion As
String ' Full file version as a
string FileVersionMSl As
Integer ' File version MSB
Low FileVersionMSh As
Integer ' File version MSB
High FileVersionLSl As
Integer ' File version LSB
Low FileVersionLSh As
Integer ' File version LSB
High ProductVersion As
String ' File product version as a
string ProductVersionMSl As
Integer ' Product version MSB
low ProductVersionMSh As
Integer ' Product version MSB
high ProductVersionLSl As
Integer ' Product version LSB
low ProductVersionLSh As Integer ' Product
version LSB high End Type
Private Type
VS_FIXEDFILEINFO dwSignature As
Long dwStrucVersionl As
Integer dwStrucVersionh As
Integer dwFileVersionMSl As
Integer dwFileVersionMSh As
Integer dwFileVersionLSl As
Integer dwFileVersionLSh As
Integer dwProductVersionMSl As
Integer dwProductVersionMSh As
Integer dwProductVersionLSl As
Integer dwProductVersionLSh As
Integer dwFileFlagsMask As
Long dwFileFlags As Long
dwFileOS As Long dwFileType As
Long dwFileSubtype As
Long dwFileDateMS As
Long dwFileDateLS As Long End
Type
Private Declare Function GetFileVersionInfo
_ Lib "Version.dll" _
Alias "GetFileVersionInfoA" _ (ByVal
lptstrFilename As String, _ ByVal dwHandle As
Long, _ ByVal dwLen As Long,
_ lpData As Any) _ As
Long
Private Declare Function GetFileVersionInfoSize
_ Lib "Version.dll" _
Alias "GetFileVersionInfoSizeA" _ (ByVal
lptstrFilename As String, _ lpdwHandle As
Long) _ As Long
Private Declare
Function VerQueryValue _ Lib "Version.dll"
_ Alias "VerQueryValueA"
_ (pBlock As Any, _
ByVal lpSubBlock As String, _ lplpBuffer As
Any, _ puLen As Long) _
As Long
Private Declare Sub MoveMemory
_ Lib "kernel32" _ Alias
"RtlMoveMemory" _ (dest As Any,
_ ByVal Source As Long,
_ ByVal length As Long)
Private Sub
GetResourceVersion( _ strFileName As String,
_ recFileVer As
FileVersion) ' Comments : Returns file version
information ' Parameters: strFileName - Name
of the file
' recFileVer - FILEVERSION
type ' Returns : Nothing
' Dim lngRC As Long Dim
lngDummy As Long Dim abytBuffer() As
Byte Dim lngBufferLen As
Long Dim lngVerPointer As
Long Dim udtVerBuffer As
VS_FIXEDFILEINFO Dim lngVerbufferLen As
Long On Error GoTo
PROC_ERR ' Get the
size lngBufferLen =
GetFileVersionInfoSize(strFileName, lngDummy)
If lngBufferLen < 1 Then
Exit Sub End If
' Set
up the byte array ReDim
abytBuffer(lngBufferLen)
' Get the file version
information lngRC =
GetFileVersionInfo(strFileName, 0&, lngBufferLen,
abytBuffer(0)) lngRC =
VerQueryValue(abytBuffer(0), "\", lngVerPointer,
lngVerbufferLen) '
Manipulate the bits MoveMemory udtVerBuffer,
lngVerPointer, Len(udtVerBuffer)
' Build
the file version string recFileVer.FileVersion
= _
Format$(udtVerBuffer.dwFileVersionMSh) & "." &
_
Format$(udtVerBuffer.dwFileVersionMSl) & "." &
_
Format$(udtVerBuffer.dwFileVersionLSh) & "." &
_
Format$(udtVerBuffer.dwFileVersionLSl)
recFileVer.FileVersionLSh =
udtVerBuffer.dwFileVersionLSh
recFileVer.FileVersionLSl =
udtVerBuffer.dwFileVersionLSl
recFileVer.FileVersionMSh =
udtVerBuffer.dwFileVersionMSh
recFileVer.FileVersionMSl =
udtVerBuffer.dwFileVersionMSl
' Build the product version
string recFileVer.ProductVersion =
_
Format$(udtVerBuffer.dwProductVersionMSh) & "."
& _
Format$(udtVerBuffer.dwProductVersionMSl) & "."
& _
Format$(udtVerBuffer.dwProductVersionLSh) & "."
& _
Format$(udtVerBuffer.dwProductVersionLSl)
recFileVer.ProductVersionLSh =
udtVerBuffer.dwProductVersionLSh
recFileVer.ProductVersionLSl =
udtVerBuffer.dwProductVersionLSl
recFileVer.ProductVersionMSh =
udtVerBuffer.dwProductVersionMSh
recFileVer.ProductVersionMSl =
udtVerBuffer.dwProductVersionMSl
PROC_EXIT: Exit Sub
PROC_ERR: MsgBox "Error: " &
Err.Number & ". " & Err.Description, ,
_
"GetResourceVersion" Resume
PROC_EXIT End Sub
Public Function
IsOLELibNewer() As Boolean ' Comments :
Determines if the installed OLE Automation
library ' (OLEAUT32.DLL) has
the sliding window algorithm '
in place. ' Parameters:
None ' Returns : True if the newer version
supporting the sliding '
window algorithm is installed, False otherwise
or ' if an internal error
occurs. ' Dim strFile As
String Dim recFile As
FileVersion On Error
GoTo PROC_ERR strFile =
"OLEAUT32.DLL"
GetResourceVersion strFile, recFile
Select Case
recFile.FileVersionLSh Case
0 ' Old
version
IsOLELibNewer =
False Case
4044 ' Old
algoritm
IsOLELibNewer = False Case Is
>= 4049 ' New
algorithm
IsOLELibNewer = True Case
Else ' Some
unknown version, assume failure
MsgBox "Unknown OLEAUT32.DLL
version."
IsOLELibNewer =
False End
Select PROC_EXIT:
Exit Function
PROC_ERR: MsgBox "Error: " &
Err.Number & ". " & Err.Description, ,
_
"IsOLELibNewer" Resume
PROC_EXIT End Function
Code Listing 1 Determining Which OLE
Library Is Installed
Custom Sliding Window Solutions
If your
application is based on a 16-bit program such as Microsoft Access,
version 2 or Microsoft Visual Basic, version 3, or and application
that uses one of the older versions of the OLE Automation Library,
the sliding window algorithm does not exist. In this case, you have
to create your own code-based algorithm. Additionally, even when you
are using a product that employs the sliding window algorithm, you
must decide if the 00-29 range is right for your application. If
not, you need to implement your own routine.
The
following code shows a sample sliding window procedure. Note that
this version always throws the century information away and supplies
the century according the limits set:
Function
SafeCentury16 (varDateIn As Variant, intPivot As Integer) As
Variant ' Comments : Returns the passed date with the century
modified ' as determined by the specified
pivot year, using ' the following
rule: ' Last Two Digits
Century ' Of Input Year
Assumed ' ----------------
--------- ' <= intPivot
21st ' > intPivot
20th ' ' Note that any century
information supplied in the ' varDateIn
parameter is thrown away. This is
because ' by the time a date gets to this
procedure, it has ' most likely undergone
the Access assumption rule ' and is
therefore suspect. ' ' This version is
for 16-bit hosts such as Access 2.0 ' and
Visual Basic 3.0 ' ' Parameters: varDateIn - variant
containing a date. This can be ' a string
or date, but must be one of the
following ' formats: ' mm/dd/yy ' dd/mm/yy ' mm/dd/yyyy ' dd/mm/yyyy ' intPivot
- year to pivot to the current century. ' Returns : variant of
type 7 (date) containing the
transformed ' date or Null if the passed
value cannot be evaluated ' as a date or
an error occurs. ' Example : SafeCentury16 ("12/13/12", 29)
returns 12/12/2012 ' SafeCentury16
("12/13/1912", 29) returns
12/12/2012 ' SafeCentury16 ("12/13/29",
29) returns 12/13/2029 ' SafeCentury16
("12/13/30", 29) returns 12/13/1930 ' Dim intYear As
Integer Dim fIsDate As Integer Dim fOk As Integer
On
Error GoTo PROC_ERR
' Assume failure fOk = False
'
Determine if the passed value can Select Case
VarType(varDateIn)
Case V_DATE ' Its a
date, simply work on the century fOk =
True
Case V_STRING ' Its a string so we
use IsDate to determine if the value is ' valid
as a date. Note that IsDate() uses the date
settings ' in the Windows Control Panel to
determine validity. Therefore ' certain date
formats (such as "Monday, June 3rd, 1995")
will ' not be considered a valid date by this
procedure. See the ' procedure comments for
valid date formats for use with this '
procedure. On Error Resume
Next fIsDate =
IsDate(varDateIn) fOk = (Err =
0) On Error GoTo PROC_ERR
Case
V_LONG ' Check to see if it is in the serial
date range fOk = (varDateIn > -657434 And
varDateIn < 2958465)
Case Else '
Can't figure out what it is
End Select
If fOk
Then ' Get the right-most two digits of the
year. Note the use of ' Format function to
ensure that the string we search in ' always
has four digits. intYear =
CInt(Right$(Format(varDateIn, "yyyy"),
2))
' Pivot the century. Note the use of
explicit, hard-coded ' centuries. While this
may appear to be non-Year 2000 ' compliant
(don't hardcode century data), it does '
specify the full four digits of the year. And
since ' we need to rely on this procedure to
make exactly ' the assumption we want, the
hard-coded values are correct. If intYear >
intPivot Then ' If the supplied
two-digit year is greater than
then ' pivot number, the returned
value is in the 20th '
century. intYear = 1900 +
intYear
Else ' If the supplied two-digit
year is less than or equal ' to the
pivot number, the returned value is in
the ' 21st
century. intYear = 2000 +
intYear End If
'
Return the variant. Note that we cast it explicitly as a
date. ' This code explicitly creates the date
in the format mm/dd/yyyy. ' If your date format
is different, change the order of '
concatenation. SafeCentury16 =
CVDate(Month(varDateIn) & "/" & Day(varDateIn) & "/"
& intYear) Else SafeCentury16 =
Null End If
PROC_EXIT: Exit
Function
PROC_ERR: SafeCentury16 =
Null Resume PROC_EXIT
End
Function
Code Listing 2 The Sliding Window
Procedure for 16-bit Basic (Access 2.0 and VB 3)
Public
Function SafeCenturyVBA( _ varDateIn As Variant, _ intPivot As
Integer) _ As Variant ' Comments : Returns the passed date
with the century modified ' as determined
by the specified pivot year. If the ' last
two digits of the supplied date are less
than ' or equal to the intPivot value, the
21st century is ' used. Otherwise, the
20th century is used. ' Note that any
century information supplied in
the ' varDateIn parameter is thrown away.
This is because ' by the time a date gets
to this procedure, VB has most ' likely
already assumed the century and the date
is ' therefore suspect. ' Parameters:
varDateIn - variant containing a date. This can
be ' a string or date, but must be one of
the
following ' formats: ' mm/dd/yy ' dd/mm/yy ' mm/dd/yyyy ' dd/mm/yyyy ' intPivot
- year to pivot to the current century. ' Returns : variant of
type 7 (date) containing the
transformed ' date or Null if the passed
value cannot be evaluated ' as a date or
an error occurs. ' Dim intYear As Integer Dim fIsDate As
Boolean Dim fOk As Boolean
On Error GoTo PROC_ERR
'
Assume failure fOk = False
' Determine
if the passed value can Select Case
VarType(varDateIn)
Case vbDate ' Its a
date, simply work on the century fOk =
True
Case vbString ' Its a string so we
use IsDate to determine if the value is ' valid
as a date. Note that IsDate() uses the date
settings ' in the Windows Control Panel to
determine validity. Therefore ' certain date
formats (such as "Monday, June 3rd, 1995")
will ' not be considered a valid date by this
procedure. See the ' procedure comments for
valid date formats for use with this '
procedure. On Error Resume
Next fIsDate =
IsDate(varDateIn) fOk = (Err =
0) On Error GoTo PROC_ERR
Case
vbLong ' Check to see if it is in the serial
date range fOk = (varDateIn > -657434 And
varDateIn < 2958465)
Case Else '
Can't figure out what it is
End Select
If fOk
Then ' Get the right-most two digits of the
year. Note the use of ' Format function to
ensure that the string we search in ' always
has four digits. intYear =
CInt(Right$(Format(varDateIn, "yyyy"),
2))
' Pivot the century. Note the use of
explicit, hard-coded ' centuries. While this
may appear to be non-Year 2000 ' compliant
(don't hardcode century data), it does '
specify the full four digits of the year. And
since ' we need to rely on this procedure to
make exactly ' the assumption we want, the
hard-coded values are correct. If intYear >
intPivot Then ' If the supplied
two-digit year is greater than
then ' pivot number, the returned
value is in the 20th '
century. intYear = 1900 +
intYear Else ' If the supplied
two-digit year is less than or
equal ' to the pivot number, the
returned value is in the ' 21st
century. intYear = 2000 +
intYear End If
' Return the variant.
Note that we we cast it explicitly as a
date. SafeCenturyVBA = CVDate(Month(varDateIn)
& _ "/" &
Day(varDateIn) &
_ "/" &
intYear) Else SafeCenturyVBA =
Null End
If
PROC_EXIT: Exit
Function
PROC_ERR: MsgBox "Error: "
& Err.Number & ". " & Err.Description, ,
_ "SafeCenturyVBA"
SafeCenturyVBA
= Null
Resume PROC_EXIT
End Function
Code Listing 3 The Sliding Window
Procedure for VBA
|
|
|