Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
This example shows an approach you might take to log data to
disk and later retrieve it for processing. It is meant to produce a temperature-depth
profile of the ocean at various points. The DataPoint class holds the
data:
//: C04:DataLogger.h
// Datalogger record layout.
#ifndef DATALOG_H
#define DATALOG_H
#include <ctime>
#include <iosfwd>
#include <string>
using std::ostream;
struct Coord {
int deg, min, sec;
Coord(int d = 0, int m = 0, int s = 0)
: deg(d), min(m), sec(s) {}
std::string toString() const;
};
ostream& operator<<(ostream&, const
Coord&);
class DataPoint {
std::time_t timestamp; // Time & day
Coord latitude, longitude;
double depth, temperature;
public:
DataPoint(std::time_t ts, const Coord& lat,
const Coord& lon, double dep, double
temp)
: timestamp(ts), latitude(lat), longitude(lon),
depth(dep), temperature(temp) {}
DataPoint() : timestamp(0), depth(0), temperature(0)
{}
friend ostream& operator<<(ostream&,
const DataPoint&);
};
#endif // DATALOG_H ///:~
A DataPoint consists of a time stamp, which is stored
as a time_t value as defined in <ctime>, longitude and latitude coordinates, and values for depth and temperature. We use inserters for easy formatting.
Here s the implementation file:
//: C04:DataLogger.cpp {O}
// Datapoint implementations.
#include "DataLogger.h"
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
ostream& operator<<(ostream& os, const
Coord& c) {
return os << c.deg << '*'
<< c.min << '\''
<< c.sec
<< '"';
}
string Coord::toString() const {
ostringstream os;
os << *this;
return os.str();
}
ostream& operator<<(ostream& os, const
DataPoint& d) {
os.setf(ios::fixed, ios::floatfield);
char fillc = os.fill('0'); // Pad on left with '0'
tm* tdata =
localtime(&d.timestamp);
os <<
setw(2) << tdata->tm_mon + 1 << '\\'
<< setw(2) <<
tdata->tm_mday << '\\'
<< setw(2) <<
tdata->tm_year+1900 << ' '
<< setw(2) << tdata->tm_hour
<< ':'
<< setw(2) <<
tdata->tm_min << ':'
<< setw(2) <<
tdata->tm_sec;
os.fill(' '); // Pad on left with ' '
streamsize prec = os.precision(4);
os << " Lat:" << setw(9)
<< d.latitude.toString()
<< ", Long:" << setw(9)
<< d.longitude.toString()
<< ", depth:" << setw(9)
<< d.depth
<< ", temp:" << setw(9)
<< d.temperature;
os.fill(fillc);
os.precision(prec);
return os;
} ///:~
The Coord::toString( ) function is necessary
because the DataPoint inserter calls setw( ) before it prints the latitude and longitude. If we used the stream inserter for Coord instead, the width
would only apply to the first insertion (that is, to Coord::deg), since
width changes are always reset immediately. The call to setf( ) causes the floating-point output to be fixed-precision, and precision( ) sets the number of decimal places to four. Notice how we restore the fill
character and precision to whatever they were before the inserter was called.
To get the values from the time encoding stored in DataPoint::timestamp,
we call the function std::localtime( ), which returns a static pointer to a tm object. The tm struct has the following layout:
struct tm {
int tm_sec; // 0-59 seconds
int tm_min; // 0-59 minutes
int tm_hour; // 0-23 hours
int tm_mday; // Day of month
int tm_mon; // 0-11 months
int tm_year; // Years since 1900
int tm_wday; // Sunday == 0, etc.
int tm_yday; // 0-365 day of year
int tm_isdst; // Daylight savings?
};
Generating test data
Here s a program that creates a file of test data in binary
form (using write( )) and a second file in ASCII form using the DataPoint inserter. You can also print it out to the screen, but it s easier
to inspect in file form.
//: C04:Datagen.cpp
// Test data generator.
//{L} DataLogger
#include <cstdlib>
#include <ctime>
#include <cstring>
#include <fstream>
#include "DataLogger.h"
#include "../require.h"
using namespace std;
int main() {
time_t timer;
srand(time(&timer)); // Seed the random number
generator
ofstream data("data.txt");
assure(data, "data.txt");
ofstream bindata("data.bin", ios::binary);
assure(bindata, "data.bin");
for(int i = 0; i < 100; i++, timer += 55) {
// Zero to 199 meters:
double newdepth = rand() % 200;
double fraction = rand() % 100 + 1;
newdepth += 1.0 / fraction;
double newtemp = 150 + rand() % 200; // Kelvin
fraction = rand() % 100 + 1;
newtemp += 1.0 / fraction;
const DataPoint d(timer, Coord(45,20,31),
Coord(22,34,18), newdepth,
newtemp);
data << d << endl;
bindata.write(reinterpret_cast<const
char*>(&d),
sizeof(d));
}
} ///:~
The file data.txt is created in the ordinary way as
an ASCII file, but data.bin has the flag ios::binary to tell the constructor to set it up as a binary file. To illustrate the formatting used for the text
file, here is the first line of data.txt (the line wraps because it s
longer than this page will allow):
07\28\2003 12:54:40 Lat:45*20'31", Long:22*34'18", depth:
16.0164, temp: 242.0122
The Standard C library function time( ) updates the time_t value its argument points to with an encoding of the
current time, which on most platforms is the number of seconds elapsed since 00:
00: 00 GMT, January 1 1970 (the dawning of the age of Aquarius?). The current
time is also a convenient way to seed the random number generator with the
Standard C library function srand( ), as is done here.
After this, the timer is incremented by 55 seconds to
give an interesting interval between readings in this simulation.
The latitude and longitude used are fixed values to indicate
a set of readings at a single location. Both the depth and the temperature are
generated with the Standard C library rand( ) function, which
returns a pseudorandom number between zero and a platform-dependent constant, RAND_MAX, defined in <cstdlib> (usually the value of the platform s largest
unsigned integer). To put this in a desired range, use the remainder operator %
and the upper end of the range. These numbers are integral; to add a fractional
part, a second call to rand( ) is made, and the value is inverted
after adding one (to prevent divide-by-zero errors).
In effect, the data.bin file is being used as a
container for the data in the program, even though the container exists on disk
and not in RAM. write( ) sends the data out to the disk in binary
form. The first argument is the starting address of the source block notice it
must be cast to a char* because that s what write( ) expects
for narrow streams. The second argument is the number of characters to write,
which in this case is the size of the DataPoint object (again, because
we re using narrow streams). Because no pointers are contained in DataPoint,
there is no problem in writing the object to disk. If the object is more
sophisticated, you must implement a scheme for serialization, which
writes the data referred to by pointers and defines new pointers when read back
in later. (We don t talk about serialization in this volume most vendor class libraries
have some sort of serialization structure built into them.)
Verifying and viewing the data
To check the validity of the data stored in binary format,
you can read it into memory with the read( ) member function for
input streams, and compare it to the text file created earlier by Datagen.cpp.
The following example just writes the formatted results to cout, but you
can redirect this to a file and then use a file comparison utility to verify
that it is identical to the original:
//: C04:Datascan.cpp
//{L} DataLogger
#include <fstream>
#include <iostream>
#include "DataLogger.h"
#include "../require.h"
using namespace std;
int main() {
ifstream bindata("data.bin", ios::binary);
assure(bindata, "data.bin");
DataPoint d;
while(bindata.read(reinterpret_cast<char*>(&d),
sizeof d))
cout << d << endl;
} ///:~
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |