Home Web Design Programming Fairlight CMI Soap Box Downloads Links Biography About... Site Map

The Holmes Page Handling Dates - Solving the Year 2000 Problem

CA-Clipper Opportunities Tips and Tricks Networking Internal Errors Source Code CA-VO


There are many reports of the problems that companies face because their software systems only support 2-digit years in dates. The year 2000 is indistinguishable from the year 1900 for many of these systems, which will fail on January 1, 2000 unless they are corrected. 
So, the big question: is CA-Clipper year 2000 compliant? Definitely yes. 
CA-Clipper always manipulates dates internally with the complete date value, including the 4-digit year. Also, dates are stored in data (DBF) files as complete values in the format YYYYMMDD, for example 19971028. 
Visit my Data File Headers and the Year 2000 page for details about DBF headers. 
However, although CA-Clipper does not have a year 2000 problem, your programs might.

Date Creation and the Format String

Many programs manipulate date values by adding or subtracting days, or by finding the number of days between two dates. These calculations are generally not a problem if they use CA-Clipper's built-in date data type, but some programs use numeric or string values to represent dates. 
For example, the year, month, and day might be represented as separate numeric values and then merged to display a complete date. This should be avoided. Instead, use the built-in date data type which is "smart" because it knows how to handle the different month lengths as well as leap years. 
Creating a date value from separate year, month, and day numbers is error prone because of the different date format strings available. A date format string is used to specify how dates should look to the end user, but the format also affects how dates must be written in source code. 
For example, the default date format in CA-Clipper is MM/DD/YY (the American format). To create a date variable from separate year, month, and day variables you would use code like this: 
    nYear  := 97
    nMonth := 10
    nDay   := 24
    dDate  := ctod( str(nMonth) + '/' + ;
                    str(nDay)   + '/' + ;
                    str(nYear) )
The three numeric variables (nYear, nMonth, and nDay) are converted to strings and then concatenated with slashes. The resulting string is then passed to the ctod() function which creates a genuine date value from the string. 
If the date format is changed to DD/MM/YY, the preceeding code will break unless it is re-written to match the order of the day, month, and year in the date format string. 
    set date format to 'DD/MM/YY'
    nYear  := 97
    nMonth := 10
    nDay   := 24
    dDate  := ctod( str(nDay)   + '/' + ;
                    str(nMonth) + '/' + ;
                    str(nYear) )
Notice that the day and month values are reversed from the previous example. 
Since the effect of the date format string is "global", it alters all date construction throughout the program from the moment the set date format command is executed. 
Generally, global enviromment settings are not good because they represent an "invisible modification" of source code. Every line of source code should have a clear, unambiguous meaning but the reliance on global enviromment settings prevents this. Put another way, the meaning of a particular line of source code cannot be determined unless the values of the various global environment settings are known. 
Since the preceeding code relies on the value of the global date format string, it is fragile and unsafe.

Two-digit Centuries and the Epoch

Another problem with the code above is the specification of the year with only a two-digit value. The ctod() function detects this and deals with it by checking the global "century reference point" as specified with the set epoch command, which controls the interpretation of date strings with only two digits for the year. 
When the string is converted to a date value, its year digits are compared with the year digits of the set epoch command. If the year digits in the date string are greater than or equal to the year digits of the set epoch command, the date is assumed to fall within the same century as the set epoch command. Otherwise, the date is assumed to fall in the following century. 
The default value for set epoch is 1900, causing dates with no century digits to be interpreted as falling within the 20th century. 
    set date format to 'dd/mm/yyyy'
    ? ctod('27/05/1904')            // Result: 27/05/1904
    ? ctod('27/05/67')              // Result: 27/05/1967
    ? ctod('27/05/04')              // Result: 27/05/1904

    set epoch to 1960
    ? ctod('27/05/1904')            // Result: 27/05/1904
    ? ctod('27/05/67')              // Result: 27/05/1967
    ? ctod('27/05/04')              // Result: 27/05/2004
Using the set epoch command as a "quick and dirty" way to fix programs with year 2000 problems is not recommended. It is not a long-term solution, as it only hides or postpones the problems. 
Depending on the type of date value, you might try the following approach. If dealing with birthdays, the epoch can be set to reflect the fact that birthdays are in the past. 
    set epoch to (year(date())-100) // 1997 - 100
    ? ctod('27/05/67')              // Result: 27/05/1967
    ? ctod('27/05/99')              // Result: 27/05/1899
Meetings and appointments are usually in the future. 
    set epoch to (year(date()))     // 1997
    ? ctod('27/05/67')              // Result: 27/05/2067
    ? ctod('27/05/99')              // Result: 27/05/1999
This approach often fails for birthdays. For example, some individuals are older than 100 years old. If a person was born in 1895 and the birthday is specified as ctod('28/10/95') and the epoch is 1897, then the individual would seem to be 2 years old (born in 1995), rather than 102 (born in 1895). The epoch setting gives you a 100-year window, which is insufficient for many purposes. 
This approach also fails with dates that fall within the same year as the current date, because of the uncertainty of whether the date is to represent this year or a hundred years in the future.

Better Date Formats

Many programs display dates as MM/DD/YY, for example 10/28/97. This is typical of the American date format, which is the CA-Clipper default. By adding set century on at the beginning of your program, the format is converted to MM/DD/YYYY, like 10/28/1997. The longer date format may have an impact on your input screens and reports. In other words, you may have to move things around to make room. 
My preference is YYYY/MM/DD because it removes the ambiguity inherent in certain date values. For example, what date is 12/03/97? Is it December 3rd or March 12th? The American MM/DD/YY format is world-renowned for the confusion it causes, whereas I believe that 1997/03/12 is clearly March 12th of 1997. 
When the year 2000 rolls around, March 12th would be 2000/03/12 rather than the strange 3/12/0. Other bizarre dates would be 1/2/3, 9/10/2, and 10/4/8, all of which would be clear if presented using the YYYY/MM/DD format! The international (ISO) standard date format is YYYY-MM-DD, which I am starting to use. 
The ordering of the year, month, and day is as important as (and goes hand in hand with) the year 2000 problem. The Internet exposes your work to a worldwide audience, so regional date formats are not acceptable anymore. The YYYY/MM/DD format is unambiguous. 
Our job as programmers is to remove ambiguity. 
All of my source code has these lines at the beginning of the program (and nowhere else): 
    set date format to 'YYYY/MM/DD'
    set century on
(By the way, set century on is redundant in this code because the date format specifies YYYY, which implies set century on. These two lines interact such that set century off would change YYYY to YY. This also means that the order of these two statements is important.) 
Although I don't like to use, or rely on, such global environment settings, in this case it is unavoidable, and in fact too important to omit. 
The effect of these lines of code is to alter all input screens and reports to use the longer and more explicit date format. Users may complain that it takes too long to type '1997/10/28', but you'll never hear them say that they don't understand it. 
Note that there are many techniques to speed up entry of dates, such as providing a pop-up calendar and providing smart default (or automatic) values.

Supported Dates and Storage Format

The CA-Clipper documentation states that CA-Clipper supports all dates in the range 0100/01/01 to 2999/12/31. In fact, this is the range of dates that can be directly set, but dates outside this range can be reached by adding and subtracting days. 
    dDate := ctod('2999/12/31')     // Create largest date.
    dDate += (365 * 400)            // Add nearly 400 years.
    ? dDate                         // Result: 3399/09/25
Thus, the highest reachable date is actually 9999/12/31 and the lowest is 0000/01/01. 
Data files, which have a file extension of DBF, are essentially text files. However, a binary header (which describes the layout of the data) appears first in the file. Each line of text is the same length and is organized according to the structure defined by the header. 
For example, given a table, CUSTOMER.DBF, with the following structure:
    fieldnametype length dec
The data looks like this when viewed with a low-level disk editor:
      Albert Ball         ON 1850.0
019611022 Charlene Duvere     
ON22300.00        *Edmond Phil
lons, Jr.BC  150.0019351117
When the data is arranged in equal-length rows, it starts to make sense.
      Albert Ball         ON 1850.0019611022
 Charlene Duvere     ON22300.00        
*Edmond Phillons, Jr.BC  150.0019351117
The dates are visible at the right of each row. The second record shows an "empty" date. It really is empty! 
The first "column" indicates whether the record is deleted or not. In this case, the 3rd record is deleted, because there is an asterisk (*) in the column. This column is normally hidden by CA-Clipper. 
When this data is displayed within a CA-Clipper browse, it looks like this:
Albert Ball  ON  1850.00 1961/10/22
Charlene Duvere  ON 22300.00     /  /  
Edmond Phillons, Jr.  BC   150.00 1935/11/17

Year 2000 Compliance Notes and Links

More detailed information on the headers of CA-Clipper and xBase tables can be found at Phil Barnett's Year 2000 and xBase Languages page. 
Although your CA-Clipper program may be corrected for any date problems, you should also examine your hardware and operating system. 
Perhaps surprisingly, many personal computers have a limited real-time clock (RTC) that does not handle 4-digit year values. When the computer is powered on, the BIOS software (in ROM) reads the date and time from the RTC and stores it in CMOS memory. Then, when the operating system (such as MS-DOS) loads, it reads the date and time from the CMOS memory and then maintains its own copy in another area of memory. The MS-DOS copy commonly "wanders" due to counter incaccuracies. The problems usually occur in the first two steps of this chain, the RTC and the BIOS. 
Test your computer by setting the date into the year 2000, then power off. If the date is still in the year 2000 when the power is turned back on, breathe a sigh of relief. Many older RTC chips wrap around to 1900, but when MS-DOS loads the date is "corrected" to 1980, which is the MS-DOS minimum. The maximum date in MS-DOS is the year 2099. 
Visit the premier Year 2000 site at http://www.year2000.com/ for full details and a program that will check your hardware's handling of future dates. You can download a program that shows you all three clocks (RTC, BIOS, and DOS), and whether they function correctly. The site also maintains a growing list of BIOS software versions and whether they are Year 2000 compatible. 
Most Unix systems store dates as signed 32-bit integers which contain the number of seconds since the January 1st 1970. Thus the final deadline to worry about in Unix is the year 2038. And I was always told that Unix was better than DOS... 
The Y2K web site at www.y2k.com explores legal issues related to the Year 2000 problem, otherwise known as the "Millenium Bomb". At the Y2K site the phrase "Year 2000 compliant" is defined in the Federal Acquisition Regulation; Year 2000 Compliance document:
     Year 2000 compliant means information technology that accurately processes date/time data (including, but not limited to, calculating, comparing, and sequencing) from, into, and between the twentieth and twenty-first centuries, and the years 1999 and 2000 and leap year calculations. Furthermore, Year 2000 compliant information technology, when used in combination with other information technology, shall accurately process date/time data if the other information technology properly exchanges date/time data with it.  
It should be clear at this point that this definition can easily be applied to a properly written CA-Clipper program. 
Also at www.y2k.com is listed Four Criteria for "Century Compliance":
Criterion Description
General integrity No value for current date will cause interruptions in normal operation.
Date integrity All manipulations of calendar-related data (dates, durations, days of week, etc.) will produce desired results for all valid date values within the application domain.
Explicit century Date elements in interfaces and data storage permit specifying century to eliminate date ambiguity.
Implicit century For any date element represented without century, the correct century is unambiguous for all manipulations involving that element.
Again at www.y2k.com there is an interesting article on contracting for year 2000 work: Y2K Remediation Contracts. The article concludes that a process-oriented contract that requires the contractor to commit to performance standards and personnel requirements is superior to the traditional result-oriented contract which focuses on the end product and the fear of economic consequences. 
According to Science News (13 September 1997), the Defence Logistics Agency of the U.S. Department of Defense is working to fix 86 automated information systems, containing some 39 million lines of code, upon which it will rely after the year 2000. 
A recent incident involved 90,0000 items in the Defense Logistics Agency's materiel management program being removed from inventory as a result of a faulty year 2000 date calculation. Correcting the problem took 400 hours. [From EDUCOM Edupage, 1997/09/18 http://www.educause.edu/] 
A little "armchair statistics": if the other 85 systems suffer similar failures it will take around 17 man-years to correct, assuming 40-hour weeks. Given that there are only 2 years left, it appears that there is a problem. Throw more programmers at it, maybe. 
Another way of calculating is to assume that code can be inspected and corrected at a rate of 1 line per minute. At that rate, it would take over 300 man-years to check all code. 
Even though my calculations are very approximate, the scope of the issue is nonetheless clear.


 CA-Clipper is Y2K compliant
• Full dates are stored in databases and MEM files
• Full dates are used internally for all date calculations
• Use set date format to 'YYYY/MM/DD'
• Add set century on to see the full date
• Check your date conversion code:
Look for missing century values and mismatches with the format string.
For example: ctod('09/19/97') fails if format is YYYY/MM/DD.
Date functions to look for: ctod(), dtoc(), set epoch to... 
The presentation of the date to the end-user should be kept distinct from the way it is specified in the source code. The date format string can be used to control how dates appear to the user, but date creation in source code should be handled by a set of standardized functions. 
The following two functions do not rely on the current date format string. 
* Function   | StoD
* Purpose    | Convert string into date. Reverse of dtos().
* Parameters | pcString - a date string, format is YYYYMMDD
* Returns    | The date value.
function StoD ( pcString )
   local dResult:=ctod(''), cOldDateFormat

   cOldDateFormat := set(_SET_DATEFORMAT, 'YYYY/MM/DD')
   dResult := ctod(transform(pcString, '@R ####/##/##'))
   set(_SET_DATEFORMAT, cOldDateFormat)

return dResult

* Function   | YMDtoDate
* Purpose    | Convert year, month, day numbers into a date.
* Parameters | pnYear  - the year, range 100-2999
*            | pnMonth - the month, range 1-12
*            | pnDay   - the day, range 1-31
* Returns    | The date value.
function YMDtoDate ( pnYear, pnMonth, pnDay )
   local dResult:=ctod(''), cOldDateFormat

   cOldDateFormat := set(_SET_DATEFORMAT, 'YYYY/MM/DD')
   dResult := ctod( str(pnYear)  + '/' + ;
                    str(pnMonth) + '/' + ;
                    str(pnDay) )
   set(_SET_DATEFORMAT, cOldDateFormat)

return dResult
The functions are used as follows: 
    dDate := StoD('19971021')
    dDate := YMDtoDate(1997, 10, 21)
Visit my Data File Headers and the Year 2000 page for details about DBF headers.

Home Web Design Programming Fairlight CMI Soap Box Downloads Links Biography About... Site Map

Site Map Send comments about this site to Greg at gregh@ghservices.com
All pages copyright © 1996-1999 GH Services™   Created 1997/10/21   Last updated 2000/10/02
All trademarks contained herein are the property of their respective owners