25 Years of Programming
An open source source for C, C++, OWL, BASIC, MDB, XLS, DOT, and more...
Home   Projects   Up   Sitemap   Search   Blog   Forum+Chat   About Us   Privacy   Terms of Use   Feedback   FAQ   Images   Services   Ads   Donate   Humor

Amateur Radio Net Control programs - C++ versions

This is the source code listing for the C++ programs in the Amateur Radio Net Control project, utilities that assist an amateur radio net control operator with running and checking members into an on the air radio net.

NET.CPP

/*  NET.CPP			2-22-96
	Copyright (C)1994-1996 Steven Whitney.
	Initially published by http://25yearsofprogramming.com.

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License (GPL)
	Version 2 as published by the Free Software Foundation.
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


For conducting amateur radio nets. Provides a fast interface for checking people in
individually by call sign or by roll call by sequential city names.
Allows recording announcements and traffic. At the end, calculates check-in and traffic
totals and generates a log of the session. Also allows printing a roster, which I used
to print on graph paper, for use during mobile operation away from the computer.

It will take several sessions to get used to the program's interface, but once you know how
to "drive" the program, it is fast, and you can keep up with checkins even if they
come in at a fast pace.

All data files must be in the current default directory (with the program).

------
TO DO:

(copied from paper notes, 6/24/02)
--allow reading a log file for review or for after a crash?  Must write it first.
--if requestee of contact is in same city as requestor, update requestee's color also.

you do need to limit the length of the strings output in displayrec(), using
string::substr().  If a too-long name is the last on the line, it wraps to
the next line, overwriting part or all of the 1st record on the next line.
This may not be that important: it would have to be on line 25 to scroll
the screen, which never happens.

in callrollcall(), bycity(), etc., break the absolute numbers that calculate screen
coordinates out into named variables, and use those so method is more clear,
and can possibly be generalized.  Put the generalized routine into a function.
calcfieldloc(int& x, int& y).  Would be useful for displaying any similar
array where user needs to choose one from those displayed on screen.

Continue to watch for other situations where erroneous input causes serious problems,
and for places where intuitive commands aren't correct (use them, instead).
Note that A,L,P checkin codes ARE commands in main menu context.

If someone has multiple traffic or traffic + announcements,
traffic count report is wrong (each call can only be credited with 1, and pgm can
only keep track of 1 per call).
also, traffic is counted even if it's not passed during the net.
there should be a way to check it off if it's passed.
Could tally using the cities list (where it's counted).

When main menu page space runs out, move "utilities" section to its own menu.

Write better documentation with an index and table of contents.

(a search by name might be interesting, but not particularly useful in real use)

--A Windows version would require significant mastery of a number of Windows methods;
  would be worth it, especially because it could use DDE to maintain its database.
--Would also be interesting and instructive to attempt converting (rewriting)
  in MSAccess, using macros and modules for some of the commands.
  See NET.MDB: it's interesting to work on, but the chances of achieving the same ease of use
  as this seem to be about zero.
--As an experiment, could store command key mappings in a file, making them user-definable.
--When extendedlog is on, if each report from a station is logged as an
  announcement, it will automatically log the time, call, and what they were reporting.
  And as people sign back OUT, you use ' ' to check them back out, until no one left.
--delete a record by setting ->ck to Visitor and doing an edit to force disk write.
  Don't make it easier.  Preferred method is to review with text editor.
--some searches could quit when past where item should be, but trivial savings.

*/
#include <classlib\time.h>
#include <classlib\arrays.h>
#pragma hdrstop

#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"

//////////////////////////////////////////////////////////////////////////////
// GLOBAL DATA
//////////////////////////////////////////////////////////////////////////////
// a city name and a count of the traffic items addressed to it
struct TOWN
{
	TOWN() { tcount = 0; }								// constructors
	TOWN(string& a) : n(a) { n.to_upper(); tcount = 0; }
	BOOL operator == (const TOWN& other) const { return(n == other.n); }
	BOOL operator < (const TOWN& other) const { return(n < other.n); }

	string n; 	            // name of the city (was char[16])
	int tcount;	            // number of pieces of TRAFFIC destined FOR THE CITY
};

//////////////////////////////////////////////////////////////////////////////
// info about one ham radio operator
struct HAM
{
	HAM() { ck = need = ' '; ord = 0; sort = bycall; }	// default
	HAM(string& a, string& b, TOWN* c);               	// normally-used
	HAM(const HAM& h)									// copy constructor (not used)
		{ call = h.call; name = h.name; city = h.city;
			ck = h.ck; need = h.need; ord = h.ord; }

	BOOL operator == (const HAM& other) const { return(call == other.call); }
	BOOL operator < (const HAM& other) const;
	HAM& operator = (const HAM& h);				// ham1 = ham2 is legal (not used)

	static enum sorttypes { bycall, bycitycall, byord } sort;
	string call;        // call sign
	string name;        // person's name, only 9 chars are shown on displays
	TOWN* city;         // home city
	char ck;          	// CHECKIN CHAR
	char need;          // IF NEEDED FOR CONTACT, matches the other party's char
	int ord;            // PLACEMENT IN CHECKIN ORDER (BUT doesn't count accurately)
};
HAM::sorttypes HAM::sort = HAM::bycall;

//----------------------------------------------------------------------------
// allows sorting in multiple ways, depending on the sort flag.
// Whenever you create a new HAM, sort order is reset to bycall.
// After that, you can change it for other sorts in a new sorted container.
BOOL HAM::operator < (const HAM& other) const
{
	switch(sort)
	{
		case bycitycall:
			if(city == other.city)
				return(call < other.call);
			return(city->n < other.city->n);
		case byord :
			return(ord < other.ord);
		case bycall:
		default:
			return(call < other.call);
	}
}
//----------------------------------------------------------------------------
// constructor
HAM::HAM(string& a,string& b,TOWN* c) : call(a), name(b), city(c)
{
	call.to_upper();
	name.to_lower();
	name[0] = toupper(name[0]);  	// 1st letter of name capitalized
	ck = need = ' ';
	ord = 0;
	sort = bycall;
}
//----------------------------------------------------------------------------
// assignment operator (adapted from PG:151).
// Note (*this) is the object being assigned TO.
HAM& HAM::operator = (const HAM& h)
{
	if(&h == this) return(*this);		// prevent assigning to itself
	call = h.call;
	name = h.name;
	city = h.city;
	ck = h.ck;
	need = h.need;
	ord = h.ord;
	return(*this);
}
//----------------------------------------------------------------------------
// MORE GLOBAL DATA

TISArrayAsVector<HAM> db(200,0,50);	// sorted database of all people
TISArrayAsVector<HAM> fi(20,0,50);	// subset of db[] found during searches

TArrayAsVector<string> memos(3,0,5); 	// ARRAY OF SINGLE-LINE MEMOS
TArrayAsVector<string> agenda(60,0,5); 	// lines from agenda file

TISArrayAsVector<TOWN> cities(50,0,10); 	// list of all the cities on file
int cindex = 0;             // index within cities[] of CURRENTLY SELECTED CITY
int resumecity = 0;         // CITY TO RESUME INPUT WITH AFTER PAGING BACKWARDS

BOOL filteron = FALSE;      // WHETHER to hide already-checked-in from displays
BOOL dbchanged = FALSE;    	// WHETHER TO write file calls.dat BEFORE EXIT
BOOL verifycalls = TRUE;	// whether to check call signs for US, Canada validity
BOOL extendedlog = FALSE;	// whether to log every net activity with time stamp.
							// The extended log is actually kept in the .MEM file
int ckinord = 0;            // ORDERS CHECKINS, BUT DOESN'T KEEP ACCURATE COUNT.
char contacts = '0';        // 2 parties of a contact have a matching contact char
char defch = 'x';           // DEFAULT CHECKIN CHARACTER (ONLY LOPX)

constream scr;			    // window for screen output
string helpline;			// each function sets it to its relevant help line

//----------------------------------------------------------------------------
// Input a string from the console (maximum length 80), using ::cgets() so that
// the user's ending <CR> won't cause screen to scroll.
// (If length > 80 is anticipated, use getline(), since scrolling will occur anyway.)
// Could be a library function.
// Returns the length of the resulting string.  Can use if(cgets(s)) dosomething.
int cgets(string& s, int maxlen = 80)
{
	if(maxlen < 1) maxlen = 1;		// entering 0 chars is meaningless
	if(maxlen > 80) maxlen = 80;	// > 80 will cause scrolling AND buf overflow
	char buf[82] = {maxlen}, *p;   	// room for 79 chars + null (max w/o scrolling)
	p = cgets(buf);
	s = p;
	return(s.length());
}
//----------------------------------------------------------------------------
// sets a string to today's date, reformatted to "mm-dd-yy" (useful for file names)
// Could be a library function.
// could this return a string&? and thus be chained like other operators?
void today(string& s)
{
char buf[10];
_strdate(buf);        				// GET DOS DATE
for(char* p = buf ; *p ; p++)       // change slashes to hyphens
	if(*p == '/')
		*p = '-';
s = buf;                			// set string
}
//----------------------------------------------------------------------------
// sets a string to the current time
// Could be a library function.
void timenow(string& s)
{
char buf[10];
_strtime(buf);        				// GET DOS DATE
s = buf;                			// set string
}
//----------------------------------------------------------------------------
// check whether a given call sign is probably a valid one
BOOL validcallsign(string& s)
{
if(!verifycalls)							// no checking: anything is ok
	return(TRUE);
if((s.length() < 4) || (s.length() > 6))  	// length: 4 to 6 chars
	return(FALSE);
if(!strchr("acknvwx",tolower(s[0])))		// start with AKNW(CVX Canada X Mex)
	return(FALSE);
if(!isdigit(s[1]) && !isdigit(s[2]))   		// digit in position 2 or 3
	return(FALSE);
return(TRUE);
}
//----------------------------------------------------------------------------
// create a memo, optionally getting additional text from user
// Assumes that if there IS starttext, and autoret is FALSE, it's an announcement;
// if no text is added to it, considers it an abort.
// returns TRUE if a memo was created and added, FALSE if not
BOOL memo(string& starttext, BOOL autoret)	// initial text & auto-exit flag
{
static string previousdate;
string memotext, timestamp;

if(extendedlog)   						// create date and time stamp
{
	today(timestamp);
	if(timestamp != previousdate)		// if date has changed since last memo
	{                                   // FIRST make them equal, to prevent
		previousdate = timestamp;       // endless recursive calls!
		memo(timestamp,TRUE);			// a memo to note that the date changed
	}
	timenow(timestamp);					// now get current time to prefix memo
	timestamp += ": ";
}
memotext = starttext;                  	// START MEMO WITH imported STRING
if(!autoret)                            // GET ADDITIONAL USER TEXT
{
	scr << setclr(WHITE) << setxy(1,25) << clreol << setcrsrtype(_SOLIDCURSOR)
		<< memotext;
	string userbuf;
	cgets(userbuf,80-memotext.length()); // get additional text, if any, from user
	scr << setcrsrtype(_NORMALCURSOR);
	if(memotext.length() && !userbuf.length())	// user aborted announcement
		return(FALSE);
	memotext += userbuf;	                    // append it to the string
}
if(memotext.length())        					// IF MEMO has anything in it,
{
	if(extendedlog)
		memotext.prepend(timestamp);			// insert the time stamp
	memos.Add(memotext);						// add it to the array.
	return(TRUE);
}
return(FALSE);
}
//----------------------------------------------------------------------------
// DISPLAYS all MEMOS
// RETURNS: int, WHICH CAN BE A MAIN MENU COMMAND
int showmemos()
{
scr.clrscr();
scr << setclr(LIGHTGRAY);
for(int i = 0 ; i < memos.GetItemsInContainer() ; i++)
{
	scr << memos[i] << endl;
	if(i && !(i % 23))
	{
		scr << setxy(1,25) << clreol << "More...";
		getch();
		scr << endl;
	}
}
scr << setclr(GREEN) << setxy(1,25) << clreol << "Enter command or <CR> for menu: ";
return(ci());
}               			   // showmemos
//----------------------------------------------------------------------------
// WRITES MEMOS TO DISK FILE MM-DD-YY.MEM
void writememos()
{
string buf;
today(buf);                               	// get today's date
buf += ".mem";                            	// ADD EXTENSION
ofstream outfile(buf.c_str());
for(int i = 0 ; i < memos.GetItemsInContainer() ; i++)
	outfile << memos[i] << endl;
if(outfile.fail())							// file write error
{
	cerr << "Possible data loss in file " << to_upper(buf) << endl;
	presskey();
}
} 								// writememos
//----------------------------------------------------------------------------
// search db for a call sign
// returns pointer to a HAM node, or 0 if call sign not found
HAM* findham(string& calltofind)          			// call sign to look for
{
calltofind.to_upper();
for(int i = 0 ; i < db.GetItemsInContainer() ; i++)    	// LOOK FOR Call sign
	if(db[i]->call == calltofind)			     		// IF FOUND,
		return(db[i]);                                  // return pointer.
return(0);                        						// wasn't found
}          			            // findham
//----------------------------------------------------------------------------
// PROCESS CONTACT REQUEST.
// ASSIGNS NEXT CONTACT REQUEST CHAR TO THE NEED FIELD OF BOTH PARTIES.
// IF NEEDED PARTY NOT FOUND, CONTACT IS NOTED FOR REQUESTOR,
// AND A MEMO IS ADDED TO MEMO PAGE, BUT NEEDED PARTY NOT ADDED TO DB
// RETURNS: TRUE IF a contact request was processed, FALSE IF NOT (user aborted)
BOOL listneed(HAM* req)       		// REQUESTING PARTY
{
string buf, memostart;

scr << setclr(WHITE) << setxy(1,25) << clreol
	<< "Contact with which call? <CR> to quit: ";
if(!cgets(buf))                    		// ALLOW ABORT
	return(FALSE);

HAM* wanted = findham(buf);
if(wanted)                              // if wanted party is in db,
{
	wanted->need = contacts;    // MARK as wanted for contact
	if(extendedlog)             // if logging everything, log the contact request
	{
		memostart = "Contact request: " + req->call + " for " + buf;
		memo(memostart,TRUE);
	}
}
else                                    // otherwise, make a memo.
{
	memostart = "Contact request: " + req->call + " for " + buf
									+ " (who is not in database).";

	memo(memostart,TRUE);       // NOTE that WANTED CONTACT party wasn't IN DB
}
req->need = contacts++;         // MARK REQUESTOR and increment the marker char
return(TRUE);
}            			   		// listneed
//----------------------------------------------------------------------------
// return pointer to a city node, first creating the node if not already listed
TOWN* findcity(string& tofind)          // name of city to look for
{
size_t u;        						// change any imbedded commas to spaces
while((u = tofind.find(",")) != NPOS)   // (commas not allowed by disk file format)
	tofind[u] = ' ';

tofind.to_upper();
for(int i = 0 ; i < cities.GetItemsInContainer() ; i++)  // LOOK FOR CITY NAME
	if(cities[i]->n == tofind)				     		 // IF FOUND,
		return(cities[i]);                               // return pointer.

string cindexcity(tofind);			// If 1st city, this WILL be cities[cindex]->n
if(cities.GetItemsInContainer())    // else (if there IS a current cindex city),
	cindexcity = cities[cindex]->n;	// save its name.  The new addition
									// might change its index within the array.)

TOWN* newcity = new TOWN(tofind);	// tofind wasn't FOUND above, so create IT,
cities.Add(newcity);                // add to list,

if(cities[cindex]->n != cindexcity)     // if [cindex] no longer is same city
	for(i = 0 ; i < cities.GetItemsInContainer() ; i++)
		if(cities[i]->n == cindexcity)                  // find that city
			cindex = i;                         		// and point [cindex] back at it.

return(newcity);                    // return pointer to the added city
}       			          		// findcity
//----------------------------------------------------------------------------
// INCREMENT TRAFFIC COUNT FOR A CITY.  Can be any city name.  If not in roster
// (no ham living there), will be temporarily added to cities list as a reminder.
// returns TRUE if a traffic item was added, FALSE if not.
BOOL logtraf(HAM* from)      			// STATION WITH TRAFFIC
{
// as an alternative, could use the citymenu() function to select one from the list.
// but it would still need this option of creating a new city.

scr << setclr(WHITE) << setxy(1,25) << clreol
	<< "Destination city or <CR> to quit: ";
string destcity;
if(!cgets(destcity))                      	// allow ABANDON ENTRY
	return(FALSE);
								// look up (or create) city
findcity(destcity)->tcount++;	// and increment traffic count
string memostart = "T: " + from->call + " for " + destcity; // memo for reference
memo(memostart,TRUE);
return(TRUE);
}									// logtraf
//----------------------------------------------------------------------------
// PROCESS CHECKIN CHARS T, A, C
// returns TRUE if any of its activities might change screen attributes
// (different color, contact request, etc.) of a displayed db record OTHER
// than the one being processed, OR change a city's traffic count, FALSE if not.
// If the entering of t,a, or c is aborted, DON'T change the ham's ->ck checkin
// code: they might have had others; and if you allow ->ck to be auto-changed
// here, you can't restore their previous char without entering new info,
// thus creating an extraneous log entry (and count).
// When extendedlog is ON, all three (tac) cause memos to be created.
BOOL handletac(HAM* h)	// pointer to node of CALL SIGN BEING PROCESSED
{
string memostart;
switch(h->ck)      		// checkin char must have been assigned before entering
{
	case 't':         			    // RECORD PRESENCE OF TRAFFIC
		return(logtraf(h));
	case 'a':          				// RECORD ANNOUNCEMENT
		memostart = "A: " + h->call + " ";
		memo(memostart,FALSE);
		return(FALSE);              // affects no other record
	case 'c':    			        // PROCESS CONTACT REQUEST
		return(listneed(h));        // contact could affect others on same page
}
return(FALSE);						// any non-tac char affects nothing
}          				         	// handletac
//----------------------------------------------------------------------------
// user ADDs NEW PERSON TO DATABASE.
// default city is cities[cindex] (currently selected city)
// RETURNS: 0=successfully added, 1=user aborted, 2=invalid call, 3=already in db
int addnewham(string& calltoadd)
{
string callbuf, namebuf, citybuf;

// for auto-addition, must: have length, be valid, and not already there
if(calltoadd.length() && validcallsign(calltoadd) && !findham(calltoadd))
{
	callbuf = calltoadd;
	scr << setclr(GREEN) << setxy(1,25) << clreol
		<< "Adding " << setclr(WHITE) << callbuf << setclr(GREEN)
		<< ", Enter " << setclr(WHITE);
}
else								// need user entry of call sign
{
	scr << setclr(WHITE) << setxy(1,25) << clreol
		<< "CALL SIGN" << setclr(GREEN) << " to add or <CR> to quit: "
		<< setclr(WHITE);
	if(!cgets(callbuf))                    			// allow ABORT
		return(1);

	if(!validcallsign(callbuf))		// invalid
		return(2);

	if(findham(callbuf))			// already there
		return(3);

	scr << setxy(1,25) << clreol;
}
scr	<< "NAME " << setclr(GREEN) << "or <CR> to quit: " << setclr(WHITE);
if(!cgets(namebuf))                               	// allow ABORT
	return(1);

scr << setclr(WHITE) << setxy(1,25) << clreol
	<< "CITY " << setclr(GREEN) << "or <CR> for " << setclr(WHITE)
	<< cities[cindex]->n << setclr(GREEN) << ": " << setclr(WHITE);
if(!cgets(citybuf))
	citybuf = cities[cindex]->n;

HAM* newham = new HAM(callbuf,namebuf,findcity(citybuf));	// got this far,
db.Add(newham);												// it's an addition for sure.
do
{
	scr << setxy(1,25) << clreol << "Checkin code \"ACLOPTVX \": ";
	newham->ck = tolower(getche());
}
while(!strchr("acloptvx ",newham->ck));         // DEFAULT NOT ALLOWED HERE

if(newham->ck != 'v')       		// IF NOT A VISITOR,
	dbchanged = TRUE;      			// WE'LL HAVE TO UPDATE CALLS.DAT FOR SURE,

if(newham->ck != ' ')           	// assign PLACE IN THE CHECKIN ORDER
	newham->ord = ckinord++;

handletac(newham); 		        	// PROCESS TRAFFIC, ANNOUNCEMENT, OR CONTACT
if(extendedlog)						// make a log entry of the checkin
	if(!strchr("tac",newham->ck)) 	// t,a,c, already memoed in handletac
	{
		string memostart = newham->call + "  Ck: " + newham->ck;
		memo(memostart,TRUE);
	}
return(0);
}									// addnewham()
//----------------------------------------------------------------------------
// EDIT A DB RECORD
// RETURNS: TRUE IF RECORD WAS FOUND IN DB, FALSE IF NOT
int editrec(string& tofind)        	// CALL SIGN TO FIND AND EDIT
{
int needsort = FALSE;   			// true if any sort field is changed
string buf;

HAM* found = findham(tofind);    // pointer to node matching call sign (if any)
if(!found)                       // CAN'T EDIT A RECORD THAT'S NOT THERE
	return(FALSE);

scr << setclr(WHITE) << setxy(1,25) << clreol
	<< "Correct CALL or <CR> for " << found->call << ": ";
if(cgets(buf) && validcallsign(buf))	// if invalid call, just ignore input
{
	needsort = TRUE;
	found->call = to_upper(buf);
}
scr << setxy(1,25) << clreol << "Correct NAME, or <CR> for " << found->name << ": ";
if(cgets(buf))
	found->name = to_upper(buf);	// leaves name all upper case during the net

scr << setxy(1,25) << clreol << "Correct CITY, or <CR> for " << found->city->n << ": ";
if(cgets(buf))
	found->city = findcity(buf);

if(needsort)     // if sort field was changed, have to re-add entry to re-sort.
{
	HAM::sort = HAM::bycall;					// specify sort order
	db.Detach(found,TShouldDelete::NoDelete);	// remove from array
	db.Add(found);                              // and sort it back in.
}
dbchanged = TRUE;                 // MARK DB AS CHANGED
return(TRUE);
}								// editrec()
//----------------------------------------------------------------------------
// display the current help line on line 25
void showhelpline()
{
	scr << setclr(GREEN) << setxy(1,25) << clreol << helpline;
}
//----------------------------------------------------------------------------
// DISPLAY ONE DB RECORD IN FORMAT USED ON INPUT SCREENS
void displayrec(HAM* h)					// RECORD BEING DISPLAYED
{
int color = 					RED;        // INITial COLOR,
if(h->ck > ' ') 		color = LIGHTBLUE;  // then BUMP UP
if(h->need > ' ') 		color = MAGENTA;    // ON INCREASING
if(strchr("ta",h->ck)) 	color = YELLOW;     // IMPORTANCE LEVELS

scr << setclr(color) << h->ck << h->need << " "
	<< setw(6) << setiosflags(ios::left) << h->call << " "
	<< setw(9) << setiosflags(ios::left) << h->name;
}
//----------------------------------------------------------------------------
// LIST EVERYONE WHO'S CHECKED IN
// RETURNS: A CHAR (int) THAT CAN BE A VALID MAIN MENU COMMAND
int listall(int s)  	// SORT ORDER: A=ALPHA(call sign), OTHER=CHECKIN ORDER
{
fi.Flush(TShouldDelete::NoDelete);  						// start empty
HAM::sort = (tolower(s) == 'a') ? HAM::bycall : HAM::byord;	// specify sort order

int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++)     	// SEARCH DB,
	if(db[i]->ck > ' ')              	// IF A CHECKIN,
		fi.Add(db[i]);                  // ADD IT TO FOUND ARRAY

scr.clrscr();
int traffic = 0;        					// COUNT OF ANNOUNCEMENTS + TRAFFIC
int checkins = fi.GetItemsInContainer();	// # CHECKINS
for(i = 0 ; i < checkins ; i++)     		// DISPLAY THE LIST
{
	if(i && !(i % 96))  		// ALLOW FULL LIST DISPLAY USING MULTIPLE PAGES,
	{                           // BUT NO PAGING BACK THROUGH LIST
		scr << setclr(LIGHTGRAY) << setxy(1,25) << clreol << "More...";
		getch();
		scr.clrscr();
	}                           	 // ROWS 1-24 AND COLUMNS 0,20,40,60
	scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
	displayrec(fi[i]);

	if(strchr("ta",fi[i]->ck))
		traffic++;
}
scr << setclr(LIGHTGRAY) << setxy(1,25) << clreol
	<< checkins << " Checkins.  " << traffic << " Traffic.  "
	<< setclr(GREEN) << "Enter command, or <CR> for menu: " << setclr(LIGHTGRAY);
int ch = ci();						// value to return
// scr << setxy(1,25) << clreol;
return(ch);
}			                       // listall()
//----------------------------------------------------------------------------
// FIND CALL SIGNS BY STRING match.
// DISPLAYS ALL MATCHES, sorted by call sign, and ALLOWS MULTIPLE CHECKINS.
// FEATURE NOT SHOWN IN HELP LINE: ACCEPTs CONTROL-CODE CURSOR MOVEMENT COMMANDS,
// TAB CHECKS IN, FOR LEFTHANDED OPERATION.
// RETURNS: 'e' IF an immediate return to this function is needed,
// FALSE IF NO matches were found, TRUE if at least 1 match,
// or a user-entered main menu command.
// remember that ^A=1, same as TRUE (though ^A is currently trapped, not returned)
int callrollcall(string& tomatch)	// STRING TO MATCH, OR THE LOWERCASE WORD "ALL"
{
fi.Flush(TShouldDelete::NoDelete);  // start empty
HAM::sort = HAM::bycall;			// specify sort order
BOOL allflag = (tomatch == "all");  // TRUE = SELECT ALL,
									// FALSE = must MATCH SEARCH STRING.
int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++)
	if(allflag || db[i]->call.contains(tomatch)) 	// put CALLS matching STRING,
	{                                               // OR ALL, into subset array.
		if(filteron && (db[i]->ck > ' '))   // IF FILTER, OMIT THOSE CHECKED IN
			continue;
		fi.Add(db[i]);                      // OTHERWISE ADD TO ARRAY
	}
int fc = fi.GetItemsInContainer();			// for speed, often-used later
if(!fc)                        				// NO MATCHES FOUND
	return(FALSE);
							// falls through to here if any matches found.
int refresh;				// whether screen needs refreshing
int ch;          			// USER INPUT
int page = -1;   			// -1=FLAG: no PAGE SELECTED YET.  96 ENTRIES/page
i = 0;           			// i is now an index into fi[].  SET TO FIRST RECORD.
string memostart;			// string for starting memo with
helpline =
	"2468 7/1=t/b Goto Index Schedule Roster Edit Memo New <CR>=quit \"ACLOPTVX 5\"";
while(1)                    // ALL EXITS ARE IN SWITCH
{
						// The switch cases (later) change i, the index into fi[].
	if((i / 96) != page)// IF PAGE CHANGE IS NECESSARY for displaying the new fi[i],
	{                   // DO IT, AND DRAW the whole SCREEN FOR THAT PAGE.
						// K COUNTS THE (UP TO) 96 ENTRIES FOR THE PAGE
		page = i / 96;  // integer math: first page is page 0
		scr.clrscr();
		showhelpline();
		for(int k = page * 96 ; ((k < fc) && (k < (page * 96 + 96))) ; k++)
		{                                                       // 4 COLUMNS
			scr << setxy(((k % 96) / 24) * 20 + 1,((k % 96) % 24) + 1);
			displayrec(fi[k]);
		}
	}                                  // PUT CURSOR AT CURRENT RECORD
	scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
	switch(ch = ci())
	{
		case(HOME):                 // TOP OF PAGE ONLY
		case(CTLQ):
			i = page * 96;          // IF WE'RE ON THE PAGE, WE MUST HAVE
			break;                  // GOTTEN THERE LEGALLY ALREADY
		case(END):                  // BOTTOM OF PAGE ONLY
		case(CTLZ):
			i = min((page * 96 + 95),fc-1);       // PAGE END OR LIST END
			break;
		case(LEFT):                 // left arrow
		case(CTLA):
			if(i-24 < 0) break;     // BUT NOT BELOW ARRAY BASE
			i -= 24;                // PAGE CHANGE CAN OCCUR HERE
			break;
		case(RIGHT):                // RIGHT ARROW
		case(CTLF):
			if(i+24 >= fc) break;   // BUT NOT BEYOND ARRAY end
			i += 24;                // PAGE CHANGE CAN OCCUR HERE
			break;
		case(UP):                   // UP ARROW
		case(CTLE):
			if(--i < 0) i = fc-1;   // WRAP AROUND TO BOTTOM OF ARRAY
			break;                  // PAGE CHANGE CAN OCCUR HERE
		case(DOWN):                 // DOWN ARROW
		case(CTLX):
			if(++i >= fc) i = 0;    // WRAP AROUND TO TOP OF ARRAY
			break;                  // PAGE CHANGE CAN OCCUR HERE
		case(PGUP):  				// previous page (or array top)
		case(CTLR):
			i = max(i-96,0);
			break;
		case(PGDN):					// next page (or array end)
		case(CTLC):
			i = min(i+96,fc-1);
			break;
		case(CHOME):  				// array top
			i = 0;
			break;
		case(CEND):					// array end
			i = fc-1;
			break;
								// LEGAL CHARACTERS FOR CHECKING IN
		case 'l':               // DEFCH CAN BE ONE OF THESE
		case 'o':
		case 'p':
		case 'x':
			defch = ch;
		case 'a':               // DEFCH CANNOT BE ANY OF THESE
		case 'c':
		case 't':
		case 'v':
				// IT'S OK TO TEST .CK BECAUSE IT HASN'T BEEN CHANGED TO CH YET
			if(fi[i]->ck == ' ')            	// ONLY IF IT'S A NEW CHECKIN,
				fi[i]->ord = ckinord++;     	// INCREMENT COUNT
		case ' ':
			fi[i]->ck = ch;  			// FINALLY, WE CAN CHANGE .CK
		case('\t'):        				// TAB
		case(FIVE):     				// KEYPAD 5 CHECKS IN WITH DEFCH
				// MUST TEST BECAUSE IT MIGHT HAVE FALLEN THROUGH FROM ABOVE
			if((ch == FIVE) || (ch == 9))
			{
				fi[i]->ck = defch;             // CHECKIN WITH DEFCH
				fi[i]->ord = ckinord++;        // AND LOG CHECKIN ORDER
			}
			refresh = handletac(fi[i]);   		// PROCESS T, A, OR C
				// update this record for sure, because even if we return,
				// this record is usually left displayed, and should be accurate
			scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
			displayrec(fi[i]);                 	// update screen info and color

			if(extendedlog)					 // make a log entry of the checkin
				if(!strchr("tac",fi[i]->ck)) // t,a,c, already memoed in handletac
				{
					memostart = fi[i]->call + "  Ck: " + fi[i]->ck;
					memo(memostart,TRUE);
				}
			if(fc == 1)             // user SELECTED THE ONLY MATCH
				return(TRUE);		// so return for another search
			if(refresh)             // If any other record might be affected,
				return('e');        // return and come back to redraw whole page
			showhelpline(); 		// gets here if: fc > 1 AND no other record affected
			break;
		case 'm':                         		// M = CREATE A MEMO
			memo(memostart.remove(0),FALSE);
			showhelpline();
			break;
		case 'e':					// edit the selected record
			editrec(fi[i]->call);	// and return because changed info
			return('e');		    // makes current screen obsolete.
		case('\r'):           		// RETURN FROM FUNCTION - to main menu
			return(TRUE);
		case 'h':		// reminder, special: toggle <H>ide flag AND come back
		default:		// MAIN MENU COMMANDS, BUT ALSO MEANS MISTAKES CAUSE EXIT
// 			fi.Flush(TShouldDelete::NoDelete);  // saves memory, but wastes time
			return(ch);
	}					// END SWITCH
}						// END WHILE(1)
}     				  		  // callrollcall()
//----------------------------------------------------------------------------
// screen title for a city display: city name, traffic count, time
void citytitle()
{
char buf[10];         									// for time display
scr.clrscr();
scr << setclr(WHITE) << cities[cindex]->n;   			// CITY NAME HEADING
if(cities[cindex]->tcount)        						// SHOW TRAFFIC count
	scr << setbk(BLUE) << setclr(WHITE) << setxy(35,1)
		<< ((int)(cities[cindex]->tcount)) << " TRAFFIC" << setbk(BLACK);
scr << setclr(GREEN) << setxy(70,1) << _strtime(buf);   // show current time
}
//----------------------------------------------------------------------------
// DRAW PAGE WITH INFO FOR ONE CITY AND ALLOW CHECKINS FOR THAT CITY
// FEATURES NOT SHOWN IN HELP LINE: ACCEPTs CONTROL-CODE CURSOR MOVEMENT COMMANDS,
// and TAB CHECKS IN, FOR LEFTHANDED OPERATION
// RETURNS: int, WHICH CAN DETERMINE NEXT CITY TO SHOW, IF ANY,
//	OR BE A VALID MAIN MENU COMMAND
int cityrollcall()
{
string memostart;		// string for starting memo with

// some cases (below) return only to come right back (to redraw screen).
// When that is the case, previousindex ensures that the cursor is repositioned
// on the same record it was on.  It is always 0 (so cursor starts at db[0]),
// unless it was set to a different desired value before returning.
static int previousindex = 0;

fi.Flush(TShouldDelete::NoDelete);  	// start empty
HAM::sort = HAM::bycitycall;			// specify sort order
int count = db.GetItemsInContainer();
for(int i = 0 ; i < count ; i++)
	if(db[i]->city == cities[cindex]) 		// put CALLS for this city
	{                                   	// into subset array.
		if(filteron && (db[i]->ck > ' '))   // IF FILTER, OMIT THOSE CHECKED IN
			continue;
		fi.Add(db[i]);
	}

citytitle();						// page heading (title,traffic,time)

int ch;          					// USER INPUT
helpline = "NO ENTRIES: 3/9=+/- <CR>=quit Find Index Edit Memo New ==resume";
count = fi.GetItemsInContainer();	// # entries for this city (often-used later)
if(!count)   			// IF NO entries, CAN ONLY ADD, OR DECIDE HOW TO RETURN
	while(1)
	{
		showhelpline();
		switch(ch = ci())
		{
			case 'm':         	    	// M = CREATE A MEMO
				memo(memostart.remove(0),FALSE);
				break;
			case '=':       // RESUME WITH RESUMECITY
				return('g');// THIS WILL BECOME ALTCH IN MAIN
			case 'n':       // add new call to this city (or another) & returns here
			default:        // INCLUDES ALL VALID COMMANDS PROCESSED IN MAIN
				return(ch);
		}
	}
					// CITY HAS ENTRIES
helpline = "2468 3+9-7t1b CR=quit Find Index Edit Memo New ==resume \"ACLOPTVX 5\"";
int page = -1;   // FLAG THAT A PAGE HASN'T BEEN SELECTED YET.  92 ENTRIES/page
int refresh;
								// i is now reused as an index into fi[].
i = min(previousindex,count-1);	// not beyond end; editing can shrink city list
previousindex = 0;  // it is set to nonzero for "return and come right back"
while(1)
{
	if((i / 92) != page)    			// SEE IF PAGE CHANGE IS NECESSARY
	{                       			// IF SO, CHANGE PAGE AND REDRAW SCREEN
		citytitle();        			// page heading (title,traffic,time)
		showhelpline();
		page = i / 92;
		for(int k = page * 92 ; ((k < count) && (k < (page * 92 + 92))) ; k++)
		{
			scr << setxy(((k % 92) / 23) * 20 + 1,((k % 92) % 23) + 2);
			displayrec(fi[k]);
		}
	}                                 	// CURSOR TO CURRENT RECORD
	scr << setxy(((i % 92) / 23) * 20 + 1,((i % 92) % 23) + 2);
	switch(ch = ci())
	{
		case(HOME):                  	// GO TO TOP OF PAGE ONLY
		case(CTLQ):
			i = page * 92;
			break;
		case(END):                   	// GO TO BOTTOM OF PAGE
		case(CTLZ):
			i = min((page * 92 + 91),count-1);
			break;
		case(LEFT):                	  	// LEFT ARROW
		case(CTLA):
			if(i-23 < 0) break;     	// FELL BELOW BASE
			i -= 23;                 	// COLUMN ENTRIES 23 RECORDS APART
			break;                   	// PAGE CHANGE CAN OCCUR HERE
		case(RIGHT):                    // RIGHT ARROW
		case(CTLF):
			if(i+23 >= count) break;    // BEYOND END OF THIS CITY
			i += 23;                    // PAGE CHANGE CAN OCCUR HERE
			break;
		case(UP):                       // UP ARROW
		case(CTLE):
			if(--i < 0) i = count-1;    // WRAP AROUND TO ARRAY BOTTOM
			break;                      // PAGE CHANGE CAN OCCUR HERE
		case(DOWN):                     // DOWN ARROW
		case(CTLX):
			if(++i >= count) i = 0;     // WRAP BACK TO ARRAY TOP
			break;                      // PAGE CHANGE CAN OCCUR HERE
		case(CPGUP):                    // previous page (or array start)
			i = max(i-92,0);            // these are a little illogical, but 1 city
			break;                      // requiring > 1 page is very rare anyway.
		case(CPGDN):      				// next page (or array end)
			i = min(i+92,count-1);      // You can also get to next page by
			break;                      // just using arrow keys.
										// LEGAL CHECKIN CHARACTERS
		case 'l':
		case 'o':
		case 'p':
		case 'x':
			defch = ch;             // COPY TO DEFAULT
		case 'a':                   // DEFCH CANNOT BE ANY OF THESE
		case 'c':
		case 't':
		case 'v':
			if(fi[i]->ck == ' ')               // IF A NEW CHECKIN,
				fi[i]->ord = ckinord++;        // LOG PLACEMENT & INCREMENT
		case ' ':
			fi[i]->ck = ch;
		case('\t'):        			// TAB
		case(FIVE):     			// KEYPAD 5 CHECKS IN WITH DEFCH
							// MUST TEST BECAUSE IT MIGHT BE HERE FROM ABOVE
			if((ch == FIVE) || (ch == 9))
			{
				fi[i]->ck = defch;             // LOG IN WITH DEFAULT CHAR
				fi[i]->ord = ckinord++;        // AND INCREMENT COUNTER
			}
			refresh = handletac(fi[i]);     // PROCESS T, A, OR C
			if(extendedlog)					// make a log entry of the checkin
				if(!strchr("tac",fi[i]->ck))// t,a,c, already memoed in handletac
				{
					memostart = fi[i]->call + "  Ck: " + fi[i]->ck;
					memo(memostart,TRUE);
				}
			if(refresh)
			{                     // If any other record might be affected,
				previousindex = i;// return and come back.  Redrawing whole page
				return('e');      // updates both this record and contact, if any.
			}                     // otherwise, just update this record, for speed.
			scr << setxy(((i % 92) / 23) * 20 + 1,((i % 92) % 23) + 2);
			displayrec(fi[i]);
			showhelpline();
			break;
		case '=':                   // RETURN TO RESUMECITY
		case(F2):
		case 'g':
			return('g');            // THIS WILL BECOME ALTCH IN MAIN
		case 'm':                   // M = CREATE A MEMO
			memo(memostart.remove(0),FALSE);
			showhelpline();
			break;
		case 'e':					// edit the selected record and return
			editrec(fi[i]->call);   // because changed info makes the current
			previousindex = i;      // screen possibly obsolete.
			return('e');            // 'e' means come right back.

		// reminders that these are reserved: they're tested for upon returning
		case(PGUP):     // previous city
		case(CTLR):
		case(PGDN):		// next city
		case(CTLC):
		case(CHOME):	// first city
		case(CEND):		// last city
		case 'h':		// toggle <h>ide filter and come back here
		case 'n':       // adds new call to this city (or another) & returns here
		default:        // INCLUDES ALL VALID COMMANDS PROCESSED IN MAIN
			return(ch);
	}            		// END SWITCH
}                   	// END WHILE CHARACTERS ARE BEING ENTERED
}			                       // cityrollcall()
//----------------------------------------------------------------------------
// CITYMENU() LIST OF CITIES TO SELECT FROM
// RETURNS: CHAR(int) -- 'g' IF SHOULD GO TO DOCITY() UPON RETURN,
// OR RETURNS ANY CHAR ENTERED, WHICH CAN BE A MAIN MENU COMMAND
int citymenu()
{
int ch;             // USER INPUT
int page = -1;    	// FLAG: a PAGE HASN'T BEEN SELECTED YET. 96 ENTRIES/page
int i = cindex;     // index into cities[], start at CINDEX
int citycount = cities.GetItemsInContainer();
char firstletter;	// first letter of currently selected city
helpline = "TAB|5|<CR>|Goto  PgUp/Dn=NextLetter  OTHER=valid command or quit";
while(1)            // ALL EXITS ARE IN SWITCH
{
	if((i / 96) != page)    // IF PAGE CHANGE IS NECESSARY,
	{                       // DO IT, AND DRAW entire SCREEN FOR THAT PAGE
							// K COUNTS THE (UP TO) 96 ENTRIES FOR THE PAGE
		scr.clrscr();
		showhelpline();
		page = (i / 96);
		for(int k = page * 96 ; ((k < citycount) && (k < (page * 96 + 96))) ; k++)
		{                                                         // 4 COLUMNS
			scr << setxy(((k % 96) / 24) * 20 + 1,((k % 96) % 24) + 1);
			scr << setclr(cities[k]->tcount ? YELLOW : RED) << cities[k]->n;
		}
	}                             // PUT CURSOR AT CURRENT RECORD
	scr << setxy(((i % 96) / 24) * 20 + 1,((i % 96) % 24) + 1);
	switch(ch = ci())
	{
		case(HOME):               			// TOP OF PAGE ONLY
		case(CTLQ):
			i = page * 96;        			// IF WE'RE ON THE PAGE, WE MUST HAVE
			break;                			// GOTTEN THERE LEGALLY ALREADY
		case(END):                 			// BOTTOM OF PAGE ONLY
		case(CTLZ):
			i = min(page * 96 + 95,citycount-1);  // PAGE END OR LIST END
			break;
		case(LEFT):               			// LEFT ARROW
		case(CTLA):
			if(i-24 < 0) break;   			// BUT NOT BELOW ARRAY BASE
			i -= 24;
			break;
		case(RIGHT):              			// RIGHT ARROW
		case(CTLF):
			if(i+24 > citycount-1) break;   // BUT NOT BEYOND ARRAY TOP
			i += 24;
			break;
		case(UP):                       	// UP ARROW
		case(CTLE):
			if(--i < 0) i = citycount-1; 	// WRAP AROUND TO BOTTOM OF ARRAY
			break;
		case(DOWN):                     	// DOWN ARROW
		case(CTLX):
			if(++i > citycount-1) i = 0;	// WRAP AROUND TO TOP OF ARRAY
			break;
		case(PGUP):							// previous letter in alphabet
		case(CTLR):
			firstletter = cities[i]->n[0];	// save current 1st letter
			while((i > 0) && (cities[i]->n[0] == firstletter))
				i--;
			break;
		case(PGDN):                         // next letter in alphabet
		case(CTLC):
			firstletter = cities[i]->n[0];	// save current 1st letter
			while((i < citycount-1) && (cities[i]->n[0] == firstletter))
				i++;
			break;
		case '\t':                   		// TAB = SELECT & GO TO THIS CITY
		case '\r':							// CR = same
		case 'g':                           // Goto
		case(FIVE):                			// KEYPAD 5 = SAME
			cindex = i;
			return('g');
		default:
			return(ch);           		// ALLOW MAIN MENU COMMANDS
	}						// END SWITCH
}                     // END WHILE(1)
}					              // citymenu()
//----------------------------------------------------------------------------
// READ CALL SIGN database from FILE and create the CITIES[] ARRAY
void loaddb()
{
string citybuf, callbuf, namebuf;

ifstream infile("CALLS.DAT");
if(!infile)
	aborts("File CALLS.DAT not found.");
while(infile)                    	// LOAD DATA INTO DB
{                                   // file format: city,call,name\n
	getline(infile,citybuf,',');	// fields must not contain imbedded commas.
	getline(infile,callbuf,',');
	getline(infile,namebuf,'\n');
	if(infile.fail())
		aborts("File CALLS.DAT is corrupt.  Repair and rerun.");
	infile >> ws;

	if(validcallsign(callbuf))       // if valid and not already in db
		if(!findham(callbuf))
			db.Add(new HAM(callbuf,namebuf,findcity(citybuf)));
}
}									// loaddb()
//----------------------------------------------------------------------------
// WRITE CONTENTS OF DATABASE TO CALLS.DAT in city-sort order.
// Since db[] is in callsign order, it goes through the city list,
// then writes out all the call signs for that city.
void writedb()
{
unlink("CALLS.BAK");
rename("CALLS.DAT","CALLS.BAK");
ofstream outfile("CALLS.DAT");

for(int j = 0 ; j < cities.GetItemsInContainer() ; j++) // for each city,
	for(int i = 0 ; i < db.GetItemsInContainer() ; i++) // write its calls to disk
		if(db[i]->city == cities[j])                    // if it matches this city,
			if(db[i]->ck != 'v')                		// (EXCLUDE VISITORS)
				outfile << db[i]->city->n << "," << db[i]->call << ","
						<< to_upper(db[i]->name) << endl;
if(outfile.fail())
{
	cerr << "Possible data loss in CALLS.DAT." << endl;
	presskey();
}
}					       		// writedb
//----------------------------------------------------------------------------
// write the checkin log (record of who checked in) to disk file mm-dd-yy.log
// if extendedlog is on, that detailed log is kept in the (.MEM) memo file.
void writelog()
{
int checkins = 0, traffic = 0;	// SESSION TOTALS
string buf;
char obuf[80];
ostrstream os(obuf,sizeof(obuf));

today(buf);          			// GET DOS DATE
buf += ".LOG";              	// CREATE .LOG FILE USING TODAY'S DATE
ofstream outfile(buf.c_str());
for(int i = 0 ; i < db.GetItemsInContainer() ; i++)
	if(db[i]->ck > ' ')
	{
		checkins++;
		if(strchr("at",db[i]->ck))
			traffic++;               // traffic count not accurate
		outfile << (char)(toupper(db[i]->ck)) << "," << db[i]->call
				<< "," << db[i]->name << endl;
	}
if(outfile.fail())
{
	cerr << "Possible data loss in " << buf << endl;
	presskey();
}
										// create a memo with session totals
os << "Session totals:  Checkins: " << checkins << "  Traffic: " << traffic << ends;
buf = os.str();
memo(buf,TRUE);
}
//----------------------------------------------------------------------------
// load agenda text from disk.
void loadagenda()
{
ifstream infile("preamble.txt");
if(!infile)
	aborts("File PREAMBLE.TXT not found.");

string buf;
while(getline(infile,buf,'\n'))
	agenda.Add(buf);
}				                  	// loadagenda
//----------------------------------------------------------------------------
// print formatted roster to disk, for printing to paper.
void printroster()
{
ofstream outfile("CALLS.PRN");
for(int j = 0 ; j < cities.GetItemsInContainer() ; j++) // for each city
{
	outfile << "\n--> " << cities[j]->n << "\n\n";		// city heading
	for(int i = 0 ; i < db.GetItemsInContainer() ; i++) // write its calls to disk
		if(db[i]->city == cities[j])                    // if it matches this city,
			if(db[i]->ck != 'v')                		// (EXCLUDE VISITORS)
				outfile << "    " << setiosflags(ios::left) << setw(6)
						<< db[i]->call << " " << to_upper(db[i]->name) << endl;
}
}									// printroster()
//----------------------------------------------------------------------------
// DRAWHELP() DISPLAYS HELP SCREEN
void drawhelp()
{
char buf[9];                // HOLDS TIME
int checkins = 0;      		// SESSION TOTALS
int traffic = 0;
for(int i = 0 ; i < db.GetItemsInContainer() ; i++)
	if(db[i]->ck > ' ')
	{
		checkins++;
		if(strchr("at",db[i]->ck))
			traffic++;
	}
// BJKUYZ		// unused command letters
scr.clrscr();
scr << setclr(LIGHTGRAY);
scr << "Call Signs:  F   <F>ind CALL SIGN(S) by partial string match (F1)" << endl;
scr << "             E   <E>dit a record" << endl;
scr << "             N   <N>ew person for database" << endl;
scr << "             R   <R>oster (show entire call sign list)" << endl;
scr << endl;
scr << "Cities:      G   <G>oto (F2): " << setclr(GREEN) << cities[cindex]->n
	<< setclr(LIGHTGRAY) << endl;
scr << "             I   <I>ndex of cities (F6)" << endl;
scr << endl;
scr << "Utility:     S   <S>chedule for the net (agenda) (F4)" << endl;
scr << "             L,A <L>ist checkins in order or by <A>lpha" << endl;
scr << endl;
scr << "Memos:       <M>emo entry  <D>isplay memos  <W>rite to disk" << endl;
scr << endl;
scr << "Switches: <H>ide checkins:"
	<< setclr(GREEN) << (filteron?"YES":"NO") << setclr(LIGHTGRAY);
scr << " <^V>erify calls:"
	<< setclr(GREEN) << (verifycalls?"YES":"NO") << setclr(LIGHTGRAY);
scr << "  Extended <^L>og:"
	<< setclr(GREEN) << (extendedlog?"YES":"NO") << setclr(LIGHTGRAY) << endl;
scr << endl;
scr << "System:      P   <P>rint formatted roster to disk" << endl;
scr << "             F9  run COMMAND.COM (temporarily exit to DOS)" << endl;
scr << "             Q   <Q>uit program. " << endl;
scr << endl;
scr << "Checkin Codes: <X> Regular checkin  <space> Remove checkin" << endl;
scr << "               <A>nnouncement       <C>ontact  <L>ate  <O>ut  <P>ortable"
	<< endl;
scr << "               <T>raffic            <V>isitor (won't write to disk)  "
	<< endl;
scr << "               <TAB> or <Keypad5>   Check in with current default" << endl;
scr << setclr(LIGHTGRAY);
scr << setxy(40,25) << "Checkins: " << setclr(GREEN) << checkins << setclr(LIGHTGRAY);
scr << "  Traffic: " << setclr(GREEN) << traffic;
scr << setxy(70,25) << _strtime(buf) << setclr(LIGHTGRAY)
	<< setxy(1,25) << "Command: ";
}									// drawhelp()
//----------------------------------------------------------------------------
// MAIN()
int main()
{
int i;                  // COUNTER
int ch;		            // USER INPUT
int altch = 0;          // RETURNED BY FUNCTION TO INDICATE TO GO DIRECTLY
						// TO NEXT FUNCTION.  BYPASSES USER INPUT.
int agendastart = 0;    // line # with which to start display
char buf2[10]; 			// for TIME display
string buf;				// user input, filenames, etc.

// video display
scr.window(1,1,80,25);
scr.textmode(C80);
scr << setbk(BLACK) << setclr(LIGHTGRAY);
scr.clrscr();

string::set_paranoid_check(1);
loadagenda();       // load net agenda text
loaddb();           // LOAD CALLS.DAT, CREATE CITIES LIST
cindex = 0;         // re-point at 1st entry (in case calls.dat isn't in city order)
while(1)
{
	scr << setclr(LIGHTGRAY);		// prevents cursor in a "leftover" color
	if(altch)       	// IF A FUNCTION SET ALTCH, WE GO DIRECTLY TO THAT CASE,
	{                   // BYPASSING USER INPUT
		ch = altch;                 // TRANSFER TO CH
		altch = 0;                  // RESET ALTCH
	}
	else                            // NEED USER INPUT
	{
		drawhelp();
		ch = ci();
	}
	switch(ch)
	{
//----------------------------------------------------------------------------
// major searches: city, bycall, bycity

		case 'i':
		case(F6):                           // SELECT FROM CITY MENU
			altch = citymenu();             // MENU COMMANDS ALLOWED
			break;

		case(F1):
		case('f'):                  // CALL SIGN match
		case('r'):                  // ENTIRE roster
			buf.remove(0);			// empty buf means we need user input
			while(!altch)           // EXITS loop if USER ABORTS OR ALTCH GETS SET
			{
				if(!buf.length())  	// if not a repeat search from below
					if(ch == 'r')   // whole roster REQUIRES NO INPUT
						buf = "all";
					else
					{
						scr << setclr(WHITE) << setxy(1,25) << clreol
							<< "Find Call or <CR> to quit: ";
						if(!cgets(buf))    	// USER ABORTED
							break;
						buf.to_upper();
					}
				switch(ch = callrollcall(buf))
				{
					case(FALSE):        // returns FALSE if no matches
						addnewham(buf);	// no match: probably need to add a call,
						buf.remove(0);	// empty buf signals a new search
						break;

					case(TRUE):         // returns TRUE if at least 1 match
						buf.remove(0);	// empty buf signals a new search
						break;

					case 'h':       			// TOGGLE "hide" FILTER
						filteron = !filteron;   // and leave buf with contents
						break;                  // to repeat the previous search
					case 'e':					// a record was changed:
						break;					// go back to redo same search

										// ANYTHING ELSE IS A COMMAND
					default:        	// TO EXIT the while LOOP
						altch = ch;     // AND GO ELSEWHERE
						break;
				}
			}                      // END WHILE NOT ALTCH
			break;

		case 'g':                     // show page for 1 city
		case(F2):
				// && EVALS LEFT TO RIGHT. IF ALTCH, cityrollcall() NOT DONE
			while(!altch && ((ch = cityrollcall()) != '\r'))
			{
				switch(ch)
				{
					// any case that doesn't set cindex or set altch
					// will cause a return to the same city just processed

					case(PGDN):   		  			// KEYPAD 3 = GO TO NEXT CITY
					case(CTLC):
						cindex++;         			// TEST FOR OVER-RUN IS BELOW
						resumecity = cindex; 		// LAST CITY REACHED
						break;
					case(PGUP):          			// GO TO PREVIOUS CITY
					case(CTLR):
						cindex = max(cindex-1,0);   // BUT NOT BELOW 0
						break;
					case(CHOME):					// go to 1st city
						cindex = 0;
						break;
					case(CEND):						// go to last city
						cindex = cities.GetItemsInContainer()-1;
						break;
					case 'h':						// change filter
						filteron = !filteron;		// go back to same city
						break;
					case 'e':					 	// a record was edited,
						break;					 	// go back to display same city.
					case 'n':            		 	// ADD NEW CALL TO THIS CITY
						addnewham(buf.remove(0));	// it will THEN REDRAW THE CITY
						break;          			// AND GO BACK TO IT

					case 'g':           // char returned here when '=' is chosen
						cindex = resumecity;    // do cityrollcall(resumecity)
					default:                    // DEFAULT INCLUDES
						altch = ch;            	// MENU COMMANDS RETURNED
						break;
				}                             	// END SWITCH
				if(cindex >= cities.GetItemsInContainer()) // LAST CITY WAS DONE
				{                             	// AND WAS EXITED WITH PGDN
					resumecity = cindex = 0;  	// RESET POINTERS
					altch = 'f';              	// GO DO (find) LATE/MISSED
				}
			}              // END WHILE CH = DOCITY() AND NOT ALTCH
			break;
//----------------------------------------------------------------------------
// agenda display
		case(F4):                 	// show current 23 lines of NET AGENDA
		case 's':					// schedule
			scr.clrscr();
			scr << setclr(LIGHTGRAY);
			for(i = agendastart ; i < (agendastart+23) ; i++)
			{
				if(i >= agenda.GetItemsInContainer())	// past end of agenda
					break;
				scr << agenda[i] << endl;
			}                                               // display time
			scr << setclr(GREEN) << setxy(70,25) << _strtime(buf2) << "\r";
			scr << "PgUp/PgDn, Command, or <CR> for menu: ";
			altch = ci();              // ALLOW ALL MENU COMMANDS
			switch(altch)  	// change agenda page, loop again to display it
			{
				case(PGDN):                        // next 23 lines
					agendastart += 23;
					if(agendastart >= agenda.GetItemsInContainer())
						agendastart = 0;
					altch = 's';                   // go display them
					break;
				case(PGUP):                        // previous 23 lines
					agendastart = max(agendastart - 23,0);
					altch = 's';                   // go display them
					break;
			}
			break;
//----------------------------------------------------------------------------
// memo
		case 'm':                	    	// M = ENTER A MEMO
			memo(buf.remove(0),FALSE);
			break;

		case('d'):                        // DISPLAY MEMOS
			altch = showmemos();          // ALLOW MENU COMMANDS
			break;
//----------------------------------------------------------------------------
// add, edit, summarize
		// this is somewhat superfluous, because you must enter entire call.
		// It's easier to Find, select, and use 'e'dit
		case('e'):                       	// EDIT A RECORD
				scr << setclr(GREEN) << setxy(1,25) << clreol
					<< "<EDIT>" << setclr(WHITE) << " Find call or <CR> to quit: ";
				if(cgets(buf))
					editrec(buf);
				break;

		case 'n':                    			// ADD NEW PERSON TO DATABASE
			if(addnewham(buf.remove(0)) == 3)   // if already in database,
				altch = 'f';					// give opportunity to Find it.
			break;

		case 'l':                  		// L = LIST CHECKINS IN NUMERICAL ORDER
		case 'a':                  		// A = LIST CHECKINS IN ALPHA ORDER
			altch = listall(ch);    	// ALLOW MENU COMMANDS
			break;
//----------------------------------------------------------------------------
// utility
		case 'h': filteron = !filteron; break;			// TOGGLE hide checkins
		case(CTLV):	verifycalls = !verifycalls; break;	// toggle call sign verify
		case(CTLL):	extendedlog = !extendedlog; break;	// toggle extended logging
		case 'p': printroster(); break;		        	// print roster to disk
		case 'w': writememos(); break;     	// Could also writedb and writelog

		case(F9):                   // RUN COMMAND.COM
			scr.clrscr();           // This works even when in a DOS session
			system("command.com");	// under Windows, and is the only way to
			scr.clrscr();			// exit to DOS in that situation, (because
			break;					// DOS is already occupied running Net)

		case 'q':                  				// Q QUIT PROGRAM
			scr << setclr(WHITE) << setxy(1,25) << clreol
				<< "QUIT PROGRAM.  Press ESC to quit: ";
			if(getche() != 27)
			{
				scr << setxy(1,25) << clreol;
				break;
			}
			scr.clrscr();
			writelog();             // write checkin log to disk
			if(dbchanged)   		// write calls.dat to disk
				writedb();
			writememos();  			// write memos to disk
			return(0);

		default:
			break;
	}                  // END SWITCH
}                      // END WHILE CHARS ARE BEING ENTERED
}                      // END MAIN

LOGTALLY.CPP

/*	LOGTALLY.CPP			2-23-96
	This file is part of the NET project, but is an optional standalone program.
	Copyright (C)1994-1996 Steven Whitney.
	Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
	Initially published by http://25yearsofprogramming.com.

POSTS THE DATA FROM A DAILY .LOG FILE (generated by net.cpp) TO A YEARLY MASTER CHECKIN FILE
(that tabulates the year) in the format:
CALLSIGN,NAME,"(366 CHARACTERS, CORRESPONDING TO DAYS IN THE YEAR)"\n

DAILY .LOG FILE NAME MUST HAVE FORMAT MM-DD-YY.LOG.
OUTPUT FILE MUST HAVE FORMAT YYYY.LOG WHERE YYYY IS 4-DIGIT YEAR.
FILES DO NOT HAVE TO BE SORTED, AND ARE NOT SORTED BY PROGRAM.
All files must be in default directory, with this program.

FOR BATCH PROCESSING (to process all daily files at once):  FOR %F IN (*.LOG) DO LOGTALLY %F
The YYYY.LOG file won't be processed, because its name is the wrong length.

------
TO DO:

I haven't checked to make sure that the master file, after posting, contains
the same info that was in the daily logs!: call sign, checkin char

Assumes years are 19xx. Needs Y2K fix.

*/
#include <classlib\time.h>
#include <classlib\arrays.h>
#pragma hdrstop

#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"

//////////////////////////////////////////////////////////////////////////////
// global variables
struct HAM
{
	HAM() {}
	HAM(string& c, string& n, char cki) : call(c), name(n), ck(cki) {}
	BOOL operator == (const HAM& other) const { return(call == other.call); }
	BOOL operator < (const HAM& other) const { return(call < other.call); }

	string call;    	// was char[7]
	string name;		// was char[11]
	char ck;			// CHECKIN CHARACTER FOR THAT DAY
};
TISArrayAsVector<HAM> s(50,0,50);

//////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------
// load array s (data to be posted) from daily log file
void loaddaylog(const char* filename)
{
cout << "\nReading daily log...";
ifstream infile(filename);
if(!infile)
	aborts("Input file not found.");

string inck, incall, inname;
while(getline(infile,inck,','))
{
	getline(infile,incall,',');
	getline(infile,inname,'\n');
	if(infile.fail())
		aborts("Log file corrupt.  Repair and rerun.");
	infile >> ws;
	s.Add(new HAM(incall,inname,inck[0]));
}
}
//////////////////////////////////////////////////////////////////////////////
//	MAIN()
int main(int argc, char **argv)
{
int i;

cout << "Usage: LOGTALLY MM-DD-YY.LOG  (include .LOG extension)";

if(argc != 2)
	exit(1);

string::set_case_sensitive(0);
string::set_paranoid_check(1);

string day = argv[1];	  			// mm-dd-yy
if((day.length() != 12) || (!day.contains(".log")))	// also screens out YYYY.LOG
	exit(1);
day.remove(8);            			// strip extension
string year = "19";   				// 19XX
year += (argv[1]+6);                // add YY from mm-dd-yy
year.remove(4);                     // strip extension

TDate thisdate(atoi(argv[1]+3),atoi(argv[1]),atoi(year.c_str())+atoi(argv[1]+6));
int thisdatesindex = thisdate.Day()-1; // location within yrdata of char for this date

loaddaylog(argv[1]);				// READ DATA FROM DAILY .LOG FILE INTO ARRAY S
//----------------------------------------------------------------------------
// post the one-day information from array s to the master file
cout << "\nPosting to master file...";

ofstream outfile;
ifstream infile((year + ".log").c_str());
if(!infile)                              	// if master file doesn't exist,
{                                           // create it
	outfile.open((year + ".log").c_str());
	outfile.close();
	infile.open((year + ".log").c_str());
}
outfile.open((year + ".new").c_str());
string incall, inname, yrdata; 				// data read from file

// an empty tally array for calls that are new this session.
// can reuse, changing only the char for this log file's day.
string newyrdata(' ',366);				// 366 spaces

while(getline(infile,incall,',')) 	// GET RECORD FROM MASTER file
{
	getline(infile,inname,',');
	infile.ignore(1);				// opening quote
	getline(infile,yrdata,'\"');	// ending quote
	if(infile.fail())
		aborts("Master (year) file corrupt.  Repair and rerun.");
	infile >> ws;

	// See if any records in s should be written before this one, to keep sorted.
	// There's no i++ in loop: records can be deleted, compacting s.
	// It's always s[0] that gets written!
	for(i = 0 ; i < s.GetItemsInContainer() ; )
		if(s[i]->call < incall)				// precedes call we're looking for,
		{                                   // so insert before it...
			newyrdata[thisdatesindex] = s[i]->ck;
			outfile << s[i]->call << "," << s[i]->name << "," << "\""
					<< newyrdata << "\"" << endl;
			s.Destroy(i);   				// i remains the same
		}
		else
			if(s[i]->call == incall)				// matches, UPDATE MASTER RECORD
			{
				yrdata[thisdatesindex] = s[i]->ck;	// SET ck for this DATE
				s.Destroy(i);						// POSTED, no longer needed
				break;       						// i remains the same
			}
			else      	// s[i]->call > incall, so quit looking
				break;
								// WRITE (POSSIBLY UPDATED) RECORD TO NEW MASTER
	outfile << incall << "," << inname << "," << "\"" << yrdata << "\"" << endl;
}
// Write any remaining new records after last master file record in alphabet.
for(i = 0 ; i < s.GetItemsInContainer() ; i++)
{
	newyrdata[thisdatesindex] = s[i]->ck;
	outfile << s[i]->call << "," << s[i]->name << "," << "\""
			<< newyrdata << "\"" << endl;
}
if(!outfile)
	aborts("File write error.  Possible data loss.");
infile.close();
outfile.close();

//----------------------------------------------------------------------------

unlink((year + ".bak").c_str());							// DELETE .BAK IF PRESENT
rename((year + ".log").c_str(),(year + ".bak").c_str());	// .LOG TO .BAK
rename((year + ".new").c_str(),(year + ".log").c_str());	// .NEW TO .LOG

cout << "\nDone.\n";
return(0);
}

LOGSPRED.CPP

/*	LOGSPRED.CPP			2-23-96
	This file is part of the NET project, but is an optional standalone program.
	Copyright (C)1994-1996 Steven Whitney.
	Published under GNU GPL (General Public License) Version 2, with ABSOLUTELY NO WARRANTY.
	Initially published by http://25yearsofprogramming.com.

FORMATS THE DATA FROM A YEARLY SUMMARY .LOG FILE (YYYY.LOG, generated by logtally.cpp)
INTO A SPREADSHEET-TYPE FILE SUITABLE FOR PRINTING (YYYY.PRN). ONLY DATES FOR WHICH THERE ARE
ANY CHECKINS AT ALL WILL BE TABULATED SO THAT ENTIRE BLOCKS OF BLANKS WON'T BE OUTPUT.
PRINTING IT WILL REQUIRE YET ANOTHER PROGRAM TO PRODUCE MULTIPLE 80-COLUMN PAGES THAT CAN BE
TAPED TOGETHER.

SAMPLE OUTPUT:
Summary checkin log for 1993:
Day of Week:		SMTWtFs		(THURSDAY AND SATURDAY TO BE LOWER CASE)
MONTH:				0000000		READING DOWN: 01=JANUARY
					1111111
DAY:				0000000		READING DOWN: JANUARY 01,02,03,ETC.
					1234567

CALL    NAME    	XLAOTCX  	Letters encode the method that person used to check in each day.
CALL    NAME    	LLXXOXP		Ex.: Late, Late, Normal, Normal, In/Out, Normal, Portable...

------
TO DO:

I haven't checked to make sure that the PRN file contains
the same info that was in the YYYY.LOG file: call signs, checkin char

The first conversion (from the original C version), for unknown reasons, wrote day of week
headings for the entire year, and also wrote varying-length (and wrong) lines to the
file for the (call sign/checkin info) line.  The methods have been completely
changed, but there remains the possibility (or does there?) that
the file-write routines can be confused by very long output lines.
On the other hand, the master file ALWAYS contains entire-year info for
each call sign, and it works ok so far.
Delete this note after it hasn't happened for a while.

Check for any Y2K problems.

*/
#include <classlib\time.h>
#pragma hdrstop

#include "c:\bcs\my.h"
#include "c:\bcs\mylib.cpp"

//////////////////////////////////////////////////////////////////////////////
int main(int argc, char **argv)
{
int i, j;
string infilename;

cout << "Use: LOGSPRED YYYY.LOG where yyyy.log is a yearly log file";

if(argc < 2)
	infilename = "1996.log";    // for testing
else
	infilename = argv[1];

string daylets("MTWtFsS");  // first letters of day names
int daycount[366];			// daily checkin totals
for(i = 0 ; i < 366 ; i++)
	daycount[i] = 0;     	// and initialize all counts to 0

//----------------------------------------------------------------------------
// read file once through to LEARN WHICH DAYS HAVE CHECKINS and get day totals
ifstream infile(infilename.c_str());
if(!infile)
	aborts("Input file not found.");
string call, name, yrdata;              // DATA FROM 19XX.LOG FILE
while(getline(infile,call,','))
{
	getline(infile,name,',');
	infile.ignore(1);
	getline(infile,yrdata,'\"');
	if(infile.fail())
		aborts("Input file corrupt.  Repair and rerun.");
	infile >> ws;

	for(i = 0 ; i < 366 ; i++)		// IF THERE WAS A CHECKIN THIS DAY,
		if(yrdata[i] > ' ')			// MARK IT AS A DAY TO REPORT ON
			daycount[i]++;			// and increment the checkin count this day
}
infile.close();
												// set up OUTPUT FILE
string year(infilename.c_str());				// 19XX.LOG
year.remove(4);									// remove extension
ofstream outfile((year + ".prn").c_str());  	// output is 19XX.PRN
outfile << "Summary checkin log for " << year << endl;

//----------------------------------------------------------------------------
// day headings: SMTWtFs
outfile << "Day of Week:        ";	// 20 SPACES ALLOWs FOR CALL + NAME
for(i = 0 ; i < 366 ; i++)
	if(daycount[i])
	{
		TDate thisdate(i+1,atoi(year.c_str()));
		outfile << daylets[thisdate.WeekDay()-1];
	}
outfile << endl;

//----------------------------------------------------------------------------
// create MMDDYY DATES, read vertically.  That is, after the 4 lines
string dd[4];   	// are printed, you can read the dates vertically down.
for(i = 0 ; i < 366 ; i++)      // julian dates
	if(daycount[i])				// IF THE DAY HAS ENTRIES
	{
		char buf[10];
		TDate thisdate(i+1,atoi(year.c_str()));		// create a date for it
		ostrstream os(buf,sizeof(buf));             // format it as MMDD
		os	<< setw(2) << setiosflags(ios::right) << setfill('0') << thisdate.Month()
			<< setw(2) << setiosflags(ios::right) << setfill('0') << thisdate.DayOfMonth()
			<< ends;
		for(j = 0 ; j < 4 ; j++)     	// 1st char goes to dd[0],
			dd[j] += buf[j];            // 2nd char goes to dd[1], etc.
	}
// output the vertical date headings
for(i = 0 ; i < 4 ; i++)				// headings require 4 lines
{
	switch(i)
	{
		// skip over space reserved for call+name, also inserting descriptive text
		case 0:  outfile << "Month: M            "; break;
		case 1:  outfile << "       M            "; break;
		case 2:  outfile << "Day:   D            "; break;
		case 3:  outfile << "       D            "; break;
	}
	outfile << dd[i] << endl;
}
outfile << endl;

//----------------------------------------------------------------------------
// PRINT DATA FROM THE .LOG FILE TO LINES OF TEXT
infile.open(infilename.c_str());		// NO TEST: ALREADY FOUND IT ONCE
while(getline(infile,call,','))         // ALREADY READ IT, TOO
{
	getline(infile,name,',');
	infile.ignore(1);             	// opening quote
	getline(infile,yrdata,'\"');
	infile >> ws;

	// substr() below limits name to field width.  setw() sets minimum width only.
	outfile << setw(6) << setiosflags(ios::left) << call << " "
			<< setw(12) << setiosflags(ios::left) << name.substr(0,12) << " ";

	for(i = 0 ; i < 366 ; i++)			// IF THIS IS A DAY WE'RE REPORTING ON,
		if(daycount[i])
			outfile << yrdata[i];       // OUTPUT THE CHECKIN CHAR

	outfile << endl;
}
outfile << endl;						// spacer line

//----------------------------------------------------------------------------
// create day total lines, for printing vertically
for(i = 0 ; i < 4 ; i++) 		// blank vertical date headings for reuse
	dd[i].remove(0);
for(i = 0 ; i < 366 ; i++)
	if(daycount[i])		// IF THE DAY HAS ENTRIES
	{
		char buf[10];
		ostrstream os(buf,sizeof(buf));             // format it as MMDD
		os	<< setw(4) << setiosflags(ios::right) << setfill(' ') << daycount[i] << ends;
		for(j = 0 ; j < 4 ; j++)     	// 1st digit goes to dd[0],
			dd[j] += buf[j];            // 2nd digit goes to dd[1], etc.
	}
// vertical print the totals
for(i = 0 ; i < 4 ; i++)
{
	switch(i)
	{
		// skip over space reserved for call+name, also inserting descriptive text
		case 0:  outfile << "Counts:     #1      "; break;
		case 1:  outfile << "(Read Down) #2      "; break;
		case 2:  outfile << "            #3      "; break;
		case 3:  outfile << "            #4      "; break;
	}
	outfile << dd[i] << endl;
}
//----------------------------------------------------------------------------

if(outfile.fail())
	cout << "File write error.  Possible data loss.";
return(0);
}									// END MAIN

 

 

Valid HTML 4.01 Transitional Valid CSS
View content labeling at ICRA.
Copyright ©2008 Steven Whitney. Last modified 02/27/2008.