Building Year-2000 Compliant Applications with Visual
Studio and Windows DNA
Year 2000
compliance permeates every facet of software development. Problems
occur when software is designed to store, transmit, pass or
calculate date-related information using only the last two digits
for the year. Under these circumstances, the year 2000 is
interpreted by these programs as 1900, which may result in the
failure of the software to perform as expected. Year 2000 compliance
problems generally exist in legacy applications, but they are also
introduced into new applications because developers take code
shortcuts to save space in data records and databases, as well as
improperly use date functions and data types. Developers need to be
aware of the impact of non-compliance, as well as development
techniques to insure compatibility with next millennium.
The Microsoft« Windows DNA architecture represents a
comprehensive new framework for integrating client/server and
internet software solutions on the Windows platform. Windows DNA is
based on the premise of software component integration and
interoperability. DNA combines "component based" software
development with a "service based" operating system. However,
integrated software components and modularity do not protect the
developer from introducing Year 2000 compliance problems in their
applications. Regardless of the tools used to develop software
today, a clear Year 2000 philosophy is needed to avoid spending time
reworking an application in future years.
Year 2000 compliance issues are best avoided by strict use of the
development tools' date data types and date functions when working
with date-related data. Most development tools have built in rules
for handling date arithmetic that will avoid Year 2000 compliance
problems. Developers should validate that the development tools they
are using are Year 2000 compliant and the rules and logic used by
the tools for date handling.
Year 2000 Overview
The Year 2000 problem itself is fairly simple: software designed
to store date-related information using only the last two digits for
the year may not perform as expected when representing dates from
the year 2000 and beyond. Since the century is assumed by these
programs to always be 19, the year 2000, for example, will be
interpreted by these programs as 1900. Non-Year 2000 compliant
software will cease to function properly because date-related
calculations will not return the expected results. For example, a
program that calculates the difference in years between 1978 and
1999 using two-digit date notation would work correctly (99 - 78 =
21 years); whereas a program that calculates the difference in years
between 1978 and 2001 using two-digit date notation would not yield
the correct result (01 - 78 = -77).
Many applications only deal with current and historical dates.
These types of applications, when not Year 2000 compliant, will only
exhibit problems when the year 2000 comes upon us. However, many
applications in production today are already exhibiting Year 2000
problems. For example, many credit card authorization system had
trouble dealing with card expiration dates beyond 1999 (which are
generally represented in MM/YY form).
The impact of non-compliance is the failure of the software to
perform as expected. In the worst case, total application failure
can be expected. At the very least, misrepresented or corrupted data
can make its way into the corporate database. This corrupt data may
be used as the basis for making key business decisions or, for
example, may fail to generate an exception or flag a customer's
record when expected.
What exactly is Year 2000 compliance? Microsoft defines a Year
2000 compliant product as one that "will not produce errors
processing date data in connection with the year change from
December 31, 1999 to January 1, 2000 when used with accurate date
data, provided all other products (e.g., other software, firmware
and hardware) used with it properly exchange date data with the
Microsoft product."
Does this mean that if you are using all Microsoft development
tools, or all Year 2000 compliant development tools, that the
applications that you develop will automatically be Year 2000
compliant? Not at all. In fact, Microsoft issues a disclaimer with
their development tools regarding Year 2000 compliance that states
"Compliance refers to the Microsoft product as delivered by
Microsoft. The Compliance Statement does not apply to user
customizable features or third party add-on features or products,
including items such as macros and custom programming and formatting
features." What this amounts to is that while the development tool
is itself Year 2000 compliant, it is still up to the developer to
practice development techniques that insure compliance is maintained
in their custom applications.
How did the problem occur?
In order to fully understand the magnitude of the problems
presented by the Year 2000, it is helpful to understand historically
how the problem occurred.
With mainframe based software development in the '70s,
conservation of expensive resources like memory was critical.
Developers were able to realize significant memory savings by
shortening the year portion of date fields from four characters to
just two. Developers today may find this difficult to understand.
Considering that each variant data type littered throughout today's
typical ASP page consumes over 22 bytes of memory, conserving those
additional two bytes seems dubious. But the practice of
assuming that the century was the 19th reportedly allowed
companies to realize millions of dollars in savings. The need to
critically scrutinize every byte of memory used, coupled with the
fact that applications were not expected to stand the test of 30
years of time, has in essence created the Year 2000 issue for the
information technology industry. Other Year 2000 problems, such as
the incorrect calculation of a leap year, or using a "dummy" date
such as "9/9/99" as a flag are generally smaller in scope. These
problems are related to the lack of insight on the part of the
developer, or, in the case of an incorrect leap year calculation, an
un-trapped error.
Mainframe-based "legacy" software written decades ago isn't the
sole contributor to the problem. Actually, software written today
continues to exhibit problems of compatibility with the next
millennium. In almost all cases, the problem can be attributed to
just two causes:
(1) Developers continue to take shortcuts by representing the
year portion of a date with just two digits.
In most parts of society, two digits are used to represent the
year portion of a date. The most common way to represent a date in
the United States is in the format "mm/dd/yy". It is a custom
that people have used in their everyday lives for decades. This
custom isn't generally a problem because humans can determine the
correct year based on the context in which it is used. Therefore, a
four-digit year isn't always necessary.
Think about it: when you sign a contract or write a check, do you
write the date's full year, or do you use mm/dd/yy notation?
Probably the latter. Unfortunately, computer code is written by
humans, and humans have difficulty overcoming their habits.
The general public often scoffs at our industry's Year 2000
problem. The general public, however, will likely face their own
personal psychological Year 2000 problems after 1999 when performing
everyday tasks like writing checks and computing people's ages.
Maybe then they will be more sympathetic to our old code!
Computers cannot make assumptions based on context but instead
must apply a single rule to interpret the correct date. The century
must be assumed either through the selection of some arbitrary
number (19) or by applying a simple rule that determines the century
based on the two digit year in question. The 32-bit Microsoft
operating systems (with current service packs installed) assume the
century is 19 if the two-digit year is 30 or greater. For two-digit
years less than 30, the century is assumed to be 20. For software
coded today using dates with two-digit years, the computer is
essentially guessing the correct date because the developer failed
to correctly utilize the tools available to them.
(2) Developers use development tools that don't properly
handle date related data, or don't use the date data type
correctly.
Many development tools today provide a special data type for
handling date-related data. In addition, many provide special
functions for performing date conversions, date comparisons, and
date math. Using these functions is necessary to prevent inaccuracy,
or loss of precision when working with date and time data. Tools
that don't provide these services force the developer to substitute
a character or string data type, or a less accurate numeric data
type. Unfortunately, date math is somewhat complex, and the
developer is forced to implement this complexity. For example,
developers are required to programmatically deal with leap years and
months that contain different numbers of days. In many situations,
developers simply ignore these exceptions or deal with them
incorrectly.
Distributed interNet Applications Architecture
The Windows Distributed interNet Applications (DNA) architecture,
introduced by Microsoft in September 1997, represents a
comprehensive new framework for integrating client/server and
internet software solutions on the Windows platform. The goal of the
DNA architecture is to integrate the development model for
applications so that they can harness the power and advanced
processing capabilities of today's PCs, yet still take advantage of
the flexibility and ease of deployment of the Web.
Windows DNA is based on the premise of the integration and
interoperability of software components. DNA combines "component
based" software development with a "service based" operating system.
One of the ways Microsoft accomplishes the goals of DNA is by
tightly integrating the core application infrastructure into the
operating system. By building functions like transaction processing,
message queuing, security and directory services into the operating
system, Microsoft has freed the developer from spending a
significant amount of project resources creating the application
infrastructure. In addition, since these services are provided by
the operating system, all levels of the application - the user
interface, business services, data services - can use them as
needed. This means that a developer writing a web-based front-end
and another creating server-side business components both have
access to the underlying infrastructure.
Component-based software is assembled from independent pieces
that interrelate to perform a specific function. Components written
to the Windows DNA architecture have well-defined COM interfaces
that make them extendable and subscribe to or utilize services
provided by the operating system which makes them compatible. For
example, a developer writing a component that needs to implement
asynchronous messaging can use the interface published by the
operating system and know that it will be compatible with any other
messaging-enabled component written to that interface.
Windows DNA applications are fundamentally different from most of
today's corporate applications. That's because Windows DNA lets
developers take advantage of Internet, LAN-based, and client/server
computing -- enabling the applications they create to do more at a
lower cost.
Overview of the Sample Application
For the remainder of this paper, we'll look at a sample
application that is built upon the Windows DNA architecture. The
application was designed to highlight the proper development
techniques for handling dates when using Microsoft Windows DNA-based
development tools. Since code snippets from the application will be
used throughout the next sections, you may want to open the sample
application for reference. The application can be opened using
Visual Studio to open the workspace Y2k.dsw from the
folder that you selected at installation time. The technologies used
include Visual Basic Scripting Edition« (VBScript), JavaScript,
Active Server Pages, Microsoft Transaction Server (MTS), ActiveX«
Data Objects and the HTML subset of DHTML. The development tools
used include Visual InterDevÖ web development system, Visual J++
development system for Java, Visual Basic and Visual C++«
development systems from Microsoft Visual StudioÖ, as well as
Microsoft SQL ServerÖ. There is an alternate client portion written
as a Microsoft Excel 97 worksheet. Don't worry if you do not have
all of the Developer Studio development tools installed on your
system. As long as you're not going to recompile the application,
you won't need all of the tools.
Requirements for running the sample application
Running the sample application is not necessary in order to
follow this paper, but may help you to understand the context of
some of the code snippets. If you wish to run the application, make
sure you are running Windows NT« Server 4.0 Service Pack 3 or later,
with Windows NT Option Pack installed, along with SQL Server 6.5
with Service Pack 2 or later. The application needs to create a
table in the "pubs" database. If you do not plan on running
the sample application, you can elect not to create the table. The
COM components (written in Visual Basic and Visual C++) can be
imported into MTS by importing the package file Y2kPackage
installed in the folder Y2K\MTS. This step is optional; the
application will still function correctly outside of MTS.
The application tracks the accounts receivable of a fictional
company. The user enters transactions for one of three customers,
and then views an accounts receivable aging report. The report
summarizes the dollar amount outstanding for the time periods 0-30
days, 31-60 days, 61-90 days, etc. The application is very simple in
its design, and is not meant to illustrate a solution to any
particular business problem. Nor is it meant to be an example of an
ideal architecture using Windows DNA. Rather, it is intended to be
used as a mechanism for understanding the underlying techniques used
when handling date-related data, including date validation, date
formatting, and data arithmetic.
The application addresses all of the pieces of a relatively
complex Windows DNA "helix". There are two client portions to the
application: a web-based version that uses a Visual J++ applet as
the main user interface for data entry; and an Excel 97 worksheet
version that interfaces directly with the business component. Notice
how in each scenario below a date passes from
component-to-component.
For the Excel-based client:
- The user enters values directly into cells in an Excel
worksheet.
- The worksheet uses VISUAL BASIC macros to perform the
client-side data validation.
- The data is then passed to a VISUAL BASIC component running in
an MTS process on the server.
- The VISUAL BASIC component validates the data, partially
through the use of a C++ COM component also hosted in MTS.
- If the data is validated, the VISUAL BASIC component uses ADO
(via the OLEDB provider for ODBC) to store the data in the
"pubs" database on SQL Server.
For the web-based
Java client:
- Javascript on the web page queries the applet for values
entered by the user and passes them to the web server for
processing via an HTTP Post.
- ASP code on the server receives the data.
- ASP passes the data to a VISUAL BASIC component running in an
MTS process on the server.
- The VISUAL BASIC component validates the data, partially
through the use of a C++ COM component also hosted in MTS.
- If the data is validated, the VISUAL BASIC component uses ADO
(via the OLEDB provider for ODBC) to store the data in the
"pubs" database on SQL Server.
The following
diagram outlines the structure of the sample application.
Figure 1 - The sample application architecture.
The application consists of the following:
File(s) |
Description |
Location |
Y2k.dsw |
Visual
Studio workspace |
Y2K |
Y2kApplet.dsp |
Visual J++
project for Java applet. |
Y2K\JavaApplet |
Y2K.xls |
Excel 97
worksheet user interface |
Y2K\Excel |
Y2kSample.dsp |
Visual C++
project for C++ COM component |
Y2K\C_helper |
Y2kWeb.dsp |
Web project
- ASP pages and Web files |
Y2K |
Y2k.VBP |
VISUAL BASIC
Project for VISUAL BASIC component |
Y2K\VB |
Y2kPackage.pak |
MTS
Package |
Y2K\MTS |
Y2k.dat |
Database
definition file |
Y2K\data |
Development Philosophy
It is helpful to establish an overall "Year 2000 compliance"
philosophy when setting out to develop an application. A philosophy
can then be integrated into the application's design strategy, or
the company's development strategy as a whole. The philosophy for
achieving Year 2000 compliance is fairly straight forward:
- Always use the development tools' date data types or otherwise
insure no loss of precision.
- Always use the development tools' data math/comparison
operators, or develop operators that insure no loss of precision
(including exception handling, such as leap years).
- Make sure the development tool and environment itself do not
have Year 2000 issues (e.g. check out http://www.microsoft.com:80/y2k for
Microsoft's compliance). Be sure to consider all of the tools
being used throughout the system, including things like report
writers, third party add-ons and controls, etc. Also be sure to
consider the underlying infrastructure, such as the data access
drivers and network protocols.
The sample application
subscribes to and illustrates this philosophy.
DNA Integration - Working with the sample code
Client Portion: Java
The sample application presents a simple user interface using an
applet written in Java on a web page (Figure 1). JavaScript
interacts with the applet to receive the values for the Customer ID,
Transaction Date, Amount, Terms and Due Date. Before receiving the
data, it first calls the applet's CheckData() function (Listing 1).
Figure 2 - The web-based user interface.
The CheckData() function first verifies that all of the required
fields have been entered. Next, it checks that the value entered for
the Transaction Date is a valid date. This is accomplished by using
the Java SimpleDateFormat class (java.text.SimpleDateFormat), which
is a subclass of the DateFormat class (java.text.DateFormat). In the
Java language, a date is represented as a specific millisecond in
time. public String CheckData()
{
//Validates the data.
//Returns an error message if data is invalid.
//Otherwise, just returns an empty string.
Calendar transDate = Calendar.getInstance();
Calendar dueDate = Calendar.getInstance();
//Check for required fields...
if (m_cboCustomer.getSelectedItem().length() == 0)
return "A customer is required.";
if (m_txtDate.getText().trim().length() == 0)
return "A transaction date is required.";
if (m_txtDate.getText().trim().length() != 10)
return "The transaction date is invalid. Proper format is 'mm/dd/yyyy'";
if (m_txtAmount.getText().trim().length() == 0)
return "A transaction amount is required.";
if (m_cboTerms.getSelectedItem() == "Due Date")
if (m_txtDueDate.getText().trim().length() == 0)
return "A due date is required.";
else if (m_txtDueDate.getText().trim().length() != 10)
return "The due date is invalid. Proper format is 'mm/dd/yyyy'";
//Validate the dates entered using the pattern mm/dd/yyyy.
//If I try to call parse() for an invalid date, an exception
//occurs, which is caught by the exception handler
//Specify the date format that I'm expecting.
SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
df.setLenient(false);
//Attempt to create a date from the text entered by the user.
try {
transDate.setTime(df.parse(m_txtDate.getText()));
}
catch (Exception e) {
//If the user entered an invalid date, return an error message
return "The transaction date is invalid. Proper format is 'mm/dd/yyyy'";
}
//Also validate the due date, if one was entered.
if (m_cboTerms.getSelectedItem() == "Due Date") {
try {
dueDate.setTime(df.parse(m_txtDueDate.getText()));
//make sure due date is the same or later
//than the transaction date...
if (transDate.after(dueDate)) {
return "The due date cannot be earlier than the transaction date.";
}
}
catch (Exception e) {
return "The due date is invalid. Proper format is 'mm/dd/yyyy'";
}
}
//Make sure the amount entered is numeric.
try {
Float.valueOf(m_txtAmount.getText()).floatValue();
}
catch (Exception e) {
if (e.getClass().toString().equals("class java.lang.NumberFormatException"))
return "Invalid numeric amount.";
else
return e.getClass().toString();
}
//Everything is Ok, just return an empty string.
return "";
}
Listing 1 - Validation in Java
The
SimpleDateFormat class in Java is used to format and parse date and
time data. It can be used to convert a string to a date, or a date
back to a string, and format it with the specified format mask. You
can specify a formatting mask to use during the constructor:
//Specify the date format that we're expecting.
SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
df.setLenient(false);
Listing 2 - Using SimpleDateFormat
This
indicates that whenever we format a date using this instance of the
SimpleDateFormat class, Java will use the format "MM/dd/yyyy". This
causes the date May 31st, 1998 to be formatted as 5/31/1998.
Remember that Java is a case sensitive language. Using the lower
case letter "m" instead of upper case will not produce the same
results - the lower case letter "m" is used for the minutes portion
when specifying a formatting mask for a time.
The second line in Listing 2 tells the SimpleDateFormat class not
to be lenient when interpreting dates. It is important to tell the
SimpleDateFormat class not to be lenient when handling dates. If you
don't, Java will not verify that the data entered is actually a
valid date, rather it will only verify that it is in the proper
format. For example, the invalid date in Listing 3 will not generate
an exception because the format of the date is valid. The default
value for setLenient is true, so always change it to false when
working with the SimpleDateFormat class.
//This will not generate an exception, even though the date is invalid.
SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
Date dt = new Date();
dt = df.parse("02/29/1997");
Listing 3
In order to verify that the
dates entered by the user are valid, we need to convert them into
Java's Date data type. We can do this by calling SimpleDateFormat's
parse() function. The parse() function takes a string and attempts
to convert it into a Date data type. If the string cannot be
converted to a date, an exception will be raised which we can trap
and use to return an error message, as displayed in Listing 3:
//Attempt to create a date from the text entered by the user.
try {
transDate.setTime(df.parse(m_txtDate.getText()));
}
catch (Exception e) {
//If the user entered an invalid date, return an error message
return "The Transaction Date is invalid. Proper format is 'mm/dd/yyyy'";
}
Listing 4 - Validating the dates
This
type of procedure should be followed with any other dates entered by
an end user. By attempting to convert string data stored in the
applet's TextField into Java's native Date data type, we are assured
that the information entered is in fact a valid date.
Now that we know all of the date information is valid, the applet
will enforce one business rule: the due date cannot be earlier than
the transaction date. In order to do this, we need to compare two
dates. When comparing dates or performing date arithmetic, always
use the development tool's operators. In Java, the SimpleDateFormat
class is used for formatting and parsing only. The class used for
date arithmetic and comparisons, including time zone conversions, is
the Calendar class (java.util.Calendar). The Calendar class can be
used to extract a certain portion of a date (such as the month or
the year) or to perform comparisons on two dates, as displayed in
Listing 5.
transDate.setTime(df.parse(m_txtDate.getText()));
dueDate.setTime(df.parse(m_txtDueDate.getText()));
//make sure due date is the same or later
//than the transaction date...
if (transDate.after(dueDate)) {
return "The Due Date cannot be earlier than the Transaction Date.";
Listing 5 - Validating the dates
Before
using the Calendar class, we must assign it a date. We do this with
the setTime() function. This function assigns a date, a time, or
both to the Calendar. The setTime() function takes a date as its
argument. We previously parsed the contents of the TextFields into
dates for the Transaction Date and Due Date when we were validating
those fields. We can move the results into their respective
Calendars at the same time.
The Calendar's after() function, which also takes a Calendar is a
parameter, returns true if the date or time of the Calendar passed
as a parameter is after the date or time of this Calendar. Comparing
the two Calendar objects, then, is just a matter of calling the
after() function on the Transaction Date's Calendar and passing in
as a parameter the Due Date's Calendar. If it is false, we
will return an error message; otherwise we return an empty string
and processing continues. By using the Calendar class's built in
date comparison functionality, we've removed all possibility of
programmer date math bugs or lack of precision.
Once the data has been validated by calling the applet's
CheckData() function, we are ready to send it to the server to be
further processed and eventually saved to the database. We'll get
the data from the applet to the web server through the use of an
HTML POST method. The HTML POST method is used to send data from a
form tag on a web page to the server to be processed. In our
example, all of the inputs on the form are of type hidden, which
means the browser will not make them visible to the user. We can
still store values in the hidden fields, then send them to the
server by using POST.
To get the values from the applet to the HTML form, we call the
applet's Customer(),Date(), Amount(), Terms() and DueDate()
functions and copy the values returned from those functions into the
form fields. Listing 6 shows the JavaScript function add() which was
taken from the Active Server Page that gets the values from the
applet. This is the code that gets called when the user clicks the
Add button on the client page (refer to Figure 1).
function add() {
var sRet = document.Y2kApplet.CheckData();
if (sRet != "") {
alert(sRet);
return false;
}
else {
with(document.forms[0]) {
hidCustomer.value = Y2kApplet.Customer();
hidDate.value = Y2kApplet.TransDate();
hidAmount.value = Y2kApplet.Amount();
hidTerms.value = Y2kApplet.Terms();
hidDueDate.value = Y2kApplet.DueDate();
submit();
}
}
Listing 6 - Getting the data from the
applet.
Collecting the data:
ASP/VBScript
The data is received by an Active Server Page that parses out the
values POSTed to it. The data is then passed to a Visual Basic (VB)
component running under MTS, which saves it to the database. The
Active Server Page uses VBScript to create an instance of the VISUAL
BASIC component. It then calls either the AddByDate method or the
AddByDay method (Listing 7), depending on whether the user specified
a due date or not.
Working with Variants
VBScript has only one data type-Variant-which can contain a
representation of many other data types. In addition, each Variant
has a sub-type that further describes the underlying value it has
been assigned. The sub-types consists of the more strongly types
data types from VISUAL BASIC (String, Long, Integer, Boolean, Date,
Single, Double, etc.) When a Variant is initially assigned a value,
VISUAL BASIC automatically chooses the appropriate sub-type based on
the value being assigned. The following table illustrates how
VBScript will assign sub-types to the variable MyVariant:
MyVariant = "Hello" |
Sub-type is String |
MyVariant =
123.456 |
Sub-type is
Double |
MyVariant =
15 |
Sub-type is
Integer |
MyVariant =
"5/5/1998" |
Sub-type is
String |
Note that in the last example in the table above, VBScript
assigned the String sub-type to MyVariant, instead of the Date
sub-type. VBScript will always choose the String data type when
being assigned character data.
Remember our development philosophy, "always use the tool's date
data type"? When using VBScript or Variants, adhering to this rule
is a little bit tricky. Variants play fast and loose with their
sub-types, often changing them without the developer even realizing
it. Basically, we need to make sure that when storing date
information in a Variant that we always force VISUAL BASIC to
sub-type our information to the Date sub-type. That is, we need to
make sure that the information in the Variant is of the Date
sub-type.
We can do this by assigning the Variant's value something that it
can not interpret as anything other than a date. In the example
above, what looked to a human like a date ("5/5/1998") was
interpreted (and thus assigned) as a string sub-type. However, as we
will see in the example, we could have explicitly coerced VISUAL
BASIC into assigning the Variant a date sub-type if we had
explicitly converted the string "5/5/1998" into a date value using
the CDate() function.
Our VISUAL BASIC component expects the Transaction Date and the
Due Date parameters to be date data types (see Listing 7). To send
the values for these fields as dates to the VISUAL BASIC component,
we use the VBScript function CDate(). CDate() takes one parameter (a
variant of course) and attempts to convert it into a date. If the
parameter passed into the CDate() function cannot be converted into
a date, a Type Mismatch error will occur.
The CDate() function uses the same logic as the VBScript function
IsDate(), which returns true if its argument is a valid date It is
important to use CDate() to force variants to the Date sub-type to
prevent compliance problems. This can be illustrated in the
following example:
VariantX = "5/5/1998" |
Sub-type of
VariantX is String |
VariantY =
"6/31/1998" |
Sub-type of
VariantY is String |
DateX =
CDate(VariantX) |
Sub-type of
DateX is Date |
DateY = CDate(VariantY) |
A Type
Mismatch error occurs because VariantY cannot be converted to
a date. |
You can determine the sub-type of any Variant in VBScript by
using the TypeName() function. The TypeName function takes a Variant
as a parameter, and returns its sub-type. This function comes in
very handy when debugging. Passing the wrong data type to an
external function or procedure (as illustrated in the table above)
is a common mistake, and is difficult to uncover.
Another useful function is the IsDate() function. IsDate() takes
a Variant as a parameter. It returns True if the parameter can be
converted to a valid date, False if it can not. IsDate() is useful
for validating whether or not user input (or external input) is a
valid date.
Dim Y2kComponent
If Request.QueryString("mode") = "add" then
Set Y2kComponent = Server.CreateObject("Y2kVB.Component")
If Request.Form("hidTerms") = 0 then
Call Y2kComponent.AddByDate(Request.Form("hidCustomer"), _
CDate(Request.Form("hidDate")), _
Csng(Request.Form("hidAmount")), _
CDate(Request.Form("hidDueDate")))
Else
Call Y2kComponent.AddByDays(Request.Form("hidCustomer"), _
CDate(Request.Form("hidDate")), _
Csng(Request.Form("hidAmount")), _
CInt(Request.Form("hidTerms")))
End If
Set Y2kComponent = Nothing
End If
Listing 7 - VBScript on an Active Serve Page calling the
VISUAL BASIC component.
Alternate Client
Portion: Excel
In addition to the Java/Internet client, the sample application
includes a Microsoft Excel client piece. Complex solutions can be
created using Excel's VBA capabilities. Using VBA, the Excel client
will connect directly to the VISUAL BASIC component running in MTS.
For simplicity, the Excel client mimics the logic of the Java
client. The VBA CheckData() validation function is similar to the
function that we used in Java-they differ only in language syntax
(see Listing 8). The syntax for handling dates in VBA is identical
to Visual Basic, which we will discuss in the next section.
Function CheckData() As String
'Used for client side validation
Dim sRet As String
With ActiveSheet
'Get the customer name
m_Customer = Worksheets("Data").Cells(ActiveSheet.Range("Customer"), 1).Value
'Make sure that a customer was chosen.
If m_Customer = "" Then
CheckData = "A customer is required."
Exit Function
End If
'Make sure that a transaction date was entered.
If .Range("TransDate") = "" Then
CheckData = "A transaction date is required."
Exit Function
'Check the transaction date to make sure it is valid.
ElseIf Not IsDate(.Range("TransDate")) Then
CheckData = "The transaction date is invalid. Proper format is 'mm/dd/yyyy'"
Exit Function
Else
m_TransDate = .Range("TransDate")
End If
'Make sure that a due date was entered.
If .Range("Terms") = 5 Then
m_Terms = 0
If .Range("DueDate") = "" Then
CheckData = "A due date is required."
Exit Function
End If
If Not IsDate(.Range("DueDate")) Then
CheckData = "The due date is invalid. Proper format is 'mm/dd/yyyy'"
Exit Function
End If
m_DueDate = .Range("DueDate").Value
Else
m_Terms = Left(Worksheets("Data").Cells(.Range("Terms"), 2).Value, 2)
End If
m_Amount = Val(.Range("Amount"))
'Verify that the amount is >= 0
If m_Amount <= 0 Then
CheckData = "A transaction amount is required to be greater than 0."
Exit Function
End If
End With
End Function
Listing 8 - The Excel VBA code for client-side
validation
The business component: Visual
Basic
Our VISUAL BASIC business component is invoked through a call to
either its AddByDate method or AddByDay method. Both of these
methods add a record to the database. The AddByDate method is used
when the user entered the actual due date for the receivable,
whereas the AddbyDay method is used if the user just selected a
number of days from the Terms. Listing 9 contains the code for both
of these functions.
The VISUAL BASIC component validates the data passed to it from
its consumer (either the ASP page or the Excel client). Because this
component represents a "business component", it validates all
parameters, regardless of whether or not they were previously
validated. The business component should function as a "black box",
meaning that it can be called from any type of client and should not
assume that client side validations were performed.
Public Sub AddByDays(ByVal Customer As String, ByVal TransactionDate As Date, ByVal Amount As Single,
ByVal Terms As Integer)
On Error GoTo Receivable_EH
Dim Cmd As adodb.Command
Dim dtDueDate As Date
Dim DateMath As Y2KSAMPLELib.DateMath
Set DateMath = CreateObject("DateMath.DateMath.1")
dtDueDate = DateMath.DateAddX(Terms, TransactionDate)
Set DateMath = Nothing
'Check some business rules...
Call CheckBusinessRules(Customer, TransactionDate, Amount, dtDueDate, "AddByDays")
Set Cmd = CreateObject("ADODB.Command")
Cmd.ActiveConnection = CONNECT_STRING
Cmd.CommandType = adCmdStoredProc
Cmd.CommandText = "y2k_addReceivable"
Cmd.Parameters("@customer").Value = Customer
Cmd.Parameters("@date").Value = TransactionDate
Cmd.Parameters("@amount").Value = Amount
Cmd.Parameters("@due_date").Value = dtDueDate
Cmd.Execute
Set Cmd = Nothing
If Not ctxContext Is Nothing Then
Call ctxContext.SetComplete
End If
Exit Sub
Receivable_EH:
Set Cmd = Nothing
If Not ctxContext Is Nothing Then
Call ctxContext.SetAbort
End If
Call RaiseError("AddByDays", Err.Number, Err.Description)
End Sub
Public Sub AddByDate(ByVal Customer As String, ByVal TransactionDate As Date, ByVal Amount As Single,
ByVal DueDate As Date)
On Error GoTo Receivable_EH
Dim Cmd As adodb.Command
'Check some business rules...
Call CheckBusinessRules(Customer, TransactionDate, Amount, DueDate, "AddByDate")
Set Cmd = CreateObject("ADODB.Command")
Cmd.ActiveConnection = CONNECT_STRING
Cmd.CommandType = adCmdStoredProc
Cmd.CommandText = "y2k_addReceivable"
Cmd.Parameters("@customer").Value = Customer
Cmd.Parameters("@date").Value = TransactionDate
Cmd.Parameters("@amount").Value = Amount
Cmd.Parameters("@due_date").Value = DueDate
Cmd.Execute
Set Cmd = Nothing
If Not ctxContext Is Nothing Then
Call ctxContext.SetComplete
End If
Exit Sub
Receivable_EH:
Set Cmd = Nothing
If Not ctxContext Is Nothing Then
Call ctxContext.SetAbort
End If
Call RaiseError("AddByDate", Err.Number, Err.Description)
End Sub
Listing 9 - Functions in the VISUAL BASIC component to add
records to the database.
Continuing with our
philosophy, we are using the Date data type in VISUAL BASIC for the
Transaction Date and the Due Date. This is very convenient because
in Visual Basic, a run-time error will be raised if we attempt to
pass non-date values to either of these parameters.
In addition, VISUAL BASIC has straight forward date math
functions such as DateAdd for adding dates and DateDiff for
determining the difference between two date. For comparing dates, we
can just use the same comparison operators we would use for numeric
data ("<", "<=", ">", ">=", etc.). As long as both of
the data types we are comparing are date data types, VISUAL BASIC
will accurately compare the two values. However, using these
operators with values that are not valid dates will trigger a
run-time error.
Listing 10 shows the VISUAL BASIC code used in the function
CheckBusinessRules to verify that the Due Date is not earlier than
the Transaction Date.
'The Due Date cannot be earlier than the transaction date.
If DueDate < TransactionDate Then
Call RaiseError(ErrorSource:=Source, _
ErrorNumber:=vbObjectError + 1002, _
ErrorDescription:="Due Date cannot be earlier than Transaction Date.")
End If
Listing 10 - Using comparison operators to compare two dates
in Visual Basic.
Note that the Date data type
also stores time information. So, use caution when comparing dates
in VISUAL BASIC using the comparison operators, because if your date
variables also store times (even unknowingly), the times will be
compared as well.
For example, the Debug.Print statement in Listing 11 will very
likely always return false. This is because d2 is assigned the value
of the Now function, which returns the current date and time,
whereas d1 is assigned the value of just the date. When the time is
not specified in a Date data type, it defaults to 12:00:00 AM.
Unless the Now function was also executed at this exact time, the
Debug.Print will return false.
Thus, if you are only interested in working with a date (as
opposed to a more precise date/time), make sure to not unknowingly
give it time precision as well.
Dim d1 As Date
Dim d2 As Date
d1 = Date
d2 = Now
Debug.Print (d1 = d2) 'will return false, except if run at exactly midnight.
Listing 11 - Comparison operators can be
deceiving.
If you are working in date/time precision,
but need to just compare dates, consider using the DateDiff
function. DateDiff can be used to find the number of days that occur
between two dates. Using DateDiff, Listing 11 can be re-written as
Listing 12. Since DateDiff is used with the "d" parameter, only the
day portion of the two dates are compared.
Dim d1 As Date
Dim d2 As Date
d1 = Date
d2 = Now
Debug.Print DateDiff("d", d1, d2) 'returns 0.
Listing 12 - Use DateDiff to compare only a portion of a
Date in Visual Basic.
Performing Data
arithmetic: C++ component
The AddByDays routine from Listing 9 is called whenever the user
did not specify the exact due date for the transaction, but instead
specified the transaction to become due after a fixed number of days
have passed. In this case, we will compute the due date for the
transaction by adding the fixed number of days to the Transaction
Date.
We could do this with the VISUAL BASIC function DateAdd, which
returns a date to which the number of the specific time interval has
been added. For example, to specify a Due Date of 30 days after the
Transaction Date, we would use DueDate =
DateAdd("d",30,TransactionDate). Instead of using the built-in
VISUAL BASIC date addition function, however, we will use a COM
component that we wrote in C++ that does essentially the same thing.
This is not necessarily a good design decision, but it allows us to
illustrate the date handling functionality of C++ within the scope
of our application.
The C++ component will run in the same MTS package as the VISUAL
BASIC component. We will create an instance of the C++ component
from within VISUAL BASIC by using the CreateObject function. The C++
component has two methods. DateDiffX functions similarly to Visual
Basic's DateDiff function, except that it only returns the
difference in days between two dates. DateAddX functions similarly
to Visual Basic's DateAdd function, except that it only adds or
subtracts days to a given date.
Refer to Listing 9 for the syntax for creating an instance of the
COM components from Visual Basic, and calling the DateAddX function.
Listing 13 displays the C++ DateAddX function.
STDMETHODIMP CDateMath::DateAddX(IN long Number, IN DATE Date, OUT DATE * NewDate)
{
*NewDate = Date + Number;
return S_OK;
}
Listing 13 - C++ component implementation of
DateAdd.
The COLEDateTime class encapsulates the
Date data type in C++, which is analogous to the VISUAL BASIC Date
data type. It is designed to be used with the COleVariant class used
in Automation. The COLEDateTime class expresses dates as the number
of days since December 30, 1899. Therefore, the date December 31,
1899 would be expressed as 1. This makes the date arithmetic for the
DateAddX function very simple - we can simply add the number of days
(30) to the date using the (+) operator. Since the Date data type is
expressed in days, the result is itself a date.
To calculate the difference between two dates, we will use
similar logic. Again, since the Date data type is expressed in days,
we can simply subtract date1 from date2 to get the number of days
between the two days.
STDMETHODIMP CDateMath::DateDiffX(IN DATE Date1, IN DATE Date2, OUT long * Difference)
{
*Difference = Date2 - Date1;
return S_OK;
}
Listing 14 - C++ component implementation of
DateDiff.
Saving the data: ADO and SQL
Server
Once the data is validated (and converted into the proper format
if necessary), we are ready to save the information into the
database using a stored procedure. The best way to ensure no loss of
precision when passing date-related data into a stored procedure is
to declare the stored procedure's parameters as Transact-SQL's
DateTime data type.
The datetime data type is stored in 8 bytes (two 4-byte
integers) -- 4 bytes for the number of days before or after the base
date of January 1, 1900, and 4 bytes for the number of milliseconds
after midnight. By using the datetime data type, the validity
of the value passed to the stored procedure is automatically
enforced. If an invalid date is passed as a parameter, Transact-SQL
will trigger a run-time error. Transact-SQL also has a less precise
smalldatetime data type stored in 4 bytes. The
smalldatetime data type consists of 2 bytes for the number of
days after January 1, 1900, and 2 bytes for the number of minutes
past midnight. Data values for smalldatetime range from
January 1, 1900, through June 6, 2079, with accuracy to the minute.
When passing dates as parameters to a stored procedure using
ActiveX Data Objects (ADO), no conversion is required between Visual
Basic's Date data type, and the ADO Parameter of type adDBDate
(Listing 9). Don't use Transact-SQL character data types for
date-related data. Not only will this be less efficient than using
the DateTime data type, it also introduces the possibility of
invalid dates getting into the database at some time (2/29/1900 for
example). By using the datetime data type, as we've seen in
all of the other examples, we are afforded some built-in protection
by the environment.
There is one limitation when using the Transact-SQL
datetime data type with Visual Basic. The datetime
data type is accurate to the millisecond, but Visual Basic's
Date data type is not. Visual Basic's Date data type
does not support milliseconds, and will not recognize a date with
millisecond precision. This means that dates returned from SQLServer
to VISUAL BASIC will be truncated. This is not an issue if you are
not concerned with millisecond precision, but what if you are?
You might want millisecond precision, for example, if you are
using a datetime column (lets call it update_date) for optimistic
concurrency support. For example, you call a function to read a
record, including the update_date. You make some changes to the
record and want to save it. Before saving the record, you want to
compare the update_date that you originally retrieved from the
database, with the current value from that record in case is was
changed sometime after you first retrieved it. In this scenario, you
would probably want millisecond precision, and therefore Visual
Basic's date data type will not work for you.
In a case like this, you would have a few choices.
- You could build your own Date class that implements any date
functions that you need, but also supports milliseconds.
- Convert T-SQL's datetime to use a string data type so
milliseconds can be displayed. If you choose this method, you need
to use caution when manipulating the data so precision is not
lost.
- Use a different data type for detecting concurrency
collisions. After all, even with millisecond precision, it is
still possible for a collision to go undetected. Instead, for
example, you could use T-SQL's timestamp data type, which
guarantees uniqueness.
Development Tool Reference
The following table summarizes the Date related capabilities of
the development tools that we have discussed throughout this paper.
Tool |
Data Type |
Arithmetic Functions |
Visual J++ |
SimpleDateFormat |
Use Calendar
class. Methods include before, after, add,
roll. |
Visual Basic, VBA |
Date, Variant (Date sub-type) |
DateAdd,
DateDiff, IsDate, Date, Time, Now, DatePart, Format, TypeName,
VarType, others |
VBScript |
Variant
(Date sub-type) |
DateAdd,
DateDiff |
Visual
C++ |
OLEDateTime |
Arithmetic
operators (-,+) |
ADO |
ADOParameter
type adDBDate |
N/A |
Transact
SQL |
datetime,
smalldatetime |
DATEADD,
DATEDIFF |
Summary
Developing applications with the Year 2000 in mind requires
developer attention, regardless of the development tools being used.
Using a compliant set of tools is not enough to insure an
application's Year 2000 compliance. Developers must adopt a
development philosophy to address Year 2000 issue in order to
prevent unknowingly introducing compliance problems. This
development philosophy includes:
- Always using the development tools' date data types or
otherwise ensure no loss of precision.
- Always using the development tools' data math/comparison
operators, or develop operators that ensure no loss of precision.
- Making sure the development tool and environment itself do not
have Year 2000 issues.
Although the specific
implementation for handling dates in the Windows DNA architecture
differs slightly from tool-to-tool, the underlying concepts are the
same for all tools. Using the development tool's built-in date data
types and date operators will minimize the impact of the new
millennium.
About The Author
Jerry Brunning is a consultant with Clarity Consulting, Inc.
Clarity is a Chicago-based consulting firm that specializes in the
design and development of client/server information systems. Jerry
is co-author of the forth-coming book "Visual Studio Enterprise
Development."
He can be reached at jbrunning@claritycon.com
or on the Web at http://www.claritycon.com:80/.
|
|