Tim is a developer with RDI Software Technologies' object technology group. He can be reached at 6300 N. River Rd., Suite 200, Rosemont, IL 60018, by phone at 708-518-0181, x618, or on CompuServe at 70304,277.
Converting files from one format to another can be as simple as importing one line of text into a database record or as complex as importing data from a variety of accounting systems into a single tax-worksheet program. Likewise, graphical data often must be converted before it can be printed to different printers or plotters (each of which requires different command sets) or displayed in various formats on a screen.
In a recent project I worked on, multi-megabyte billing-report text files on mainframes needed to be converted to several DBF files. Each page of the report had to be checked for various criteria in differing locations, split into different sections, and put into a memo field in its entirety for later viewing.
Our approach to the problem was to build a parser-based conversion engine using C++ templates that, as it turns out, works well for both converting files between different formats and printing or displaying graphical data. By using templates to design the conversion engine, we were able to design a "black-box" conversion class. This class allows us to use the various required data types and processes without rewriting the basics of the conversion engine for each new combination.
An additional advantage of templates is the division of each major part into separate, manageable components. Instead of developing the entire conversion process as one block, each section was individually designed, tested, and maintained.
In its most basic form, a converter must be able to input data and output data and have input, conversion, and output mechanisms. For example, importing a text file to a DBF format requires the following: data from the text file; the data record to write to the DBF file; a function to read in the text-file data; a function to convert the text to the record format; and finally, a function to write the output record to the DBF file.
To be classified as an "engine," the individual sections of the converter must be replaceable. The input data type could be anything from an individual character or integer to a structure or a class instance. Likewise, the output data type can range from an individual character through a class instance. The input class retrieves the input data and supplies it to the conversion engine. The convert class processes the input data, converting it into the output data format. The output class receives the output data and puts it where it needs to go, whether to screen, disk, and so on.
The Converter class is defined with each of the five conversion requirements being part of the template, as shown in Figure 1(a), which is excerpted from the CONVERTR.HPP header file (Listing One, page 98). A pointer is stored to an instance of each of the five sections with a method provided to allow on-the-fly changing of each. The current stage and status are stored with a method for access to them.
(a)
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
class Converter { ... };
(b)
INPUT->getData ( INTYPE &id );
CONVERT->process ( INTYPE &id, OUTTYPE &od );
OUTPUT->putData ( OUTTYPE &od );
The idea of an engine implies a consistent interface. Therefore, each of the three required classes has required methods; their prototypes are shown in Figure 1(b). By using the standard means of access, the engine and each class can remain consistent for each new usage. Each of the required methods receives the data by reference and returns a status of OK, Done, or Error. This can easily be extended to handle more explicit messages, but for ease of explanation, I use just these three.
Since each class is a stand-alone definition, they may contain more than the minimum required methods. Each can have its own instance data and additional methods. Each class, or certain methods, can be made a friend to the other classes and allow communication beyond that used in the conversion-engine class. For instance, the convert class may need to call a rewind function in the input class if it needs to return to previous data.
CVTEST.CPP (Listing Two, page 98) demonstrates the use of the Converter class engine by converting a range of numbers (int) into a char * format and outputting them onto the console using cout; see Figure 2. This gives an example of using the template class, though on a much smaller scale than an actual file conversion.
To demonstrate the flexibility of the engine, I used a simple integer for the input data type, while a class is used for the output data type. The Output-Data class is a simple means of demonstrating the use of a class for a data type. It uses the itoa() function to convert the input data integer into an array of char. A setNumber() method is provided for this as well as a getNumber() method for returning the address of the character array. To take advantage of the type checking provided in C++, typedefs are used for the input and output data types.
The input class constructor accepts a range of numbers to increment through. In this example, error checking is not provided, though it would probably be used in most cases. The beginning, ending, and current count are stored as member variables and are initialized in the constructor. The required getData() method checks the current count against the ending value and returns CONVERTR_DONE if it is greater. Otherwise, it copies the current count into the input data instance (passed by reference), increments, and returns CONVERTR_OK.
The convert class consists entirely of the required process() method. This method calls the setNumber() method of the OutputData class, passing it the input data, and then returning CONVERTR_OK.
Similarly, the output class consists entirely of the required putData() method. This method uses cout and the OutputData class getNumber() method to display the output data on the console. It then returns CONVERTR_OK to the conversion-engine class.
Though the example used is simple, it demonstrates the usage of the conversion-engine template class. By replacing the five requirements--input data type, output data type, input, convert, and output--a file converter or graphical output mechanism can be developed with relative ease. The full project mentioned earlier was designed with a paging class for the input data type and a record-instruction class for the output data type. This allowed for an entire page of the billing report to be processed as a unit and for the development of a set of instructions for writing the particular page to the DBFS.
While there are alternatives to templates, such as using a standard set of base classes for the conversion engine with derived classes for each section, the approach described here provides both the flexibility of a variety of formats and a mechanism for the data and its processing.
Special thanks to Eric Nagler (TeamB) for his compilations with the Metaware High C/C++ compiler and to Pete Becker (Borland) for his explanations of Borland's implementation of the AT&T CFRONT 3.0 C++ standard.
_FILE CONVERSION USING C++ TEMPLATES_ by Tim Butterfield[LISTING ONE]
// --------- CONVERTR.HPP --- Converter class ---------
#ifndef _CONVERTR_HPP_
#define _CONVERTR_HPP_
#define CONVERTR_OK 0
#define CONVERTR_DONE 1
#define CONVERTR_ERROR -1
#define STAGE_INPUT 0
#define STAGE_CONVERT 1
#define STAGE_OUTPUT 2
#define STAGE_DONE 3
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
class Converter
{
public:
// constructor
Converter
( CONVERT &c, INPUT &i, INTYPE &id, OUTPUT &o, OUTTYPE &od );
inline void setInput ( INPUT &i ); // set a new INPUT object
inline void setConvert ( CONVERT &c ); // set a new CONVERT object
inline void setOutput ( OUTPUT &o ); // set a new OUTPUT object
inline void setInData ( INTYPE &id ); // set a new INTYPE object
inline void setOutData ( OUTTYPE &od ); // set a new OUTTYPE object
int getStatus() { return status; } // return the converter status
int getStage() { return stage; } // return the current stage
void run(); // run the converter
private:
INPUT *input; // input class
CONVERT *convert; // conversion class
OUTPUT *output; // output class
INTYPE *indata; // passed by reference to input::getData()
// and convert::process()
OUTTYPE *outdata; // passed by reference to convert::process()
// and output::putData()
int status; // current status - ok == 0, done > 0, error < 0
int stage; // current process
};
// ------- converter class methods -------
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
Converter ( CONVERT &c, INPUT &i, INTYPE &id, OUTPUT &o, OUTTYPE &od )
{
setInput( i ); // set input class
setConvert( c ); // set conversion class
setOutput( o ); // set output class
setInData( id ); // set input data item
setOutData( od ); // set output data item
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
setInput ( INPUT &i )
{
input = &i; // save pointer to input class
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
setConvert ( CONVERT &c )
{
convert = &c; // save pointer to conversion class
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
setOutput ( OUTPUT &o )
{
output = &o; // save pointer to output class
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
setInData ( INTYPE &id )
{
indata = &id; // save pointer to input data
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
setOutData( OUTTYPE &od )
{
outdata = &od; // save pointer to output data
}
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
void Converter < CONVERT, INPUT, INTYPE, OUTPUT, OUTTYPE >::
run()
{
status = CONVERTR_OK;
do
{
// go through each stage of the convertion
for ( stage = STAGE_INPUT;
status == CONVERTR_OK && stage < STAGE_DONE;
stage++ )
{
switch ( stage ) // which stage is the converter in
{
case STAGE_INPUT: // get data
status = input->getData( *indata );
break;
case STAGE_CONVERT: // process data
status = convert->process( *indata, *outdata );
break;
case STAGE_OUTPUT: // put data
status = output->putData( *outdata );
break;
default: // shouldn't get here
break;
}
}
} while ( status == CONVERTR_OK ); // not done, no errors
}
#endif // #ifndef _CONVERTR_HPP_
// ------- EOF: CONVERTR.HPP -------
[LISTING TWO]
// ------- CVTEST.CPP --- Converter class test program -------
#include <stdlib.h> // itoa() in OutputData class
#include <iostream.h> // cout
#include <iomanip.h> // endl
#include "convertr.hpp" // Converter class
// ------- converter data types -------
// simple class for storing the output data
class OutputData
{
public:
// constructor
OutputData ( ) { number[0] = '\0'; } // clear number
// retrieve the number
char *getNumber ( ) { return number; }
// set a new number
void setNumber ( int n ) { itoa( n, number, 10 ); }
private:
char number[6]; // make room for number
};
// use of typedefs allows better type checking
typedef int inType; // input data block type
typedef class OutputData outType; // output data block type
// ------- converter Input class -------
// class declaration
class Input
{
public:
Input ( int b, int e ); // constructor
int getData ( inType &id ); // called by converter::run()
private:
int begin; // beginning count
int end; // ending count
int cur; // current count
};
// constructor
Input::Input ( int b, int e )
{
begin = b; // set beginning count
end = e; // set ending count
cur = begin; // set current count
}
// get data for the converter
int Input::getData ( inType &id )
{
if ( cur > end ) // past the end yet ?
{
return ( CONVERTR_DONE ); // yes, return "done"
}
id = cur++; // put data in "input block", increment count
return ( CONVERTR_OK ); // return "ok"
}
// ------- converter conversion class -------
// class declaration
class Convert
{
public:
int process ( inType &id, outType &od ); // called by converter::run()
};
// process the data for the converter engine
int Convert::process ( inType &id, outType &od )
{
od.setNumber( id ); // convert to output data type
return ( CONVERTR_OK ); // return "ok"
}
// ------ converter Output class -------
// class declaration
class Output
{
public:
int putData ( outType &od ); // called by converter::run()
};
// put data from the converter
int Output::putData ( outType &od )
{
// display the output "block"
cout << "[" << od.getNumber() << "]" << endl;
return ( CONVERTR_OK ); // return "ok"
}
// ------- main -------
void main ( )
{
// class instances required by the converted
Input i(1,10); // initialize input class instance
Convert c; // initialize convert class instance
Output o; // initialize output class instance
// data blocks required by the converter
inType id; // initialize input data block
outType od; // initialize output data block class instance
// create an instance of the converter using the defined types
Converter< Convert, Input, inType, Output, outType >
cv( c, i, id, o, od );
// run the converter
cv.run();
}
// ------- EOF: CVTEST.CPP -------
Figure 1: (a) The Conversion template class declaration; (b) required conversion engine support class method prototypes.
(a)
template
< class CONVERT, class INPUT, class INTYPE, class OUTPUT, class OUTTYPE >
class Converter { ... };
(b)
INPUT->getData ( INTYPE &id );
CONVERT->process ( INTYPE &id, OUTTYPE &od );
OUTPUT->putData ( OUTTYPE &od );
Copyright © 1993, Dr. Dobb's Journal