![](/file/18315/Y2K_Resource_Nov99.iso/webcon~1/usa/year2k/images/spacer.gif)
Microsoft Year 2000 Readiness Disclosure
& Resource Center |
![](/file/18315/Y2K_Resource_Nov99.iso/webcon~1/usa/year2k/images/spacer.gif) |
![](/file/18315/Y2K_Resource_Nov99.iso/webcon~1/usa/year2k/images/spacer.gif) |
![](/file/18315/Y2K_Resource_Nov99.iso/webcon~1/usa/year2k/images/spacer.gif) |
Developing Year 2000 Compliant
Applications in Microsoft Visual Basic |
![](/file/18315/Y2K_Resource_Nov99.iso/webcon~1/usa/year2k/images/spacer.gif) |
5. Going Under the Covers: Dating
Assignments
Most of
the work with dates in Visual Basic involves processing data
taken from an outside source. The source can be a database, a file,
an interface, the operating system, or the user. In all instances,
we are subject to data that is often in a string format and might be
formatted in a way that is outside our direct control.
To make
a system year 2000 compliant, we can either enforce the rule that
all dates supplied must be in a four-digit year format, or make the
system perform a conversion to a compliant format. Often, the latter
method is considered easier and more cost-effective, especially
where the user interface is concerned. In each case, sooner or later
we will have to deal with dates that have only two-digit years.
Assigning Noncompliant Dates: Visual Basic Default
Behavior
To
predict the resultant date from an assignment, we need to find out
what Visual Basic will do by default to convert to its native
Date data type when presented with a noncompliant date. Invariably,
a noncompliant date will originate from a string, whether it is the
contents of a text box or a database field.
ItÆs
time for a little detective work. We want to find out what
Visual Basic does when asked to assign a date when the century
is missing. As an example, try the following code:
Dim dteMyDate
As Date
dteMyDate = CDate("12 Feb 01")
MsgBox
Format$(dteMyDate, "dd MMM yyyy")
Under
most circumstances, Visual Basic will give us the answer 12 Feb
2001. If it does not, bear with meùthis is leading somewhere. Now
substitute the following code for the second line:
dteMyDate =
CDate("12 Feb 35")
This
time the answer is likely to be 12 Feb 1935! So what is going on?
Visual Basic is being smart. When the line of code
dteMyDate = CDate("12 Feb 35") is executed, Visual Basic spots
the fact that only two digits were given for the year, and applies
an algorithm to expand it to four. This is something humans do
intuitively, but computers, literal beasts that they are, need
rules. The algorithm used can be expressed like this:
If Years <
30 Then Century Is 21st ' 20xx Else ' >= 29 Century Is
20th ' 19xx End If
Another,
easier way to visualize this is to consider all dates with only two
digit years to be within a 100-year window, starting at 1930 and
ending at 2029, as shown in Figure 3.
Figure
3 The 100-year date window used by
Visual Basic. (If your browser does not support
inline frames, click here to view Figure
3 in a separate page.)
The
results of our detective work might not be consistent because there
is one final complicatio n at work here. A system library file
OLEAUT32.dll specifies the date behavior for all of the 32-bit
implementations of Visual Basic. This is one of the libraries
at the heart of Microsoft ActiveX« technologies platform controls
scripting server framework and Component Object Model (COM).
Currently, we know of several versions of this file. This table
lists them:
OLEAUT32
File Version |
Size
(bytes) |
Date
Window |
2.1
|
232,720 |
Current Century
|
No
Version Information |
257,560 |
Current Century
|
2.20.4044 |
470,288 |
Current Century
|
2.20.4049 |
473,872 |
1930 û 2029 |
2.20.4054 |
491,792 |
1930 û 2029 |
2.20.4103 |
491,280 |
1930 û 2029 |
2.20.4112 |
490,256 |
1930 û 2029 |
2.20.4118 |
492,304 |
1930 û 2029 |
2.20.4122 |
503,808 |
1930 û 2029 |
2.30.4261 (Installed with
VB6) |
598,288 |
1930 û 2029*
| *
User-configurable date window
It is
important to note that, as of Visual Basic 6.0, developers have
a sliding date-window. Although the default
behavior of your application will be to use 1930-2029 as your
window, the user can set this to any preferred window using Control
Panel.
It is
also worth noting that the Visual Basic Setup wizard includes
the current version of OLEAUT32.dll. This is
important , since Visual Basic 6.0 executables will not work
with earlier versions of OLEAUT32. It is not sufficient to copy the
.exe and its run-time .dll onto your target machine. You must
provide a proper setup that includes, and registers where necessary,
the additional dependencies such as OLEAUT32.dll. The Setup wizard
is the minimum requirement for this task.
The
final issue with the default behavior of Visual Basic and
OLEAUT32 is the range of the window itself. It is very biased toward
past dates. This window is starting to get restrictive on the dates
it can interpret. Certainly, in some financial areas it is not
uncommon to enter dates 25 or even 30 years in the future. As an
example, look at the standard mortgage, which has a term of 30
years. If I enter the date of the final payment for a new mortgage
taken out in May 1998, it would extend through May 2028, just one
year before the end of this window. That doesnÆt leave a great deal
of breathing space.
We want
to implement an improved interpretation algorithm that leaves us
immune to possible disruptive changes to the window used by
OLEAUT32, and gives us more breathing space than the current 2029
ceiling. Although there are no silver bullets to address this issue,
we can do much to improve this default behavior.
Although
this is definitely a step forward, similar functionality currently
has not been made available on either Windows 95 or Microsoft
Windows NT« operating system. For this reason, it is still of
minimal use to the Visual Basic developer, unless the target
operating environment can be guaranteed to be Windows 98. I have no
doubt that in time this functionality will spread across the entire
Windows family of operating systems. Unfortunately, time is a
priceless commodity in this particular field of endeavor.
Stop the Presses: Microsoft Releases Windows 98
Windows
98 puts another angle on our windowing discussion. If you select the
Date tab in the Regional Settings dialog box in
Windows 98, youÆll see a new field where you can change the date
window in your system settings. Changing the data in this field
alters the behavior of OLEAUT32.dll, moving the default window that
expands two digit years. For this feature to work with a
Visual Basic application you need OLEAUT32.dll version
2.30.xxxx or later, otherwise the setting is ignored. Unfortunately,
Windows 98 ships with version 2.20.4122 of this file, which does not
support the new window, so if you intend to make use of it, you must
install a more recent version on the target machine.
(Visual Basic 6.0 ships with version 2.30.4261.)
Assigning Noncompliant Dates: The Sliding Window as an
Alternative
By
default, Visual Basic implements a fixed
window algorithm for interpreting ambiguous dates. It uses a
100-year window that is fixed to the range 1930 to 2029 (barring
changes to OLEAUT32). This means that any ambiguous date will be
interpreted as lying within that 100-year window.
A more
flexible alternative is to use a custom implementation of a sliding
window algorithm. The sliding window works by taking the
noncompliant initial date and ensuring that it is converted to a
date within the 100-year window, but in this case a window that
moves with the current year. This is done by using a range of 100
years, bounded at the bottom by a pivot year that is calculated as
an offset from the year of the current system date. This means that
as the current year changes, the window changes with it. This
algorithm provides a future-proof method of
interpreting ambiguous dates because the window will always extend
the same distance before and after the current year. Additionally,
we are no longer using the OLEAUT32 algorithm, so changes to it will
not affect us.
Figure 4
shows how a sliding window moves with the current system year,
keeping a balanced window. Compare this to the default window in
Figure 3, which is already biased toward past dates. If you imagine
this same situation 10 years in the future, the difference is more
marked.
Figure
4 A sliding 100-year window with a pivot year
offset of û50. The first diagram shows the window for the year 1998,
the second for 2008. (If your browser does not support
inline frames, click here to view Figure
4 in a separate page.)
Listing
8-1 below shows the function dteCSafeDate, which uses this sliding window algorithm to
convert a date expression passed to it into a Visual Basic Date
type. If you use this routine instead of assigning the date directly
to a variable, or use it in place of Visual Basic date
conversion functions, you can bypass default windowing behavior of
Visual Basic and apply your own more flexible date window.
Note The CSafeDate class is included
on the companion file, tmsy2k.zip in the folder Chap08\SubClass
Windowed.
The dteCSafeDate function also allows you to select how
many years in the past you would like your pivot year to be, tuning
the window to the particular needs of your business. If you leave
this at the default, -50, the pivot date will always be calculated
as 50 years prior to the current year.
Private Const
ERROR_TYPE_MISMATCH As Long = 13
Public Function
dteCSafeDate(ByVal ivExpression As Variant, _ Optional ByVal
inPivotOffset As Integer = -50, _ Optional ByRef iobWindowed As
Boolean = False) _ As Date ' Convert the passed Date literal
to a VB Date data type, replacing ' VB's conversion functions. It
will bypass VB's date windowing ' (if necessary) by applying our
own sliding window prior to the ' final
conversion. '------------------------------------------------- '
If we are converting a string to a date, we delegate most of
the ' work to the VBA Conversion and DateTime routines. This
takes ' advantage of the fact that VB will be able to translate
literals ' containing months as names. We step in ourselves only
to provide ' the century where one is not
present. '-------------------------------------------------
'
The literal is broken down into these parts before '
reassembling as a Date. Dim nYear As Integer Dim nMonth As
Integer Dim nDay As Integer Dim dTime As Double
' This
is used in our own windowing algorithm. This is the ' lowest year
in our 100 year window used to assign century '
information. Dim nPivotYear As Integer
' This is used to
indicate a special case, arising from a ' literal that contains
the year as '00'. This will be ' replaced temporarily with 2000
so that we can parse the date, ' but this flag tells our routine
that the 2000 was not ' originally there and to treat it as
00. Dim bFlag00 As Boolean
' We temporarily assign the
date to get some basic information ' about it. Dim dteTempDate
As Date
' This indicates to the calling code whether we used
our window ' during our conversion. Initialize it to indicate
that we ' haven't yet, we will overwrite this later in the
routine if ' necessary. iobWindowed = False
Select Case
VarType(ivExpression)
Case vbDate ' The Date literal is
already a Date data type. Just ' assign it
directly. dteCSafeDate = ivExpression
Case vbDouble,
vbSingle ' If the Date literal is a Double, convert it directly
to ' a date. dteCSafeDate =
VBA.Conversion.CDate(ivExpression)
Case vbString ' If the
literal is a string, we have quite a bit of ' work to do as the
string might be in any number of ' different (international)
formats.
' Check that the literal is valid to be made into a
Date. If Not VBA.Information.IsDate(ivExpression)
Then
'----------------------------------------------- '
There is a date 02/29/00 (or equivalent) that OLEAUT32 '
currently windows to be 02/29/2000, which is a valid ' date. If
the used window were to change in the future, ' this may be
reported as invalid at this point, even ' though our window may
make it valid. Check for this ' date by looking for 00 in the
literal and replacing it ' with '2000,' which will be valid
regardless. We do not ' use the year as 2000 when applying our
window, but it ' does allow us to continue while ignoring the
assumed '
year. '----------------------------------------------- Dim
nPos As Integer nPos = InStr(ivExpression, "00") If 0 = nPos
Then
' The date did not contain the year 00, so there '
was some other reason why it is not valid. ' Raise the standard
VB Type Mismatch Error. Err.Raise
ERROR_TYPE_MISMATCH
Else
' Replace the 00 with 2000,
and then retest to ' see if it is valid. IvExpression =
Left$(ivExpression, nPos - 1) & _ "2000" &
_ Mid$(ivExpression, _ nPos + 2) bFlag00 = True
If
Not VBA.Information.IsDate(ivExpression) Then ' The date is still
not valid, so accept ' defeat and raise the standard VB Type '
Mismatch error and exit. Err.Raise ERROR_TYPE_MISMATCH End
If End If End
If
'--------------------------------------------------- '
If we have gotten here the passed date literal is one that '
VB/OLEAUT32 understands, so convert it to a temporary date ' so
that we can use VB built-in routines to do the hard ' work in
interpreting the passed literal. Doing this makes ' our routine
compatible with any international formats ' (and languages) that
would normally be
supported. '-------------------------------------------------- dteTempDate
= VBA.Conversion.CDate(ivExpression)
' First we get the year
of the Date and see if it was ' included fully in the date
literal. If the century was ' specified, assign the date directly
as there is no need to ' apply any windowing. ' ** If bFlag00
is set then we ourselves put ' the 2000 in there so fail this
test regardless. ** nYear = VBA.DateTime.Year(dteTempDate) If
0 <> InStr(ivExpression, CStr(nYear)) And _ bFlag00 = False
Then
' We found the year in the passed date. Therefore '
the date already includes century information, so ' convert it
directly into a date. dteCSafeDate =
dteTempDate
Else
'-------------------------------------------------- '
The passed date literal does not include the ' century. Use VB's
DateTime functions to get the ' constituent parts of the passed
date. Then ' overwrite the century in the year with one '
calculated from within our 100-year
window. '-------------------------------------------------- nMonth
= VBA.DateTime.Month(dteTempDate) nDay =
VBA.DateTime.Day(dteTempDate) dTime =
VBA.DateTime.TimeValue(dteTempDate)
' Remove any century
information that VB would have ' given the year. nYear = nYear
Mod 100
' Get the pivot year from the current year and
the ' offset argument. nPivotYear =
VBA.DateTime.Year(VBA.DateTime.Now) + _ inPivotOffset
'
Get the century for the pivot year and add that to ' the
Year. nYear = nYear + (100 * (nPivotYear \ 100))
' If the
Year is still below the bottom of the ' window (Pivot Year), add
100 years to bring it ' within the window. If nYear <
nPivotYear Then nYear = nYear + 100 End
If
'-------------------------------------------------- '
We now have all of the parts of the date; it is ' now time to
reassemble them. We do this by ' recreating the date as a string
in the ISO8601 ' International Date format (yyyy-mm-dd) to
prevent ' any ambiguities caused by regional formats. ' '
The alternative is to use the function DateSerial ' but this will
cause unexpected results if assigned ' values outside the correct
range (i.e. assigning ' Y1900, M2, D29 results in a date value
of ' Mar/01/1900 as the month is rolled over to '
accommodate the extra day). It is better to cause ' an error in
this circumstance as that is what ' CDate would
do. '-------------------------------------------------- dteCSafeDate
= CStr(nYear) & "-" & CStr(nMonth) _ & "-" &
CStr(nDay) & " " _ & Format$(dTime, "hh:mm:ss")
'
Set the passed iobWindowed argument to True, ' indicating to the
calling code that we had to ' apply a window to the
year. iobWindowed = True
End If
Case Else
'
Any other variable type is not possible to convert Err.Raise
ERROR_TYPE_MISMATCH
End Select
End Function
Listing 8-1 A date conversion function
incorporating a sliding window algorithm.
This is
a large but generally straightforward function. Check the data type
of the incoming expression. If it is numeric or already a Date, it
cannot be ambiguous, so we convert the value directly to a Date and
return it. The only intrinsic data type that can hold an ambiguous
date is the String, so we also check for this.
With
strings, we would prefer not to write the code to interpret the
nearly infinite number of possible combinations of format, language,
and order that can comprise a valid date expression, so we cheat. We
still get Visual Basic to perform all of the conversion, but we
ensure that there is a century present within the expression before
the final conversion takes place, adding it ourselves if necessary.
With this in mind, the first thing we do is check if the expression
contains century information. If it does contain the century, it is
not ambiguous, so again we can get Visual Basic to perform the
conversion, because no windowing is necessary.
Check
for century information by letting Visual Basic temporarily
convert the expression to a Date; then look for the year of the
resulting date within the original expression. If it is found, the
expression is safe and can be converted as is. If not, the date will
need a window applied, to assign it a century before the final
conversion.
We must
deal with one special case at this stage. Currently there is a date,
Feb 29 00 (or a similar format), that the existing
Visual Basic/OLEAUT32 window will interpret as Feb 29 2000,
which is a valid date. If you have tried entering this particular
date into the older 16-bit versions of Visual Basic you might
have found that it is rejected as invalid. This is because it
interpreted it as Feb 29 1900, whichùif you have been paying
attentionùnever existed. While this will not be an issue with the
current window, only one in every four possible interpretations of
Feb 29 00 is actually a valid date. Therefore, we have code to
account for this expression that might be rejected when we use
Visual Basic to perform this temporary interpretation, but that
we can interpret differently later in the routine. We do this by
replacing the 00 for the year with 2000 so that it can be
interpreted successfully by Visual Basic, regardless of the
window applied.
If the
expression does not contain the century, we will have to do some
work. To avoid the default window, we have to make sure that the
date has a century before the final conversion. We temporarily
convert the expression to a Date, which we immediately break down
into its constituent year, month, day, and time parts. The year is
the only one of concern, so we remove any century that
Visual Basic has assigned, and assign the correct century from
our own window, which is calculated as 100 years starting from the
current system date minus the offset to the pivot year. Once this is
done, we reassemble the four parts of the date, including the new
year, and let Visual Basic perform the final conversion to the
Date.
All of
this probably seems quite long-winded, but the effort is well worth
the flexibility that it gives you to specify your own date
interpretation window.
This
function is a simple replacement for the date assignment functions
CDate, CVDate, and DateValue,
as shown in the code below. You can use the same algorithm to
create a function to replace the DateSerial
function.
Dim dteMyDate
As Date
' Convert the ambiguous expression to
"04/16/2035". dteMyDate = dteCSafeDate("04/16/35", -50) MsgBox
FormatDateTime$(dteMyDate, vbLongDate)
More on Assignments: Implicit Coercion
So the
good news is that if everybody in your organization uses the dteCSafeDate function to do their date conversions,
the interpretation will be watched for you in a way that is superior
to the default. Oh, if only everything were that simple.
One of
the strongest criticisms currently aimed at Visual Basic is
that it is weakly typed, meaning data can be easily coerced from one
type to another. Other languages such as Pascal, and to a certain
extent C and C++, make you explicitly perform type conversion, also
known as casting. Visual Basic is too helpfulùit will do the
conversion for you.
Sure, it
is one less thing for you to worry about. If you want to make an
assignment, Visual Basic will help you along. But try this one
on for size:
Dim A As
Integer Dim B As Single
B = 3.1415926 A = B *
2
MsgBox A
And the
answer is à 6. If you assign a real number to an integer,
Visual Basic will assume you mean it, and discard the fraction.
We refer to this as implicit conversion. You probably worked this
one out as you typed it in, but what if the declarations were in
separate parts of the application, or one was a public property of a
component? Faults like this are among the most difficult to trace,
and Visual Basic makes them easy to create. A strongly typed
language would have prevented you from assigning a Single directly
to an integer by producing an error at compile time, forcing you to
convert the data explicitly.
The
relevance of this type conversion to the Date issue is that you can
implicitly convert other data types to Dates within
Visual Basic as easily. We have covered the explicit
conversions with the dteCSafeDate function, but
this function will sit idly on the bench if there is code that is
making direct assignments to Dates. The following code illustrates
this perfectly:
Dim dteDate1
As Date Dim dteDate2 As Date
' Include the dteCSafeDate
function shown above. dteDate1 = dteCSafeDate("12/04/35",
-50) dteDate2 = "12/04/35" MsgBox DateDiff("d", dteDate1,
dteDate2)
Just
looking at the code, you would expect to see 0 displayed. When you
actually see û36525 displayed you might be surprised, especially as
this sort of thing is an intermittent fault. If I had used the date
12/04/98, the response would be 0. This is due to the differences in
the date windows used. When Visual Basic executes the line of
code dteDate2 = "12/04/35" it does an implicit
CDate("12/04/35") for us, whether we wanted it
to or not.
One way
to get around this fault is to add a new data type to the language,
the CSafeDate class. This is a class module that contains a Date
data type internally, but allows you to perform additional
functionality when an assignment is made through the Property
Procedures, in this case applying our own sliding window algorithm
to expand any ambiguous dates as they are assigned. Listing 8-2
shows an implementation of the CSafeDate class (minus a private copy
of the dteCSafeDate function). The DateValue property is set to be the default, allowing
us to use the class in a way that is very similar to a standard
Date.
Option
Explicit
Private m_dteInternalDate As Date Private
m_iPivotOffset As Integer Private m_bWindowed As
Boolean Private Const ERROR_TYPE_MISMATCH As Long =
13
Private Sub Class_Initialize() ' Initialize this class'
internal properties. m_iPivotOffset = -50 End
Sub
Public Property Get DateValue() As
Variant DateValue = m_dteInternalDate End
Property
Public Property Let DateValue(ByVal vNewValue As
Variant) ' Assign the passed expression to the internally '
held VB Date. If it cannot be assigned, dteCSafeDate ' will raise
a Type Mismatch Error. m_dteInternalDate =
dteCSafeDate(vNewValue, m_iPivotOffset, _ m_bWindowed) End
Property
Public Property Get PivotOffset() As Integer
PivotOffset = m_iPivotOffset End Property
Public
Property Let PivotOffset(ByVal iiOffset As
Integer) m_iPivotOffset = iiOffset End
Property
Public Property Get IsWindowed() As
Boolean IsWindowed = m_bWindowed End
Property
Public Property Get IsLeapYear() As Boolean '
This read-only property indicates whether ' the stored Date value
is in a leap year. IsLeapYear _ = 29 _ =
VBA.DateTime.Day(VBA.DateTime.DateSerial(
_ VBA.DateTime.Year(m_dteInternalDate), 2, 29)) End
Property
Listing 8-2 The CSafeDate class
The
CSafeDate class allows us to apply the same algorithm to dates that
are implicitly assigned to those that are explicitly assigned using
the dteCSafeDate function. This time the result of
the DateDiff function is the expected 0. Both
dates are expanded to the year 2035.
Dim dteDate1
As New CSafeDate Dim dteDate2 As New CSafeDate
' Include
the dteCSafeDate function ' and the CSafeDate
Class. dteDate1.DateValue = dteCSafeDate("12/04/35",
-50) dteDate2.DateValue = "12/04/35" MsgBox DateDiff("d",
dteDate1.DateValue, dteDate2.DateValue)
There is
one last area where implicit coercion can occur. Consider the
following code segment:
MsgBox
Year("Feb/25/25")
This is
perfectly valid Visual Basic syntax. If you were to write the
declaration for the Year function, it would look
something like the following:
Public
Function Year(MyDate As Date) As Integer
The
danger sign here is the argument MyDate As Date;
if you provide an expression that Visual Basic can convert to a
date, it will do it for you. Again the language performs a quiet,
implicit coercion for you. So if we really want to do a thorough job
in replacing the date windowing of Visual Basic, you have to do
something about this.
A
Look at Subclassing
An often
overlooked feature of Visual Basic is the ability to subclass
many of its native functions. What do we mean by subclassing? In
this instance, subclassing means that we are taking a function that
exhibits a known behavior and reimplementing and possibly modifying
its behavior while keeping the external interface unchanged.
Subclassing is possible because of the way the language is
structured. The built-in functions are actually methods and in some
cases properties of the Visual Basic library and its
subordinates. Earlier we looked at the various date functions built
into the language and their locations. You can use the Object
Browser from within the Visual Basic IDE to view these
functions at their locations within the Visual Basic modules
and classes. (Press the F2 function key to open the Object Browser.)
When you make a call to one of these functions, you generally
specify its name and arguments, not its location. As we saw in the
previous example of the Year function, you donÆt
call the function as VBA.DateTime.Year. Because
you donÆt specify the location in the function call,
Visual Basic has to search for the function at compile time,
starting with the closest scope first: the current module. If that
search fails, Visual Basic will look at public methods of the
other code components within the current project. If this also
fails, it will finally look at the referenced objects that are
listed in the References dialog box, starting at
the top with the three Visual Basic libraries, which is where
the built-in implementation of these Date functions resides.
From the
example above you can see that if you write a function called Year within your application, and it is within scope
when you make a call to the Year function, your
version will be called in preference to VBA.DateTime.Year. In practice this means we can
improve certain areas of the language without forcing changes to any
of the code that makes use of it. The date logic of
Visual Basic is one such area. So guess what we are going to
do!
WouldnÆt
it be great if we could write CVDate, CDate, and
DateValue functions that apply our own sliding
window algorithms instead of the original fixed window? This is a
perfect case for subclassing, so letÆs try. Take the above dteCSafeDate function and rename it CVDate. It works. So does
renaming it DateValue, but if you try to rename it
to CDate you immediately get the compile error
shown in Figure 5.
Figure
5 Compile error when trying to subclass Cdate.
(If your browser does not support inline frames, click here
to view Figure
5 in a separate page.)
You
cannot currently subclass CDate. If you try,
Visual Basic gives you a wonderfully lucid error. This is
unfortunate, because subclassing works for many of the functions
built into the language, and is a great way of seamlessly extending
it. The ability to subclass has been in the product since
Visual Basic 4.0, and Microsoft is aware of the behavior of Cdate, but has chosen, for language definition
reasons, to keep the behavior as is.
As it
turns out, there is a reason that CDate still
canÆt be subclassed. CDate is a cast operator and
as such doesnÆt have a Visual Basic helper functionùit is really built in. CDbl, CLng
CInt, and so on, donÆt work for the same
reason. CVDate works because it is a wrapper
around the internal routineùitÆs a helper!
Microsoft knows this behavior is inconsistent across
Visual Basic 4.0, Visual Basic 5.0, and Visual Basic
6.0. They havenÆt promised a fix, because they say that going
through the helpers slows the code. This is a matter of language
definition.
That was
the bad news. The good news is that the majority of the other date
functions can be subclassed. We have already shown that it is
possible to subclass CVDate and DateValue. Other functions discussed in the chapter
so far that you cannot subclass in this way are Format$ and Format because Visual Basic provides two
functions with the same name. If you provide a third, it is
confused. And you cannot subclass the Date
properties Gets and Lets.
Because Date is a Visual Basic reserved word, it will not let
you use the word Date for anything other than declaring a Date
variable. Even if that were not the case, you would probably run
into the same problem as you did with Format and Format$ since you have
matching Date and Date$ properties.
Still,
there is a great deal of scope for providing your own
implementations of the remaining functions. Listing 8-3 shows a
subclassed Year function. The key to this
implementation is that our version of the Year
function accepts a Variant as an argument, not the Date data type of
the original. By using a Variant in this way we are not forcing
Visual Basic to coerce the expressions into a Date when we call
the functionùthe Variant just lets it through as is. Once the
expression is in, we assign it to a local CSafeDate variable that will apply any expansion
necessary, and we get a fully expanded date to pass to the original
VBA.DateTime.Year function. All we are really
doing is making sure any date expression is unambiguous before
calling the original function.
Public
Function Year(ByRef DateExpression As Variant) As
Integer '----------------------------------------------- '
Replaces the Year function, applying a better date
window. '-----------------------------------------------
Dim
dteTempDate As New CSafeDate
' Convert the passed expression
to a SafeDate. ' If the expression is invalid we will get a
Type ' Mismatch error, which we echo back to the calling
code. dteTempDate.DateValue = DateExpression
' Now we have
a fully expanded date; call the VB function. Year =
VBA.DateTime.Year(dteTempDate.DateValue)
Set dteTempDate =
Nothing
End Function
Listing 8-3 Subclassing the Year
function
This
documentÆs companion file, tmsy2k.zip contains a WindowedDates project that contains two files, the
CSafeDate class, and a module containing an implementation of every
date function in which it is possible to subclass the original. This
project is located in the Chap08\SubClass Windowed folder.
Sometimes You Have to Get Strict
The
previous pages have introduced a number of elements that when used
together provide a nearly complete way of applying a better
windowing algorithm than that provided by default. You can take an
alternative track here. Instead of trying to make the language more
flexible, you can make it stricter by using a class and subclassed
functions in the same way as before. This time, however, youÆll
reject any date expressions that do not have any century information
in them.
At the
center of this strategy is an algorithm that can tell whether a date
expression has a century in it. Listing 8-4 shows the CStrictDate class that uses this algorithm to test any
expressions as they are assigned, rejecting those that fail its
test. This class can be used in place of the Date data type to
enforce a strict policy of year 2000 compliance on all dates stored.
The class will reject the assignment of a date expression when the
century information is not present.
Note This class is in the companion
zipfile tmsy2k.zip in the folder Chap08\SubClass Strict.
'------------------------------------------------- ' This
is an implementation of a Strict Date data type. ' In this class,
only valid and unambiguous dates are ' stored. If an assignment
is attempted using an ' ambiguous date expression such as
'02/02/98,' this ' is rejected as if it were an invalid
value. '-------------------------------------------------- Option
Explicit
' This is where the date is actually stored. ' As
all dates this defaults to '1899-12-30' Private m_dteInternalDate
As Date
' This is the error that is raised if an attempt
is ' made to assign an invalid date (as VB's Date
does). Private Const ERROR_TYPE_MISMATCH As Long =
13
Private Function bPiIsStrictDate(ByVal Expression As
Variant) _ As
Boolean '------------------------------------------------- '
This function will return true if the passed ' date expression
is a valid and unambiguous date. ' If the expression is either
ambiguous or ' invalid, it will return
false. '-------------------------------------------------
Dim
bIsDate As Boolean
' OK, VB can do the hard work. Can this
value ' be converted to a date? bIsDate =
VBA.Information.IsDate(Expression)
' Additional check if the
literal is a string. ' Is it an ambiguous date? If bIsDate =
True And VarType(Expression) = vbString Then
' Search for the
year within the passed string literal. If 0 = InStr(1,
_ VBA.Conversion.CStr(Expression),
_ VBA.DateTime.Year(VBA.Conversion.CDate(Expression)),
_ vbTextCompare) Then
' We could not find the full 4-digit
year in the ' passed literal; therefore the date is
ambiguous ' and so we mark it as invalid. bIsDate =
False End If End If
' Return whether this is a valid
date or not. bPiIsStrictDate = bIsDate End
Function
Public Property Get DateValue() As
Variant
' Return the date value stored
internally. DateValue = m_dteInternalDate
End
Property
Public Property Let DateValue(ByVal Expression
As Variant)
If bPiIsStrictDate(Expression) Then
' If
the date expression does conform to our ' validation rules, store
it. m_dteInternalDate =
VBA.Conversion.CDate(Expression)
Else
' Otherwise
emulate VB and raise a standard error. Err.Raise
ERROR_TYPE_MISMATCH End If End Property
Public Property
Get IsLeapYear() As
Boolean '------------------------------------------ ' This
read-only property indicates ' whether the stored Date value is
in ' a leap
year. '------------------------------------------
IsLeapYear
= 29 _ = VBA.DateTime.Day(VBA.DateTime.DateSerial(
_ VBA.DateTime.Year(m_dteInternalDate), 2, 29))
End
Property
Listing 8-4 The CStrictDate Class
At the
center of this class is the bPiIsStrictDate
function, which performs a job similar to Visual Basic IsDate function. In the case ofbPiIsStrictDate, an extra test
is performed to make sure that the passed expression not only can be
converted to a Date, but is also unambiguous.
|