// Version 1.04 8/05/98

int dbase_oprov=200;// OPro version number*100, read it properly later
int dbase_handle;
int dbase_status;// keeps track eg of whether events claimed
int dbase_progress=0;// 0 at start, 1 if have received START event,
// ^ set to 2 when receive first DATA event
// ^ =3 after main header loaded when a user-accessible routine first called
string dbase_path;
string db_mergename="";// file being merged by user in OPro
int dbase_mergeusehdr=1;// option selected by user in OPro
string dbase_mainname="";// name of main CSV file
string dbase_username1="";// name of user CSV file 1
string dbase_username2="";// name of user CSV file 2
string dbase_maindate="";// date stamp of main CSV file
string dbase_userdate1="";// date stamp of user CSV file 1
string dbase_userdate2="";// date stamp of user CSV file 2
string dbase_dateformat="%24:%mi:%se %dy-%m3-%ce%yr";// Format string for date
string dbase_csv0;// string to hold complete, tokenised, main CSV file
string dbase_csv1;// ditto, user CSV file 1
string dbase_csv2;// ditto, user CSV file 2
int dbase_ncols0,dbase_ncols1,dbase_ncols2;// no of cols in each CSV file
int dbase_nrows0,dbase_nrows1,dbase_nrows2;// no of rows in each CSV file
int dbase_usehdr0=1,dbase_usehdr1=1,dbase_usehdr2=1;// 1 if use header lines for CSV files
// ^ can set dbase_usehdr0 from user's merge settings dbase_mergeusehdr, others must be guessed!
string dbase_itemfmt="";// string for db_itemfmt()
string dbase_lookupcondn="";// string for db_lookupcondn()
string db_strresult;// string for type() to return as result, global
int db_result;// int for type() to return as result, global
int dbase_loadmain=1;// 1 if load CSV files, 0 if already loaded
int dbase_loaduser1=1;// 1 if load CSV files, 0 if already loaded
int dbase_loaduser2=1;// 1 if load CSV files, 0 if already loaded
int dbase_debug=0;// set=1 if press Alt at start, prints debugging messages at top of screen...

// Now for reference variables
string dbase_refnames="\n";// empty list (array)
int dbase_refmax=0;// no names in list
string dbase_reffmt0="";// string to output at start of ref
string dbase_reffmt1="";// string to output at end of ref
// end of reference variables

// Now for definitions which must be removed for OPro>2.48:
// NB these are not actually used in OP 2.48, but must be defined
/*
int sins(string &s,string &t,int i)
{return i;}
void openfields(void)
{}
string macro(string &s)
{return s;}
*/
//

void dbase_mmstart(int user,int file,string & name)
// Called when mail merge starts on a file
// name is the name of the CSV file, if there is one, otherwise it is
// a null string.  Not in OPro 2.48
// file and user are as usual for events
{// dbase_loadmain=1;dbase_loaduser1=1;dbase_loaduser2=1;
 // ^ no need to invalidate these as check date stamps
 //if (dbase_mainname!=name) dbase_maindate="";// invalidate date if name changed
 dbase_progress=1;// 1=starting, not Boolean
 db_mergename=name;// make global, file being merged by user in OPro
 dbase_debug=(bbc_inkey(-3)!=0);
}

void dbase_mmdataevent(int user,int file,int record)
// Called when mail merge starts each record
// record is the line number in the CSV file, starts at 0 if ignore headers,
//  else starts at 1.  Not in OPro 2.48
// file and user are as usual for events
{if (dbase_progress==1)
 {dbase_progress=2;// making progress
  dbase_mergeusehdr=(record>0);// 1 if use header line for main CSV file
 }
}

string dbase_expandfilename(string &filename)
// checks if filename includes ":", if not, adds directory of document
{int c,i;
 string fullname="";
 if ((filename/":")<0)
 {i=fileinfo(currentfile(),fullname);// set fullname to current document's full name,
  // ^ bit 0 of i=1 if has been saved ie name includes path?
  if ((i & 1)==0) return filename;// if never saved has invalid pathname
  for (i=slen(fullname)-1;(i>=0) && ((c=schar(fullname,i))!='.') && (c!=':');i--) {}
  // ^ scan back from end of string looking for . or :
  if (i<0) return filename;// bad document name
  return mids(fullname,0,i+1)+filename;// include last . of doc name,+ all of name given
 }
 else return filename;// includes ":" so treat as full path
}

int dbase_filesame(string &file,string & oldfile,string &olddate,string &newdate)
// check if file file is same as old file oldfile (which had date olddate)
// exit=1 if same, else=0, sets newdate to stamp of file
{int flag=0;
 if (file==oldfile)
 {gettimestamp(dbase_dateformat,newdate,file);
  flag=(newdate==olddate);// 1 if files same
 }
 else // not same, flag already=0
 {newdate=olddate;// ensure set, just in case
 }
 return flag;// newdate set
}

// read string s in CSV format, converting it to line for database string (starting with 0x01) in t
// exits= no of els found
int dbase_readcsvline(string &s,string &t)
{
 int achar,c=0,i=0,j,k,l,quote;// c counts items
 string el;
 t="";
 l=slen(s);// faster outside loop (pre OP 2.49)
 while (i<l)
 {el="";
  quote=0; // not in "..."
  if (mids(s,i,1) == "\"") {quote=1;i++;} // found a quote, skip it
  j=i; // j=start of bit to copy
  while ((i<l) && (((achar=schar(s,i)) != ',') || (quote))) // NB achar set before (quote) checked
  {i++; // at next char or end... could also check for \n etc and copy bit by bit
   switch (achar)
   {case '\"': // check if next char is also a " (if quote), else end quoted region
    {if (quote)
     {if ((i<l) && (schar(s,i)=='\"'))
      {el+=mids(s,j,i-j);// doubled " means just one ", so copy string so far, incl 1st "
       j=i;// at new start of part to copy
       i++;// past 2nd " since the 1st was used
      }
      else
      {el+=mids(s,j,i-1-j);// don't copy " at end
       while ((i<l) && (schar(s,i) != ',')) i++;// skip anything before next , (usu nothing)
       j=i;// so nothing copied again
       quote=0;// no longer in quotes
      }
     }
    } break;
   }
  }
  k=i; // at end of this item
  if (quote && mids(s,k-1,1) == "\"") k--; // go back past " if there was one at start
  el+=mids(s,j,k-j);
  c++; // next item
  if ((i<l) || (el!="")) t+=chars(0x01)+chars(c+31)+el;
  // ^ c>31 always, avoids 0,1 and \n (and other ctrl codes)
  /* if (bbc_inkey(-3) != 0)
  {bbc_vdu(4);  //....
   printi(c);
   prints(" "+el+" "+itos(slen(t))+"\n");
   bbc_vdu(5);
  } */
  if (i<l) {i++;} // past separator (comma)
 }
 return c;
}

// read data from 'csv' file dbase_path ...
// a is string to store file in (OPro copes with it being made huge)
// exits = no of items in 1st row, returns snrows=itos(no of rows)
// If file not found, exits with ncols=-1 and snrows=err message
// max is max no of lines to load (eg 1 to get headings only, 0=all)
int dbase_readdata(string filename,string &a,string &snrows,int max)
{
 int c,h,i,ncols,r=0;// r is row counter
 string el,s,t;
 a = "\n"; // always have LF at start (so can search for line no.)
 h = fileopen(filename, "rb");
 if (h)
  while((!fileeof(h)) && ((r<max) || (max==0)))
  {//s="";// in case EOF due to empty line at end...
   i=filereads(s, h); // get one line of CSV file, !=0 if error
   if (i==0)
   {i=0; // at start of s
    c=dbase_readcsvline(s,t); // t gets set by dbase_readcsvline() to array of items (one row)
    if (r==0) ncols=c;// use 1st row in case others are shorter
    if (c>0)
    {a+=itoxs(r)+t+"\n"; // row number (after previous LF), then (0x01+item) many times, then LF at end
     r++;
    }
   }
  }
 else
 {
  snrows="File "+filename+" not found or not readable! ";// try to send this back as text message
  ncols=-1;// -1 means not found
 }
 if (h)
  fileclose(h);
 snrows=itos(r);// can only return strings!
 return ncols;// no of items in 1st row
}

// find item at (row>=0,col>=0) in string s, and set string t to item
// exits <0 if not found
int dbase_findel(string &s, int row, int col, string &t)
{
 int c,r,p, b, e,l;
 t="\n"+itoxs(row)+chars(0x01); // line starts with \n,row in hex,0x01,item etc now find it
 if ((p = (s / t)) >= 0)
 {
  b=p+slen(t)-1; // past \n and line no, at 0x01 (which starts each item)
  if (dbase_oprov<249)
   {l=slen(s);
    for (e=b+1;e<l && schar(s,e) != '\n';++e) {}; // find line
   }
  else
   {e=sins(s,"\n",b+1);// only available in OPro >=2.49
   }
  if (e<0) e=slen(s)+1;// if sins() didn't find \n
  t=mids(s,b,e-b);// get whole line except row number, no LF at end
  if ((p=t/(chars(0x01)+chars(col+32))) >= 0) // find column (item), p<0 if not found
  {t=mids(t,p+2,slen(t)-p-2);// past 2-byte label
   if ((e=t/chars(0x01)) < 0) e=slen(t);// at next col or end
   t=mids(t,0,e);// item
  }
 }
 if (p<0) t="No such item (row "+itos(row+1)+", col "+itos(col+1)+")!";// handles either if() statement
 return p;
}

int dbase_finduserCSVfields(string &field,string &sfile)
// find number of field "field" in CSV file string sfile, =-1 if not found
{int fieldno=0,flag=1,rc;
 string t;
 do
 {rc=dbase_findel(sfile,0,fieldno,t);// sets t=name of field found, row 0
  flag=(t!=field);// case sensitive, or could put + in front of each
  if (flag) fieldno++;
 } while (flag && (rc>=0))
 if (rc<0) fieldno=-1;// shows not found
 return fieldno;
}

int dbase_hdrnum(string &field) // convert item name field to number 0 etc (since no header),
// ^ =-1 if bad name
{int i;
 i=stoi(field)-1;// user should use numbers 1,2, etc, if name, gives 0-1=-1
}

int dbase_finduserCSVfield(string &field,int file)
// find number of field "field" in CSV file number file, =-1 if not found
{int fieldno=-1;// just in case
 if (dbase_debug) {bbc_vdu(4);prints("in finduserCSVfield: "+field+"\n");bbc_vdu(5);}//...
 switch (file)
 {case 0:
   if (dbase_usehdr0)
   {return dbase_finduserCSVfields(field,dbase_csv0);}// passed by reference
   else
   {return dbase_hdrnum(field);} // user shd use "1","2" etc
   break;
  case 1:
   if (dbase_usehdr1)
   {return dbase_finduserCSVfields(field,dbase_csv1);}
   else
   {return dbase_hdrnum(field);}
   break;
  case 2:
   if (dbase_usehdr2)
   {return dbase_finduserCSVfields(field,dbase_csv2);}
   else
   {return dbase_hdrnum(field);}
   break;
 }
 return fieldno;
}

int dbase_codeuserCSVfield(string &field,string &result,int file)
// find number of field "field" in CSV file number file, result=chars(type)+chars(fieldno)
// or result=error message if not found; exit=0 if OK, =2 if no such item
{int fieldno;
 fieldno=dbase_finduserCSVfield(field,file);
 if (fieldno<0)
 {result="No such item ("+field+") in file "+itos(file)+"!";
  if (file==0 && !dbase_usehdr0) {result+=" Not using first line as names, perhaps you should click on 'Ignore headings record'";}
  return 2;
 }
 else
 {result=chars(file+1)+chars(fieldno+32);// code by adding 32 so normal char, fast
  return 0;
 }
}

string dbase_fixcmd(string &result,string char,string nchars)
// scan string, change char to nchars so eval(")/GSTrans (|) OK
{string t="";
 int i;
 while (result!="")
 {i=result/char;
  if (i<0)
  {t+=result;result="";}
  else
  {t+=mids(result,0,i)+nchars;
   result=mids(result,i+1,slen(result)-i-1);// rest of string after char, avoids needs for sins(,,i)
  }
 }
 return t;
}

int dbase_parsestring(string &condn,string &result,int flag)
/*  scans condn, converting
 [field] to reference to main CSV file, chars(0x01)+itos(fieldno)+"\n",
 <field> to reference to user CSV file 1, 0x02 fieldno\n.
 and #field# to reference to user CSV file 2, 0x03 fieldno\n,
Result is in result (which MUST BE DIFFT string from condn)
Use dbase_eval(result,answer,,) to evaluate string when needed.
flag=1 if enclose substituted fields in quotes;
exits =rc=0 if OK, =1 if missing bracket,=2 if item not found */
{int rc=0;//return code, =0 if OK
 int achar,nextc,i,j,len;
 string tstring,t,quote="";
 if (dbase_debug) {bbc_vdu(4);prints("in parsestring: "+condn+"\n");bbc_vdu(5);}//...
 if (flag==1) quote="\"";// enclose fields in quotes for evaluation
 result="";// empty at start
 len=slen(condn);// slen() inefficient pre OP 2.49
 for (i=0;i<len;i++)
 {if (i+1<len) {nextc=schar(condn,i+1);} else {nextc=0;}
  switch (achar=schar(condn,i))
  {case '[':// main file (0)
    if (nextc=='[')
    {i++;}// double [ means just one
    else
    {if (nextc!=']') // else [] means just []
     {tstring=mids(condn,i+1,len-i-1);// start search from i+1
      j=tstring / "]";//find end
      if (j>0)
      {rc=dbase_codeuserCSVfield(mids(tstring,0,j),t,0);//rc=0 if OK,=2 if item not found
       // ^ 2nd 0=file 0, adds 32 so normal char, fast
       if (rc==0)
       {result+=quote+t+quote;
        achar=0;// so don't copy it again;
        i+=j+1;// past ]
       }
       else
       {result=t;return rc;}// return error message, rc!=0
      }
      else {result="Missing ] in "+condn+"!";return 1;}// missing ]
     }
    }
    break;
   case '<':// user file 1
    if (nextc=='<')
    {i++;}
    else
    {if (nextc!='>')
     {tstring=mids(condn,i+1,len-i-1);
      j=tstring / ">";
      if (j>0)
      {rc=dbase_codeuserCSVfield(mids(tstring,0,j),t,1);// 1=file 1
       if (rc==0)
       {result+=quote+t+quote;
        achar=0;
        i+=j+1;// past >
       }
       else
       {result=t;return rc;}
      }
      else {result="Missing > in "+condn+"!";return 1;}
     }
    }
    break;
   case '#':// user file 2
    if (nextc=='#')
    {i++;}
    else
    {tstring=mids(condn,i+1,len-i-1);
     j=tstring / "#";
      if (j>0)
      {rc=dbase_codeuserCSVfield(mids(tstring,0,j),t,2);// 2=file 2
       if (rc==0)
       {result+=quote+t+quote;
        achar=0;
        i+=j+1;// past #
       }
       else
       {result=t;return rc;}
      }
      else {result="Missing # in "+condn+"!";return 1;}
     }
    break;
  }
  if (achar!=0) {result+=chars(achar);}// add to result unless done already (setting achar=0)
 }
 return rc;// result also updated
}

int dbase_substboth(string &exprn,string &result,int file1row,int file2row,int flag)
// exprn is coded condn (as produced by dbase_parsestring())
// flag=0 if don't change strings, else =1 if check strings and substitute \" for "
// exit=rc=0 if OK, result=text
{int rc=0;//return code, =0 if OK
 int file,achar,nchar;
 int i=0;//ptr to char in exprn
 string t;//for tempy result
 if (dbase_debug) {bbc_vdu(4);prints("in substboth\n");bbc_vdu(5);}//...
 result="";//none yet
 while (i<slen(exprn))
 {if (3 < (achar=schar(exprn,i)))
  {result+=chars(achar);}// copy to result
  else
  {i++;
   nchar=schar(exprn,i);// next char=field no.+32
   switch (achar)
   {case 0x01:// main file (0)
     t=field(nchar-31);// 32-1 since field(1) is the first
     break;
    case 0x02:// user file 1
     achar=dbase_findel(dbase_csv1,file1row,nchar-32,t);// scrap ret code achar
     // ^ achar<0 if not found, t set eg to "not found", don't stop, pretend OK
     break;
    case 0x03:// user file 2
     achar=dbase_findel(dbase_csv2,file2row,nchar-32,t);
     break;
   }
   if (flag==1) t=dbase_fixcmd(t,"\"","\\\"");// convert " to \" in fields from CSV files
   result+=t;
  }
  i++;// next char
 }
 if (dbase_debug) {bbc_vdu(4);prints("ending substboth: "+result+"\n");bbc_vdu(5);}//...
 return rc;// result set
}

int dbase_eval(string &exprn,string &result,int file1row,int file2row)
// exprn is coded condn (as produced by dbase_parsestring())
// result is "" if TRUE, "-" if FALSE
// exit=rc=0 if OK
{int rc=0;//return code, =0 if OK
 rc=dbase_substboth(exprn,result,file1row,file2row,1);// substitute for fields
 if (rc==0)
 {if (result/"|">=0) result=dbase_fixcmd(result,"|","||");// scan string, double | so GSTrans OK
  // ^ check first for speed, rare occurrence
  if (dbase_oprov>248)
  {
   macro("{db_result=("+result+")}");// eval result, to db_result; new in OP 2.49
  }
  else
  {
   type("{db_result=("+result+")}");// eval result, to db_result; suggested by DP, not ideal?
  }
  if (db_result!=0) result=""; else result="-";
 }
 else result="--";
 return rc;
}

int dbase_subst(string &fmt,int file1row,string &lookupcondn,string &result)
// convert parsed string fmt by substituting for row file1row in user file 1,
// looking up in file 2 according to lookupcondn if nec, returns result in result
// exit =rc=0 if OK
{int rc=0,i;//return code, =0 if OK, temp
 int file2row=1;// ....0 // row to use in user file 2, 0=heading in case no lookupcondn
 string t="-";// mustn't start=""
 if (dbase_debug) {bbc_vdu(4);prints("in subst\n");bbc_vdu(5);}//...
 if ((lookupcondn!="") && (dbase_nrows2>0) && ((fmt/chars(0x03))>0)) // skip if no field from file 2
 {if (dbase_usehdr2) i=1; else i=0;// row to start from
  for (file2row=i;(t!="") && (file2row<dbase_nrows2) && (rc==0);file2row++)
  {rc=dbase_eval(lookupcondn,t,file1row,file2row);// t="" if eval'ed to TRUE (<>0)
  }// loop ends if t=="", with file2row set 1 too high
  file2row--;// was incremented before checking
  if (rc)  // outside loop in case faster
  {result="Error evaluating lookup condition ("+lookupcondn+")!";return rc;}
 }
 rc=dbase_substboth(fmt,result,file1row,file2row,0);
 return rc;
}

string db_loadmain(string &filename) // load main CSV file, name filename
// This shdn't be necessary, except for 1st row to get field names;
// ideally would get file name automatically...
// Exits ="" or error message if any, use {macv=db_loadmain("name")} for
// message or just {db_loadmain("name")}
{string t="",newdate="";
 dbase_progress=3;// main file now loaded (only useful if OPro 2.48 which has no events)
 // ^ set it early to avoid looping if error
 filename=dbase_expandfilename(filename);// add document's directory if no ":"
 if ((dbase_loadmain) || !dbase_filesame(filename,dbase_mainname,dbase_maindate,newdate))
 {dbase_ncols0=dbase_readdata(filename,dbase_csv0,t,1);
  // ^ load to dbase_csv0, t=itos(nrows) or err message if exits<0, 1=just 1 row (headings)
  if (dbase_debug) {bbc_vdu(4);prints("in loadmain: ");prints(" "+t+" "+filename+"\n");bbc_vdu(5);}//...
  if (dbase_ncols0>=0)
  {dbase_nrows0=stoi(t);t="";// no error so set t=""
   dbase_loadmain=0;// no need to load main CSV file again
   dbase_mainname=filename;// keep name so can check if changed next time
   dbase_maindate=newdate;// keep date stamp so can check if changed next time
   /* if (dbase_oprov<249) db_mergename=dbase_mainname; */ // can't read actual name in OP 2.48, so pretend it's same
   /* if (dbase_mainname==db_mergename) dbase_usehdr0=dbase_mergeusehdr; */ // same as OP's CSV file, do always, below
  }
  else
  {dbase_ncols0=0;dbase_nrows0=0;dbase_loadmain=1;dbase_mainname="";dbase_usehdr0=1;}
  // ^ no file loaded
 }
 if (dbase_oprov<249) db_mergename=dbase_mainname;// can't read actual name in OP 2.48, so pretend it's same
 if (dbase_mainname==db_mergename) dbase_usehdr0=dbase_mergeusehdr;// same as OP's CSV file
 // ^ do this each time, in case user has clicked on "Ignore headings record"
 return t;// error message or ""
}

string db_loadfile1(string &filename) // load user CSV file 1, name filename
// Exits ="" or error message if any
{string t="",newdate="";
 filename=dbase_expandfilename(filename);// add document's directory if no ":"
 if ((dbase_loaduser1) || !dbase_filesame(filename,dbase_username1,dbase_userdate1,newdate))
 {dbase_ncols1=dbase_readdata(filename,dbase_csv1,t,0);
  // ^ load to dbase_csv1, t=itos(nrows) or err message if exits<0, 0=all rows
  if (dbase_ncols1>=0)
  {dbase_nrows1=stoi(t);t="";// no error so set t=""
   dbase_loaduser1=0;// no need to load user CSV file 1 again
   dbase_username1=filename;// keep name so can check if changed next time
   dbase_userdate1=newdate;// keep date stamp so can check if changed next time
  }
  else
  {dbase_ncols1=0;dbase_nrows1=0;dbase_loaduser1=1;dbase_username1="";dbase_usehdr1=1;}
  // ^ no file loaded
 }
 return t;// error message or ""
}

string db_loadfile2(string &filename) // load user CSV file 2, name filename
// Exits ="" or error message if any
{string t="",newdate="";
 filename=dbase_expandfilename(filename);// add document's directory if no ":"
 if ((dbase_loaduser2) || !dbase_filesame(filename,dbase_username2,dbase_userdate2,newdate))
 {dbase_ncols2=dbase_readdata(filename,dbase_csv2,t,0);
  // ^ load to dbase_csv2, t=itos(nrows) or err message if exits<0, 0=all rows
  if (dbase_ncols2>=0)
  {dbase_nrows2=stoi(t);t="";// no error so set t=""
   dbase_loaduser2=0;// no need to load user CSV file 2 again
   dbase_username2=filename;// keep name so can check if changed next time
   dbase_userdate2=newdate;// keep date stamp so can check if changed next time
   if (dbase_username2==dbase_mainname) dbase_usehdr2=dbase_usehdr0;// same as main file
  }
  else
  {dbase_ncols2=0;dbase_nrows2=0;dbase_loaduser2=1;dbase_username2="";dbase_usehdr2=1;}
  // ^ no file loaded
 }
 return t;// error message or ""
}

string db_lookinmain(void) // user-accessible command to use main merge file as user file 2
{return db_loadfile2(db_mergename);
}

void dbase_checkmainloaded(void)
// Called by various user-called routines to load main CSV file's header if nec
{if (dbase_progress==2) // load main file's header
 {dbase_progress=3;// making progress, main file now loaded
  db_loadmain(db_mergename);// load headings from file, use same file as in OPro
 }
}

string db_if(string &condn,string exprn1,string exprn2)
// condn is an exprn, if(condn) exit=exprn1; else exit=exprn2;
// condn,exprn1,exprn2 are all parsed to cope with field names,
// [] fields (in main file) are OK, though there are no valid row pointers in user file 1,
// uses dbase_lookupcondn to lookup in user file 2 if nec
// NB uses exprn1 as tempy string, so no "&" to reference it
{int rc=0;
 string exprn,t;
 dbase_checkmainloaded();// ensure main file header set up
 rc=dbase_parsestring(condn,exprn,1);// sets exprn to parsed condn, flag =1 ie put strings in quotes
 if (rc!=0) {return exprn;}// error message
 rc=dbase_eval(exprn,t,0,0);// t="" if eval'ed to TRUE (<>0), no file1row,file2row(=0)
 if (rc) {return " Error evaluating condition ("+condn+")!";}
 if (t!="") //="" iff TRUE
 {exprn1=exprn2;}// condn false, so use exprn2, NB exprn1 not called by reference
 if (mids(exprn1,0,1)=="{")
 {//t=macro("{newpage}");// ...
  t=macro(exprn1);// send text as a macro command (eg {newpage}) instead
  return t;
 }
 else
 {rc=dbase_parsestring(exprn1,exprn,0);// sets fmt to parsed s, flag =0 ie don't put strings in quotes
  if (rc!=0) {return "Problem with text expression ("+exprn1+"): "+exprn;}
  rc=dbase_subst(exprn,0,dbase_lookupcondn,t);// lookup in user file 2 (any use?)
 }
 return t;
}

string db_skipcondn(string &condn)
// condn is an exprn, if(condn) skip to next record
// condn is parsed to cope with field names,
// [] fields (in main file) are OK, though there are no valid row pointers in user files 1 & 2
{int rc=0;
 string exprn,t;
 dbase_checkmainloaded();// ensure main file header set up
 rc=dbase_parsestring(condn,exprn,1);// sets exprn to parsed condn, flag =1 ie put strings in quotes
 if (rc!=0) {return exprn;}// error message
 do {
  rc=dbase_eval(exprn,t,0,0);// t="" if eval'ed to TRUE (<>0), no file1row,file2row(=0)
  if (rc) {return " Error evaluating condition ("+condn+")!";}
  if (t=="") //="" iff TRUE
  {rc=nextrecord();}// skip to next record if true,!=0 at eof
 } while (t=="" && (rc==0));
}

string dbase_allitems(string &field,string &condn,int flag)
// condn is eg [field]=="value" && <mainfield>==[field2], ie an exprn
// Finds all rows where expression is satisfied; flag=0 if find all, =1 if just find first,
// if =2, field is fieldtosum, so sums values in <field> in each rec of user file 0
// satisfying exprn; =3 as 2, but just count items
{int file=1;// search user file 1 only
 int nfound=0,x=0,i; // for count,sum,temp
 int fieldno;// field to sum
 int rc=0;// return code, 0=OK
 int file1row,file2row;// row pointers in user files 1 and 2,>=0
 string exprn,t,text;// parsed condn, t=temp, full result
 dbase_checkmainloaded();// ensure main file header set up
 if (flag==2)
 {
  fieldno=dbase_finduserCSVfield(field,1);
 }
 if (condn != "")
 {
  rc=dbase_parsestring(condn,exprn,1);// sets exprn to parsed condn, flag =1 ie put strings in quotes
  if (rc!=0) {return exprn;}
  if (dbase_usehdr1) i=1; else i=0;// row to start from
  for (file1row=i;file1row<dbase_nrows1;file1row++)
  {rc=dbase_eval(exprn,t,file1row,0);// t="" if eval'ed to TRUE (<>0), no file2row(=0)
   if (rc) {return text+" Error evaluating condition ("+condn+")!";}// text has result so far
   if (t=="")
   {nfound++;
    if (flag<2)
    {rc=dbase_subst(dbase_itemfmt,file1row,dbase_lookupcondn,t);//lookup in user file 2, expand itemfmt
     text+=t;
     if (flag==1) return text;// exit if just finding 1st occurrence
    }
    else
    {if (flag==2) {rc=dbase_findel(dbase_csv1,file1row,fieldno,t);x+=stoi(t);}
    // ^ get field in t, sum it (integers only)
    }
   }
  }
  if (flag==2) text=itos(x); else if (flag==3) text=itos(nfound);
  // ... Can't use ddl(text); since error "Can't edit, mail merge active"
  // ^ try sending as DDL so can use styles
  return text;// .. any way to send styles/effects?
 }
 else
  return "Missing expression in db_allitems()/db_finditem()/db_countitems()!";
}

string db_allitems(string condn)
{return dbase_allitems("",condn,0);
}

string db_finditem(string condn)
{return dbase_allitems("",condn,1);
}

string db_sumitems(string &field,string condn)
{return dbase_allitems(field,condn,2);
}

string db_countitems(string condn)
{return dbase_allitems("",condn,3);
}

string db_itemfmt(string s)
{int rc;
 string fmt;
 dbase_checkmainloaded();// ensure main file header set up
 rc=dbase_parsestring(s,fmt,0);// sets fmt to parsed s, flag =0 ie don't put strings in quotes
 if (rc!=0) {return "Problem with item format ("+s+", "+fmt+"): "+fmt;}
 dbase_itemfmt=fmt; // global
}

string db_lookupcondn(string s)
{int rc;
 string exprn;
 dbase_checkmainloaded();// ensure main file header set up
 rc=dbase_parsestring(s,exprn,1);// sets exprn to parsed s, flag =1 ie put strings in quotes
 if (rc!=0) {return "Problem with lookup condition ("+s+", "+exprn+") ";}
 dbase_lookupcondn=exprn; // global
}

// Start of reference commands
int dbase_par(string &t,string &par,int ptr) // scans t from ptr looking for par then "," or " ",
// skips any ","s or " "s at start, sets par=param scanned,
// exits=new ptr (at next delimiter)
{int c,i,len;
 len=slen(t);
 while (ptr<len && ((c=schar(t,ptr))==' ' || c==',')) ptr++;// past "," or " "
 i=ptr;//start of par
 while (ptr<len && (c=schar(t,ptr))!=' ' && c!=',') ptr++;// past par, to ","
 par=mids(t,i,ptr-i);
 return ptr;
}

string db_ref(string list)
{string t,result="";
 int c,i,j,len,ptr=0,ptr1;
 len=slen(list);
 while (ptr<len) // extract names from list
 {ptr=dbase_par(list,t,ptr); // get t=one name
  if (t!="") // may be at end of list
  {t=+t;// force to uppercase
   t=chars(0x01)+chars(0+32)+t+chars(0x01);// col 0, there's a col 1 too
   i=dbase_refnames/t;
   if (i<0)
   {// add item
    j=dbase_refmax+1;// next available number
    dbase_refmax++;// inc next available number
    dbase_refnames+=t+chars(1+32)+itos(j)+"\n";// store, return j, t already includes tokens
   }
   else
   {i+=1+slen(t);//at 2nd item on row
    t=mids(dbase_refnames,i,10);// item won't be more than 10 chars long, don't bother to get more
    // ^ could use sins() if it were in all versions of OP (appeared in 2.49)
    j=stoi(t);// ignores \n at end
   }
   t=itos(j);
   result+=t+",";// don't put any space after ",", will do so later
  }
 }
 if (schar(result,i=slen(result)-1)==',') result=mids(result,0,i);// remove last ,
 // tidy list, converting sequences to ranges eg 2,3,4 becomes 24
 len=slen(result);
 list="";
 for (ptr=0;ptr<len;)
 {ptr=dbase_par(result,t,ptr);// sets t=number, ptr to ","
  i=stoi(t);// 1st ref no.
  j=i;// ref expected in sequence
  do
  {ptr1=ptr;
   ptr=dbase_par(result,t,ptr);// sets t=num, skipping , at start
   c=stoi(t);// this ref num
   j++;// next in sequence
  } while (c==j && ptr<len)
  if (c==j) {ptr1=ptr;j++;}//at end, last was in the sequence
  t=itos(i);
  if (j==i+2)
  {t+=","+itos(i+1);}
  else
  {if (j>i+2) t+=""+itos(j-1);}
  if (list=="") list=t; else list+=", "+t;
  ptr=ptr1;// at next par
 }
 return dbase_reffmt0+list+dbase_reffmt1;
}

string db_setfmt(int item,string fmt)
{switch(item)
 {case 1:dbase_reffmt0=fmt;
  case 2:dbase_reffmt1=fmt;
 }
 return "";
}

string db_clrstr(void)
{dbase_refnames="\n";// empty list
 dbase_refmax=0;// no names in list
 return "";
}

// End of reference commands

// set up EVENT_MERGESTART and EVENT_MERGEDATA events if OPro>=2.49
void dbase_setevent(int i)
{
 if (i)
 {if (dbase_oprov>248)
  {addeventhandler(0x310, 0, "dbase_mmstart");
   addeventhandler(0x312, 0, "dbase_mmdataevent");
  }
 }
 else
 {if (dbase_oprov>248)
  {remeventhandler(0x310, 0, "dbase_mmstart");
   remeventhandler(0x312, 0, "dbase_mmdataevent");
  }
 }
 dbase_status = i;
}


// deal with 'Database' menu entry
int dbase_entry(int entry, int subcode)
{
 if(subcode == -1)
  dbase_setevent(!dbase_status);
 return(0);
}


// tick or un-tick 'Database' menu entry

int dbase_flags(int entry, string &text)
{
 return(dbase_status);
}


// deal with 'Database' menu entries

int dbase_menu_entry(int entry, int subcode)
{string t;
 switch(entry)
 {case 0: // insert standard set of merge fields
         // doesn't work in OP 2.49b2?
         t="MERGE_01={merge 100 \"setup\"";
         if (dbase_oprov<249) t+=" \"{macv=db_loadmain(\\\"\\\")}"; else t+=" \"";
         // ^ not needed for OP 2.49 on
         t+="{macv=db_loadfile1(\\\"\\\")}{macv=db_loadfile2(\\\"\\\")}\"}";
         ddl(t);
         ddl("MERGE_02={merge 101 \"lookupcondn\" \"{macv=db_lookupcondn(\\\"\\\")}\"}");
         ddl("MERGE_03={merge 102 \"itemfmt\" \"{macv=db_itemfmt(\\\"\\\")}\"}");
         ddl("MERGE_04={merge 103 \"finditems\" \"{macv=db_finditem(\\\"\\\")}\"}");
         ddl("MERGE_05={merge 104 \"allitems\" \"{macv=db_allitems(\\\"\\\")}\"}");
         if (dbase_oprov>248) {openfields();}
         break;
  case 1:dbase_loadmain=1;dbase_loaduser1=1;dbase_loaduser2=1;// ensure CSV files loaded at next merge
         break;
 }
 return(0);
}

int dbase_menu(int open)
{
 return(dbase_handle);
}


// create 'Database' sub-menu and add it to 'Applets' menu
// read data and enable abbreviation expansion

void main(void)
{string t;
 dbase_oprov=programversion();// OPro version number*100, global
 script_menu_initialise();

 dbase_handle = create_menu("{DBASE_00}");
 // addentry_menu(dbase_handle, "dbase_menu_entry","","","","{DBASE_01}");
 addentry_menu(dbase_handle, "dbase_menu_entry","","","","{DBASE_01}");
 //addentry_menu(dbase_handle, "dbase_menu_entry","","","","{DBASE_02}");

 addentry_menu(script_handle,"dbase_entry","dbase_flags","dbase_menu","","{DBASE_00}");
 dbase_status = 1;// on by default (claims some mail merge events)
 if (dbase_status == 1) dbase_setevent(1);// don't do anything if off by default
}
