/*  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) */
/*	Logical get_latlonparams(latlon_optfile,outlat,outlon,inlat,outlat) */
/*	Logical get_timedateparams(timedate_optfile,outtime_structarray, */
/*		intime_structarray,fragbuflist)				*/
/*	Logical 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_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 2.7c  19 Jul 2004"
/*  19 Jul 04  v 2.7c		WJS					*/
/*	Move 26 Mar enhancement out of here.  It must be done at open	*/
/*	  time to take into account the problem where 2 sinks are	*/
/*	  directed to the same as-yet-unopened file.  Until one sink	*/
/*	  opens the file, the other doesn't "know" it's the same	*/
/*   1 Jun 04  v 2.7c		WJS					*/
/*	Bug fix: get_comment_sources wasn't propagating source_type for	*/
/*	  "special" varlist & level 0 datafile comment sources		*/
/*  28 Apr 04  v 2.7c		WJS					*/
/*	Bug fix: startchild_defgb now startchild			*/
/*  26 Mar 04  v 2.7c		WJS					*/
/*	Remove restriction that common sink destinations had to be	*/
/*	  spec'd identically as strings (eg, both absolute or both	*/
/*	  relative; no hard/soft links, etc)				*/
/*  11 Feb 04  v 2.7c		WJS					*/
/*	Fixes to avoid some compiler warnings				*/
/*		[Begin v 2.7c]						*/
/*   6 Aug 03. v 2.7b		WJS					*/
/*	Bug fix: invarN_ and invar_N_ alternate formats did not work	*/
/*		[Begin v 2.7b]						*/
/*  10 Jul 03. v 2.7a		WJS					*/
/*	Fill in files[i].buf_size field					*/
/*	Begin mods to better check buffer lengths.			*/
/*	  Change calls to transvar because its arg changed		*/
/*	  Use copy_to_fixed_len_buffer					*/
/*	  In general, use buffer sizes rather than size of string	*/
/*	    buffer can hold						*/
/*		[Begin v 2.7a]						*/
/*   2 Jul 03. v 2.7		WJS					*/
/*	Bug fix: latlon.var fields need init'ing			*/
/*  24 Jun 03. v 2.7		WJS					*/
/*	datacomments opt file						*/
/*      extract_wjstbl now logical function				*/
/*	key_N function replaces some .objects_parameter-specific code	*/
/*	Accept time_N_ as well as timeN_ and invar_N_ as well as	*/
/*	  invarN (not that we actually went as far as to use key_N for	*/
/*	  everything as we should have...)				*/
/*  14 May 03. v 2.7		WJS					*/
/*	Enhanced latlon processing					*/
/*	Possible fix - default lat/lon conventions don't seem to have	*/
/*	  defined							*/
/*		[Begin v 2.7]						*/
/*  [1.0 -> 2.6  comments in defgb_revision.doc. 26 Mar 04]		*/

#include "defgb.h"
#ifdef VMS
  pid_t waitpid();	/* ? in wait.h if vms version > 7 ?		*/
#endif


  /*  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();
int get_int_from_string();
Logical get_logical_from_string();
void ioname__();
int iovarlevel__();
char *lengthen_str();
int lookup_lev0varlist();
char *lookup_wjstbl();
int getrec_proccomment();
char *nxttok();
char *strdupl();	/* Gets mem for copy of string & copies it	*/
int transvar();

#if READ_COMMANDS
char *startchild();
#endif

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

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;
}

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;
}

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

/*  Returns TRUE or FALSE depending on whether datacomments were 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;

  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)  ||
	    (strncmp(descrip,REMOTE_OBJECT_PREFIX,
				strlen(REMOTE_OBJECT_PREFIX)) == 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;
      } 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;
      } else {
	strdupl(&outfiles[out_index-1].source,source,msg);
	analy_source(&outfiles[out_index-1],seps,dirstring);
      }

      free (msg);
    }

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

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

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 ("   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]=diags_wjstbl[strlen(diags_wjstbl)-1];
  wjstbl_chars[1]=diags_wjstbl[0];
  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 = varlistopts_wjstbl[strlen(varlistopts_wjstbl)-1];
  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_fixed_len_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,trim,consec_seps,embed_seps,
	sepstring_ptr, alt_sepstring)
Logical *consec_seps,*embed_seps,*trim;
char **sepstring_ptr,*alt_sepstring;
struct fileinfo *datafield_optfile;
/*  Get the switches from the datafieldopts optional file		*/
{
  char *keyword,*keyval,*end_keyval,*ptr;
  char datafield_wjstbl[DATAFIELDOPTS_WJSTBL+1];
  char wjstbl_white_space;
  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 = datafield_wjstbl[strlen(datafield_wjstbl)-1];

  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';
  }


/****** NO data_field_trim FOR NOW
  keyword = "data_field_trim";
  if (  (keyval = lookup_wjstbl(keyword,datafield_wjstbl)) != NULL  ) {
    end_keyval = strchr(keyval,wjstbl_white_space);
    *end_keyval = '\0';
    *trim = get_logical_from_string 
		   (keyval," datafieldopts optfile keyword ",keyword);
    *end_keyval = wjstbl_white_space;
    nitems_processed++;
  }
******/

  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;
}

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

    /* Lazy way to search list of legal strings. q's are irrelevant	*/
    /*  (& define them, too! should be done differently)		*/
  legal_format_wjstbl = "/decdeg/q /degdecmin/q ";

  if (  (ptr = lookup_wjstbl(key,wjstbl)) == NULL  ) return FALSE;
  else {
      /*  Assume *ptr is not white space-else some wjstbl error		*/
    if (  strchr(ptr,WJSTBL_WHITE_SPACE) > (ptr + MAXLATLONFORMATSIZE)  )
      err ("Lat/lon format string too long.  String = ",ptr);
    sscanf(ptr,"%s",buffer);
    if (lookup_wjstbl(buffer,legal_format_wjstbl) == NULL)
      err ("Illegal lat/lon format.  Format = ",ptr);
  }

  return TRUE;
}

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;
  }
}

Logical get_latlonparams(latlon_optfile,outlat,outlon,inlat,inlon)
struct fileinfo *latlon_optfile;
struct latlonformat *outlat,*outlon,*inlat,*inlon;
/*  Returns TRUE or FALSE depending on whether latlon params were spec'd */
/*									*/
/*  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 latlon_wjstbl[LATLON_WJSTBL+1];
  char *ptr;
  int nitems_supplied;
  int nitems_processed = 0;

    /*  Set up so that no work will be done by default (all vars	*/
    /*  illegal								*/
  inlat->var = -1;
  outlat->var = -1;
  inlon->var = -1;
  outlon->var = -1;

  inlat->legal_cardinal = LEGAL_LATCARDINALS;
  outlat->legal_cardinal = LEGAL_LATCARDINALS;
  inlon->legal_cardinal = LEGAL_LONCARDINALS;
  outlon->legal_cardinal = LEGAL_LONCARDINALS;

  inlat->convention = *inlat->legal_cardinal;
  outlat->convention = *outlat->legal_cardinal;
  inlon->convention = *inlon->legal_cardinal;
  outlon->convention = *outlon->legal_cardinal;

  inlat->sep[0] = DEFAULT_LATLONSEP;
  inlat->sep[1] = '\0';
  inlon->sep[0] = DEFAULT_LATLONSEP;
  inlon->sep[1] = '\0';

  COPY_INTO_FIXED_LEN_BUFFER(inlat->format,DEFAULT_LATLONFORMAT,
							"inlat format");
  COPY_INTO_FIXED_LEN_BUFFER(outlat->format,DEFAULT_LATLONFORMAT,
							"outlat format");
  COPY_INTO_FIXED_LEN_BUFFER(inlon->format,DEFAULT_LATLONFORMAT,
							"inlon format");
  COPY_INTO_FIXED_LEN_BUFFER(outlon->format,DEFAULT_LATLONFORMAT,
							"outlon format");

  nitems_supplied = 
     create_wjstbl(latlon_optfile,latlon_wjstbl,LATLON_WJSTBL);
  if (nitems_supplied == 0) return FALSE;

  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 < 0)  &&  (outlat->var >= 0)  )
    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 < 0)  &&  (outlon->var >= 0)    )
    err ("Longitude work requested but longitude input variable name not",
							          "supplied");

  if (inlat->var >= 0) {
    nitems_processed++;
    if (outlat->var < 0) outlat->var = inlat->var;
    else nitems_processed++;
  }
  if (inlon->var >= 0) {
    nitems_processed++;
    if (outlon->var < 0) 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 < 0)				)
    err ("Latitude work requested ",
	 "but latitude input variable name not supplied");

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

  return TRUE;
}

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 < 0) 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;
}

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	*/
/*  f, F, g, G, K, N, o, O, P, q, Q, R, v, V & z.  Good luck!		*/
{
  char timedate_wjstbl[NUM_OUTTIMES][TIMEDATE_WJSTBL+1];
  char all_timedates[NUM_OUTTIMES*TIMEDATE_WJSTBL + 1];
  int timedates_tags[NUM_OUTTIMES];
  int num_outtimes;

  int nitems_supplied[NUM_OUTTIMES],tot_items_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];

  tot_items_supplied = create_wjstbl(timedate_optfile,all_timedates,
						NUM_OUTTIMES*TIMEDATE_WJSTBL);
  if (tot_items_supplied == 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);
  }

    /*  Associate sets of timedates by putting each set in its own wjstbl */
  num_outtimes = 0;
  sep_str[0] = *all_timedates;
  sep_str[1] = '\0';
  ptr = strtok(all_timedates+1,sep_str);
  lenprefix = strlen(TIMEDATE_SEQUENCE_PREFIX);

  while (ptr != NULL) {

      /*  ptr pointing to keyword, possibly leading off w/ timeN_ or	*/
      /*  time_N_							*/
    if (strncmp(ptr,TIMEDATE_SEQUENCE_PREFIX,lenprefix) == 0) {
      ptr += lenprefix;
      if (*ptr == '_') ptr++;
      i = strtol(ptr,&ptr2,10);
      if (ptr2 == ptr)
	err("Timedate keyword does not include sequence number.  Keyword is ",
	    ptr - lenprefix);
      ptr = (*ptr2 == '_') ? ptr2+1 : ptr2; 
    } else
      i = 1;

      /*  See if we've already encountered this output time.		*/
    for (k=0; k<num_outtimes; k++)
      if (timedates_tags[k] == i) break;
        /*  Add it if not						*/
    if (k == num_outtimes) {
      if ((++num_outtimes) > NUM_OUTTIMES)
	errn("Too many output times requested.  Max = ",NUM_OUTTIMES);
      timedates_tags[k] = i;
      *timedate_wjstbl[k] = '\0';
      nitems_supplied[k] = 0;
    }

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

    ptr = strtok(NULL,sep_str);
  }

  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;
    else 
      out_td[k][HOUR_FRAG].conv = 
	(*ptr == 'l') ? CONVERT_LOCAL_TO_UTC  : CONVERT_UTC_TO_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;
	default:
	    /*  Put # in next message as marker	for bad format char	*/
	  tmp = "Julian output format # not one of gGPKjJvV 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);
    }

      /*  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;
    }

      /*  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 (out_td[k][i].var >= 0) {
        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"));
	}
        nitems_processed++;
      }
    if (nitems_supplied[k] > nitems_processed) {
      char tmp[20];
      sprintf (tmp,"time%d_",timedates_tags[k]);
      errn (buildstring("Unprocessed ",tmp," parameters.  Number skipped = ",
			" unproc param msg"),
	    nitems_supplied[k] - nitems_processed);
    }

      /*  All julian output work done here; ie, we never called		*/
      /*  affects_frag w/JULIAN_FRAG, etc.				*/
    if (out_td[k][JULIAN_FRAG].var > 0) {      
      if (  (out_td[k][JULIAN_FRAG].lev < out_td[k][MONTH_FRAG].lev) ||
	    (out_td[k][JULIAN_FRAG].lev < out_td[k][DAY_FRAG].lev)	)
	err ("Time/date Julian day/fractional year output ",
	     "requires month and day information");
	/*  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':
	  if (out_td[k][JULIAN_FRAG].lev < out_td[k][YEAR_FRAG].lev)
	    err ("Time/date Julian day/fractional year output ",
	         "requires year information");
      }
	/*  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 ("If time information is available, it must occur at a level ",
	     "before Julian day/fractional year output");
    }

    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)
      if (  (out_td[k][HOUR_FRAG].lev < out_td[k][TIME_OFFSET_FRAG].lev)   ||
	    (out_td[k][MINUTE_FRAG].lev < out_td[k][TIME_OFFSET_FRAG].lev)    )
        err ("Time zone conversion requires time displacement input",
	     " on a closer-to-0 level than the time");
 
      /*  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");
      case 'V':  case 'J':  case 'G':  case 'P':
        *out_td[k]
		  [MONTH_FRAG + DAY_FRAG - calculate_julian]
		  .source_field_type
	         = '*';
        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 
	       = '*';

    free (keyword);
  }

    /*  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++)
      if (  (j = out_td[k][i].var) >= 0  )
	if (var_used[j]) {
	  ioname__(j,varname);
	  err ("Single variable named for 2 different time outputs. Var = ",
	       varname);
	} else
	  var_used[j] = TRUE;

  return num_outtimes;
}

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 = -1;

      out[j][i].var = -1;	/*  Mark everything "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].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][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][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 = '*';
  }

  return;
}

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";

  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];

  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)  ||
	    (strncmp(descrip,REMOTE_OBJECT_PREFIX,
					strlen(REMOTE_OBJECT_PREFIX)) == 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;
}
