/*  ioopen_routines			WJS  Mar 96			*/
/*    Contains externally referenced routines:				*/
/*	Logical configure_output(diag_optfile,output_info,files)	*/
/*	int create_wjstbl(optfile,wjstbl,maxlen_tbl)			*/
/*	Logical get_datafield_options(datafield_optfile,		*/
/*		consec_seps,embed_seps,trim)				*/
/*	void get_files(files,indirect_name,indirect_file,inner_param_list) */
/*	int get_idmissing (idmissing_optfile,missings_by_var,		*/
/*						missings_for_all)	*/
/*	Logical get_latlonparams(latlon_optfile,outlat,outlon,inlat,outlat) */
/*	Logical get_timedateparams(timedate_optfile,outtime_structarray, */
/*		intime_structarray,fragbuflist)				*/
/*	char get_comment_sources(datacomments_optfile,			*/
/*				 comment_filearray,			*/
/*				 varlist_optfile,			*/
/*				 level0_datafile)			*/
/*	void init_outinfo (outinfo_struct)				*/
/*	void init_file_structs (files,indirect_file)			*/
/*	void init_latlon_structs 					*/
/*		(outlonformat,inlonformat,outlatformat,inlatformat)	*/
/*	void init_time_structs (in,out,buflist,cumtime,fractime)	*/
/*	Logical open_and_log_opt (file)					*/
/*	Logical remove_vars(removals_file,ntotal,list_after_removals,ncnt) */
/*    all called by ioopen_ (& only by ioopen_)				*/
/*									*/
/*	Some notes on parsing the various keyword strings:  the check-	*/
/*    ing is not foolproof, and not consistently applied, especially	*/
/*    in cases of potentially overlapping strings (eg, suppose there	*/
/*    is some future "translation" keyword-how does this interact with	*/
/*    "pre-translation-" prefix.  The main unrequired rule that keeps	*/
/*    things working is that the separator characters like - and _ do	*/
/*    not appear in the middle of significant strings... except for	*/
/*    the hyphen in pre-translation-!  Sigh...				*/

#define IOOPEN_ROUTINES_VERSION "ioopen_routines  version 3.1  5 Jun 2013"
/*   5 Jun 13  v 3.1 		WJS					*/
/*	coordinatedmissingdata
/*   4 Jun 13  v 3.1 		WJS					*/
/*	identifymissingdata						*/
/*	Noted general problem of comparisons of first part of strings	*/
/*    being treated as representative of whole string.  Did some work	*/
/*    on those.	 Seems to be a 15 yr-old problem			*/
/*	Noted problem of exact comparisons being done by plunking a	*/
/*    null into a string that is reused.  Should fix that (15 yrs old)	*/
/*    Hmm, most of the places I looked at unplunk the null w/the char	*/
/*    displaced by original plunking.  Maybe I wasn't so dumb after all	*/
/*	Rewrote some wjstbl routines.  Here for now - need to go to	*/
/*    utils for real							*/
/*		[Needs defgb.h 5.6]					*/
/*		[Begin v 3.1]						*/

/*   8 Oct 12  v 3.0e 		WJS					*/
/*	ISO 8601 time output						*/
/*	Bug fix: yearmonthday output did not diagnose situation where	*/
/*	  input did not provide year or month or day			*/
/*	Some commented-out sections relating to possible future		*/
/*	  generated data capability					*/
/*		[Needs defgb.h 5.5b]					*/
/*		[Begin v 3.0e]						*/
/*   7 Feb 11  v 3.0d 		WJS					*/
/*	Bug fix: test for non-level0-file, non-varlist datacomments	*/
/*	  source did not check for null pointer				*/
/*		[Begin v 3.0d]						*/
/*  30 May 09  v 3.0c 		WJS					*/
/*	Change "closer to level 0" diagnostics to reflect situation	*/
/*	  where problem is actually a missing datum rather than a	*/
/*	  "farther" datum						*/
/*		[Begin v 3.0c]						*/
/*   6 Nov 08  v 3.0b 		WJS					*/
/*	strip_*_blanks stuff						*/
/*		[Begin v 3.0b]						*/
/*  22 May 08  v 3.0a 		WJS					*/
/*	Have get_comment_sources return source format if known		*/
/*		[Needs defgb.h 5.3]					*/
/*		[Begin v 3.0a]						*/
/*  29 Feb 08  v 3.0 		WJS					*/
/*	Use is_a_remote_object instead of just initial //		*/
/*		[Begin v 3.0]						*/
/*									*/
/*  [1.0 -> 2.9  comments in defgb_revision.doc.  5 Oct 12]		*/

#include "defgb.h"
#ifdef VMS
  /*	For some reason, next statement started causing trouble		*/
  /*    Defn should be in sys/types.h on a unix box.  Some google	*/
  /*	article says it's in unistd.h on VMS.  core.h includes the	*/
  /*    latter, and defgb.h includes core.h (eventually).  We never	*/
  /*	use the value returned by waitpid, so see if we can fake things	*/
/*  pid_t waitpid();	 ? in wait.h if vms version > 7 ?		*/
  int waitpid();	/* ? in wait.h if vms version > 7 ?		*/
#endif

struct split_wjstbl {
  int n;		/* Number of tables after split			*/
  int *tag;		/* User-supplied tag for each table (might	*/
			/*   want to go char string some time)		*/
  int *count;		/* Number items in each table			*/
  char **wjstbls;	/* Result - wjstbl[n][max_table_size]		*/
};


  /*  Uses no global variables; defines only these...			*/
char *version_ioopen_routines = IOOPEN_ROUTINES_VERSION;
  /*  Next array global to IOO for convenience with respect to err msgs	*/
  /*  Also, several routines use a buffer of this size for this purpose	*/
  /*  so this saves alloc/dealloc					*/
static char varname[MAXVARNAMESIZE+1];

  /*  Requires following routines:					*/
void add_to_comment_stream();
void analy_source();
char *buildstring();	/* Dynamic string concatenator			*/
char *copy_into_fixed_length_buffer();
void err();
void errn();
					/* Logical extract_wjstbl(); */
char *get_int_from_keyword();
int get_int_from_string();
Logical get_logical_from_string();
void ioname__();
int iovarlevel__();
int is_a_remote_object();
Logical legal_varnum();
Logical legal_level();
char *lengthen_str();
int lookup_lev0varlist();
					/* char *lookup_wjstbl(); */
int getrec_proccomment();
char *nxttok();
int strcasecmp_wjs();
char *strdupl();	/* Gets mem for copy of string & copies it	*/
int transvar();

#if READ_COMMANDS
char *startchild();
#endif

/*******************					*****************/

  /*  Next 2 routines belong in utils					*/
void err_non_null_terminated_arg2 (arg1,arg2,len_arg2)
  /*  Assumes we can write into arg2					*/
char *arg1,*arg2;
int len_arg2;
{
  *(arg2+len_arg2) = '\0';
  err (arg1,arg2);
}

Logical match_non_null_terminated_arg2 (arg1,arg2,len_arg2)
char *arg1,*arg2;
int len_arg2;
{
  return    (  strlen(arg1) == len_arg2  )
			&&
	    (  strncmp(arg1,arg2,len_arg2) == 0  );		
}

  /*  The next rewrite of the wjstbl functions belong in utils.c.	*/
  /*  It is here, static, to						*/
  /*  decouple the utils and defgb releases.  When utils is updated,	*/
  /*  remove function here and add function name to the doc above	*/

static char *lookup_wjstbl_w_len (s,wjstbl,value_len)
char s[],wjstbl[];
int *value_len;
/*	Looks up string s in a "wjs table".  If found, return a pointer	*/
/*	to the "value" corresponding to the key; else return NULL	*/
/*	If found, return the length of the value as well		*/
/*	A "wjs table" is a string consisting of a number of objects.	*/
/*	Each object is of the form					*/
/*		separator_character,					*/
/*		key_string,						*/
/*		separator_character,					*/
/*		value_string,						*/
/*		white_space_character(s)				*/
/*	The 2 separator chars must be the same, and must not be white	*/
/*	space characters or characters found in any key or value.  	*/
/*	There may be multiple blanks					*/
{
  char *found,*ptr,*start_value,*end_ptr;
  int key_len;

  if (wjstbl == NULL) err("lookup_wjstbl received null wjstbl","");
  if (wjstbl[0] == '\0') return NULL;

  key_len = strlen(s);
  ptr = (char *)malloc(key_len + 3);
  if (ptr == NULL) 
    errn ("Could not allocate mem for copy of key in lookup_wjstbl. Nbytes=",
	   key_len+3);

    /*  Build string consisting of input string surrounded by 		*/
    /*  separator characters						*/
  ptr[0] = WJSTBL_SEPARATOR;
  strcpy(ptr+1,s);
  ptr[key_len+1] = WJSTBL_SEPARATOR;
  ptr[key_len+2] = '\0';

  found=strstr(wjstbl,ptr);
  free(ptr);

  *value_len = 0;
  if (found == NULL) return NULL;

  ptr = found+key_len+2;
  end_ptr = strchr(ptr,WJSTBL_WHITE_SPACE);
    /*  Next is really an error in the wjstbl construction, but there	*/
    /*  are no semantics to return this.  The returned length of 0 can	*/
    /*  be used ... just have to change all the code everywhere		*/
  if (end_ptr == NULL) return NULL;

  *value_len = end_ptr - ptr;
  return ptr;
}

static char *lookup_wjstbl (s,wjstbl)
char s[],wjstbl[];
{
  int len;
  return lookup_wjstbl_w_len (s,wjstbl,&len);
}

static Logical extract_wjstbl_w_len
	(synonym,max_len_synonym,variable,wjstbl,actual_len_synonym,dyn_synonym)
char **dyn_synonym;
char *variable,*synonym,*wjstbl;
int max_len_synonym;
int *actual_len_synonym;
/*  Function returns NOT_VALID if string-to-be-returned will not fit	*/
/*    into available buffer space.  Otherwise returns TRUE/FALSE based	*/
/*    on whether synonym found/not found				*/
/*  max_len_synonym is 2 switches as well as potentially a size		*/
/*    It determines if the return information should be to the user-	*/
/*    supplied buffer or to a dynamically allocated buffer.		*/
/*    It also determines if anything should be returned if "the lookup"	*/
/*    fails								*/
/*  max_len_synonym							*/
/*     0	dynamically allocate synonym buffer and return it to	*/
/*		*dyn_synonym argument.  Do not alter synonym buffer	*/
/*		If there is no synonym, put copy of variable in buffer	*/
/*		In case of allocation failure, return attempted allo-	*/
/*		cation size in *actual_len_synonym			*/
/*    !0	Size of synonym buffer is abs val of max_len_synonym	*/
/*    >0	If there is no synonym, put copy of variable in synonym	*/
/*    <0	If there is no synonym, do not alter synonym		*/
{
  char *trans_loc,*synonym_ptr,white_space;
  int i,trans_len,len;
  char *return_buffer;

  trans_loc=lookup_wjstbl_w_len (variable,wjstbl,&trans_len);

    /*  Next if block: lookup failed - variable not in wjstbl		*/
  if (trans_loc == NULL) {
      /*  Returned synonym length will be length of variable, since	*/
      /*  we will generally make a copy of that.  It will also be the	*/
      /*  "can't fit" size.  In the dynamic case, increment it to get	*/
      /*  "tried to allocate" size					*/
    *actual_len_synonym = len = strlen(variable);
    if (max_len_synonym == 0) {
      return_buffer = (char *)malloc(len+1);
      if (return_buffer == NULL) {
	(*actual_len_synonym)++;
	return NOT_VALID;
      }
      strcpy (return_buffer,variable);
      *dyn_synonym = return_buffer;
    } else {
        /*  Next takes into account use of sign of max_len_synonym as a	*/
        /*  "copy the key if lookup fails" flag				*/
      if (len > max_len_synonym) return NOT_VALID;
        /*  Next test is just for efficiency - no need to copy if	*/
        /*  intent was to replace key w/value anyway			*/
      if (variable != synonym) strcpy (synonym,variable);
    }
    return FALSE;
  }

    /*  lookup succeeded.  Validate buffer size				*/
      /*  Returned synonym length will be the val from lookup_wjstbl,	*/
      /*  since that's the string we intend to return.  It will also	*/
      /*  be the "can't fit" size.  In the dynamic case, increment	*/
      /*  it to get "tried to allocate" size				*/
  *actual_len_synonym = trans_len;
  if (max_len_synonym == 0) {
    return_buffer = (char *)malloc(trans_len+1);
    if (return_buffer == NULL) {
      (*actual_len_synonym)++;
      return NOT_VALID;
    }
  } else {
    len = (max_len_synonym > 0) ? max_len_synonym  :  -max_len_synonym;
      /*  trans_len is length of string; max_len* length of buffer -	*/
      /*  hence the foolery to account for the terminating \0		*/
    if (trans_len >= len) return NOT_VALID;
    return_buffer = synonym;
  }

    /*  lookup succeeded & buffer size good.  Transfer info back to	*/
    /*  caller								*/
  strncpy (return_buffer,trans_loc,trans_len);
  *(return_buffer + trans_len) = '\0';
  if (max_len_synonym == 0)  *dyn_synonym = return_buffer;
  return TRUE;
}

static Logical extract_wjstbl(synonym,max_len_synonym,variable,wjstbl)
char *variable,*synonym,*wjstbl;
int max_len_synonym;
{
  int actual_len;
  char *dyn_buffer;
  return extract_wjstbl_w_len
	(synonym,max_len_synonym,variable,wjstbl,&actual_len,&dyn_buffer);
}

static Logical extract_wjstbl_dyn(synonym,len_synonym,variable,wjstbl)
char *variable,*wjstbl;
char **synonym;
int *len_synonym;
{
  return extract_wjstbl_w_len(NULL,0,variable,wjstbl,len_synonym,synonym);
}

static Logical match_wjstbl_value_n(match_value,wjstbl_value,wjstbl_value_len)
char *match_value,*wjstbl_value;
int wjstbl_value_len;
/*  wjstbl_value is pointer into wjstbl returned by lookup_wjstbl	*/
/*  wjstbl_value_len is # chars to the wjstbl whitespace char which	*/
/*    terminates wjstbl_value.  This is returned by lookup_wjstbl_w_len	*/
/*    If you don't have this, you can use match_wjstbl_value (less	*/
/*    efficient, and depends on "wjstbl internals"			*/
/*    Routine also works if *value is normal null-terminated string and	*/
/*    *len is strlen of that string					*/
/*  Returns TRUE if match_value matches wjstbl_value			*/
/*	    FALSE if no match						*/
{
  return match_non_null_terminated_arg2
			(match_value,wjstbl_value,wjstbl_value_len);
}

static Logical match_wjstbl_value(match_value,wjstbl_value,wjstbl_whitespace)
char *match_value,*wjstbl_value;
char wjstbl_whitespace;
/*	THIS ROUTINE MAY NOT BE USED ANYPLACE & hence it's untested	*/
/*  wjstbl_value is pointer into wjstbl returned by lookup_wjstbl	*/
/*  wjstbl_whitespace is the value-terminating character in the wjstbl	*/
/*    being used.  The last char of a wjstbl is always this whitespace	*/
/*    if you know the # chars to the wjstbl whitespace char which	*/
/*    terminates wjstbl_value (returned by lookup_wjstbl_w_len),	*/
/*    can use match_wjstbl_value_n (cleaner & faster)			*/
/*  Returns TRUE if match_value matches wjstbl_value			*/
/*	    FALSE if no match						*/
{
  char *match_ptr,*wjstbl_ptr;

  match_ptr = match_value;
  wjstbl_ptr = wjstbl_value;
  while (*match_ptr != '\0') {
    if (*wjstbl_ptr == wjstbl_whitespace) return FALSE;    
    if (*wjstbl_ptr != *match_ptr) return FALSE;
    match_ptr++;
    wjstbl_ptr++;
  }
  return (*wjstbl_ptr == wjstbl_whitespace);
}

/***************  end wjstbl routine rewrite		*****************/

char *get_string_from_escaped_string (string,errmsg1,errmsg2)
char *string,*errmsg1,*errmsg2;
/*  Turns string of form ab\t,\n to equivalent 5 character string	*/
/*  a, b, tab, comma, newline						*/
/*  Creates output string and returns pointer to it; NULL if trouble	*/
/*    (although I don't think it will return NULL - it seems to err() )	*/
{
  char *errtmp1,*errtmp2;
  char *outstr,*inptr,*outptr;
  Logical backslash_zero = FALSE;

  strdupl(&outstr,string,"get_string_from_escaped_string");
  inptr = outstr;
  outptr = outstr;
  while (  (*outptr = *(inptr++)) != '\0'  )  {
    if (*outptr == '\\')
      switch (*(inptr++)) {
	case 't':
	  *outptr = '\t';
	  break;	    
	case 'r':
	  *outptr = '\r';
	  break;
	case 'v':
	  *outptr = '\v';
	  break;
	case 'n':
	  *outptr = '\n';
	  break;
	case 'b':
	  *outptr = '\b';
	  break;
	case 'f':
	  *outptr = '\f';
	  break;
	case '0':
	  backslash_zero = TRUE;
	  *outptr = '\0';
	  break;
	case '\\':
	  *outptr = '\\';
	  break;
	case '\'':
	  *outptr = '\'';
	  break;
	case '\"':
	  *outptr = '\"';
	  break;
	case '\0':
	  inptr--; /*  Make sure we don't go beyond end. Fall thru to err */
	default:
	  *inptr = '\0';	/*  Set up to terminate loop	*/
	  *outstr = '\0';	/*  Set error indicator		*/
	  break;
      }
    if ((*outstr == '\0') && (! backslash_zero)) {
      free (outstr);
      outstr = NULL;
      break;
    } else outptr++;
  }	  

  if (outstr == NULL) {
      /*  Unterminated or illegal \ sequence				*/
    errtmp1 = buildstring("Illegal escaped string ->",string,"<-\n   ",
	" get_string_from_escaped_string");
    errtmp2 = buildstring(errmsg1,errmsg2,NULL," get_string_from_escaped_string");
    err (errtmp1,errtmp2);
    free (errtmp1);
    free (errtmp2);
  }

  if (backslash_zero && (outptr != outstr + 1)) {
    errtmp1 = buildstring("\\0 mixed into escaped string ->",string,"<-\n   ",
	" get_string_from_escaped_string");
    errtmp2 = buildstring(errmsg1,errmsg2,NULL," get_string_from_escaped_string");
    err (errtmp1,errtmp2);
    free (errtmp1);
    free (errtmp2);
  }

  return outstr;
}



void check_levs(frag_lev,frag_desc,component_lev,component_desc,plural)
int frag_lev,component_lev;
char *frag_desc,*component_desc,*plural;
/*  Inputs needed to generate an output time must occur on the same	*/
/*  level as the output time or on earlier levels.  If this is not the	*/
/*  case, generate an error.  Of course, said inputs must actually	*/
/*  be in the data set - diagnose that, too				*/
/*    frag is what we're generating; = output				*/
/*    component is what goes into it; = input				*/
{
  char *s,*errmsg_pt1,*errmsg_pt2;

  if (frag_lev >= component_lev) return;	/*  Things are OK	*/

    /*  A level of MAXLEVELS means that var isn't in object		*/
  s = (component_lev == MAXLEVELS) ? " " : "closer-to-level-0 ";
  errmsg_pt1 = buildstring(frag_desc," output require",plural);
  errmsg_pt2 = buildstring(s,component_desc," information");
  if ((errmsg_pt1 == NULL) || (errmsg_pt2 == NULL))
    err("Problem w/timedate output and its input",
	"Further problem getting memory to describe issue better. Sorry");
  err (errmsg_pt1,errmsg_pt2);
  return;			/*  Not that we should get here		*/
}



Logical open_and_log_opt (file)
  /*  Checks validity of optional input data.  Input is a pointer to 	*/
  /*  a file_info structure whose validity is to be checked.  Logging 	*/
  /*  is done.  The file is opened, if appropriate.  Return is TRUE or	*/
  /*  FALSE depending on whether valid data exists			*/
struct fileinfo *file;
{
  char *source;
  char *msg,*ptr;
  char *brack_method_name;
  int i;

  source=file->source;
  if (file->force_flag > 0 && source == NULL) 
    err (file->descrip," required option missing");
  if (file->force_flag < 0 && source != NULL) 
    err (file->descrip," option provided and it must not be");

  if (source == NULL) return FALSE;

    /*  buildstring used in next line to get space & properly position	*/
    /*  METHODNAME.  Then we need to put blanks & delims in place	*/
  brack_method_name = buildstring(TAG_DELIM,METHOD_NAME," ",
    " bracketing method name");
  *brack_method_name = ' ';
  *(brack_method_name+1) = *TAG_DELIM;
  *(brack_method_name+strlen(brack_method_name)-1) = *(TAG_DELIM+1);
  ptr=strchr(brack_method_name,OMIT_FROM_TAG_CHAR);
  if (ptr != NULL) {
      /*  Replace last blank preceding OMIT_FROM_TAG_CHAR with },null	*/
      /*    Find last nonblank						*/
    while (*(--ptr) == ' ')
      ;
      /*    Advance beyond it and put in tag termination		*/
    *(++ptr) == '}';
    *(++ptr) == '\0';
  }

  errno = 0;	/* Be sure errno refers to opens we do here...		*/

  switch (file->source_type)  {
    case INDIRECT_FILE_LINE:
      msg=buildstring(file->descrip," data: contained in indirect input file",
		      NULL,    " have_opt_ msg");
      file->open=FALSE;
      break;

    case COMMAND_FILE:
      msg=buildstring(file->descrip, " data: comes from script ",
		      source,     " have_opt_ msg");
#if READ_COMMANDS
      ptr = startchild(file->source, NULL, &file->stream);
	/*  Not sure if bad startchild result would always give		*/
	/*  bad return status						*/
      if (  (file->stream == NULL) && (ptr == NULL)  )  ptr = "Reason unknown";
      if (ptr != NULL) {
	msg=lengthen_str("Error opening ",
			 file->descrip,
			 " script ",
			 300,
			 " have_opt_ msg");
	msg=lengthen_str(msg,
			 source,
			 ": \n	",
			 300,
			 " have_opt_ msg");
        err (msg,ptr);
      }

      file->open=TRUE;
#else
      err(msg,"\n\tbut method not compiled with script-reading capability");
#endif
      break;

    case DATA_FILE:
      file->stream=fopen(source,"r");
      if (file->stream == NULL) {
	msg = lengthen_str("Error opening ",file->descrip," file ",300,
			   " have_opt_ msg");
	msg = lengthen_str(msg,source,"\nProblem: ",300,
			   " have_opt_ msg");
        err (msg,strerror(errno));
      }
      msg=buildstring(file->descrip, " file: ",
		      source,     " have_opt_ msg");

      file->open=TRUE;
      break;

    default:
      msg=buildstring(
	    "Illegal source type for ",
	    file->descrip,
	    " data\n   Bad source = ",
	    NULL,
	    " have_opt_ msg"
	  );
      err (msg,source);
      break;
  }

  file->eof=FALSE;
  file->eod=FALSE;
  file->nrecs=0;

  add_to_comment_stream(brack_method_name,msg);
  free (msg);
  free (brack_method_name);

  return TRUE;
}

int key_N(key,string,n)
char *key,*string;
int *n;
/*    This routine determines if string is of the form key[_N].  N, if	*/
/*  specified, must be > 0.						*/
/*    Returns  -1 if key not spec'd					*/
/*    		0 if exactly key spec'd					*/
/*		1 if exactly key_N spec'd (w/legal N)			*/
/*		2 if key_N w/legal N spec'd & something else follows	*/
/*  If returning 1 or 2, N is returned in arg n				*/
{
  char underscore,whitespace;
  char dummy[1+1] = {'\0','\0'};
  int i;

  i = strlen(key);
  if (strncmp(string,key,i) != 0) return -1;

    /*  Check for _N string.  Using sscanf not only does		*/
    /*  decode for us, but also handles some whitespace issues		*/
  i = sscanf(string+i,"%c%d%c%1s",&underscore,n,&whitespace,dummy);

  if (i == 0) return 0;
  if (i == 1)
    err ("In key_N format, N must be present.  Key_N = ",string);
  if (i > 4)
    err ("Internal coding error in key_N.  scanf returned ",i);

  if (underscore != '_')
      /*  This stops use of key as legal prefix for other keywords	*/
      /*  Might be overly restrictive, but probably not...		*/
    err ("In key_N format, _ must be present.  Key_N = ",string);

  if (i > 2)
      /*  Check that thing after _N is a space.				*/
    if ( ! isspace(whitespace))
      err ("In key_N format, bad char follows N.  Key_N = ",string);

  if (*n <= 0)
      err ("In key_N format, N must be positive.  Key_N = ",string);

    /*  Passed all the checks.  Treat "key_N " like "key_N" for return	*/
    /*  value purposes							*/
  return (i == 4) ? 2 : 1;
}

char get_comment_sources(file,outfiles,varlist_file,level0_file)
struct fileinfo *file,outfiles[],*varlist_file,*level0_file;

/*  If datacomments were spec'd, returns UNKNOWN_FORMAT or DEF_FORMATTED */
/*  Returns '\0' if datacomments were not spec'd */

/*  Input file contains a list of "files" (a la defgb - files, scripts	*/
/*  objects or immediate strings).  list length limited by size of	*/
/*  outfiles (presumed to be MAX_DATACOMMENTS_SOURCES).  For each, 	*/
/*  determine its order (from source_NN keyword  or its "line number")	*/
/*  and init the appropriate outfile structure w/ the "file"		*/
{
  int position,out_index;
  char *seps,*msg,*source,*descrip,*end_token1;
  char *dirstring,*ptr;
  char format;
  Logical is_varlist_file,is_level0_file;

  if (! open_and_log_opt(file)) return '\0';
  if (getrec_proccomment(file,file->comment_postfix) == 0)
    err ("EOF/error before datacomments file data records","");

  strdupl(&dirstring,file->source,
			" saving datacomments file's directory string");
    /*  If there is a directory, split it from file spec		*/
    /*  after its trailing /						*/
  if (  (ptr = strrchr(dirstring,DIRSEP)) == NULL  )  {
    free (dirstring);
    dirstring = NULL;
  } else *(++ptr) = '\0';      /* Truncate after found /		*/

  seps = file->item_separators;
  position = 1;    /* Line number (= "start-from-1" array position)	*/

  while (TRUE) {

    descrip = file->buf + strspn(file->buf,seps);

	/*  Skip lines consisting entirely of separators		*/
    if (*descrip != '\0') {

	/*  File logically consists of entries of the form		*/
	/*	source							*/
	/*  or								*/
	/*	descrip  =  source					*/
	/*  descrip is a string of the form source_N, where N gives the	*/
	/*    order in which source is to be processed (missing N	*/
	/*    defaults to 1)  If descrip not present, N is		*/
	/*    calculated from position within file.  There may be at	*/
	/*    most one entry (explicit or calculated) for each N	*/
	/*  source is an indirect file source (see get_files) except	*/
	/*    that the allowable pointers are .varlist & .level_0,	*/
	/*    pointing to the varlist and level 0 data file, re-	*/
	/*    spectively, for this object				*/
      source = end_token1 = strpbrk(descrip,seps);
      if (end_token1 != NULL) source += strspn(end_token1,seps);

	/*    Single entry if first thing we encounter is "special"	*/
	/*    string delimiter, or if 2nd field empty			*/
      if (  (*descrip == SOURCE_IS_DATA) ||
      	    (*descrip == *EXEC_DELIM)    ||
      	    (*descrip == *OBJECT_DELIM)  ||
	    (is_a_remote_object(descrip) != 0) ||
	    (strcmp(descrip,VARLIST_PTR) == 0) ||
	    (strcmp(descrip,LEVEL0_PTR) == 0) ||
	    (source == NULL)		 ||
	    (*source == '\0')			) {

	  /*  No descrip field.  Source is string; file type comes from	*/
	  /*  where we are in file; vars in file are post-translation	*/
	  /*  unless we are talking about the DATAFILE			*/
	source = descrip;
	out_index = position;
      } else {
	  /*  Have both source and descrip.  Terminate descrip, then	*/
	  /*  process it to find file type and translation prefix	*/
	*end_token1 = '\0';
	switch (key_N(DATACOMMENTS_KEYWORD,descrip,&out_index)) {
	  case -1:
	    out_index = 1;
	    break;
	  case 0:
	    err ("Illegal keyword in datacomments opt file. Keyword = ",
								descrip);
	    break;
	  case 1: case 2: case 3:
	    break;
	  default:
	    err ("Internal coding error - illegal return from key_N\n","");
	}
      }
      if (out_index > MAX_DATACOMMENTS_SOURCES)
	  errn ("Too many datacomments sources requested\n"
		"Requested at least ", out_index		);

	/*  Note that source is not trimmed of trailing seps at this	*/
	/*  point.  Cannot process left-to-right because could be a	*/
	/*  multi-token string (eg, command w/params)			*/

      if (*source == '\0') err ("0 length indirect file entry for ",msg);
      if (outfiles[out_index-1].source != NULL) 
	errn ("2nd entry for datacomments source number ",out_index);


      msg=buildstring(" saving ",outfiles[out_index-1].descrip,
							" source info","");
      if (strcmp(source,VARLIST_PTR) == 0) {
	if (varlist_file->source == NULL  )
	  err ("varlist opt file listed as datacomment source",
		" but no varlist opt file specified"		);
	strdupl(&outfiles[out_index-1].source,varlist_file->source,msg);
	outfiles[out_index-1].source_type = varlist_file->source_type;
	format = DATACOMMENTS_DEF_FORMATTED;
      } else if (strcmp(source,LEVEL0_PTR) == 0) {
	if (level0_file->source == NULL)
	  err ("level 0 data file listed as datacomment source",
		" but for some reason its name string is NULL"	);      
	strdupl(&outfiles[out_index-1].source,level0_file->source,msg);
	outfiles[out_index-1].source_type = level0_file->source_type;
	format = DATACOMMENTS_DEF_FORMATTED;
      } else {
	strdupl(&outfiles[out_index-1].source,source,msg);
	analy_source(&outfiles[out_index-1],seps,dirstring);
	  /* Take a shot at trying to see "files are the same"		*/
	  /* This test can easily fail (take, for example, original	*/
	  /* code which did not do null pointer checks - not that this	*/
	  /* parenthetical note has anything to do w/the logical	*/
	  /* difficulty of the testing).				*/
	  /* Better would be to look at	inode #, etc			*/
	is_varlist_file = (varlist_file->source != NULL) &&
				(strcmp(source,varlist_file->source) == 0);
	  /* Not sure null level0_file->source is legit here, but that	*/
	  /* issue is certainly dealt w/elsewhere			*/
	is_level0_file = (level0_file->source != NULL) &&
				(strcmp(source,level0_file->source) == 0);
        format = (is_varlist_file || is_level0_file)
				?
		DATACOMMENTS_DEF_FORMATTED : DATACOMMENTS_UNKNOWN_FORMAT;
      }

      free (msg);
    }

    if (getrec_proccomment(file,NULL) == 0) break;
    position++;
  }

  if (dirstring != NULL) free (dirstring);
  return format;
}

int create_wjstbl (file,output_list,output_list_size)
struct fileinfo *file;
char *output_list;
int output_list_size;
	/* To associate strings with variables, create string 		*/
	/*	w/ format SVSAsB per pair, where			*/
	/* 	 S = is any non-whitespace character not in either	*/
	/*		pair ("Separator")				*/
	/*	 V = variable (will be translated, if appropriate)	*/
	/*	 As= associated string					*/
	/*	 B = at least one whitespace character			*/
	/* Variables can appear in any order in input file, but may	*/
	/*  appear only once.  A string may be both a variable name	*/
	/*  and an associated string, but this asks for trouble...	*/
	/*								*/
	/* Returns count of # elements in table				*/
	/* Returns list in provided argument ('\0' if no input file)	*/ 
{
  int paircnt;
  char *tok,*output_list_ptr,*this_var;
  char *entry_ptr,*next_entry_ptr;
  char *next_item_ptr;
  char *rec_id = NULL,*msg = NULL,*opt_file_id;
  char *comment_postfix;

    /*  Consistency of item separator list and entry separator list	*/
    /*  checked in init_file_structs, but really should be checked here	*/
    /*  in case something changes, or if we ever have individual	*/
    /*  lists per opt file.						*/

  if (! open_and_log_opt(file)) {
    *output_list = '\0';
    return 0;
  }

  opt_file_id = buildstring ("\n   Problem in ",file->descrip," optional file",
			     " creating opt_file_id string");

  output_list_ptr=output_list;
  paircnt=0;
  comment_postfix = file->comment_postfix;

      /*  Loop per record in file					*/
  while (getrec_proccomment(file,comment_postfix) != 0) {
	/*  Throw out comments from now on (after first non-comment)	*/
    comment_postfix = NULL;
    rec_id = buildstring ("\n   Opt file record = ",file->buf,NULL,
			  " creating rec_id string");

    next_entry_ptr = file->buf;

	/*  Loop per entry (pair) in record				*/
	/*    Allow item alt separators to protect entry separators	*/
	/*    that happen to occur within items				*/
    while (  (entry_ptr = nxttok (next_entry_ptr,
				  file->entry_separators,
				  &next_entry_ptr,
				  file->item_alt_separators,
				  FALSE,
				  FALSE)		   )
			!= NULL					) {

	/*  Ignore empty pairs.  Should take care of empty lines, too	*/
      if (  (tok = nxttok(entry_ptr,
			  file->item_separators,
			  &next_item_ptr,
			  NULL,
			  FALSE,
			  FALSE)		)
		 == NULL				) break;

  	  /*  Got first of pair (typically a variable name or keyword)	*/
      if (file->pre_trans) {
	if (TRANSVAR_CALL(varname,tok) == -1) {
	  msg = buildstring
		  ("Translated variable name too long\n   Name (pre-trans) = ",
		    tok,
		    rec_id,
		    "creating err msg 2");
	  err (msg,opt_file_id);
	} else tok=varname;
      }

      if (lookup_wjstbl(tok,output_list) != NULL) {
	msg =buildstring ("Entry appears twice in list\n   Entry = ",tok,rec_id,
			" creating err msg 3");
	err (msg,opt_file_id);
      }
      if (output_list_ptr + strlen(tok) + 2 > output_list + output_list_size) {
	msg = buildstring ("List overflow attempting to add item ",tok,rec_id,
			" creating err msg 4");
	err (msg,opt_file_id);
      }
      if (strchr(tok,WJSTBL_SEPARATOR) != NULL) {
	msg = buildstring("Table separator appears in item ",tok,rec_id,
			" creating err msg 5");
	err (msg,opt_file_id);
      }
      *output_list_ptr++ = WJSTBL_SEPARATOR;
      this_var = output_list_ptr;
      while (*tok != '\0') *output_list_ptr++ = *tok++;

	/*  Get its associated string.  Might be enclosed in		*/
	/*  apostrophes (depending on which opt file this is)		*/
      if (  (tok = nxttok (next_item_ptr,
			   file->item_separators,
			   &next_item_ptr,
			   file->item_alt_separators,
			   FALSE,
			   FALSE)  )
		 == NULL				) {
	  /*  Error if no associated string				*/
	msg = buildstring("Incomplete entry\n   Variable/keyword = ",
			   this_var,
			   rec_id,
			   " creating err msg 6");
	err (msg,opt_file_id);
      }
      if (nxttok (next_item_ptr,file->item_separators,NULL,NULL,FALSE,FALSE)  
	    != NULL) {
	msg = buildstring("Third item in entry ",tok,rec_id,
			  " creating err msg 7");
	err (msg,opt_file_id);
      }

      if (output_list_ptr + strlen(tok) + 2 > output_list + output_list_size) {
	msg = buildstring 
		("List overflow attempting to add variable/keyword ",
		   this_var,rec_id,	   " creating err msg 8");
	err (msg,opt_file_id);
      }
      if (strchr(tok,WJSTBL_SEPARATOR) != NULL) {
	msg = buildstring("Table separator appears in item ",tok,rec_id,
			  " creating err msg 9");
	err (msg,opt_file_id);
      }

      *output_list_ptr++ = WJSTBL_SEPARATOR;
      while (*tok != '\0') *output_list_ptr++ = *tok++;
      *output_list_ptr++ = WJSTBL_WHITE_SPACE;

      paircnt++;
    }
  }
  *output_list_ptr = '\0';

  if (file->eof) {
    if (fclose(file->stream) != 0)
      err ("Failure to close opt file.  errno msg is ",strerror(errno));
    file->open = FALSE;
      /*  See comments at waitpid in dataeod (in defgb.c)		*/
    waitpid((pid_t)-1,NULL,WNOHANG);
    errno = 0;
  }

  free(opt_file_id);
  if (rec_id != NULL) free(rec_id);
  if (msg != NULL) free (msg);

  return paircnt;
}

Logical remove_vars (removals_file,ntotal,list_after_removals,ncnt)
struct fileinfo *removals_file;
int *ntotal,list_after_removals[],ncnt;
  /*  Makes ntotal, the number of variables presented to outer, from	*/
  /*	ncnt, the number of variables in the level 0 file, and a list	*/
  /*	of variables to remove.  Also provides the mapping array,	*/
  /*	list_after_removals to implement the removal.  Note that	*/
  /*	outer may implement variable selection, further reducing the	*/
  /*	list of variables shipped out of the method			*/
  /*  Returns TRUE or FALSE depending on whether variable removals were	*/
  /*	spec'd								*/
  /*  List of variables to be removed comes in via removals optional	*/
  /*    file.  Order of variables is irrelevant				*/
  /*  pointers array & its size, ncnt, are base data for this function.	*/
  /*	At this point, however, pointers[i]=i, so that simplifies things*/
  /*  Idea is to prepare list_after_removals, a version of pointers 	*/
  /*	w/o the indices of the variables in the removals list		*/
  /*  list_after_removals starts as a copy of pointers.  To remove a 	*/
  /*    variable, find its position in the master variable list, which	*/
  /*	is its position in pointers.  Mark this position in 		*/
  /*    list_after_removals.  After all are marked, compress out the	*/
  /*    marked elements							*/
  /*  Below the index of a removed variable, the list_after_removals 	*/
  /*    list is the same as the pointers list.  At and above, 		*/
  /*	list_after_removals points to the next higher pointers element	*/
  /*  *ntotal is the size of list_after_removals and (presumably) the	*/
  /*	number of variables returned to outer				*/

  /*	 4 Mar 98.  No error if removals list variable not in lev0 list	*/
  /*	25 Mar 96.  Check that we have vars before & after removals	*/
{
  int i;
  char *tok;

    /*  Create list without any removed variables			*/
  for (i=0; i<ncnt; i++) list_after_removals[i]=i;

  if ( ! open_and_log_opt(removals_file) ) return FALSE;

  if (getrec_proccomment(removals_file, removals_file->comment_postfix) == 0)
      err ("EOF/error before removals file data records","");

	/*  At start of loop, we have record in buf.  Thereafter, read	*/
	/*  until EOF							*/
  while (TRUE) {
    tok=strtok(removals_file->buf,removals_file->item_separators);
    while (tok != NULL) {
      if (removals_file->pre_trans) {
	if (TRANSVAR_CALL(varname,tok) == -1)
	  err ("Translated variable name too long\n   Name (pre-trans) = ",tok);
	tok=varname;
      }
	/*  If variable name found, mark it as removed			*/
      if (  (i=lookup_lev0varlist(tok)) >= 0  )  list_after_removals[i] = -1;
      tok=strtok(NULL,removals_file->item_separators);
    }
    if (getrec_proccomment(removals_file,NULL) == 0) break;
  }

  if (removals_file->eof) {
    if (fclose(removals_file->stream) != 0)
      err ("Failure to close removals file.  errno msg is ",strerror(errno));
    removals_file->open=FALSE;
  }

  *ntotal=0;
  for (i=0; i<ncnt; i++) 
    if (list_after_removals[i] != -1) 
      list_after_removals[(*ntotal)++] = list_after_removals[i];

  if (*ntotal == 0) err ("Removed all variables","");

  return TRUE;
}

int get_var_from_keyword(keyword,wjstbl,file_assoc_w_wjstbl)
struct fileinfo *file_assoc_w_wjstbl;
char *keyword,*wjstbl;
/*  Handle situation where wjstbl is of the form keyword/variable.	*/
/*  Look up keyword in table.  Get its variable name.  Translate if	*/
/*  necessary.  Be sure that resulting name is in master variable list 	*/
/*  Return -1 if keyword not present					*/
/*  Implicitly return variable name in global varname			*/
{
  Logical i;

  if (  (i = extract_wjstbl(varname,MAXVARNAMESIZE,keyword,wjstbl)) 
	  == NOT_VALID  					   ) 
    err (
	 buildstring(
	   "Parameter provided for ",
	   file_assoc_w_wjstbl->descrip,
	   " calculation too long\n   Param = ",
						" get_var_from_keyword"),
	 keyword);
  else if (i == FALSE) return -1;

  if (file_assoc_w_wjstbl->pre_trans)
    if (TRANSVAR_CALL(varname,varname) == -1)
      err (
	 buildstring(
	   "Translated variable in ",
	   file_assoc_w_wjstbl->descrip,
	   " calculation too long\n   Truncated, post-trans variable = ",
						" get_var_from_keyword"),
	 varname);

  i = lookup_lev0varlist(varname);
  if (i < 0)
    err (
	 buildstring(
	   "Variable provided for ",
	   file_assoc_w_wjstbl->descrip,
	   " calculations not in dataset\n   Variable = ",
						" get_var_from_keyword"),
	 varname);

  return i;
}

Logical has_prefix (possible_prefix,prefix,rest_of_string)
char *prefix,*possible_prefix,*rest_of_string;
/*  Check that prefix is the same as possible_prefix, and that next	*/
/*  character is, in fact, beginning of rest_of_string			*/
/*  (Note that test for "beginning of rest_of_string" takes care of	*/
/*  situation where prefix is longer than possible_prefix and a match	*/
/*  is made on a substring of prefix)					*/
{
  int len;

  len=strlen(prefix);
  return ( (strncmp(possible_prefix,prefix,len) == 0)  &&
	   (possible_prefix+len == rest_of_string)  );
}

char *found_at_end(string,suffix)
char *string,*suffix;
/*  Returns pointer to suffix if suffix is found at end of string;	*/
/*  NULL else.  Note that this is NOT logically the same as has_prefix,	*/
/*  which checks for overlapping strings.  This routine doesn't.	*/
{
  char *ptr;

  if (  (ptr = strstr(string,suffix)) != NULL  )
    if (  ptr+strlen(ptr) == string+strlen(string)  ) return ptr;
  return NULL;
}

int *select_lev(keyword,output_info)
char *keyword;
struct outinfo *output_info;
{
  char *ptr;
  int *retval;

    /*  Logically a loop over all possible levels.  However, this	*/
    /*  requires some way to associate keywords with the pointers to	*/
    /*  the levels, as well as a way to tie the "addl-" prefix to	*/
    /*  "the dup_ version of a particular level pointer"		*/
  if (  (ptr = found_at_end(keyword,"trace")) != NULL  )
    if (ptr == keyword) retval = &output_info->trace_level;
    else if (has_prefix(keyword,"addl-",ptr))
      retval = &output_info->dup_trace_level;
  else if (  (ptr = found_at_end(keyword,"error")) != NULL  )
    if (ptr == keyword) retval = &output_info->error_level;
    else if (has_prefix(keyword,"addl-",ptr))
      retval = &output_info->dup_error_level;
  else retval = NULL;

  return retval;
}

int *select_debug1_type(keyword,output_info)
char *keyword;
struct outinfo *output_info;
{
  char *ptr;
  int *retval;

    /*  See select_lev, if overwhelming similarity hasn't hit already	*/
    /*  Slight difference because the "print_" key is at beginning in-	*/
    /*  stead of end, and prefix will precede the print_, not the ioval	*/
  if (  (ptr = found_at_end(keyword,"print_iovalstr")) != NULL  )
    if (ptr == keyword) retval = &output_info->iovalstr;
    else if (has_prefix(keyword,"addl-",ptr))
      retval = &output_info->dup_iovalstr;
  else if (  (ptr = found_at_end(keyword,"print_iovalreal")) != NULL  )
    if (ptr == keyword) retval = &output_info->iovalreal;
    else if (has_prefix(keyword,"addl-",ptr))
      retval = &output_info->dup_iovalreal;
  else retval = NULL;

  return retval;
}

Logical *select_logical_type(keyword,output_info)
char *keyword;
struct outinfo *output_info;
{
  char *ptr;
  Logical *retval;

  if (strcmp(keyword,"inserted_msgs") == 0) 
    retval = &output_info->inserted_comments;
  else if (strcmp(keyword,"inserted_msg_tag") == 0) 
    retval = &output_info->inserted_comment_id;
  else if (strcmp(keyword,"comment_source_tag") == 0) 
    retval = &output_info->opt_file_comment_id;
  else retval = NULL;

  return retval;
}

Logical configure_output(diag_optfile,output_info,files)
struct fileinfo *diag_optfile,files[];
struct outinfo *output_info;
/*  Returns TRUE or FALSE depending on whether output info was provided	*/
/*									*/
{
  int i,j,max;
  int *intptr;
  Logical *logptr;
  char *ptr,*ptr2;
  char *keyword,*keyval,*old_sink_name,*mode;
  char diags_wjstbl[DIAGS_WJSTBL+1];
  char time_buf[1+2+3+2+1+2+2+1+2+1];  /*  See strftime call		*/
  char wjstbl_chars[2+1];	/* Whitespace char; separator char	*/
				/* See wjstbl doc			*/
  struct outfile *errout;
  time_t now;

  if (create_wjstbl(diag_optfile,diags_wjstbl,DIAGS_WJSTBL) == 0)
    return FALSE;

  wjstbl_chars[0]=WJSTBL_WHITE_SPACE;
  wjstbl_chars[1]=WJSTBL_SEPARATOR;
  wjstbl_chars[2]='\0';

  now = time(NULL);
  if (strftime(time_buf,
	       sizeof time_buf,
	       "_%y%b%d-%H%M-%S",
	       localtime(&now)    )
	   == 0				)
    err ("Cannot make time string for unique- file name.  Buffer too short","");

  keyword = strtok(diags_wjstbl,wjstbl_chars);

  while (keyword != NULL) {
    if (keyword == NULL) err ("Bad internal table format","");
    keyval = strtok(NULL,wjstbl_chars);
    if (keyval == NULL) err ("Bad internal table format","");
      /*  Scheme: since we do the various types w/ an if/else-if 	*/
      /*  structure, order is slightly significant (a keyword could	*/
      /*  conceivably fit more than one pattern).  Do "independent"	*/
      /*  keywords first-those considered in their entirety-no prefixes	*/
      /*  suffixes, etc.  Then do keywords that have just prefixes and/	*/
      /*  or suffixes.  Then do keywords with "embedded" key strings.	*/
      /*  Right now, we use the strings _sink, _level & _lines as	*/
      /*  significant suffixes.  Almost all keywords take the addl-	*/
      /*  prefix.  This makes the print_ prefix "embedded", as well as	*/
      /*  the new- and unique- prefixes.  (Prefixes ending in - are	*/
      /*  user-supplied; strings involving _ are parts of keywords that	*/
      /*  we use to group types of parameters)				*/
      /*  Sigh.								*/
    if (  (logptr = select_logical_type(keyword,output_info)) != NULL  ) {
	/*  Process a logical - happens to be only "simple" keyword	*/
      *logptr = get_logical_from_string
				(keyval," diag optfile keyword ",keyword);
    } else if (  (ptr = found_at_end(keyword,"_level")) != NULL  ) {
	/*  Process a level						*/
      *ptr = '\0';			/*  Lop off _level		*/
      if (  (intptr = select_lev(keyword,output_info)) == NULL  ) {
	*ptr = '_';			/*  Restore _level		*/
	err ("Invalid diag optfile _level keyword ",keyword);
      }
      *intptr = get_int_from_string(keyval,0,99,wjstbl_chars[0],
	" diag optfile _level keyword");

    } else if (  (ptr = found_at_end(keyword,"maxscriptdiags")) != NULL  ) {
	/*  Process maxscriptdiags.  Does NOT accept addl- prefix	*/
      if (ptr == keyword) 
	output_info->maxscriptdiags =
	  get_int_from_string(keyval,0,INT_MAX,wjstbl_chars[0],
	    " diag optfile maxscriptdiags keyword");
      else err ("Invalid prefix in diag optfile keyword ",keyword);

    } else if (  (ptr = found_at_end(keyword,"_sink")) != NULL  ) {
	/*  Process a sink						*/
      *ptr = '\0';			/*  Lop off _sink		*/
      for (i=0; i<NUM_USER_SPECIFIABLE_SINKS; i++) {
	errout = &output_info->file[i];
	if (  (ptr = found_at_end(keyword,errout->descrip)) != NULL  )  {

	    /*  Found a legit sinkname keyword				*/
	    /*    Put _sink back on for possible use in error msgs	*/
	  *(ptr+strlen(ptr)) = '_';
	  if (errout->processed)
	    err ("Diag sink specified more than once.  Sink = ",keyword);
	  errout->processed = TRUE;

	    /*  Test for addl- prefix					*/
	  *ptr = '\0';			/*  Only look at prefixes	*/
	    /*  Test for addl- prefix and logically make it part of	*/
	    /*  keyword body (instead of prefix) if found		*/
	  if (  (ptr2 = found_at_end(keyword,"addl-")) != NULL  )  {
	    errout->dup = TRUE;
	    ptr = ptr2;
	  }

	    /*  Get output mode and save sink name, "uniquing" if needed */
	  old_sink_name = errout->sink;
	  if (keyword == ptr) {
	    mode = "a";
	    strdupl(&(errout->sink),keyval,	  " saving output sink name");
	  } else				/*  Still more text in keyword	*/
	    if (has_prefix(keyword,"new-",ptr)) {
	      mode = "w";
	      strdupl(&(errout->sink),keyval,	  " saving output sink name");
	    } else if (has_prefix(keyword,"unique-",ptr)) {
	      mode = "a";
	      errout->sink = buildstring(keyval,time_buf,NULL,
		" building unique sink name");
	    } else {
		/* Restore keyword for message				*/
	      if (ptr2 != NULL) ptr += strlen("addl-");
	      *ptr = *(errout->descrip);
	      err ("Invalid prefix in diag optfile keyword ",keyword);
	    }

	    /*  Handle situation where a) compile-time sink different 	*/
	    /*  from runtime sink & b) compile-time sink open.		*/
	  if (  *(errout->open_ptr)			 && 
		(strcmp(errout->sink,old_sink_name) != 0)	) {
	    if (fclose(*errout->stream_ptr) != 0)
	      err ("Failure to close compile-time err sink.  errno msg is ",
		strerror(errno));
	    *(errout->open_ptr) = FALSE;
	    *(errout->mode_ptr) = NULL;
	    errout->mode_ptr = output_info->file[i].mode_ptr;
	  }

	    /*  Save mode, checking for consistency			*/
	  if ( *(errout->mode_ptr) == NULL) {
	    strdupl(&output_info->outsink_modes[i],mode);
	    errout->mode_ptr = &output_info->outsink_modes[i];
	  } else if (strcmp(*(errout->mode_ptr),mode) != 0)
	    err ("> 1 sink to one file, but different modes spec'd\n   File = ",
	      errout->sink);

	  break;
	}
      }

    } else if (  (ptr = strstr(keyword,"print_")) != NULL  ) {
	/*  Process trace request.  Should be last to protect against	*/
	/*  an embedded print_ string not being a keyword		*/
	/*    2 flavors; optfilename_lines and debug1-compatible	*/
      if (  (ptr2 = found_at_end(ptr,"_lines")) == NULL  ) {
	  /*   Type added to be compatible w/old DEBUG1 switch		*/
        if ( (intptr = select_debug1_type(keyword,output_info)) == NULL )
	  err ("Invalid diag optfile print_ keyword ",keyword);
      } else {
	  /*  optfilename_lines type					*/
	*ptr2 = '\0';			/*  Lop off _lines		*/
	for (i=0; i<NFILETYPES; i++) 
	    /*  +6 is to skip over leading "print_"			*/
	  if (strcmp(ptr+6,files[i].descrip) == 0) break;
	*ptr2 = '_';			/*  Put back _lines		*/
	if (i < NFILETYPES) {
	  if (ptr == keyword) intptr = &files[i].print_lines;
	  else if (has_prefix(keyword,"addl-",ptr))
	    intptr = &files[i].dup_print_lines;
          else err ("Invalid prefix in diag optfile keyword ",keyword);
	  if (i == DIAGNOSTICS) 
	    err ("Cannot trace diagnostic lines from diagnostics file","");
	} else
	    /*  This next message could be spurious.  What we'd really	*/
	    /*  like to do is put this keyword back into the processing	*/
	    /*  stream.  If we "functionize" this "else if" structure,	*/
	    /*  take care of this.  Meantime, OK since this is last 	*/
	    /*  else-if block						*/
	  err ("Invalid diagnostic keyword ",keyword);
      }
      *intptr = get_int_from_string(keyval,0,INT_MAX,wjstbl_chars[0],
	" diag optfile print_ keyword");

    } else err ("Invalid diagnostic keyword ",keyword);

    keyword = strtok(NULL,wjstbl_chars);
  }

    /*  Must come out of here with non-null device for each sink, or	*/
    /*  else turn off all output to non-legal sinks.  Alternative is	*/
    /*  a test before each sink use-- ecch!  defgb.h and init_outinfo	*/
    /*  default to non-null devices, so bad device means user managed	*/
    /*  to null it out (or we screwed up).  Set file to /dev/null	*/
    /*  Uninitialized output modes are checked and, if necessary, set	*/
    /*  before opening.  Used to do it here, but didn't always come	*/
    /*  through here...							*/
  for (i=0; i<NUM_SINKS; i++) {
    errout = &output_info->file[i];
    if (errout->sink == NULL)
      strdupl(&(errout->sink),NULL_DEVICE," creating null output sink name");
    if (  *(errout->sink) == '\0'  )
      strdupl(&(errout->sink),NULL_DEVICE," creating null output sink name");
  }

  return TRUE;
}

Logical get_varlist_options(varlistopts_optfile,varlists_end,
	varname_for_lev1_specs,coll_from_objobj_obj)
struct fileinfo *varlistopts_optfile;
int *varlists_end;
char **varname_for_lev1_specs,**coll_from_objobj_obj;
/*  Get the switches from varlistopts optional file		*/
{
  char *keyword,*keyval,*end_keyval,*ptr;
  char varlistopts_wjstbl[VARLISTOPTS_WJSTBL];
  char wjstbl_white_space,dummy;
  int i;
  int size_v_lev1_specs;
  int nitems_supplied;
  int nitems_processed = 0;

    /*  Default to "not specified"				*/
  *varlists_end = NO_VARLISTS_END_SPECIFIED;
  *varname_for_lev1_specs = '\0';

  nitems_supplied =
    create_wjstbl(varlistopts_optfile,varlistopts_wjstbl,VARLISTOPTS_WJSTBL);
  if (nitems_supplied == 0) return FALSE;
  wjstbl_white_space = WJSTBL_WHITE_SPACE;
  dummy = '\0';			/* Anything != wjstbl_white_space	*/

  keyword = "varlists_end";
  if (  (keyval = lookup_wjstbl(keyword,varlistopts_wjstbl)) != NULL  ) {
    i = sscanf(keyval,"%d%c",varlists_end,&dummy);
    if (  (i != 2) || (dummy != wjstbl_white_space) )
      err ("Badly formatted varlists_end field (or defgb err).  Field = ",
	keyval);
    if (*varlists_end < MIN_VARLISTS_END)
      err ("Illegal varlists_end value.  Field = ",keyval);
    nitems_processed++;
  }

  keyword = FLAG_FOR_COLL_FROM_OBJOBJ;
  if (  (keyval = lookup_wjstbl(keyword,varlistopts_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    size_v_lev1_specs = sizeof(varname);
    *varname_for_lev1_specs = (char *)malloc(size_v_lev1_specs);
    if (*varname_for_lev1_specs == NULL )
			err("No memory for varname_for_lev1_specs","");
    if (varlistopts_optfile->pre_trans)
      if (transvar(*varname_for_lev1_specs,size_v_lev1_specs,keyval) == -1) {
	err ("Translation for varname_for_level1_specs variable too"
	     " long\n Name (pre-trans) = ",keyval			);
      }
    else
	/*  Could probably strdupl here and malloc in the other half	*/
	/*  of this if, but not sure if some other logic doesn't assume	*/
	/*  all varname buffers are same size				*/
      copy_into_buffer 
		(*varname_for_lev1_specs,size_v_lev1_specs,keyval,
		 "copying into varname_for_lev1_specs"			);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }

    /*  Supply default if not spec'd.					*/
  keyword = "coll_from_objobj_obj";
  if (  (keyval = lookup_wjstbl(keyword,varlistopts_wjstbl)) == NULL  )  {
    keyval = COLL_FROM_OBJOBJ_OBJ;
    end_keyval = NULL;
  } else {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
  }

  strdupl(coll_from_objobj_obj,keyval,"No memory for coll_from_objobj_obj");

  if (end_keyval != NULL) {
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }

  if (nitems_supplied > nitems_processed)
    errn ("Unprocessed varlistopts parameters.  Number skipped = ",
      nitems_supplied-nitems_processed);

  return TRUE;
}

Logical get_datafield_options(
			datafield_optfile,
			consec_seps, 
			embed_seps,
			sepstring_ptr, 
			alt_sepstring, 
			strip_leading_blanks, 
			strip_trailing_blanks, 
			all_blank_treatment)
struct fileinfo *datafield_optfile;
Logical *consec_seps,*embed_seps;
char **sepstring_ptr;
char *alt_sepstring;
Logical *strip_leading_blanks,*strip_trailing_blanks;
int *all_blank_treatment;
/*  Get the switches from the datafieldopts optional file		*/
{
  char *keyword,*keyval,*end_keyval,*ptr;
  char datafield_wjstbl[DATAFIELDOPTS_WJSTBL+1];
  char wjstbl_white_space;
  Logical leading_blanks,trailing_blanks,tmp_blanks;
  int nitems_supplied;
  int nitems_processed = 0;

  nitems_supplied =
    create_wjstbl(datafield_optfile,datafield_wjstbl,DATAFIELDOPTS_WJSTBL);
  if (nitems_supplied == 0) return FALSE;
  wjstbl_white_space = WJSTBL_WHITE_SPACE;

  keyword = "significant_consecutive_separators_in_free_field";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    *consec_seps = get_logical_from_string 
			(keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }

  keyword = "significant_embedded_separators_in_fixed_field";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    *embed_seps = get_logical_from_string 
			(keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }

  keyword = "separators";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    *sepstring_ptr = get_string_from_escaped_string 
			(keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }


  keyword = "alt_separators_in_free_field";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    ptr = get_string_from_escaped_string 
			(keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
    if (strlen(ptr) < 3) strcpy(alt_sepstring,ptr);
    else err ("alt_separator string too long.  String = ",ptr);
    if (alt_sepstring[1] == '\0') alt_sepstring[1] = alt_sepstring[0];
    alt_sepstring[2] = '\0';
  }

  leading_blanks = trailing_blanks = NOT_VALID;
  keyword = "strip_blanks";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    leading_blanks = trailing_blanks = get_logical_from_string 
		   (keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }
  keyword = "strip_leading_blanks";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    tmp_blanks = get_logical_from_string 
		   (keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
    if (leading_blanks == NOT_VALID) {
      leading_blanks = tmp_blanks;
    } else {
      if (leading_blanks != tmp_blanks) 
	err ("Conflicting values for strip_blanks and strip_leading_blanks");
    }
  }
  keyword = "strip_trailing_blanks";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    tmp_blanks = get_logical_from_string 
		   (keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
    if (trailing_blanks == NOT_VALID) {
      trailing_blanks = tmp_blanks;
    } else {
      if (trailing_blanks != tmp_blanks) 
	err ("Conflicting values for strip_blanks and strip_trailing_blanks");
    }
  }
  if (leading_blanks != NOT_VALID) *strip_leading_blanks = leading_blanks;
  if (trailing_blanks != NOT_VALID) *strip_trailing_blanks = trailing_blanks;

    /*  We really want a character string, so the fooling w/integers	*/
    /*  seems counter-productive.  However, it substantially eases the	*/
    /*  task of specifying a compile-time value				*/
  keyword = "treat_all_blank_strings_as";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    nitems_processed++;
    if (strcmp(keyval,"MISSING") == 0) 
		*all_blank_treatment = TREAT_ALL_BLANK_STRINGS_AS_MISSING;
    else if (strcmp(keyval,"SINGLE_BLANK") == 0)
		*all_blank_treatment = TREAT_ALL_BLANK_STRINGS_AS_SINGLE_BLANK;
    else 
      err ("Bad value for treat_all_blank_strings_as keyword.  Bad value is ",
	   keyval);
  }

  if (nitems_supplied > nitems_processed)
    errn ("Unprocessed datafieldopts parameters.  Number skipped = ",
      nitems_supplied-nitems_processed);

  ptr = REQUIRED_SEPARATORS;
  while (*ptr != '\0')
    if (strchr(*sepstring_ptr,*(ptr++)) == NULL)
      err ("Separator string missing one or more required separators\n",
	"Characters not printed here since they may be unprintable-sorry");
  if (  (strpbrk(alt_sepstring,*sepstring_ptr)	!= NULL)  ||
	(strpbrk(alt_sepstring,EXEC_DELIM)	!= NULL)  ||
	(strpbrk(alt_sepstring,OBJECT_DELIM)	!= NULL)     )
    err("Alt_separators character may not be separator character\n",
	"Characters not printed here since they may be unprintable-sorry");

  return TRUE;
}

struct split_wjstbl *split_wjstbls(intbl,max_outtbls,size_outtbl,prefix)
char *intbl,*prefix;
int max_outtbls,size_outtbl;
{
  int num_out,lenprefix;
  int i,k;
  char sep_str[2];
  char *ptr,*ptr2;
  int *tags,*num_items_per_table;
  struct split_wjstbl *split_tbls;
  char **outtbls;

  i = max_outtbls * sizeof(int);
  tags = (int *) malloc (i);
  if (tags == NULL) errn
	("Could not not get memory for tags in split_wjstbls.  Bytes = ,",i);
  num_items_per_table = (int *) malloc (i);
  if (num_items_per_table == NULL) errn
	("Could not not get memory for counts in split_wjstbls.  Bytes = ,",i);
  i = max_outtbls * sizeof(char *);
  outtbls = (char **) malloc (i);
  if (outtbls == NULL) errn
   ("Could not not get memory for wjstbl ptrs in split_wjstbls.  Bytes = ,",i);


  num_out = 0;
  sep_str[0] = *intbl;
  sep_str[1] = '\0';
  ptr = strtok(intbl+1,sep_str);
  lenprefix = strlen(prefix);

  while (ptr != NULL) {

      /*  ptr pointing to keyword, possibly leading off w/ prefix_tag_	*/
      /*  (underscores optional)					*/
    if (strncmp(ptr,prefix,lenprefix) == 0) {
      ptr += lenprefix;
      if (*ptr == '_') ptr++;
      i = strtol(ptr,&ptr2,10);
      if (ptr2 == ptr) 
	err(
	    buildstring(prefix,
		" keyword prefix does not include a tag.  Keyword is ",
	        ptr - lenprefix,
		"no tag err msg in split_wjstbls"),
	    "");
      ptr = (*ptr2 == '_') ? ptr2+1 : ptr2; 
    } else {
      i = 1;
    }

      /*  See if we've already encountered this output time.		*/
    for (k=0; k<num_out; k++)
      if (tags[k] == i) break;
        /*  Add it if not						*/
    if (k == num_out) {
      if ((++num_out) > max_outtbls) {
	ptr = buildstring("Too many ",prefix,"s requested.  Max = ",
			  "building too many err msg in split_wjstbls");
	errn(ptr,max_outtbls);
      }
      outtbls[k] = (char *)malloc(size_outtbl);
      if (outtbls[k] == NULL) errn
	("Could not not get memory for wjstbl in split_wjstbls.  Bytes = ",
	 size_outtbl);
      *outtbls[k] = '\0';
      tags[k] = i;
      num_items_per_table[k] = 0;
    }

      /*  Add this entry to the wjstbl for this output time		*/
    strcat(outtbls[k],sep_str);			/*  Separator char	*/
    strcat(outtbls[k],ptr);			/*  Keyword		*/
    strcat(outtbls[k],sep_str);			/*  Separator char	*/
    ptr = strtok(NULL,sep_str);			/*  Value for		*/
    strcat(outtbls[k],ptr);			/*    keyword		*/
    num_items_per_table[k]++;

    ptr = strtok(NULL,sep_str);
  }

  split_tbls = (struct split_wjstbl *)malloc(sizeof(struct split_wjstbl));
  if (split_tbls == NULL) errn
    ("Could not not get memory for split_tbls in split_wjstbls.  Bytes = ",
       sizeof(struct split_wjstbl));
  split_tbls->n = num_out;
  split_tbls->tag = tags;
  split_tbls->count = num_items_per_table;
  split_tbls->wjstbls = outtbls;

  return split_tbls;
}


void free_split_wjstbl(split_tbl)
struct split_wjstbl *split_tbl;
{
  int i;

  for (i = 0; i < split_tbl->n; i++)
    free(split_tbl->wjstbls[i]);

  free(split_tbl->tag);
  free(split_tbl->count);
  free(split_tbl->wjstbls);

  return;
}


Logical get_latlonformat(buffer,key,wjstbl)
char *key,*buffer,*wjstbl;
{
  char *ptr;
  int len;

  ptr = lookup_wjstbl_w_len(key,wjstbl,&len);
  if (ptr == NULL) return FALSE;

  if (len > MAXLATLONFORMATSIZE)
      err ("Lat/lon format string too long.  String = ",ptr);

  sscanf(ptr,"%s",buffer);
  if (strcmp(buffer,"decdeg") == 0) return TRUE;
  if (strcmp(buffer,"degdecmin") == 0) return TRUE;

  err ("Illegal lat/lon format.  Format = ",ptr);
}

Logical get_latlonchar(ptr_to_sepchar,key,wjstbl)
char *key,*ptr_to_sepchar,*wjstbl;
{
  char *sepstring_ptr,*keyval,*end_keyval;

  if (  (keyval = lookup_wjstbl(key,wjstbl)) == NULL  ) {
    return FALSE;
  } else {
    end_keyval = strchr(keyval,WJSTBL_WHITE_SPACE);
    *end_keyval = '\0';
    sepstring_ptr = get_string_from_escaped_string 
	(keyval," latlonparams optfile separator or convention string",keyval);
    *end_keyval = WJSTBL_WHITE_SPACE;
/*    sepstring_ptr cannot be NULL - get_string_from_escaped_string	*/
/*    would err ()							*/
    if (*(sepstring_ptr + 1) != '\0') 
      err ("Lat/lon separator or convention string > 1 char.  String = ",
	   sepstring_ptr);
    *ptr_to_sepchar = *sepstring_ptr;
    free (sepstring_ptr);
    return TRUE;
  }
}

void do_latlon(latlon_wjstbl,
	       outlat,outlon,inlat,inlon,
	       nitems_supplied,latlon_optfile)
char *latlon_wjstbl;
struct latlonformat *outlat,*outlon;
struct latlonformat *inlat,*inlon;
int nitems_supplied;
struct fileinfo *latlon_optfile;
{
  int nitems_processed = 0;

  inlat->var = get_var_from_keyword("inlatname",latlon_wjstbl,latlon_optfile);
  outlat->var = get_var_from_keyword
			          ("outlatname",latlon_wjstbl,latlon_optfile);
  if (  (inlat->var == INPUT_NOT_SUPPLIED)  &&  
				(outlat->var != OUTPUT_NOT_REQUESTED)  )
    err ("Latitude work requested but latitude input variable name not",
							          "supplied");
  inlon->var = get_var_from_keyword("inlonname",latlon_wjstbl,latlon_optfile);
  outlon->var = get_var_from_keyword
				  ("outlonname",latlon_wjstbl,latlon_optfile);
  if  ( (inlon->var == INPUT_NOT_SUPPLIED)  &&  
				(outlon->var != OUTPUT_NOT_REQUESTED)    )
    err ("Longitude work requested but longitude input variable name not",
							          "supplied");

  if (inlat->var != INPUT_NOT_SUPPLIED) {
    nitems_processed++;
    if (outlat->var == OUTPUT_NOT_REQUESTED) outlat->var = inlat->var;
    else nitems_processed++;
  }
  if (inlon->var != INPUT_NOT_SUPPLIED) {
    nitems_processed++;
    if (outlon->var == OUTPUT_NOT_REQUESTED) outlon->var = inlon->var;
    else nitems_processed++;
  }

  if (get_latlonchar(&inlat->convention,"inlatconvention",latlon_wjstbl)) {
    nitems_processed++;
    if (inlat->convention != '\0')
      if (strchr(inlat->legal_cardinal,inlat->convention) == NULL) 
        err ("Illegal input latitude convention.  Convention = ",
	     inlat->convention);
    inlat->convention = toupper(inlat->convention);
  } 
  if (get_latlonchar(&inlon->convention,"inlonconvention",latlon_wjstbl)) {
    nitems_processed++;
    if (inlon->convention != '\0')
      if (strchr(inlon->legal_cardinal,inlon->convention) == NULL) 
        err ("Illegal input longitude convention.  Convention = ",
	     inlon->convention);
    inlon->convention = toupper(inlon->convention);
  }
  if (get_latlonchar(&outlat->convention,"outlatconvention",latlon_wjstbl)) {
    nitems_processed++;
    if (outlat->convention != '\0')
      if (strchr(outlat->legal_cardinal,outlat->convention) == NULL) 
        err ("Illegal output latitude convention.  Convention = ",
	     outlat->convention);
    outlat->convention = toupper(outlat->convention);
  }
  if (get_latlonchar(&outlon->convention,"outlonconvention",latlon_wjstbl)) {
    nitems_processed++;
    if (outlon->convention != '\0')
      if (strchr(outlon->legal_cardinal,outlon->convention) == NULL) 
        err ("Illegal output longitude convention.  Convention = ",
	     outlon->convention);
    outlon->convention = toupper(outlon->convention);
  }
    
  if (get_latlonchar(&inlat->sep[0],"inlatseparator",latlon_wjstbl))
    nitems_processed++;
  if (get_latlonchar(&inlon->sep[0],"inlonseparator",latlon_wjstbl))
    nitems_processed++;

  if (get_latlonformat(inlat->format,"inlatformat",latlon_wjstbl))
    nitems_processed++;
  if (get_latlonformat(outlat->format,"outlatformat",latlon_wjstbl))
    nitems_processed++;
  if (get_latlonformat(inlon->format,"inlonformat",latlon_wjstbl))
    nitems_processed++;
  if (get_latlonformat(outlon->format,"outlonformat",latlon_wjstbl))
    nitems_processed++;

  if (nitems_supplied > nitems_processed)
    errn ("Unprocessed latlon parameters.  Number skipped = ",
      nitems_supplied-nitems_processed);

  if (  ( (strcmp(inlat->format,outlat->format) != 0) || 
	  (inlat->convention != outlat->convention) )
      &&
	(inlat->var == INPUT_NOT_SUPPLIED)				)
    err ("Latitude work requested ",
	 "but latitude input variable name not supplied");

  if (  ( (strcmp(inlon->format,outlon->format) != 0) || 
	  (inlon->convention != outlon->convention) )
      &&
	(inlon->var == INPUT_NOT_SUPPLIED)				)
    err ("Longitude work requested ",
	 "but longitude input variable name not supplied");

  return;
}


int get_latlonparams(latlon_optfile,outlat,outlon,inlat,inlon)
struct fileinfo *latlon_optfile;
struct latlonformat outlat[NUM_LATLON],outlon[NUM_LATLON];
struct latlonformat inlat[NUM_LATLON],inlon[NUM_LATLON];
/*  Returns number of lat/lons requested				*/
/*									*/
/*  defgb will do 2 different types of work on latitudes and longitudes	*/
/*  To do anything, it needs an inlatname (or inlonname) variable name	*/
/*  from the latlonparams optional input file.  It will put its output	*/
/*  in the same variable unless the corresponding out*name is spec'd	*/
/*  See timedateparams notes for variable name "rules"			*/
/*  One calculation is field negation, and the other is format 		*/
/*  To support the work, get_latlonparams is responsible for getting	*/
/*  the format (including which characters separate minutes from de-	*/
/*  grees & degrees from NSEW), which-direction-positive convention,	*/
/*  and variable names for input and output latitude and longitude from */
/*  the optional input file.  If input and output formats and/or 	*/
/*  conventions differ,	ioreadrec will call routines to do the work.	*/
{
  char all_latlons[NUM_LATLON*LATLON_WJSTBL+1];
  struct split_wjstbl *per_latlon_tbls;
  char **latlon_wjstbl;
  int *nitems_supplied;
  int num_latlon;
  int i;

  if (create_wjstbl(latlon_optfile,
		    all_latlons,
		    NUM_LATLON*LATLON_WJSTBL)
	== 0					) return 0;

    /*  Make 1 wjstbl for each output latlon requested			*/
  per_latlon_tbls = 
	split_wjstbls (all_latlons,
			NUM_LATLON,
			LATLON_WJSTBL,
			LATLON_SEQUENCE_PREFIX);
  num_latlon = per_latlon_tbls->n;
  nitems_supplied = per_latlon_tbls->count;
  latlon_wjstbl = per_latlon_tbls->wjstbls;

  for (i = 0; i < num_latlon; i++)
    do_latlon(latlon_wjstbl[i],
	      &outlat[i],&outlon[i],&inlat[i],&inlon[i],
	      nitems_supplied[i],latlon_optfile);

  free_split_wjstbl(per_latlon_tbls);

  return num_latlon;
}

char *valid_time_conv(ptr)
char *ptr;
{
  if (  (strncmp(ptr,"gmt",3) == 0) ||
	(strncmp(ptr,"GMT",3) == 0) ||
	(strncmp(ptr,"utc",3) == 0) ||
	(strncmp(ptr,"UTC",3) == 0)	)
    return "utc";
  if (  (strncmp(ptr,"LOCAL",5) == 0) ||
	(strncmp(ptr,"Local",5) == 0) ||
	(strncmp(ptr,"local",5) == 0)  )
    return "local";
  return NULL;
}

void affects_frag(out_struct,inlev,field_type,informat_index)
int inlev,informat_index;
char *field_type;
struct outtime *out_struct;
/*  A piece of an input variable will affect the output time/date	*/
/*  fragment sent to this func.  Check					*/
/*  that it hasn't already being supplied from an earlier input		*/
/*  format field; then that the input data "arrives with or before" 	*/
/*  it.  Then, if OK, link that particular output fragment to the index	*/
/*  the input structure that will provide its information		*/
/*  Input for unrequested output not an error in anticipation of full	*/
/*  input description followed by later decision to omit some output.	*/
{
  char *errtmp;


    /*  Save lots of info even if output variable was not wanted	*/
    /*  Allows us to use input to affect "other" output; eg, yr/mo/da	*/
    /*  calcs must be done in Julian case even if only time is wanted	*/
    /*  on output.  Similarly, "small" time pieces can affect rounding	*/
    /*  of large ones even if small ones aren't output			*/

  if (*out_struct->source_field_type == '\0')
    *out_struct->source_field_type = *field_type;
  else {
    errtmp = buildstring (field_type,  " and ",
		          out_struct->source_field_type,
			  " building time/date error string");
    err ("Duplicate time/date input fields, types b and ",errtmp);
    free(errtmp);
  }

  if (out_struct->lev < inlev) {
    errtmp = buildstring ("Output time/date  ",
			 out_struct->var_type, 
			 "\n   variable occurs before its input variable ",
			 " building time/date error string");
    errn (buildstring(errtmp,
			TIMEDATE_INPUT_KEYWORD_PREFIX,
			NULL,
			" building time/date error string"),
	    informat_index+1);
    free(errtmp);
  }

  out_struct->intime_index = informat_index;

    /*  Save level if variable wasn't asked for.  Level set 		*/
    /*  elsewhere for wanted variables.					*/
  if (out_struct->var == OUTPUT_NOT_REQUESTED) out_struct->lev = inlev;

  return;
}

void do_outtime_frags(template_type,fmt_type,level,ntemplates,
	out_td,buflist,index,calc_j,calc_hhmm)
struct outtime out_td[];
char *buflist[];
char template_type;
char **fmt_type;
int level,ntemplates;
int *index,*calc_j,*calc_hhmm;
{
char template_string[1+1] = {'\0', '\0'};

*fmt_type = NULL;	/*  Represents default numeric input		*/

template_string[0] = template_type;

  /*  Check all output fragments affected by this field.	*/
switch (template_type) {

  case 'b':  case 'B':		/* Month name (abbr or full)	*/
    *fmt_type = "[A-Z,a-z]";
  case 'm':			/* Month number			*/
    affects_frag(&out_td[MONTH_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[MONTH_FRAG].fragbuf;
    break;

  case 'd':			/* Day of month			*/
    affects_frag(&out_td[DAY_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[DAY_FRAG].fragbuf;
    break;

  case 'F':			/* DST flag			*/
    *fmt_type = "[A-Z,a-z,0-9]";
  case 'f':  			/* DST offset			*/
    affects_frag(&out_td[DST_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[DST_FRAG].fragbuf;
    break;

  case 'H':  case 'I':		/* Hour (24 or 12 hour clock)	*/
    affects_frag(&out_td[HOUR_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[HOUR_FRAG].fragbuf;
    break;

  case 'j':  case 'v':	/* Julian w leapyear (1 or 0 = 1 Jan)	*/
  case 'J':  case 'V':	/* Julian w/o leapyear (1 or 0 = 1 Jan)	*/
    affects_frag(&out_td[MONTH_FRAG],level,template_string,ntemplates);
    affects_frag(&out_td[DAY_FRAG],level,template_string,ntemplates);
      /*  No point in doing Julian calculations once each for	*/
      /*  month & day.  Set up so that "first" output (variable	*/
      /*  closer to level 0-month if a tie) does calculation.	*/
      /*  Eventually set other variable's template type to	*/
      /*  something which means "get your value from saved	*/
      /*  info " - see calc_timedateparams.  Don't do it here	*/
      /*  or changed template type could appear in diagnostic	*/
      /*  and be non-meaningful					*/
      /*  Note that if output was not requested, .lev is large	*/
    if (out_td[MONTH_FRAG].lev <= out_td[DAY_FRAG].lev) {
      buflist[(*index)++] = out_td[MONTH_FRAG].fragbuf;
      *calc_j = MONTH_FRAG;
    } else {
      buflist[(*index)++] = out_td[DAY_FRAG].fragbuf;
      *calc_j = DAY_FRAG;
    }
    break;

  case 'M':			/* Minute of hour		*/
    affects_frag(&out_td[MINUTE_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[MINUTE_FRAG].fragbuf;
    break;

  case 'N':			/* 24 hour time			*/
	      /*  See Julian comments - same stuff applies		*/
    affects_frag(&out_td[HOUR_FRAG],level,template_string,ntemplates);
    affects_frag(&out_td[MINUTE_FRAG],level,template_string,ntemplates);
    if (out_td[HOUR_FRAG].lev <= out_td[MINUTE_FRAG].lev) {
      buflist[(*index)++] = out_td[HOUR_FRAG].fragbuf;
      *calc_hhmm = HOUR_FRAG;
    } else {
      buflist[(*index)++] = out_td[MINUTE_FRAG].fragbuf;
      *calc_hhmm = MINUTE_FRAG;
    }
    break;

  case 'p':			/* AM/PM flag			*/
    *fmt_type = "[A-Z,a-z]";
    affects_frag(&out_td[AMPM_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[AMPM_FRAG].fragbuf;
    break;

  case 'Z': 	  		/* time zone name		*/
    *fmt_type = "[A-Z,a-z,0-9,+,-]"; /* ? maybe GMT+7 is a name	*/
    affects_frag(&out_td[TIME_OFFSET_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[TIME_OFFSET_FRAG].fragbuf;
    break;
  case 'R':			/* displ. from prime (hrs)	*/
  case 'z': 	  		/* time zone  -12 to 12; W pos.	*/
    *fmt_type = "[0-9,+,-]";
    affects_frag(&out_td[TIME_OFFSET_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[TIME_OFFSET_FRAG].fragbuf;
    break;

  case 'S':			/* Second of minute		*/
      /*  Specification of this also affects FRACTION_FRAG, since that	*/
      /*  eventually represents fractions of minutes.  Couldn't figure	*/
      /*  out how to do it here, so as of 2.6a (& ioreadrec_ 1.5a)	*/
      /*  FRACTION_FRAG.source_type_field gets set in ioreadrec_ when	*/
      /*  appropriate (I hope!) (12 May 00)				*/
    affects_frag(&out_td[SECOND_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[SECOND_FRAG].fragbuf;
    break;

  case 'y':  case 'Y':		/* Year (without or with century) */
    affects_frag(&out_td[YEAR_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[YEAR_FRAG].fragbuf;
    break;

      /*  Each fraction affects the fraction fragment, and all		*/
      /*  time fragments shorter than it is.  For example, an		*/
      /*  hour fraction affects minutes and seconds (but not		*/
      /*  hours, which has its own template specifier).			*/
      /*  Therefore, there are no break statements in the next		*/
      /*  group of cases and the order of the cases is significant	*/
      /*  All values will be extracted from FRACTION buffer		*/

  case 'g':  case 'K':	/* Fraction of year w leapyear (1 or 0 = 1 Jan)   */
  case 'G':  case 'P':	/* Fraction of year w/o leapyear (1 or 0 = 1 Jan) */
    affects_frag(&out_td[MONTH_FRAG],level,template_string,ntemplates);
    affects_frag(&out_td[DAY_FRAG],level,template_string,ntemplates);
	      /*  See Julian comments - same stuff applies		*/
    *calc_j =
      (out_td[MONTH_FRAG].lev <= out_td[DAY_FRAG].lev) ? MONTH_FRAG : DAY_FRAG;
  case 'o':			/* Fraction of day		*/
    affects_frag(&out_td[HOUR_FRAG],level,template_string,ntemplates);
  case 'O':			/* Fraction of hour		*/
    affects_frag(&out_td[MINUTE_FRAG],level,template_string,ntemplates);
  case 'q':			/* Fraction of minute		*/
    affects_frag(&out_td[SECOND_FRAG],level,template_string,ntemplates);
  case 'Q':			/* Fraction of second		*/
    affects_frag(&out_td[FRACTION_FRAG],level,template_string,ntemplates);
    buflist[(*index)++] = out_td[FRACTION_FRAG].fragbuf;
    break;

  case TIMEDATE_SKIPFIELD_CHAR: /* No affected frags,		*/
    *fmt_type = "c";		/*  but must			*/
    break;			/*   accept character(s)	*/

  default:
    err (buildstring(
		"Unrecognized template specifier ",
		template_string,
		" for input time/date variable ",
		" bad_template_spec"			),
	 varname);
    break;
}

return;
}

void err_sing_field_templ_mismatch(time_piece,template)
char *time_piece,*template;
{
  char *ptr;

  if (  (ptr = strchr(template,WJSTBL_WHITE_SPACE)) != NULL  ) *ptr = '\0';

  ptr = lengthen_str("\
Time template problem\n\
  Mismatch between integer and decimal parts of single-field template \n\
    specifier.  Decimal part specifies a fraction of a ",
		     time_piece,
		     ";\n    integer part does not specify a ",
		     512,
		     "single-field template1"
		    );
  ptr = lengthen_str(ptr,time_piece,"\n  Template = ",512,
						"single-field template2");
  ptr = lengthen_str(ptr,
		     template,
		     "\n  Template is for input time/date variable ",
		     512,
		     "single-field template3"
		    );
  err (ptr,varname);
  return;
}

Logical widthless_template(template)
char **template;
/*  Templates can either specify the widths of subfields of input or	*/
/*  not.  If not, the input can still supply more than one output	*/
/*  field if the input is numeric and has a fraction attached.		*/
/*    Widthless input is represented by templates of the form		*/
/*  X  or XY, where X represents what the integer part of the		*/
/*  field represents, and Y represents both that there is a		*/
/*  fractional part, and the format of that fractional part.		*/
/*  X., X.X & X.Y templates are also accepted and turned into XY.	*/
/*    This routine returns a widthless/widths flag.  It also changes	*/
/*  the input template pointer to a pointer to the appropriate XY	*/
/*  template if necessary.						*/
{
  static char XY_template[2+1] = { '\0', '\0', WJSTBL_WHITE_SPACE };
  char X,Y;
  char *ptr;

  ptr = *template;
  X = *(ptr++);
  Y = *(ptr++);
    /*  If first char of template is repeated, we have "standard"	*/
    /*  template, so no multifragment situation				*/
  if (X == Y) return FALSE;
    /*  If format is just X, we have multifragments, but don't have to	*/
    /*  fiddle with buffer						*/
  if (Y == WJSTBL_WHITE_SPACE) return TRUE;

/*  Change X., X.X, & X.Y templates to XY.  Check X/Y combos for	*/
/*  validity.  Move template to its own buffer				*/

  if (Y == '.') Y = *(ptr++);
  if (Y == WJSTBL_WHITE_SPACE) Y = X;
  else if (*ptr != WJSTBL_WHITE_SPACE) {
    if (  (ptr = strchr(*template,WJSTBL_WHITE_SPACE)) != NULL  ) *ptr = '\0';
      err (buildstring(
		"Single-field template specifier must be of form X, X.,\
 X.X, or X.Y\n  Template = ",
		*template,
		" for input time/date variable ",
		" bad_single_template_fmt"				   ),
	   varname);
  }

  if (X == Y)
      /*  User has given us X. or X.X. Supply Y			*/
    switch (X) {
      case 'd': case 'j': case 'J': case 'v': case 'V':	/* days		*/
	Y = 'o';					/* frac day	*/
	break;
      case 'H': case 'I':				/* hours	*/
	Y = 'O';					/* frac hours	*/
	break;
      case 'M':						/* minutes	*/
	Y = 'q';					/* frac minutes	*/
	break;
      case 'S':						/* seconds	*/
	Y = 'Q';					/* frac seconds	*/
	break;
      case 'y': case 'Y':				/* years	*/
	Y = 'P';		/*  A guess - jul day 0-364; no leaps	*/
	break;
      default:
	if (  (ptr = strchr(*template,WJSTBL_WHITE_SPACE)) != NULL  )
	  *ptr = '\0';
	err (buildstring(
		"Unrecognized single-field template specifier ",
		*template,
		" for input time/date variable ",
		" bad_single_template_spec"				   ),
	     varname);

    }
  else
      /*  User has given us XY or X.Y. Check validity			*/
    switch (Y) {
	/*  One of the year fractions must go with one of the years	*/
      case 'g':  case 'G':  case 'K':  case 'P':
	if (  (X == 'Y') || (X == 'y')  ) break;
	err_sing_field_templ_mismatch("year",*template);
	  /*  day frac must go with day or one of the Julian days	*/
      case 'o':
	switch (X) {
	  case 'd':  case 'j':  case 'J':  case 'v':  case 'V':
	    break;
	  default:
	    err_sing_field_templ_mismatch("Julian day",*template);
	}
	break;
      case 'O':		/*  hour frac must go with 24 or 12 hour hour	*/
	if (  (X == 'H') || (X == 'I')  ) break;
	err_sing_field_templ_mismatch("hour",*template);
      case 'q':		/*  minute frac must go with minute		*/
	if (X == 'M') break;
	err_sing_field_templ_mismatch("minute",*template);
      case 'Q':		/*  second frac must go with second		*/
	if (X == 'S') break;
	err_sing_field_templ_mismatch("second",*template);
      default:
	if (  (ptr = strchr(*template,WJSTBL_WHITE_SPACE)) != NULL  )
	  *ptr = '\0';
	err (buildstring(
		"Unrecognized decimal part of single-field template ",
		*template,
		" for input time/date variable ",
		" bad_dec_single_template_spec"				),
	     varname);
    }

  XY_template[0] = X;
  XY_template[1] = Y;

  *template = XY_template;
  return TRUE;
}

void force_use_of_frag (out_td,outvar_level)
struct outtime *out_td;
int outvar_level;
{
/*  If user did not request fragment, request it "for ourselves"	*/
  if (out_td->var == OUTPUT_NOT_REQUESTED) {
    out_td->var = TIME_INTERNAL_USE;
    out_td->lev = outvar_level;
  }
  return;
}

int get_timedateparams(timedate_optfile,out_td,in_td,fragbuflist)
struct fileinfo *timedate_optfile;
struct outtime out_td[NUM_OUTTIMES][NUM_TIMEDATE_FRAGS];
struct intime in_td[NUM_OUTTIMES][NUM_TIMEDATE_FRAGS];
char *fragbuflist[NUM_OUTTIMES][2 * NUM_TIMEDATE_FRAGS];
/*  Returns number of output times requested				*/
/*	General idea is that there are fragments of time/date info	*/
/*  scattered among the input variables and we want to reformat and/or	*/
/*  reassemble some of them into standard formats.			*/
/*	We will identify all the fragments supplied and tie them to	*/
/*  output structures as well as the input variables they come from.	*/
/*  At the level an input variable is read, it is "mined" for its frag-	*/
/*  ments and those fragments are reformatted if necessary.  The	*/
/*  fragments are stored in the output time structures they belong to.	*/
/*  At the level the output value is to appear, it is assembled from	*/
/*  the saved fragments.						*/
/*	We special-case the common situation where an input field	*/
/*  contains only 1 "kind" of time.  Such fields are either that	*/
/*  kind of time, or that kind and all smaller kinds.  For example, a	*/
/*  day field w/o a decimal pt is just a day, while a day field w/ a	*/
/*  decimal point specifies day, hour, min, & sec.  To an extent, this	*/
/*  means many fields can be described both in this special case, and	*/
/*  in full "template" style.  The special case is easier to input,	*/
/*  since widths are irrelevant, but the real reason for it is that	*/
/*  we decode special fields w/floating point, allowing input of E	*/
/*  format, which we can't do any other (easy) way			*/
/*	This routine handles the creation of the links between input	*/
/*  and output structures (and the necessary validity checks, etc)	*/
/*  There are logically 10 fragments-year, month, day, hour, am/pm	*/
/*  flag, min, sec, fraction of a second, time zone and daylight	*/
/*  savings time flag. (Other input fragments decompose into these; eg,	*/
/*  fraction of a minute is seconds and fractions of seconds).  These	*/
/*  could all appear in 1 input variable, or each could appear in its	*/
/*  own, or any combination.  The input structures consist of a 10	*/
/*  element structure, each element corresponding to 1 input template,	*/
/*  and a 10 element vector of pointers to buffers to contain decoded	*/
/*  reformatted fragments.  There is one output structure per fragment	*/
/*  containing, among other things, the buffers mentioned.  There is	*/
/*  logically one structure per output agglomeration, too, for those	*/
/*  output variables that consist of more than one fragment.		*/
/*  Exactly one output structure contains a pointer to the template it	*/
/*  comes from.  The template structure, however, contains pointers to	*/
/*  each fragment it will supply.  The idea is to decode only once, no	*/
/*  matter how many fragments there are, and no matter if fragments are	*/
/*  to be used at lower levels.  It works as long as at least one frag-	*/
/*  ment is used at the same level as the input variable.		*/
/*	Note about adding new template types:				*/
/*  K&R defines "ANSI standard" stuff for strftime.  The "world" has	*/
/*  added more.  As of 25 Jul 96, I found use of a, A, b, B, c, C, d,	*/
/*  D, e, h, H, I, j, J, k, l, m, M, n, p, r, S, t, T, U, w, W, x, X,	*/
/*  y, Y, & Z.  Not all are formats, but all are used.  I've added	*/
/*  E, f, F, g, G, K, N, o, O, P, q, Q, R, s, v, V & z.  Good luck!	*/
{
  char all_timedates[NUM_OUTTIMES*TIMEDATE_WJSTBL + 1];
  struct split_wjstbl *per_outtime_tbls;
  char **timedate_wjstbl;
  int *timedate_tags;
  int num_outtimes;

  int *nitems_supplied;
  int ntemplates,lenprefix,len_inpfield;
  int nitems_processed;
  int fragbuflist_index;

    /*  Could probably calculate N_ALTERNATE_INVAR_FMTS by use of	*/
    /*  sizeof, etc, but didn't...					*/
#define N_ALTERNATE_INVAR_FMTS 4
  char *invar_fmts[N_ALTERNATE_INVAR_FMTS] = {"%d%s","_%d%s","%d_%s","_%d_%s"};

  int i,j,k,level;
  char *ptr,*ptr2,*tmp,*fmt_ptr,*fmt_type,*keyword;
  char template_string[1+1] = {'\0', '\0'};
  char sep_str[1+1];
  struct intime *in_ptr;
  div_t digs;

  int calculate_julian;
  int calculate_hhmm;

  Logical var_used[NVAR];
  Logical got_yrmoday_input,got_hourmin_input;

  if (create_wjstbl(timedate_optfile,
		    all_timedates,
		    NUM_OUTTIMES*TIMEDATE_WJSTBL)
	 == 0						) return 0;

  if (isalnum(TIMEDATE_SKIPFIELD_CHAR)) {
      /*  Use template_string as buffer for error message		*/
    *template_string = TIMEDATE_SKIPFIELD_CHAR;
    err ("Bad time/date skipfield character.  Char is ",template_string);
  }

    /*  Make 1 wjstbl for each output time requested			*/
  per_outtime_tbls = 
	split_wjstbls (all_timedates,
			NUM_OUTTIMES,
			TIMEDATE_WJSTBL,
			TIMEDATE_SEQUENCE_PREFIX);
  num_outtimes = per_outtime_tbls->n;
  nitems_supplied = per_outtime_tbls->count;
  timedate_tags = per_outtime_tbls->tag;
  timedate_wjstbl = per_outtime_tbls->wjstbls;

  for (k=0; k<num_outtimes; k++) {

    nitems_processed = 0;
    fragbuflist_index = 0;
    template_string[0] = '\0';
      /*  Logically, these are set if needed.  However, it's convenient	*/
      /*  to use them whether needed or not.  Any valid fragment ID	*/
      /*  (including 0) should do here, but chose these "relevant" ones.. */
    calculate_julian = MONTH_FRAG;
    calculate_hhmm = HOUR_FRAG;

      /*  Set up UTC<->local conversion option				*/
    ptr = lookup_wjstbl("intimeconvention",timedate_wjstbl[k]);
    if (ptr == NULL)
      ptr = "local";
    else
      if (  (tmp = valid_time_conv(ptr)) == NULL  )
        err ("Input time convention not UTC, GMT, or local.  Convention = ",
	     ptr);
      else {
        ptr = tmp;
        nitems_processed++;
      }

    ptr2 = lookup_wjstbl("outtimeconvention",timedate_wjstbl[k]);
    if (ptr2 == NULL)
      ptr2 = "local";
    else
      if (  (tmp = valid_time_conv(ptr2)) == NULL  )
        err ("Output time convention not UTC, GMT, or local.  Convention = ",
	      ptr2);
      else {
        ptr2 = tmp;
        nitems_processed++;
      }

    if (strcmp(ptr,ptr2) == 0) {
      out_td[k][HOUR_FRAG].conv = NO_TIME_ZONE_CONVERSION;
      out_td[k][ISO8601_FRAG].conv = 
	(*ptr == 'l') ? OUTPUT_IS_LOCAL  : OUTPUT_IS_UTC ;
    } else { 
      out_td[k][HOUR_FRAG].conv = 
	(*ptr == 'l') ? CONVERT_LOCAL_TO_UTC  : CONVERT_UTC_TO_LOCAL ;
      out_td[k][ISO8601_FRAG].conv = 
	(*ptr == 'l') ? OUTPUT_IS_UTC  : OUTPUT_IS_LOCAL ;
    }

      /* keyword is prefix + # (assumed <= 99) + longer of postfixes	*/
      /* ("name" & "template").  Underscores are optional		*/
    lenprefix = strlen(TIMEDATE_INPUT_KEYWORD_PREFIX);
    keyword = buildstring(TIMEDATE_INPUT_KEYWORD_PREFIX,
			  "_99_template",
			  NULL,
			  " building time/date keyword");

      /*  Get any output variables.					*/
    i = get_var_from_keyword
			("outjulianname",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
      ptr = lookup_wjstbl("outjulianformat",timedate_wjstbl[k]);
      if (ptr == NULL)
		err ("No format for output julian date variable ",varname);
      if (  *(ptr+1) != WJSTBL_WHITE_SPACE  ) err (
	"Julian output format longer than 1 char for output time variable ",
	varname				   	  );
	/*  outjulianformat is an "odd" item, like the time conventions	*/
	/*  so count it here.  The "regular" items (inname, intemplate,	*/
	/*  and outname) are counted while doing some checking		*/
      nitems_processed++;
      switch (*ptr) {
	case 'j':  case 'J':  case 'v':  case 'V':
	case 'g':  case 'G':  case 'P':  case 'K':
	  break;
	case 's':  case 'E':
	  if (get_int_from_keyword(&out_td[k][JULIAN_FRAG].baseyear,
				   "outjulianbaseyear",
				    timedate_wjstbl[k],
				    0,9999,"decoding julian base year")
		== NULL	)  {
	    out_td[k][JULIAN_FRAG].baseyear = DEFAULT_JULIAN_BASE_YEAR;
	  } else {
	    nitems_processed++;
	  }
	  if (out_td[k][JULIAN_FRAG].baseyear < MINIMUM_JULIAN_BASE_YEAR)
	    errn ("Base year for time-since-base-year computation is too small\
.  Offending value is ",out_td[k][JULIAN_FRAG]);
	  break;
	default:
	    /*  Put # in next message as marker	for bad format char	*/
	  tmp = "Julian output format # not one of gGPKjJvVsE for \
output time variable ";
	  if (  (ptr2=strchr(tmp,'#')) != NULL  )  *ptr2 = *ptr;
	  err (tmp,varname);
	  break;
      }
      out_td[k][JULIAN_FRAG].conv = *ptr;
      out_td[k][JULIAN_FRAG].var = i;
      out_td[k][JULIAN_FRAG].lev = iovarlevel__(i);
    }
    i = get_var_from_keyword
			("outyearname",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
      out_td[k][YEAR_FRAG].var = i;
      out_td[k][YEAR_FRAG].lev = iovarlevel__(i);
    }
    i=get_var_from_keyword
			("outmonthname",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
      out_td[k][MONTH_FRAG].var = i;
      out_td[k][MONTH_FRAG].lev = iovarlevel__(i);
    }
    i = get_var_from_keyword
			("outdayname",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
      out_td[k][DAY_FRAG].var = i;
      out_td[k][DAY_FRAG].lev = iovarlevel__(i);
    }
    i = get_var_from_keyword
		("outyearmonthdayname",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
      out_td[k][YEARMONTHDAY_FRAG].var = i;
      i = iovarlevel__(i);
      out_td[k][YEARMONTHDAY_FRAG].lev = i;
	/*    We do YEARMONTHDAY by setting up to be sure that YEAR, 	*/
	/*  MONTH, and DAY are done, whether by us or by user		*/
	/*  We honor user levels if user requested components of 	*/
	/*  YEARMONTHDAY, but will check that YEARMONTHDAY level is,	*/
	/*  ultimately, >= all components				*/
	/*    No input fragments will contribute to YEARMONTHDAY_FRAG	*/
      force_use_of_frag(&out_td[k][YEAR_FRAG],i);
      force_use_of_frag(&out_td[k][MONTH_FRAG],i);
      force_use_of_frag(&out_td[k][DAY_FRAG],i);
    }

      /*  Use HOUR to indicate that output time has been requested,	*/
      /*  since can't have output time without it.  Asking for time is	*/
      /*  equiv to asking for all its fragments.  Better: any specified	*/
      /*  input time fragments "belong" to same output variable.  In	*/
      /*  any case, do not need variables or levels for MINUTE_FRAG or	*/
      /*  other possible components of time.  However, for convenience	*/
      /*  of checking, set up the components				*/
    i = get_var_from_keyword
			("outtimename",timedate_wjstbl[k],timedate_optfile);
    out_td[k][HOUR_FRAG].var = i;
    out_td[k][MINUTE_FRAG].var = i;
    out_td[k][SECOND_FRAG].var = i;
    out_td[k][FRACTION_FRAG].var = i;
    if (i >= 0) {
      i = iovarlevel__(i);
      out_td[k][HOUR_FRAG].lev = i;
      out_td[k][MINUTE_FRAG].lev = i;
      out_td[k][SECOND_FRAG].lev = i;
      out_td[k][FRACTION_FRAG].lev = i;
    }

      /*  Similarly, time offset is required if output time displacement */
      /*  is to be calculated and do not need vars/levels for other	*/
      /*  offset frags							*/
    i = get_var_from_keyword
		("outdisplacementname",timedate_wjstbl[k],timedate_optfile);
    out_td[k][TIME_OFFSET_FRAG].var = i;
    out_td[k][DST_FRAG].var = out_td[k][TIME_OFFSET_FRAG].var;
    if (i >= 0) {
      i = iovarlevel__(i);
      out_td[k][TIME_OFFSET_FRAG].lev = i;
      out_td[k][DST_FRAG].lev = i;
    }

    i = get_var_from_keyword
		("outISO8601name",timedate_wjstbl[k],timedate_optfile);
    if (i >= 0) {
/*	See comments in YEARMONTHDAY section.  This section "down here"	*/
/*	since it must be after output times (if output times spec'd,	*/
/*	must honor those levels)					*/
      out_td[k][ISO8601_FRAG].var = i;
      i = iovarlevel__(i);
      out_td[k][ISO8601_FRAG].lev = i;
      force_use_of_frag(&out_td[k][YEAR_FRAG],i);
      force_use_of_frag(&out_td[k][MONTH_FRAG],i);
      force_use_of_frag(&out_td[k][DAY_FRAG],i);
      force_use_of_frag(&out_td[k][HOUR_FRAG],i);
      force_use_of_frag(&out_td[k][MINUTE_FRAG],i);
      force_use_of_frag(&out_td[k][SECOND_FRAG],i);
    }

      /*  Input variables, templates, etc				*/
    for (ntemplates=0; ntemplates<NUM_TIMEDATE_FRAGS; ntemplates++) {
      in_ptr = &in_td[k][ntemplates];
      for (i = 0; i < N_ALTERNATE_INVAR_FMTS; i++) {
	sprintf(keyword+lenprefix,invar_fmts[i],ntemplates+1,"name");
	if (  (in_ptr->var =
	    get_var_from_keyword(keyword,timedate_wjstbl[k],timedate_optfile))
				  >= 0					    )
	  break;
      }
      if (in_ptr->var < 0) break;

      for (i = 0; i < N_ALTERNATE_INVAR_FMTS; i++) {
	sprintf(keyword+lenprefix,invar_fmts[i],ntemplates+1,"template");
	if (  (ptr = lookup_wjstbl(keyword,timedate_wjstbl[k])) != NULL  )
	  break;
      }
      if (ptr == NULL)
		err ("No template for time/date input variable ",varname);



        /*  Get all output fragments from this input variable, and	*/
        /*  build format that will extract substrings for these frag-	*/
	/*  ments.							*/

      in_ptr->fragbuf_index = fragbuflist_index;
      level = iovarlevel__(in_ptr->var);
      fmt_ptr = in_ptr->format;
      in_ptr->nfrags_in_substring = 0;

      if (widthless_template(&ptr))  {
	  /*  "Widthless" input - input is decoded as one string, re-	*/
	  /*  gardless of number of fragments it contains		*/
        do_outtime_frags(*ptr,
			 &fmt_type,
			 level,
			 ntemplates,
			 out_td[k],
			 fragbuflist[k],
			 &fragbuflist_index,
			 &calculate_julian,
			 &calculate_hhmm	);
	  /*    Widthless field numeric input is always read in float-	*/
	  /*  ing point.  Be sure fields & frac fields do not change	*/
	  /*  fmt type.  Note that decodes from bufs w/these chars will	*/
	  /*  have to check anyway, since chars accept non-legit	*/
	  /*  numbers							*/
	if (fmt_type == NULL) fmt_type = "[0-9,.,+,-,E,e]";
	in_ptr->nfrags_in_substring++;
	  /*  If there is a second fragment from this variable, process it */
	if ( *(ptr+1) != WJSTBL_WHITE_SPACE ) {
	  in_ptr->nfrags_in_substring++;
	  do_outtime_frags(*(ptr+1),
			   &ptr2,
			   level,
			   ntemplates,
			   out_td[k],
			   fragbuflist[k],
			   &fragbuflist_index,
			   &calculate_julian,
			   &calculate_hhmm	);
	  if (ptr2 != NULL)
	    err ("defgb internal error: do_outtime_frags changed fmt_type","");
        }

	  /*  Set up format for this widthless input.  By defn, number	*/
	  /*  of strings to be decoded for widthless input is 1		*/
        *(fmt_ptr++) = '%';
        strcpy(fmt_ptr,fmt_type);
        in_ptr->nstrings = 1;

      } else {

	  /*  Template says that input contains fixed-field substrings	*/
	  /*  (well, maximum length substrings anyway)			*/
	in_ptr->nstrings = 0;

	while (*ptr != WJSTBL_WHITE_SPACE) {
          *template_string = *ptr;

          if ( ispunct(*ptr) && (*ptr != TIMEDATE_SKIPFIELD_CHAR) )
	    *(fmt_ptr++) = *(ptr++);
          else {
	    len_inpfield=strspn(ptr,template_string);
	    do_outtime_frags(*ptr,
			     &fmt_type,
			     level,
			     ntemplates,
			     out_td[k],
			     fragbuflist[k],
			     &fragbuflist_index,
			     &calculate_julian,
			     &calculate_hhmm  );
	      /*  Default numerics to unsigned integer input.  Be sure	*/
	      /*  that integer data uses this format since code does	*/
	      /*  not check when decoding.				*/
	    if (fmt_type == NULL) fmt_type = "[0-9]";

	      /*  Make %nns format string for this piece of the field.	*/
	      /*    Make nn string from length of field, a la		*/
	      /*  sprintf(fmt_ptr,"%%ds",len_inpfield) [I hope], assum-	*/
	      /*  ing field widths of < 100 characters (and assuming	*/
	      /*  char representation of digit n+1 is one greater than	*/
	      /*  representation of digit n).  Did it this way... for	*/
	      /*  fun, I guess						*/
	      /*    Instead of s, use an set of legal characters	*/
	      /*  appropriate to the template type.			*/
	    *(fmt_ptr++) = '%';

	      /*  If field is to be skipped, indicate in format.  Else	*/
	      /*  increment # fragments this template			*/
	    if (*template_string == TIMEDATE_SKIPFIELD_CHAR) 
	      *(fmt_ptr++) = SSCANF_SKIPFIELD_CHAR;
	    else in_ptr->nstrings++;

	    if (len_inpfield < 10) *(fmt_ptr++) = '0' + len_inpfield;
	    else {
	      digs = div(len_inpfield,10);
	      *(fmt_ptr++) = '0' + digs.quot;
	      *(fmt_ptr++) = '0' + digs.rem;
	    }
	    strcpy(fmt_ptr,fmt_type);
	    fmt_ptr += strlen(fmt_type);

	    ptr += len_inpfield;
          }
        }
      }

      if (in_ptr->fragbuf_index == fragbuflist_index)
        err (keyword," time/date keyword did not specify any input fields");

        /*  Finally, add a check on the real data by trying to get one	*/
        /*  more character than format said is there.  If successful,	*/
        /*  we'll know there is a data irregularity			*/
      strcat(fmt_ptr,"%1s");
    }

    nitems_processed += 2 * ntemplates;
    for (i=0; i<NUM_OUTTIME_VARIABLES; i++)
      if (legal_varnum(out_td[k][i].var)) {
        if (*out_td[k][i].source_field_type == '\0') {
	  ioname__(out_td[k][i].var,varname);
	  err ("No input time/date variables provide info for output ",
	       buildstring(out_td[k][i].var_type," ",varname,"time_date_err"));
	}
	  /*  Next section doesn't really fit, since multifield stuff	*/
	  /*  doesn't really fit.  Also, we're lumping "the 2" together	*/
	  /*  which doesn't really follow logically (maybe a 3rd kind	*/
	  /*  of multifield output will be radically different) Oh well	*/
        if (*out_td[k][i].source_field_type == MULTIFIELD_OUTTIME_SOURCES) {
	  ioname__(out_td[k][i].var,varname);
	  got_yrmoday_input = 
			(*out_td[k][YEAR_FRAG].source_field_type != '\0')
						&&
			(*out_td[k][MONTH_FRAG].source_field_type != '\0')
						&&
			(*out_td[k][DAY_FRAG].source_field_type != '\0');
	  if ( ! got_yrmoday_input)
	    err ("No input time/date variables provide yrmoda info for output ",
		buildstring(out_td[k][i].var_type," ",varname,"time_date_err"));
	  if (i == ISO8601_FRAG) {
	    got_hourmin_input =
			(*out_td[k][HOUR_FRAG].source_field_type != '\0')
						&&
			(*out_td[k][MINUTE_FRAG].source_field_type != '\0');
	    if ( ! got_hourmin_input) err
	      ("No input time/date variables provide hrmin info for output ",
		buildstring(out_td[k][i].var_type," ",varname,"time_date_err"));
	  }
	}
        nitems_processed++;
      }
    if (nitems_supplied[k] > nitems_processed) {
      char tmp[20];
      sprintf (tmp,"time%d_",timedate_tags[k]);
      errn (buildstring("Unprocessed ",tmp," parameters.  Number skipped = ",
			" unproc param msg"),
	    nitems_supplied[k] - nitems_processed);
    }

      /*  Output variables that requires multiple sources of input must	*/
      /*  "have" that info by their output time.  (Variables that need	*/
      /*  single sources have that problem, too, but they are checked	*/
      /*  in affects_frag and these aren't				*/
    if ( legal_varnum(out_td[k][YEARMONTHDAY_FRAG].var) ) {
      check_levs (out_td[k][YEARMONTHDAY_FRAG].lev,"yearmonthday",
		  out_td[k][YEAR_FRAG].lev,"year","s ");
      check_levs (out_td[k][YEARMONTHDAY_FRAG].lev,"yearmonthday",
		  out_td[k][MONTH_FRAG].lev,"month","s ");
      check_levs (out_td[k][YEARMONTHDAY_FRAG].lev,"yearmonthday",
		  out_td[k][DAY_FRAG].lev,"day","s ");
    }
    if ( legal_varnum(out_td[k][ISO8601_FRAG].var) ) {
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][YEAR_FRAG].lev,"year","s ");
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][MONTH_FRAG].lev,"month","s ");
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][DAY_FRAG].lev,"day","s ");
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][HOUR_FRAG].lev,"day","s ");
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][MINUTE_FRAG].lev,"day","s ");
      check_levs (out_td[k][ISO8601_FRAG].lev,"ISO8601",
		  out_td[k][SECOND_FRAG].lev,"day","s ");
    }
    if ( legal_varnum(out_td[k][JULIAN_FRAG].var) ) {      
	/*  If leap years significant, need year input.  Check year	*/
	/*  output, since input can come from variety of sources	*/
      switch (out_td[k][JULIAN_FRAG].conv) {
	case 'K':  case 'v':  case 'j':  case 'g':  case 's':  case 'E':
	  check_levs (out_td[k][JULIAN_FRAG].lev,
			"Time/date Julian-day/fractional-year/time-since-epoch",
		      out_td[k][YEAR_FRAG].lev,"year"," ");
      }
      check_levs (out_td[k][JULIAN_FRAG].lev,
			"Time/date Julian-day/fractional-year/time-since-epoch",
		  out_td[k][MONTH_FRAG].lev,"month"," ");
      check_levs (out_td[k][JULIAN_FRAG].lev,
			"Time/date Julian-day/fractional-year/time-since-epoch",
		  out_td[k][DAY_FRAG].lev,"day"," ");
	/*  Finally, because we're lazy and want to use the other	*/
	/*  output variables to go into output julian, insist that	*/
	/*  output julian occurs last.  No logical necessity for this	*/
      if (  (out_td[k][JULIAN_FRAG].lev < out_td[k][HOUR_FRAG].lev) &&
	    (*out_td[k][HOUR_FRAG].source_field_type != '\0')		)
	err ("Available time information must occur closer to level 0 than ",
	     "the Julian-day/fractional-year/time-since-epoch output level");
    }

    if (*out_td[k][HOUR_FRAG].source_field_type == 'I')
      if (*out_td[k][AMPM_FRAG].source_field_type == '\0')
        err ("Time/date I format requires AM/PM information","");

    if (out_td[k][HOUR_FRAG].conv != NO_TIME_ZONE_CONVERSION) {
      check_levs (out_td[k][HOUR_FRAG].lev,"hour",
		  out_td[k][TIME_OFFSET_FRAG].lev,"time displacement","s ");
      check_levs (out_td[k][MINUTE_FRAG].lev,"minute",
		  out_td[k][TIME_OFFSET_FRAG].lev,"time displacement","s ");
    }
 
      /*  Converting Julian day to day of month may require leap year info */
      /*    calculate_julian is whichever of MONTH_FRAG or DAY_FRAG 	*/
      /*    that occurs "first" (or at all, if month or day output not	*/
      /*    requested).  This is the output frag which determines when	*/
      /*    Julian input is used; hence year must be there "by that	*/
      /*    time".  If things OK, change the "other" fragment's source	*/
      /*    type so that it will use saved date information		*/
    switch (*out_td[k][calculate_julian].source_field_type) {
      case 'v':  case 'j':  case 'g':  case 'K':
        if (out_td[k][calculate_julian].lev < out_td[k][YEAR_FRAG].lev)
	  err ("Time/date Julian day/fractional year input ",
	       "requires year information");
	/*  Deliberately fall through					*/
      case 'V':  case 'J':  case 'G':  case 'P':
        *out_td[k]
		  [MONTH_FRAG + DAY_FRAG - calculate_julian]
		  .source_field_type
	         = JULIAN_SOURCES;
        break;
      default:
        break;
    }

      /*  See calculate_julian comments above				*/
    if (*out_td[k][calculate_hhmm].source_field_type == 'N')
      *out_td[k]
		[HOUR_FRAG + MINUTE_FRAG - calculate_hhmm]
		.source_field_type 
	       = JULIAN_SOURCES;

    free (keyword);
  }			/* end of (k=0; k<num_outtimes; k++) loop	*/

    /*  Check that output variables are unique.  Logically this should	*/
    /*  be done as they are processed, but this is lots easier		*/
  for (j=0; j<NVAR; j++)
    var_used[j]=FALSE;
  for (k=0; k<num_outtimes; k++) {
    for (i=0; i<NUM_OUTTIME_VARIABLES; i++) {
      j = out_td[k][i].var;
      if (legal_varnum(j)) {
	if (var_used[j]) {
	  ioname__(j,varname);
	  err ("Single variable named for 2 different time outputs. Var = ",
	       varname);
	} else {
	  var_used[j] = TRUE;
	}
      }
    }
  }

  free_split_wjstbl(per_outtime_tbls);

  return num_outtimes;
}

/**** to continue work on generated data, include ioopen_gendata.c here	*/

/*****************  init's below  ************************************	*/

void init_latlon_structs (outlon,inlon,outlat,inlat)
struct latlonformat outlat[NUM_LATLON],outlon[NUM_LATLON];
struct latlonformat inlat[NUM_LATLON],inlon[NUM_LATLON];
{
  int i;

  for (i=0; i<NUM_LATLON; i++) {
    inlat[i].var = INPUT_NOT_SUPPLIED;
    outlat[i].var = OUTPUT_NOT_REQUESTED;
    inlon[i].var = INPUT_NOT_SUPPLIED;
    outlon[i].var = OUTPUT_NOT_REQUESTED;

    inlat[i].legal_cardinal = LEGAL_LATCARDINALS;
    outlat[i].legal_cardinal = LEGAL_LATCARDINALS;
    inlon[i].legal_cardinal = LEGAL_LONCARDINALS;
    outlon[i].legal_cardinal = LEGAL_LONCARDINALS;

    inlat[i].convention = *inlat[i].legal_cardinal;
    outlat[i].convention = *outlat[i].legal_cardinal;
    inlon[i].convention = *inlon[i].legal_cardinal;
    outlon[i].convention = *outlon[i].legal_cardinal;

    inlat[i].sep[0] = DEFAULT_LATLONSEP;
    inlat[i].sep[1] = '\0';
    inlon[i].sep[0] = DEFAULT_LATLONSEP;
    inlon[i].sep[1] = '\0';

    COPY_INTO_FIXED_LEN_BUFFER(inlat[i].format,DEFAULT_LATLONFORMAT,
							"inlat format");
    COPY_INTO_FIXED_LEN_BUFFER(outlat[i].format,DEFAULT_LATLONFORMAT,
							"outlat format");
    COPY_INTO_FIXED_LEN_BUFFER(inlon[i].format,DEFAULT_LATLONFORMAT,
							"inlon format");
    COPY_INTO_FIXED_LEN_BUFFER(outlon[i].format,DEFAULT_LATLONFORMAT,
							"outlon format");
  }
  return;
}

void init_time_structs (in,out,buflist,cumtime,fractime)
struct intime in[NUM_OUTTIMES][NUM_TIMEDATE_FRAGS];
struct outtime out[NUM_OUTTIMES][NUM_TIMEDATE_FRAGS];
struct timefrac fractime[NUM_OUTTIMES];
struct cumtime cumtime[NUM_OUTTIMES];
char *buflist[NUM_OUTTIMES][2 * NUM_TIMEDATE_FRAGS];
{
  int i,j;
  char *scratch;

  scratch = out[0][0].fragbuf;

  for (j=0; j<NUM_OUTTIMES; j++) {
    cumtime[j].year = -1;
    fractime[j].fracyear = FALSE;
    for (i=0; i<NUM_TIMEDATE_FRAGS; i++) {
      in[j][i].var = INPUT_NOT_SUPPLIED;

      out[j][i].var = OUTPUT_NOT_REQUESTED;
      out[j][i].lev = MAXLEVELS; /* Also indicates non-requested option	*/
      out[j][i].max = INT_MAX;	/*  Mark everything "no maximum"	*/
      out[j][i].fmt = NULL;
      out[j][i].intime_index = -1;	/* More "not requested"		*/
      out[j][i].baseyear = -1;	/*  Not provided			*/
      out[j][i].conv = '\0';
      *out[j][i].source_field_type = '\0';
      *(out[j][i].source_field_type + 1) = '\0';

        /*  All buflists need to be legit addresses but not all get	*/
        /*  used.  See get_timedate_frag for more info			*/
      buflist[j][i] = scratch;
      buflist[j][NUM_TIMEDATE_FRAGS + i] = scratch;
    }

    out[j][JULIAN_FRAG].var_type = "julian";
    out[j][YEAR_FRAG].var_type = "year";
    out[j][MONTH_FRAG].var_type = "month";
    out[j][DAY_FRAG].var_type = "day";
    out[j][YEARMONTHDAY_FRAG].var_type = "yearmonthday";
    out[j][ISO8601_FRAG].var_type = "ISO8601";
    out[j][HOUR_FRAG].var_type = "time";  /* Represents all pieces of time */
    out[j][MINUTE_FRAG].var_type = out[j][HOUR_FRAG].var_type;
    out[j][SECOND_FRAG].var_type = out[j][HOUR_FRAG].var_type;
    out[j][FRACTION_FRAG].var_type = out[j][HOUR_FRAG].var_type;
    out[j][AMPM_FRAG].var_type = out[j][HOUR_FRAG].var_type;
    out[j][TIME_OFFSET_FRAG].var_type = "displacement from prime meridian";
    out[j][DST_FRAG].var_type = out[j][TIME_OFFSET_FRAG].var_type;

      /*  out[].max not defined here, since many values are logically	*/
      /*  constant, and others are inconsistently used (eg, max days	*/
      /*  is max permissible day; max fraction is smallest impermissible) */
      /*  See calc_timedateparams for use of various fields		*/

      /*  Some formats can't be set here (I think).  Code that needs	*/
      /*  them must handle situation.  Some formats are constants in	*/
      /*  the code.  To avoid consistency problems, those are not here	*/
    out[j][YEAR_FRAG].fmt = "%d";
    out[j][MONTH_FRAG].fmt = "%02d";
    out[j][DAY_FRAG].fmt = "%02d";
    out[j][YEARMONTHDAY_FRAG].fmt = "%d";
    out[j][TIME_OFFSET_FRAG].fmt = "%+05d";

      /*  Julian out comes from multiple source field types (everything	*/
      /*  that comes in!), so get_timedateparams can't fill this	*/
      /*  field in the normal way.  Seeing it's abnormal already, do it	*/
      /*  here rather than there					*/
    *out[j][JULIAN_FRAG].source_field_type = JULIAN_SOURCES;
      /*  YEARMONTHDAY & ISO8601 logically have same issues as JULIAN,	*/
      /*  but w/initial implementation anyway, source_field_type	*/
      /*  doesn't get used (except to pass "must have source field"	*/
      /*  input check)							*/
    *out[j][YEARMONTHDAY_FRAG].source_field_type = MULTIFIELD_OUTTIME_SOURCES;
    *out[j][ISO8601_FRAG].source_field_type = MULTIFIELD_OUTTIME_SOURCES;
  }

  return;
}

char *do_idmissing(wjstbl,return_struct,nitems_supplied)
  /*  Returns list of variables to which id_missing_struct "applies"	*/
char *wjstbl;
int nitems_supplied;
struct idmissing **return_struct;
{
  int nitems_processed,len,special_word_len;
  struct idmissing *missing_value_struct;
  char chr,comp1,comp2;
  char *value;
  char *temp;
  char *special_word;
  char *varlist;
  Logical ok,value_is_all_notdecodables,word1_is_datum,mixed_case;

  missing_value_struct = (struct idmissing *)malloc(sizeof(struct idmissing));
  if (missing_value_struct == NULL) errn
("Could not not get memory for missing_value_struct in do_idmissing Bytes = ",
       sizeof(struct idmissing));

  nitems_processed = 0;

  ok = extract_wjstbl_dyn(&varlist,&len,"varlist",wjstbl);
  if (ok == NOT_VALID) errn(
"Allocation failure for string associated with varlist keyword. Failed # bytes=",
		len);
  if (ok) nitems_processed++;
  else varlist = IDMISSING_DEFAULT_VARLIST;

  special_word = lookup_wjstbl_w_len
			("not-decodable_special_word",wjstbl,&special_word_len);
  if (special_word == NULL) {
    special_word = NOTDECODABLE_SPECIAL_STRING;
    special_word_len = strlen(special_word);
  } else {
    if (special_word_len > MAXLEN_NOTDECODABLE_SPECIAL_STRING) errn
("String associated with not-decodable_special_word keyword too long. Max len = ",
	MAXLEN_NOTDECODABLE_SPECIAL_STRING);
    nitems_processed++;
  }

  ok = extract_wjstbl_dyn(&value,&len,"value",wjstbl);
  if (ok == NOT_VALID) errn(
"Allocation failure for string associated with value keyword. Failed # bytes=",
		len);
  if (  ! ok  ) err
    ("identifymissingdata: no value supplied in a set of pairs associated \
w/varlist ",varlist);
  nitems_processed++;
  value_is_all_notdecodables = (strcasecmp_wjs(value,special_word) == 0);
  missing_value_struct->do_comparison_test = (  ! value_is_all_notdecodables  );

  temp = lookup_wjstbl_w_len("datatype",wjstbl,&len);
  if (temp == NULL) 
    err ("identifymissingdata: no datatype supplied for value ",value);
  nitems_processed++;
  if (match_wjstbl_value_n("integer",temp,len)) {
    if ( ! value_is_all_notdecodables) {
      GET_INTEGER_FROM_STRING(missing_value_struct->val_as_long,value,ok);
      if (  ! ok  )  err
	("identifymissingdata: could not decode as integer. value: ",value);
    }
    missing_value_struct->datatype = IDMISSING_DATATYPE_LONG;
  } else if (match_wjstbl_value_n("floatingpoint",temp,len)) {
    if ( ! value_is_all_notdecodables) {
      GET_NUMBER_FROM_STRING(missing_value_struct->val_as_double,value,ok);
      if (  ! ok  )  err
	("identifymissingdata: could not decode as floatingpoint. value: ",
									value);
    }
    missing_value_struct->datatype = IDMISSING_DATATYPE_FLOAT;
  } else if (match_wjstbl_value_n("character",temp,len)) {
    if (value_is_all_notdecodables) err
      ("identifymissingdata: cannot specify  datatype=character  with  value=",
								special_word);
    missing_value_struct->val_as_string = value;
    missing_value_struct->datatype = IDMISSING_DATATYPE_CHAR;
  } else {
    err ("identifymissingdata: illegal option for datatype keyword. option: ",
									temp);
  }

  temp = lookup_wjstbl_w_len("nonnumeric",wjstbl,&len);
  if (temp == NULL) {
    chr = (value_is_all_notdecodables) ?
		IDMISSING_NOTDECODABLE_MISSING : IDMISSING_NOTDECODABLE_DEFAULT;
  } else {
    nitems_processed++;
    if (match_wjstbl_value_n("legal",temp,len))
				chr = IDMISSING_NOTDECODABLE_ALLOW; 
    else if (match_wjstbl_value_n("illegal",temp,len))
				chr = IDMISSING_NOTDECODABLE_ABORT; 
    else if (match_wjstbl_value_n("missing",temp,len))
				chr = IDMISSING_NOTDECODABLE_MISSING;
    else err
        ("identifymissingdata: illegal option for nonnumeric keyword. option: ",
									temp);
    if (value_is_all_notdecodables && (chr != IDMISSING_NOTDECODABLE_MISSING))
      err ("identifymissingdata: must specify  nonnumeric=missing  with value=",
								special_word);
  }
  missing_value_struct->notdecodable_action = chr;

  ok = extract_wjstbl_dyn(&temp,&len,"tolerance",wjstbl);
  if (ok == NOT_VALID) errn(
"Allocation failure for string associated with tolerance keyword. Failed # bytes=",
		len);
  if (ok) {
    nitems_processed++;
    GET_NUMBER_FROM_STRING(missing_value_struct->tolerance,temp,ok);
    if (  ! ok  )  err 
	("identifymissingdata: could not decode as floatingpoint. tolerance: ",
	 								temp);
  } else {
    missing_value_struct->tolerance = 0.;
  }

  temp = lookup_wjstbl_w_len("comparison",wjstbl,&len);
  if (temp == NULL) {
    missing_value_struct->comparison = IDMISSING_DATUM_EQ_VAL;
  } else {
    nitems_processed++;
      /*  Syntax: key1_XX or key1_XX_key2, where keys are either	*/
      /*  'datum' or 'value', and XX are comparison ops.  Accept	*/
      /*  key1_XX_ since it's easier than making the doc clearer	*/
      /*  Case of XX significant if character compares; ignore else	*/
    ok = (len == 8) || (len == 9) || (len == 14);
    if (  ! ok  ) err_non_null_terminated_arg2 
      ("identifymissingdata: bad syntax in comparison spec: ",temp,len);
    ok = (  *(temp+5) == '_'  ); 
    if (  ! ok  ) err_non_null_terminated_arg2 
      ("identifymissingdata: bad syntax in comparison spec: ",temp,len);
    ok = (len > 8)   &&   (  *(temp+8) == '_'  ); 
    if (  ! ok  ) err_non_null_terminated_arg2 
      ("identifymissingdata: bad syntax in comparison spec: ",temp,len);
    word1_is_datum = (strncmp("datum",temp,5) == 0);
    ok = word1_is_datum || (strncmp("value",temp,5) == 0);
    if (  ! ok  ) err_non_null_terminated_arg2 
      ("identifymissingdata: bad syntax in comparison spec: ",temp,len);
    if (len >=14) {
      ok = (word1_is_datum) ?
			(strncmp("value",temp+9,5) == 0)
					:
			(strncmp("datum",temp+9,5) == 0);
      if (  ! ok  ) err_non_null_terminated_arg2 
        ("identifymissingdata: bad syntax in comparison spec: ",temp,len);
    }

    chr = IDMISSING_BAD_COMPARISON_OPERATOR;
    comp1  =  toupper (*(temp+6));
    comp2  =  toupper (*(temp+7));
    if (missing_value_struct->datatype == IDMISSING_DATATYPE_CHAR) {
      mixed_case = 
		(  (comp1 == *(temp+6)) && (comp2 != *(temp+7))  ) 
					||
		(  (comp2 == *(temp+7)) && (comp1 != *(temp+6))  );
      if (mixed_case) err 
	("identifymissingdata: mixed case in comparison for char data in",temp);
      missing_value_struct->comparison = (comp1 == *(temp+6)) ?
				IDMISSING_CASEBLIND : IDMISSING_CASESENSITIVE;
    }
    if ((comp1 == 'E') && (comp2 == 'Q')) chr = IDMISSING_DATUM_EQ_VAL;
    else if ((comp1 == 'L') && (comp2 == 'T')) 
      chr = (word1_is_datum) ? IDMISSING_DATUM_LT_VAL : IDMISSING_DATUM_GT_VAL;
    else if ((comp1 == 'L') && (comp2 == 'E')) 
      chr = (word1_is_datum) ? IDMISSING_DATUM_LE_VAL : IDMISSING_DATUM_GE_VAL;
    else if ((comp1 == 'G') && (comp2 == 'T')) 
      chr = (word1_is_datum) ? IDMISSING_DATUM_GT_VAL : IDMISSING_DATUM_LT_VAL;
    else if ((comp1 == 'G') && (comp2 == 'E')) 
      chr = (word1_is_datum) ? IDMISSING_DATUM_GE_VAL : IDMISSING_DATUM_LE_VAL;

    if (chr == IDMISSING_BAD_COMPARISON_OPERATOR) err_non_null_terminated_arg2 
	("identifymissingdata: bad comparison operator in",temp,len);
    missing_value_struct->comparison = chr;
  }

  if (nitems_supplied > nitems_processed)
    errn ("Unprocessed identifymissingdata parameters.  Number skipped = ",
      nitems_supplied-nitems_processed);

  *return_struct = missing_value_struct;
  return varlist;
}

void add_into_idmissing_list(list_start,ptr_to_add)
struct idmissing ***list_start,*ptr_to_add;
{
  struct idmissing **ptr;
  ptr = *list_start;
  while (*ptr++ != NULL) ;
    /*  ptr now points to location after NULL; = new end of list	*/
  *ptr-- = NULL;
  *ptr = ptr_to_add;
  return;
}

int get_idmissing (idmissing_optfile,missings_by_var,missings_for_all)
struct fileinfo *idmissing_optfile;
struct idmissing **missings_by_var[NVAR],***missings_for_all;

/*  Returns number of vars that have "alternate" missing vals defined	*/
/*    For each var w/an alternate, create an entry in 			*/
/*  missings_by_var, an array indexed by varnum.  Each entry		*/
/*  points to a list of structures.  Each structure defines an 		*/
/*  alternate value test.  Each list is NULL terminated.		*/
/*  If there are alternates that apply to all values, make a list of	*/
/*  those, too, and return it in missings_for_all			*/
/*	The order of the						*/
/*  lists is important in that string alternates should be processed	*/
/*  before numeric alternates (else there is a problem when a string	*/
/*  alternate cannot be decoded and the user has said that non-		*/
/*  decodables should abort)						*/
/*    The structs that define an alternate are dynamically allocated	*/
{
  char all_idmissings[NUM_IDMISSING*IDMISSING_WJSTBL+1];
  struct split_wjstbl *per_idmissing_tbls;
  char **idmissing_wjstbl;
  int *nitems_supplied;
  int num_idmissing;
  struct idmissing *missing_struct;
  struct idmissing **per_var_missing_list,**all_var_missing_list;
  char *varlist;
  char *nxt,*tok;
  int i,list_len,ntoks,varnum;

  if (create_wjstbl(idmissing_optfile,
		    all_idmissings,
		    NUM_IDMISSING*IDMISSING_WJSTBL)
						  	== 0) 
    return 0;

  list_len = (NUM_IDMISSING+1) * (sizeof(struct idmissing *));
  all_var_missing_list = (struct idmissing **)malloc(list_len);
  if (all_var_missing_list == NULL) errn
("Could not not get memory for all_var_missing_list in get_idmissing Bytes = ",
       list_len);
  *all_var_missing_list = NULL;

    /*  Make 1 wjstbl for each missing value requested			*/
  per_idmissing_tbls = 
	split_wjstbls (all_idmissings,
			NUM_IDMISSING,
			IDMISSING_WJSTBL,
			IDMISSING_SEQUENCE_PREFIX);
  num_idmissing = per_idmissing_tbls->n;
  nitems_supplied = per_idmissing_tbls->count;
  idmissing_wjstbl = per_idmissing_tbls->wjstbls;

    /*  this is a per-value loop.  We want to return a per-var struct	*/
  for (i = 0; i < num_idmissing; i++) {
    varlist = do_idmissing(idmissing_wjstbl[i],&missing_struct,
							nitems_supplied[i]);
    tok = nxttok(varlist,IDMISSING_VARLIST_SEP_STRING,&nxt,NULL,FALSE,FALSE);
    ntoks = 0;
    while (tok != NULL) {
      ntoks++;

      if (  strcasecmp_wjs(tok,IDMISSING_FOR_ALL_VARIABLES) == 0  ) {
	if (ntoks != 1) err
("identifymissingdata: ALL string specified along with other variables in",tok);
	add_into_idmissing_list(&all_var_missing_list,missing_struct);

      } else {

	varnum = lookup_lev0varlist(tok);
	if (varnum == ILLEGAL_VARNUM) err
("identifymissingdata: cannot find varname in object.  Bad varname: ",tok);
	per_var_missing_list = missings_by_var[varnum];
	if (per_var_missing_list == NULL) {
	  per_var_missing_list = (struct idmissing **)malloc(list_len);
	  if (per_var_missing_list == NULL) errn
("Could not not get memory for per_var_missing_list in get_idmissing Bytes = ",
       list_len);
	  *per_var_missing_list = NULL;
	}
	add_into_idmissing_list(&per_var_missing_list,missing_struct);
	missings_by_var[varnum] = per_var_missing_list;
      }

      tok = nxttok(nxt,IDMISSING_VARLIST_SEP_STRING,&nxt,NULL,FALSE,FALSE);
    }
  }
  free_split_wjstbl(per_idmissing_tbls);

  if (all_var_missing_list != NULL) *missings_for_all = all_var_missing_list;  
  return num_idmissing;
}

int get_coordmissing(file,coordmissings)
int *coordmissings[NVAR];
struct fileinfo *file;
  /*  This code essentially copied from create_wjstbl in order to get	*/
  /*  the record/entry/item logic.  Unfortunately, that kind of dwarfs	*/
  /*  what this routine does; esp because of the generality of the	*/
  /*  error msgs.  Oh well ...						*/
  /*    Data comes in as independent vars followed by a list of their	*/
  /*  dependent vars.  We output an array indexed by dependent varnums.	*/
  /*  Each array element points to a list of independent vars, the	*/
  /*  missing status of any of which means the dependent var is missing	*/
{
  int paircnt;
  char *tok,*output_list_ptr,*this_var;
  char *entry_ptr,*next_entry_ptr;
  char *next_item_ptr;
  char *rec_id = NULL,*msg = NULL,*opt_file_id;
  char *comment_postfix;
  int len_dependent_varnum_list;
  char *ptr,*end_ptr;
  int independent_varnum,dependent_varnum;
  int level_of_independent;
  int *dependent_varnum_list;
  int i;
  Logical independents[NVAR]; 

  if (! open_and_log_opt(file)) return 0;

  opt_file_id = buildstring ("\n   Problem in ",file->descrip," optional file",
			     " creating opt_file_id string");

  if (  (strchr(file->entry_separators,COORDMISSING_VARLIST_SEP_CHAR) != 0)
				||
	(strchr(file->item_separators,COORDMISSING_VARLIST_SEP_CHAR) != 0)
				||
	(strchr(file->item_alt_separators,COORDMISSING_VARLIST_SEP_CHAR) != 0)
     )
		err ("Internal error: coordinated_missing list separator \
char conflicts with its entry/item/alt separator strings","");

    /*  See comments about this list size in ioreadrec_routines ...	*/
    /*  esp if you're about to change it				*/
  len_dependent_varnum_list = (NVAR+1)*sizeof(int *);
  for (i=0; i<NVAR; i++) independents[i] = FALSE;

  paircnt=0;
  comment_postfix = file->comment_postfix;

      /*  Loop per record in file					*/
  while (getrec_proccomment(file,comment_postfix) != 0) {
	/*  Throw out comments from now on (after first non-comment)	*/
    comment_postfix = NULL;
    rec_id = buildstring ("\n   Opt file record = ",file->buf,NULL,
			  " creating rec_id string");

    next_entry_ptr = file->buf;

	/*  Loop per entry (pair) in record				*/
	/*    Allow item alt separators to protect entry separators	*/
	/*    that happen to occur within items				*/
    while (  (entry_ptr = nxttok (next_entry_ptr,
				  file->entry_separators,
				  &next_entry_ptr,
				  file->item_alt_separators,
				  FALSE,
				  FALSE)		   )
			!= NULL					) {

	/*  Ignore empty pairs.  Should take care of empty lines, too	*/
      if (  (tok = nxttok(entry_ptr,
			  file->item_separators,
			  &next_item_ptr,
			  NULL,
			  FALSE,
			  FALSE)		)
		 == NULL				) break;

      paircnt++;

  	  /*  Got first of pair (typically a variable name or keyword)	*/
      if (file->pre_trans) {
	if (TRANSVAR_CALL(varname,tok) == -1) {
	  msg = buildstring
		  ("Translated variable name too long\n   Name (pre-trans) = ",
		    tok,
		    rec_id,
		    "creating err msg coord2");
	  err (msg,opt_file_id);
	} else {
	  tok=varname;
	}
      }

      independent_varnum = lookup_lev0varlist(tok);
      level_of_independent = iovarlevel__(independent_varnum);
      if (independent_varnum == ILLEGAL_VARNUM) {
	msg = buildstring
		  ("Cannot find variable name in object\n   Name (pre-trans) = ",
		    tok,
		    rec_id,
		    "creating err msg coord3");
	err (msg,opt_file_id);
      }
      if (independents[independent_varnum]) {
	msg = buildstring("More than one entry for independent var ",tok,rec_id,
						" creating err msg coord8");
	err (msg,opt_file_id);
      }
      independents[independent_varnum] = TRUE;

	/*  Get its associated string.  Might be enclosed in		*/
	/*  apostrophes (depending on which opt file this is)		*/
      if (  (tok = nxttok (next_item_ptr,
			   file->item_separators,
			   &next_item_ptr,
			   file->item_alt_separators,
			   FALSE,
			   FALSE)  )
		 == NULL				) {
	  /*  Error if no associated string				*/
	msg = buildstring("Incomplete entry\n   Variable/keyword = ",
			   this_var,
			   rec_id,
			   " creating err msg coord6");
	err (msg,opt_file_id);
      }
      if (nxttok (next_item_ptr,file->item_separators,NULL,NULL,FALSE,FALSE)  
	    != NULL) {
	msg = buildstring("Third item in entry ",tok,rec_id,
			  " creating err msg coord7");
	err (msg,opt_file_id);
      }

      ptr = tok;
	/*  Interesting issue about what happens if  tok  is at address	*/
	/*  1, leading to question about something being legitimately	*/
	/*  located at address 0 ...					*/
      end_ptr = ptr - 1;
      while (end_ptr != NULL) {
	ptr = end_ptr + 1;
	end_ptr = strchr(ptr,COORDMISSING_VARLIST_SEP_CHAR);
	if (end_ptr != NULL) *end_ptr = '\0';
	if (file->pre_trans) {
	  if (TRANSVAR_CALL(varname,ptr) == -1) {
	    msg = buildstring
		    ("Translated variable name too long\n   Name (pre-trans) = ",
		      ptr,
		      rec_id,
		      "creating err msg coord10");
	    err (msg,opt_file_id);
	  } else {
	    ptr=varname;
	  }
	}
	dependent_varnum = lookup_lev0varlist(ptr);
	if (dependent_varnum == ILLEGAL_VARNUM) {
	  msg = buildstring
		    ("Cannot find variable name in object\n   Name (pre-trans) = ",
		      tok,
		      rec_id,
		      "creating err msg coord11");
	  err (msg,opt_file_id);
	}
	if (level_of_independent > iovarlevel__(dependent_varnum)) {
	  msg = buildstring
		    ("Dependent variable occurs at lower level \
than independent variable.  Dependent variable = ",
		      ptr,
		      rec_id,
		      "creating err msg coord12");
	  err (msg,opt_file_id);
	}
	if (coordmissings[dependent_varnum] == NULL) {
	  dependent_varnum_list = (int *)malloc(len_dependent_varnum_list);
	  if (dependent_varnum_list == NULL) {
	    msg = buildstring
		      ("Could not get memory for dependent_varnum_list",
		        opt_file_id,
		       "Bytes = ",
		       "creating err msg coord13");
	    errn (msg,len_dependent_varnum_list);
	  }
	  coordmissings[dependent_varnum] = dependent_varnum_list;
	  i = 0;
	} else {
	  dependent_varnum_list = coordmissings[dependent_varnum];
	  for (i=0; i<NVAR; i++) {
	    if (dependent_varnum_list[i] == ILLEGAL_VARNUM) break;
	  }
	  if (i == NVAR) 
	    err ("Internal error: no terminator for dependent_varnum_list",
		   opt_file_id);
	}
	dependent_varnum_list[i++] = independent_varnum;
	dependent_varnum_list[i] = ILLEGAL_VARNUM;
      }
    }
  }

  if (file->eof) {
    if (fclose(file->stream) != 0)
      err ("Failure to close opt file.  errno msg is ",strerror(errno));
    file->open = FALSE;
      /*  See comments at waitpid in dataeod (in defgb.c)		*/
    waitpid((pid_t)-1,NULL,WNOHANG);
    errno = 0;
  }

  free(opt_file_id);
  if (rec_id != NULL) free(rec_id);
  if (msg != NULL) free (msg);

  return paircnt;
}


/***********  for generated data
void init_generateddata_structs (gendata_structs)
struct gendata gendata_structs[NUM_GENDATA];
{
  int i;
  for (i=0; i<NUM_GENDATA; i++) {
    gendata_structs->tag[i] = ILLEGAL_GENDATA_TAG;
    gendata_structs->var[i] = ILLEGAL_VARNUM;
    gendata_structs->lev[i] = ILLEGAL_LEVEL;
    gendata_structs->formula_type[i] = ILLEGAL_GENDATA_FORMULA_TYPE;
    gendata_structs->missing_value[i] = MISSING_VALUE_STRING;
    gendata_structs->formula[i] = NULL;
    gendata_structs->first_index[i] = 1;
    gendata_structs->missing_input_means_missing_output[i] = TRUE;
    gendata_structs->kill_if_extract_is_nd[i] = TRUE;
    gendata_structs->kill_if_extract_string_too_short[i] = TRUE;
    gendata_structs->action_if_string_to_be_padded_too_long[i] = "abort";
  }
  return;
}
************* for generated data */

void init_outinfo (ptr)
struct outinfo *ptr;
{
  int i;
  static char empty[1] = {'\0'};

  for (i=0; i<NUM_SINKS; i++) {
    ptr->outsink_streams[i] = NULL;
    ptr->outsink_open_flags[i] = FALSE;
    ptr->outsink_modes[i] = NULL;
    ptr->file[i].stream_ptr = &(ptr->outsink_streams[i]);
    ptr->file[i].open_ptr = &(ptr->outsink_open_flags[i]);
    ptr->file[i].mode_ptr = &(ptr->outsink_modes[i]);
    ptr->file[i].processed = FALSE;
    ptr->file[i].dup = FALSE;
    ptr->file[i].fileinfo.st_dev = NO_SUCH_DEVICE;
  }

    /*  Descrips match opt file keywords less the _sink			*/
  ptr->file[DATA_COMMENT_SINK].descrip = "data_comment";
  ptr->file[NON_DATA_COMMENT_SINK].descrip = "other_comment";
  ptr->file[ERROR_SINK].descrip = "error";
  ptr->file[DIAG_SINK].descrip = "debug";
  ptr->file[PRIMARY_ERROR_SINK].descrip = "primary_error";/* User cannot */
  ptr->file[PRIMARY_DIAG_SINK].descrip = "primary_diag";  /*  spec these */

  ptr->file[DATA_COMMENT_SINK].sink = ADD_TO_BUFFER_FILE;
  ptr->file[NON_DATA_COMMENT_SINK].sink = DEFAULT_OTHER_COMMENT_FILE;
  ptr->file[ERROR_SINK].sink = DEFAULT_ERROR_FILE;
  ptr->file[DIAG_SINK].sink = DEFAULT_DEBUG_FILE;
  ptr->file[PRIMARY_ERROR_SINK].sink = PRIMARY_ERROR_FILE;
  ptr->file[PRIMARY_DIAG_SINK].sink = PRIMARY_DEBUG_FILE;

  ptr->inserted_comments = DEFAULT_INSERTED_MSGS;
  ptr->inserted_comment_id = DEFAULT_INSERTED_MSG_TAG;
  ptr->opt_file_comment_id = DEFAULT_COMMENT_SOURCE_TAG;

  ptr->trace_level = DEFAULT_TRACE_LEVEL;
  ptr->dup_trace_level = DEFAULT_TRACE_LEVEL;
  ptr->iovalstr = DEFAULT_DATA_TRACE;

  ptr->n_iovalstrs = 0;
  ptr->n_iovalreals = 0;

  ptr->error_level = DEFAULT_ERROR_LEVEL;
  ptr->dup_error_level = DEFAULT_ERROR_LEVEL;
  ptr->maxscriptdiags = DEFAULT_MAXSCRIPTDIAGS;

  return;
}

void init_file_structs (files,indirect_file,comment_sources)
struct fileinfo files[],*indirect_file,comment_sources[];
/*  Fill in those fields of the fileinfo structures relevant to the	*/
/*  various types of files.  Fill in some for logical purposes, even if	*/
/*  they aren't used at present.  What's the bet I miss lots?  Well, 	*/
/*  that's why I put all this in one place...				*/
/*  datafiles[*] all start out as a copy of files[DATAFILE], so they	*/
/*  are not initialized here						*/
/*  Field definitions are with structure defn in defgb.h		*/
{
  int i;

  if (strpbrk(DEFAULT_ITEM_SEPARATORS,DEFAULT_ENTRY_SEPARATORS) != NULL)
    err ("Compile-time entry separator appears in compile-time item ",
      "separator list");

  indirect_file->source_type=DATA_FILE;
  indirect_file->descrip="indirect";
  indirect_file->open=FALSE;
  indirect_file->eof=FALSE;
  indirect_file->eod=FALSE;
    /* +1 for \n;  +1 to detect record overflow				*/
  if (  (indirect_file->buf=(char *)malloc(MAXREC+1+1)) == NULL  )
    errn ("Could not allocate space for indirect file buffer.  # bytes = ",
	 MAXREC+1+1);
  indirect_file->nrecs=0;
  indirect_file->comment_postfix=
    buildstring(" {",indirect_file->descrip,"}",	" building postfix");
  indirect_file->entry_separators=NULL;
  strdupl(&indirect_file->item_separators,DEFAULT_ITEM_SEPARATORS,
    " copying item separator list");

  files[DATAFILE].descrip="datafile";
  files[TRANSVAR].descrip="transvar";
  files[INPWIDTHS].descrip="inpwidths";
  files[REMOVALS].descrip="removals";
  files[TIMEDATEPARAMS].descrip="timedateparams";
  files[DIAGNOSTICS].descrip="diagnostics";
  files[LATLONPARAMS].descrip="latlonparams";
  files[DATAFIELDOPTS].descrip="datafieldopts";
  files[DISPWIDTHS].descrip="dispwidths";
  files[VARLIST].descrip="varlist";
  files[VARLISTOPTS].descrip="varlistopts";
  files[DATACOMMENTS].descrip="datacomments";
  files[COMMENTSOURCE].descrip="commentsource";
  files[IDMISSING].descrip="identifymissingdata";
  files[COORDMISSING].descrip="coordinatedmissingdata";
  files[GENERATEDDATA].descrip="generateddata";

  for (i=0;i<NFILETYPES;i++) {
    files[i].stream=NULL;
    files[i].source=NULL;
    files[i].source_type=ILLEGAL_SOURCE;
    files[i].eod=FALSE;
    files[i].eof=FALSE;
    files[i].open=FALSE;
      /* +1 for \n;  +1 to detect record overflow			*/
    files[i].buf_size=MAXREC+1+1;
    if (  (files[i].buf=(char *)malloc(files[i].buf_size)) == NULL  )
      errn ("Could not allocate space for file buffer.  # bytes = ",
	    files[i].buf_size);
    files[i].accepts_trans_prefix=TRUE;
    files[i].nrecs=0;
    files[i].maxlenrec=-1;
    files[i].print_lines=0;
    files[i].lastvar=0;
    files[i].force_flag=0;
    strdupl(&files[i].entry_separators,DEFAULT_ENTRY_SEPARATORS,
		" copying entry separator list");
    strdupl(&files[i].item_separators,DEFAULT_ITEM_SEPARATORS,
		" copying item separator list");
    files[i].item_alt_separators[0] = '\0';
    files[i].comment_postfix=
      buildstring(" {",files[i].descrip,"}",	" building postfix");
  }

  files[DATAFILE].objlevel=0;
  files[DATAFILE].entry_separators=NULL;
  files[DATAFILE].item_separators=SEPARATORS;
  files[DATAFILE].force_flag=1;
  files[DATAFILE].comment_postfix=NULL;
    /* Initialize ALT_SEPARATORS like this in case compile-time 	*/
    /* constant is shorter than 2 characters.				*/
    /* For now, a file either gets no alt_separators, or it gets the	*/
    /* same default alt_separators.  DATAFILE's alt_separators can be	*/
    /* spec'd at runtime via DATAFIELDOPTS.  Init'ed above to none...	*/
  files[DATAFILE].item_alt_separators[0] = ALT_SEPARATORS[0];
  files[DATAFILE].item_alt_separators[2] = '\0';
  switch (i = strlen(ALT_SEPARATORS)) {
    case 1:  case 2:
      files[DATAFILE].item_alt_separators[1] = ALT_SEPARATORS[i-1];
      break;
    default:
      err ("Compile-time ALT_SEPARATOR string 0 len or too long.  String = ",
	ALT_SEPARATORS);
      break;
  }

  files[TRANSVAR].force_flag=TRANS_OPT;
  files[TRANSVAR].accepts_trans_prefix=FALSE;

  files[INPWIDTHS].force_flag=INPWIDTH_OPT;
  files[DISPWIDTHS].force_flag=DISPWIDTH_OPT;

  files[REMOVALS].entry_separators=NULL;
  strdupl(&files[REMOVALS].item_separators,REMOVAL_SEPARATORS,
    " copying removals separator list");
  files[REMOVALS].force_flag=REMOVAL_OPT;

  files[TIMEDATEPARAMS].force_flag=TIMEDATE_OPT;

  files[DIAGNOSTICS].accepts_trans_prefix=FALSE;

  files[LATLONPARAMS].force_flag=LATLON_OPT;

  files[DATAFIELDOPTS].accepts_trans_prefix=FALSE;
  files[DATAFIELDOPTS].force_flag=DATAFIELDOPTS_OPT;
  strcpy(files[DATAFIELDOPTS].item_alt_separators,
	 files[DATAFILE].item_alt_separators);

    /*  Since VARLIST and DATAFILE are assumed to have same header	*/
    /*  format, must have same separators, etc				*/
  files[VARLIST].item_separators=files[DATAFILE].item_separators;
  files[VARLIST].entry_separators=files[DATAFILE].entry_separators;
  strcpy(files[VARLIST].item_alt_separators,
	 files[DATAFILE].item_alt_separators);
  files[VARLIST].force_flag=VARLIST_OPT;

  files[VARLISTOPTS].force_flag=VARLISTOPTS_OPT;

  files[DATACOMMENTS].force_flag=DATACOMMENTS_OPT;
  files[DATACOMMENTS].accepts_trans_prefix=FALSE;

    /*  We never look for a trans prefix in a COMMENTSOURCE file anyway	*/
    /*  files[COMMENTSOURCE] is just a template and its space is wasted	*/
    /*  ... wonder how we could get around that.			*/
  files[COMMENTSOURCE].accepts_trans_prefix=FALSE;
  for (i=0; i < MAX_DATACOMMENTS_SOURCES; i++) 
      /*  Note all comment_sources files share the buffer allocated to	*/
      /*  files[COMMENTSOURCE].  Should be OK since comment_sources	*/
      /*  are processed sequentially					*/
    comment_sources[i] = files[COMMENTSOURCE];

  files[IDMISSING].force_flag=IDMISSING_OPT;
  files[COORDMISSING].force_flag=COORDMISSING_OPT;

  return;
}

void get_files (files,indirect_name,indirect_file,
		inner_param_list,len_inner_param_list)
char *indirect_name,*inner_param_list[];
int len_inner_param_list;
struct fileinfo files[],*indirect_file;
/*	get_files fills in the files array of file information from	*/
/*	data found in file *indirect_name				*/
{

  char *source,*descrip,*prefix;
  char *msg,*end_token1,*seps;
  char *brack_method_name;
  char *dirstring,*ptr;
  int i,file_type,position,lentmp;

  seps = indirect_file->item_separators;

  if (strchr(seps,DIRSEP) != NULL) 
    err ("Directory separator cannot be used as indirect file separator","");

  indirect_file->stream=fopen(indirect_name,"r");
  if (indirect_file->stream == NULL) {
    msg = buildstring("Error opening indirect file ",indirect_name,
		      "\nProblem: ",			" open_indir_ msg");
    err(msg,strerror(errno));
  }
  indirect_file->open=TRUE;
  indirect_file->eod=FALSE;
  indirect_file->eof=FALSE;
    /*  Following line assumes that indirect name won't change in	*/
    /*  calling program.  Since we don't really care about this info at	*/
    /*  the moment (we're saving for completeness more than anything	*/
    /*  else), not making an effort to check, etc			*/
  indirect_file->source=indirect_name;

  brack_method_name = buildstring(" {",METHOD_NAME,"}",	
    " bracketing method name");
  msg = buildstring("Indirect file: ",indirect_name,NULL," indirect file msg");
  add_to_comment_stream (brack_method_name,msg);
  free (msg);
  free (brack_method_name);

  strdupl(&dirstring,indirect_name," saving .ind file's directory string");
    /*  If there is a directory, split it from file spec		*/
    /*  after its trailing /						*/
  if (  (ptr = strrchr(dirstring,DIRSEP)) == NULL  )  {
    free (dirstring);
    dirstring = NULL;
  } else *(++ptr) = '\0';      /* Truncate after found /		*/

  if (getrec_proccomment(indirect_file,indirect_file->comment_postfix) == 0)
    err ("EOF/error before indirect file data records","");
  position = 0;

  while (TRUE) {

    descrip = indirect_file->buf + strspn(indirect_file->buf,seps);

	/*  Skip lines consisting entirely of separators		*/
    if (*descrip != '\0') {

	/*  File logically consists of entries of the form		*/
	/*	source							*/
	/*  or								*/
	/*	descrip  =  source					*/
	/*  descrip is one of the .descrip strings, above, optionally	*/
	/*    preceded by a prefix.  If descrip not present, it's	*/
	/*    calculated from position within file.  There may be at	*/
	/*    most one entry (explicit or calculated) for a descrip	*/
	/*  source is either filespec, or text string (hopefully for-	*/
	/*    matted the way a record in filespec would be), or a 	*/
	/*    pointer to a filespec or text string.  If filespec, 	*/
	/*    nothing else is allowed on line.  Text string is, by 	*/
	/*    defn, rest of line.  Only pointer presently allowed is	*/
	/*    keyword of form .objects_parameter_N, which means that	*/
	/*    the filespec or text string comes from parameter N of	*/
	/*    the .objects file.  If _N is not part of the string	*/
	/*    assume N is the position within this file (plus 1).  	*/
	/*    Example: .objects_parameter on the datafile line means	*/
	/*    that the datafile will be found in .objects parameter 1	*/
	/*    (0 is the datafile position within the indirect file,	*/
	/*    + 1).  Not coincidentally at all, we happen		*/
	/*    to have a list of pointers to those parameters...		*/
	/*    The source could also be a command (possibly with params)	*/
	/*    which when executed produces data formatted the way a	*/
	/*    record in filespec would be.  At present, only DATAFILE	*/
	/*    may have this kind of source, but logically...		*/
	/*    DATAFILEs may also be from JGOFS objects.			*/
	/*    Since command w/params may have embedded separators,	*/
	/*    treat commands like text strings.  Not sure if JGOFS	*/
	/*    object spec's can have embedded separators, but assume	*/
	/*    yes, since that will work if not true, and we have to do	*/
	/*    the work anyway						*/

	/*    Get start of 2nd field w/o using strtok (which would	*/
	/*    destroy embedded separators, if present).  This code	*/
	/*    should work even if no separators after 1st field, or	*/
	/*    nothing but separators after 1st field			*/
      source = end_token1 = strpbrk(descrip,seps);
      if (end_token1 != NULL) source += strspn(end_token1,seps);

	/*    Single entry if first thing we encounter is "special"	*/
	/*    string delimiter, or if 2nd field empty			*/
      if (  (*descrip == SOURCE_IS_DATA) ||
      	    (*descrip == *EXEC_DELIM)    ||
      	    (*descrip == *OBJECT_DELIM)  ||
	    (is_a_remote_object(descrip) != 0) ||
	    (source == NULL)		 ||
	    (*source == '\0')			) {

	  /*  No descrip field.  Source is string; file type comes from	*/
	  /*  where we are in file; vars in file are post-translation	*/
	  /*  unless we are talking about the DATAFILE			*/
	source=descrip;
	file_type=position;
	files[file_type].pre_trans = ((file_type == DATAFILE) 
		|| (file_type == VARLIST) || (file_type == VARLISTOPTS));
	msg=buildstring(files[file_type].descrip," optional file","");

      } else {

	  /*  Have both source and descrip.  Terminate descrip, then	*/
	  /*  process it to find file type and translation prefix	*/

	*end_token1 = '\0';

	prefix=descrip;

	  /*  Search descrip for any of the legal types			*/
	  /*  Legal type might follow prefix, but has no postfix; eg,	*/
	  /*    "found" strings must end at same byte as input string	*/
	for (file_type=0; file_type<NFILETYPES; file_type++) 
	  if ( (descrip=strstr(prefix,files[file_type].descrip)) != NULL) 
	    if (  (descrip + strlen(files[file_type].descrip)) == end_token1  )
	      break;
	if (file_type >= NFILETYPES) 
	  err ("Unknown file descriptor in indirect file\n   Descriptor = ",
	    prefix);

	  /*  Handle translation prefix.				*/
	if (prefix == descrip) 
	    /*  No prefix.  Everything but DATAFILE, VARLIST, & 	*/
	    /*  VARLISTOPTS defaults to FALSE				*/
	  files[file_type].pre_trans = (  (file_type == DATAFILE) ||
					  (file_type == VARLIST)  ||
					  (file_type == VARLISTOPTS)  );
	else {
	    /*  Prefix found.  "pre-translation-" and			*/
	    /*      "post-translation-" are the only legal ones. 	*/
	  if (has_prefix(prefix,"pre-translation-",descrip))
	    files[file_type].pre_trans = TRUE;
	  else if (has_prefix(prefix,"post-translation-",descrip))
	    files[file_type].pre_trans = FALSE;
	  else err ("Illegal file descriptor prefix in indirect file\n   \
Prefix precedes descriptor ",descrip);

	    /*      Makes no sense to spec trans opt for some files	*/
	  if ( ! files[file_type].accepts_trans_prefix )
	    err ("Cannot supply prefix for descriptor ",descrip);
	}

	msg = buildstring("file descriptor ",descrip,"");
      }

	/*  We now know what kind of file is being described.  Deal 	*/
	/*  with its name (or save its data)				*/
	/*  See if it's a .objects_parameter key			*/
      switch (key_N(OBJ_KEY,source,&i)) {
	case -1:
	    /*  Wasn't a .objects_parameter_N key.  We're done		*/
	  break;
	case 0:
	    /*  .objects_parameter.  Get N from line number, then fall	*/
	    /*  through to check validity of N				*/
	  i = position + 1;
	case 1:
	  if (i >= len_inner_param_list)
	    errn ("Requested .objects param beyond end of param list\n"
		  "  Bad param = ",i);
	  if (inner_param_list[i]==NULL)
	    errn ("Requested .objects param already processed\n"
		  "  Bad param = ",i);
	    /*  Process argument as source for file[file_type]		*/
	  source = inner_param_list[i];
	    /*  Next test OK, but implies that under some circumstances	*/
	    /*  .objects_parameter_N IS legal in an argument from a	*/
	    /*  .objects file entry.  Technically OK, but would 	*/
	    /*  probably be confusing...				*/
	  if (strncmp(OBJ_KEY,source,strlen(OBJ_KEY)) == 0)
	    errn (".objects param may not request another .objects param\n"
		  "  Bad param = ",i);
	  break;
	case 2:
	  err ("Nothing may follow .objects_parameter_N keyword\n"
	       " Illegal string: ",source);
	  break;
	default:
	  errn ("Internal coding error - illegal return from key_N\n"
		"  illegal return value = ",i);
      }

      if (*source == '\0') err ("0 length indirect file entry for ",msg);
      if (files[file_type].source != NULL) 
	err ("2nd entry for optional file\n   Attempted entry = ",source);

	/* Save source string and set up source type			*/
      msg=buildstring(" saving ",files[file_type].descrip," source info","");
      strdupl(&files[file_type].source,source,msg);
      free (msg);
      analy_source(&files[file_type],indirect_file->item_separators,dirstring);

	/*  If source came from a .objects parameter, mark it processed	*/
	/*  as far as outer is concerned.				*/
      if (i != 0) *source='\0';
    }

    if (getrec_proccomment(indirect_file,NULL) == 0) break;
    position++;
  }

  if (indirect_file->eof) {
    if (fclose(indirect_file->stream) != 0)
      err ("Failure to close indirect file.  errno msg is ",strerror(errno));
    indirect_file->open = FALSE;
  }

  if (dirstring != NULL) free (dirstring);
  return;
}
