(***************************************************************************
 :Program.    XStat
 :Author.     Jürgen Weinelt
 :Address.    Zur Kanzel 1, D-97762 Hammelburg, Germany
 :Address.    USENET: jow@sun.rz.uni-wuerzburg.de
 :Address.    USENET: jow@hcast.adsp.sub.org
 :Address.    USENET: jow@hcast.franken.de
 :Version.    $VER: XStat 1.13 (28-Aug-93)
 :Copyright.  Freeware
 :Language.   Modula-2
 :Translator. M2Amiga V4.2
 :History.    1.00 initial release
 :History.    1.01 bug fix: used to guru when xfer = 0 bytes
 :History.    1.02 added peak cps rating
 :History.    1.03 added monthly statistics
 :History.    1.04 switched to DosL.ReadArgs(), fixed nasty current date
 :History.         bug with WB2.1 (problems with locale and "TODAY")
 :History.    1.05 added per host statistics
 :History.    1.06 integrated "per host" statistics into "HOSTNAME" option
 :History.         (pattern matching using dos.library)
 :History.    1.07 bug fix: linked list of Statistics-records was not
 :History.         sorted correctly
 :History.    1.08 added STDERR option; replaced all "ø"'s with "av."
 :History.    1.09 added "LOCAL" and "IGNORE" to XStat.data
 :History.    1.10 added multiple names feature to XStat.data N-lines
 :History.         fixed a small bug in the NI/NO feature
 :History.    1.11 multiple XStat.data N-lines implemented
 :History.         XStat.data N-line entry ".default." implemented
 :History.         Xferstat "EOF" bug fixed
 :History.         string length bumped to 256
 :History.    1.12 added ".illegal." for missing host names in XFerStat
 :History.         XStat now skips the 1st 10 chars of the 2nd XferStat line
 :History.    1.13 output now shows the first and last date that was used
 :History.         added "PERIOD" and "LAST" options
 :Contents.   Extracts statistics from UUCiCo "Xferstat" logfile. Requires
 :Contents.   one of the "Xferstat" UUCICOs (tested with the SWB UUCiCo
 :Contents.   and the one from wUUCP - thanks wusel!)
 **************************************************************************)



MODULE XStat;
(*$ StackChk:=FALSE *)
(*$ CaseChk:=FALSE *)
(*$ Volatile:=FALSE *)



(*$ DEFINE BETA:=FALSE *)



IMPORT Arts;
IMPORT ASCII;
IMPORT Break;
IMPORT DosD;
IMPORT DosL;
IMPORT ExecD;
IMPORT ExecL;
IMPORT InOut;
IMPORT R;
IMPORT RealConversions;
IMPORT RealInOut;
IMPORT String;

IMPORT SYSTEM;



CONST
 strlen=255;        (* was 100 before 1.11 *)

 smult=1;           (* seconds per second :-) *)
 mmult=60*smult;    (* seconds per minute *)
 hmult=60*mmult;    (* seconds per hour *)
 dmult=24*hmult;    (* seconds per day *)

 dsun=0*dmult;      (* sunday offset into week (in seconds) *)
 dmon=1*dmult;      (* monday offset into week (in seconds) *)
 dtue=2*dmult;      (* tuesday offset into week (in seconds) *)
 dwed=3*dmult;      (* wednesday offset into week (in seconds) *)
 dthu=4*dmult;      (* thursday offset into week (in seconds) *)
 dfri=5*dmult;      (* friday offset into week (in seconds) *)
 dsat=6*dmult;      (* saturday offset into week (in seconds) *)

 defxstatdata="UULIB:XStat.data";
 defxferstat="UUSPOOL:XferStat";

 vers="XStat 1.13" (*$ IF BETA *) +" *BETA*" (*$ ENDIF *) ;
 version="$VER: "+vers+" (28-AUG-93)";
 argTemplate="FILE/K,DATA/K,V=VERBOSE/S,M=MONTH/K,NI=NOINCOM/S,NO=NOOUTGO/S,"+
             "Q=QUIET/S,H=HOSTNAME/K,FD=FROMDATE/K,TD=TODATE/K,SE=STDERR/S,"+
             "P=PERIOD/N,L=LAST/N";

 extendedhelp="\nXStat "+argTemplate+"\n"+
             "\n DATA      XStat.data file\t\t  def="+defxstatdata+
             "\n FILE      Xferstat file\t\t  def="+defxferstat+
             "\n VERBOSE   verbose mode switch  \t  def=off"+
             "\n MONTH     monthly, override fd/td\t  def=off (format MMM-YY)"+
             "\n NOINCOM   don't evaluate incoming calls  def=off"+
             "\n NOOUTGO   don't evaluate outgoing calls  def=off"+
             "\n QUIET     suppress non-fatal errors\t  def=off"+
             "\n HOSTNAME  evaluate calls to host\t  def=all"+
             "\n FROMDATE  ignore calls before date\t  def=01-JAN-78"+
             "\n TODATE    ignore calls after date\t  def=current date/time"+
             "\n STDERR    send error messages to StdErr  def=off\n"+
             "\n PERIOD    n days from fd, override td\t  def=0\n"+
             "\n LAST      n days before td, override fd  def=0\n\n";



TYPE
 StrPtr=POINTER TO ARRAY[0..255] OF CHAR;   (* just a dummy declaration     *)

 IntPtr=POINTER TO LONGINT;                 (* only needed because of the   *)
                                            (* brain dead /N-handling in    *)
                                            (* ReadArgs                     *)


 DosArgs=
   RECORD
     file: StrPtr;
     data: StrPtr;
     verb: LONGINT;
     month: StrPtr;
     noin: LONGINT;
     noout: LONGINT;
     quiet: LONGINT;
     host: StrPtr;
     fdate: StrPtr;
     tdate: StrPtr;
     stderr: LONGINT;
     period: IntPtr;
     last: IntPtr;
   END;

 StringT=ARRAY[0..strlen] OF CHAR;
 StringTPtr=POINTER TO StringT;



 File=
   RECORD
     fh: DosD.FileHandlePtr;
     eof: BOOLEAN;
   END;



 CostPtr=POINTER TO Cost;
 Cost=
   RECORD
     next: CostPtr;
     start: LONGINT;        (* start time for mpu and unit                  *)
     stop: LONGINT;         (* end time for mpu and unit                    *)
     mpu: REAL;             (* price per unit                               *)
     unit: REAL;            (* unit duration                                *)
   END;



 XSDataPtr=POINTER TO XSData;
 XSData=
   RECORD
     next: XSDataPtr;
     host: StringT;       (* host name                                    *)
     local: BOOLEAN;      (* count transmissions, no cost, no units       *)
     ignore: BOOLEAN;     (* ignore this host entirely                    *)
     double: BOOLEAN;     (* if true, don't free the attached costlist    *)
     costlist: CostPtr;   (* list of Cost records                         *)
   END;



 Connect=
   RECORD
     host: StringT;                 (* host name *)
     callout: BOOLEAN;              (* TRUE: outgoing call *)
     start: LONGINT;                (* session start time *)
     stop: LONGINT;                 (* session end time *)
     sdate: DosD.Date;              (* session start time in dos format *)
     inbb: LONGINT;                 (* gross bytes in *)
     outbb: LONGINT;                (* gross bytes out *)
     inbn: LONGINT;                 (* net bytes in *)
     outbn: LONGINT;                (* net bytes out *)
     cost: REAL;                    (* connection phone costs *)
     units: LONGINT;                (* phone units consumed *)
     seconds: LONGINT;              (* online time in seconds *)
     local: BOOLEAN;                (* was local -> no cost, no units *)
     ignore: BOOLEAN;               (* do not count this at all *)
   END;



  StatisticsPtr=POINTER TO Statistics;
  Statistics=
    RECORD
      next: StatisticsPtr;
      name: StringT;
      in: BOOLEAN;
      connects: LONGINT;
      inbb: LONGINT;
      outbb: LONGINT;
      inbn: LONGINT;
      outbn: LONGINT;
      cost: REAL;
      units: LONGINT;
      seconds: LONGINT;
      bpeak: LONGINT;
      npeak: LONGINT;
      localconnects: LONGINT;
      ignoreconnects: LONGINT;
    END;



  Args=
    RECORD
      xstatdata: StringT;           (* xstat.data name and path;          *)
      xferstat: StringT;            (* xferstat name and path;            *)
      verbose: BOOLEAN;             (* lotsa output;              ; off   *)
      from: DosD.Date;              (* from date;                 ; zero  *)
      to: DosD.Date;                (* to date;                   ; TODAY *)
      host: ARRAY[0..400] OF CHAR;  (* host name pattern;         ; all   *)
      in: BOOLEAN;                  (* process incoming calls;    ; on    *)
      out: BOOLEAN;                 (* process outgoing calls;    ; on    *)
      quiet: BOOLEAN;               (* suppress non-fatal errors; ; off   *)
      monthly: BOOLEAN;             (* monthly mode;              ; off   *)
      perhost: BOOLEAN;             (* perhost mode;              ; off   *)
      stderr: BOOLEAN;              (* stderr mode;               ; off   *)
   END;



VAR
  inf: File;
  ln1,ln2,temp: StringT;
  log: Connect;
  totali,totalo: Statistics;
  xsroot: XSDataPtr;
  args: Args;
  currency: StringT;
  rdargsPtr: DosD.RDArgsPtr;
  statroot: StatisticsPtr;
  index: INTEGER;
  stdErr: DosD.FileHandlePtr;



(*---------------------------------------------------------------------------
  FreeCostChain: FreeMem() a linked list of "Cost" records
  ---------------------------------------------------------------------------*)
PROCEDURE FreeCostChain(d:CostPtr);
  VAR
    n: CostPtr;
  BEGIN
    REPEAT
      n:=d^.next;
      ExecL.FreeMem(d,SIZE(d^));
      d:=n;
    UNTIL n=NIL;
  END FreeCostChain;



(*---------------------------------------------------------------------------
  FreeStat: FreeMem() a linked list of "Statistics" records
  ---------------------------------------------------------------------------*)
PROCEDURE FreeStat(s: StatisticsPtr);
  VAR
    n: StatisticsPtr;
  BEGIN
    (* some memory may be lost if an error occurs while the XStat.data file  *)
    (* is being parsed... that's because the n^.tmp field is not checked     *)
    (* here. the problem is, checking n^.tmp might cause a "memory freed     *)
    (* twice" guru under certain conditions. losing a few bytes seems like   *)
    (* the smaller evil in this case.                                        *)
    REPEAT
      n:=s^.next;
      ExecL.FreeMem(s,SIZE(s^));
      s:=n;
    UNTIL n=NIL;
  END FreeStat;



(*---------------------------------------------------------------------------
  FreeXData: FreeMem() a linked list of "XSData" records, freeing the
             attached "Cost" record lists as well (if double=FALSE)
  ---------------------------------------------------------------------------*)
PROCEDURE FreeXData(d: XSDataPtr);
  VAR
    n: XSDataPtr;
  BEGIN
    REPEAT
      n:=d^.next;
      IF (d^.costlist#NIL) AND (NOT d^.double) THEN
        FreeCostChain(d^.costlist);
      END;
      ExecL.FreeMem(d,SIZE(d^));
      d:=n;
    UNTIL n=NIL;
  END FreeXData;



(*---------------------------------------------------------------------------
  MyError: Display error messages, terminate program if necessary
           redirect errors to STDERR if requested by new 1.08 cli switch
  ---------------------------------------------------------------------------*)
(*$ CopyDyn:=FALSE *)
PROCEDURE MyError(x1,x2,x3: ARRAY OF CHAR; fatal: BOOLEAN);
  VAR
    lidummy: LONGINT;
  BEGIN
    IF stdErr#NIL THEN
      IF fatal THEN
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("FATAL ERROR "),12);
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x1),String.Length(x1));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\o"),1);
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x2),String.Length(x2));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\o"),1);
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x3),String.Length(x3));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\n"),2);
        Arts.Exit(DosD.fail);
      ELSIF (NOT args.quiet) OR ((x1[0]="F") & (x1[1]="A") & (x1[2]="T")) THEN
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x1),String.Length(x1));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\o"),1);
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x2),String.Length(x2));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\o"),1);
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR(x3),String.Length(x3));
        lidummy:=DosL.Write(stdErr,SYSTEM.ADR("\n\n"),2);
      END;
    ELSE
      IF fatal THEN
        InOut.WriteString("FATAL ERROR ");
        InOut.WriteString(x1); InOut.WriteLn;
        InOut.WriteString(x2); InOut.WriteLn;
        InOut.WriteString(x3); InOut.WriteLn;
        InOut.WriteLn;
        Arts.Exit(DosD.fail);
      ELSIF (NOT args.quiet) OR ((x1[0]="F") & (x1[1]="A") & (x1[2]="T")) THEN
        InOut.WriteString(x1); InOut.WriteLn;
        InOut.WriteString(x2); InOut.WriteLn;
        InOut.WriteString(x3); InOut.WriteLn;
        InOut.WriteLn;
      END;
    END;
  END MyError;



(*---------------------------------------------------------------------------
  GetHead: Remove first char from a string, return it in char variable
  ---------------------------------------------------------------------------*)
PROCEDURE GetHead(VAR s: StringT; VAR c: CHAR);
  BEGIN
    c:=s[0];
    String.DeleteChar(s,0);
  END GetHead;



(*---------------------------------------------------------------------------
  KillSpaces: Remove leading spaces from a string
  ---------------------------------------------------------------------------*)
PROCEDURE KillSpaces(VAR l1: StringT);
  VAR
    c: CHAR;
  BEGIN
    WHILE l1[0]=" " DO
      GetHead(l1,c);
      Break.TestBreak();
    END;
  END KillSpaces;



(*---------------------------------------------------------------------------
  Get: Remove first "word" from a string. "Word" means "a sequence of
       non-spaces, terminated by at least one space". Return this word
       as a separate string
  ---------------------------------------------------------------------------*)
PROCEDURE Get(VAR s1,s2: StringT): BOOLEAN;
  VAR
    c: CHAR;
  BEGIN
    s2[0]:=ASCII.nul;
    KillSpaces(s1);
    GetHead(s1,c);
    WHILE (c#" ") AND (c#ASCII.nul) DO
      Break.TestBreak();
      String.ConcatChar(s2,c);
      GetHead(s1,c);
    END;
    RETURN String.Length(s2)>0;
  END Get;



(*---------------------------------------------------------------------------
  ConvDate: Convert ASCII representations of date and time to Dos format
  ---------------------------------------------------------------------------*)
(*$ CopyDyn:=FALSE *)
PROCEDURE ConvDate(ds,ts: ARRAY OF CHAR; VAR dat: DosD.Date): BOOLEAN;
  VAR
    dt: DosD.DateTime;
    res: LONGINT;
  BEGIN
    dt.format:=DosD.formatDOS;
    dt.flags:=DosD.DateTimeFlagSet{};
    dt.strDate:=SYSTEM.ADR(ds);
    dt.strTime:=SYSTEM.ADR(ts);
    res:=DosL.StrToDate(SYSTEM.ADR(dt));
    dat:=dt.date;
    RETURN res=0;
  END ConvDate;



(*---------------------------------------------------------------------------
  ParseDosArgs: Parse argument string and set "args" record accordingly
  ---------------------------------------------------------------------------*)
PROCEDURE ParseDosArgs(VAR s: StringT);
  VAR
    ts: StringT;
    dargs: DosArgs;
    rc: SYSTEM.ADDRESS;
    pres: LONGINT;
  BEGIN
    rdargsPtr:=DosL.AllocDosObject(DosD.dosRdArgs,NIL);
    IF rdargsPtr=NIL THEN
      MyError("in command line parser",
              "not enough memory",
              "can't parse command line arguments",TRUE);
    END;

    rdargsPtr^.source.buffer:=SYSTEM.ADR(s);
    rdargsPtr^.source.length:=String.Length(s);
    rdargsPtr^.source.curChr:=0;
    rdargsPtr^.daList:=0;
    rdargsPtr^.buffer:=NIL;
    rdargsPtr^.bufSiz:=0;
    rdargsPtr^.extHelp:=SYSTEM.ADR(extendedhelp);
    rdargsPtr^.flags:=DosD.RDAFlagSet{};

    IF DosL.ReadArgs(SYSTEM.ADR(argTemplate),SYSTEM.ADR(dargs),rdargsPtr)=NIL THEN
      MyError("in command line parser","illegal command line",s,TRUE);
    END;

    IF dargs.file#NIL THEN
      String.Copy(args.xferstat,dargs.file^);
    END;

    IF dargs.data#NIL THEN
      String.Copy(args.xstatdata,dargs.data^);
    END;

    args.verbose:=(dargs.verb#0);
    IF args.verbose THEN
      MyError("WARNING: the VERBOSE option is obsolete and should NOT be used anymore.",
              "         It will be replaced with by a configurable output option soon.",
              "         Stay tuned! ;-)",FALSE);
    END;

    IF dargs.fdate#NIL THEN
      IF ConvDate(dargs.fdate^,"00:00:00",args.from) THEN
        MyError("in command line","illegal FDATE",dargs.fdate^,TRUE);
      END;
    END;

    IF dargs.tdate#NIL THEN
      IF ConvDate(dargs.tdate^,"23:59:59",args.to) THEN
        MyError("in command line","illegal TDATE",dargs.tdate^,TRUE);
      END;
    END;

    IF dargs.host#NIL THEN
      pres:=DosL.ParsePatternNoCase(dargs.host,
                                    SYSTEM.ADR(args.host),
                                    SIZE(args.host)-2); (* just being cautious :-) *)
      IF pres<0 THEN
        MyError("in command line","pattern in HOST too complex",dargs.host^,TRUE);
      END;
      args.perhost:=(pres=1);
    END;

    IF args.in THEN
      args.in:=(dargs.noin=0);
    END;

    IF args.out THEN
      args.out:=(dargs.noout=0);
    END;

    args.quiet:=args.quiet OR (dargs.quiet#0);

    args.stderr:=(dargs.stderr#0);

    IF dargs.month#NIL THEN
      ts:="01-";
      String.Concat(ts,dargs.month^);
      IF ConvDate(ts,"00:00:00",args.from) THEN
        MyError("in command line","illegal MONTH",dargs.month^,TRUE);
      END;

      ts:="31-";
      String.Concat(ts,dargs.month^);
      IF ConvDate(ts,"23:59:59",args.to) THEN
        ts:="30-";
        String.Concat(ts,dargs.month^);
        IF ConvDate(ts,"23:59:59",args.to) THEN
          ts:="29-";
          String.Concat(ts,dargs.month^);
          IF ConvDate(ts,"23:59:59",args.to) THEN
            ts:="28-";
            String.Concat(ts,dargs.month^);
            IF ConvDate(ts,"23:59:59",args.to) THEN
              MyError("in command line","illegal MONTH",dargs.month^,TRUE);
            END;
          END;
        END;
      END;

      IF (DosL.dosVersion<40) THEN
        MyError("WARNING: Due to a bug in the Kickstart version you're using, the MONTHLY",
                "         option will yield unexpected results under certain conditions.",
                "         You should probably use FROMDATE and PERIOD instead.",
                FALSE);
      END;
      IF (DosL.dosVersion=40) THEN
        MyError("WARNING: Some revisions of your Kickstart may still have a bug which might",
                "         cause the MONTHLY option to yield unexpected results under certain",
                "         conditions. You should probably use FROMDATE and PERIOD instead.",
                FALSE);
      END;

      args.monthly:=TRUE;
    END;

    IF (dargs.period#NIL) & (dargs.last#NIL) & (dargs.period^#0) & (dargs.last^#0) THEN
      MyError("in command line","PREVIOUS and LAST specified simultaneously",s,TRUE);
    END;

    IF dargs.period#NIL THEN
      args.to:=args.from;
      args.to.days:=args.to.days+dargs.period^;
    END;

    IF dargs.last#NIL THEN
      args.from:=args.to;
      args.from.days:=args.from.days-dargs.last^;
    END;

    IF rdargsPtr#NIL THEN
      DosL.FreeArgs(rdargsPtr);
      DosL.FreeDosObject(DosD.dosRdArgs,rdargsPtr);
      rdargsPtr:=NIL;
    END;
  END ParseDosArgs;



(*---------------------------------------------------------------------------
  PresetArgs: Set default values for "args" record
  ---------------------------------------------------------------------------*)
PROCEDURE PresetArgs();
  BEGIN
    args.xstatdata:=defxstatdata;
    args.xferstat:=defxferstat;
    args.verbose:=FALSE;
    args.from.days:=0;   (* "01-JAN-78" "00:00:00" *)
    args.from.minute:=0; (* "01-JAN-78" "00:00:00" *)
    args.from.tick:=0;   (* "01-JAN-78" "00:00:00" *)
    DosL.DateStamp(SYSTEM.ADR(args.to));
    args.host[0]:=ASCII.nul;
    args.in:=TRUE;
    args.out:=TRUE;
    args.quiet:=FALSE;
    args.monthly:=FALSE;
  END PresetArgs;



(*---------------------------------------------------------------------------
  GetLine: Read a line from the input file
  ---------------------------------------------------------------------------*)
PROCEDURE GetLine(VAR line: StringT);
  VAR
    res: LONGINT;
  BEGIN
    IF inf.fh#NIL THEN
      res:=DosL.FGets(inf.fh,SYSTEM.ADR(line),strlen);
      inf.eof:=(res=0) AND (DosL.IoErr()=0);
    END;
    IF (String.Length(line)>0) AND (line[String.Length(line)-1]=ASCII.lf) THEN
      line[String.Length(line)-1]:="\o";
    END;
  END GetLine;



(*---------------------------------------------------------------------------
  GetLog: Read a two-line log entry from Xferstat
  ---------------------------------------------------------------------------*)
PROCEDURE GetLog(VAR l1,l2: StringT);
  BEGIN
    l1[0]:="\o";
    l2[0]:="\o";
    REPEAT
      Break.TestBreak();
      GetLine(l1);
    UNTIL (inf.eof) OR (l1[0]="<") OR (l1[0]=">");
    IF NOT inf.eof THEN
      GetLine(l2);
    ELSE
      l1[0]:="\o";
    END;
  END GetLog;



(*---------------------------------------------------------------------------
  Skip: Skip the first "word" of a string; refer to "Get()" for an
        explanation ot the term "word"
  ---------------------------------------------------------------------------*)
PROCEDURE Skip(VAR s: StringT);
  VAR
    dum: BOOLEAN;
    x: StringT;
  BEGIN
    dum:=Get(s,x);
  END Skip;



(*---------------------------------------------------------------------------
  GetLNum: Get the first "word" of a string, and convert it to a LONGINT
           (if possible). Refer to "Get()" for an explanation of the
           term "word"
  ---------------------------------------------------------------------------*)
PROCEDURE GetLNum(VAR s: StringT; VAR x: LONGINT): BOOLEAN;
  VAR
    sn: StringT;
    res: LONGINT;
  BEGIN
    IF Get(s,sn) THEN
      res:=DosL.StrToLong(SYSTEM.ADR(sn),x);
      RETURN res>0;
    END;
    RETURN FALSE;
  END GetLNum;



(*---------------------------------------------------------------------------
  GetNum: Call "GetLNum()" and make sure the result is an INTEGER
  ---------------------------------------------------------------------------*)
PROCEDURE GetNum(VAR s: StringT; VAR x: INTEGER): BOOLEAN;
  VAR
    l: LONGINT;
  BEGIN
    IF GetLNum(s,l) THEN
      IF l>MAX(INTEGER) THEN
        x:=0;
      ELSE
        x:=INTEGER(l);
        RETURN TRUE;
      END;
    ELSE
      x:=0;
    END;
    RETURN FALSE;
  END GetNum;



(*---------------------------------------------------------------------------
  GetReal: Get the first "word" of a string, and convert it to a REAL
           (if possible). Refer to "Get()" for an explanation of the
           term "word"
  ---------------------------------------------------------------------------*)
PROCEDURE GetReal(VAR s: StringT; VAR x: REAL): BOOLEAN;
  VAR
    sn: StringT;
    err: BOOLEAN;
  BEGIN
    IF Get(s,sn) THEN
      RealConversions.StrToReal(sn,x,err);
      RETURN NOT err;
    END;
    RETURN FALSE;
  END GetReal;



(*---------------------------------------------------------------------------
  GetTime: Get the first two "words" of a string, and convert them to
           Dos format date and time (if possible). Refer to "Get()" for an
           explanation of the term "word"
  ---------------------------------------------------------------------------*)
PROCEDURE GetTime(VAR s: StringT; VAR t: LONGINT; VAR dos: DosD.Date): BOOLEAN;
  VAR
    ds,ts: StringT;
    dt: DosD.DateTime;
  BEGIN
    IF Get(s,ds) THEN
      IF Get(s,ts) THEN
        dt.format:=DosD.formatCDN;
        dt.flags:=DosD.DateTimeFlagSet{};
        dt.strDate:=SYSTEM.ADR(ds);
        dt.strTime:=SYSTEM.ADR(ts);

        IF DosL.StrToDate(SYSTEM.ADR(dt))#0 THEN
          dos:=dt.date;
          t:=(dos.days MOD 7)*dmult+(dos.minute*mmult)+(dos.tick DIV 50);
          RETURN TRUE;
        END;
      END;
    END;
    RETURN FALSE;
  END GetTime;



(*---------------------------------------------------------------------------
  TestXStatHeader: Check if the input file's header is "H XSTAT DATA"
  ---------------------------------------------------------------------------*)
PROCEDURE TestXStatHeader();
 VAR
  s1,s,t: StringT;
  c: CHAR;
 BEGIN
  GetLine(s);
  s1:=s;
  GetHead(s,c);
  IF c="H" THEN
   IF Get(s,t) OR (String.Compare(t,"XSTAT")#0) THEN
    IF NOT (Get(s,t) OR (String.Compare(t,"DATA")#0)) THEN
     MyError("in XStat.data header",
             s1,"keyword DATA not found",TRUE);
    END;
   ELSE
    MyError("in XStat.data header",
            s1,"keyword XSTAT not found",TRUE);
   END;
  ELSE
   MyError("in XStat.data header",
           s1,"header identifier H not found",TRUE);
  END;
 END TestXStatHeader;



(*---------------------------------------------------------------------------
  ParseXSDataNEntry: Parse a "host name" line from XStat.data
  ---------------------------------------------------------------------------*)
PROCEDURE ParseXSDataNEntry(VAR s,s1: StringT; VAR ccxsd: XSDataPtr);
  VAR
    cxsd: XSDataPtr;
    xsd: XSData;
    t: StringT;
    first: BOOLEAN;
  BEGIN
    IF (ccxsd#NIL) AND (ccxsd^.costlist=NIL) AND (NOT ccxsd^.local) AND (NOT ccxsd^.ignore) THEN
(* -------- obsolete --------
      MyError("in XStat.data",
              "N-line without subsequent C-lines for",
              ccxsd^.host,TRUE);
    END;
   -------------------------- *)
      first:=FALSE;
    ELSE
      first:=TRUE;
    END;

    IF Get(s,t) THEN
      REPEAT
        xsd.next:=NIL;
        xsd.host:=t;
        xsd.costlist:=NIL;
        xsd.local:=FALSE;
        xsd.ignore:=FALSE;
        xsd.double:=FALSE;
        cxsd:=xsroot;
        IF xsroot#NIL THEN
          IF String.Compare(xsd.host,cxsd^.host)=0 THEN
            MyError("in XStat.data",
                    "host name already used for",
                    xsd.host,TRUE);
          END;
          WHILE cxsd^.next#NIL DO
            IF String.Compare(xsd.host,cxsd^.next^.host)=0 THEN
              MyError("in XStat.data",
                      "host name already used for",
                      xsd.host,TRUE);
            END;
            Break.TestBreak();
            cxsd:=cxsd^.next;
          END;
          cxsd^.next:=ExecL.AllocMem(SIZE(xsd),ExecD.MemReqSet{});
          IF cxsd^.next=NIL THEN
            MyError("","not enough memory","",TRUE);
          END;
          cxsd:=cxsd^.next;
          IF first THEN
            first:=FALSE;
            ccxsd:=cxsd;
          ELSE
            xsd.double:=TRUE;
          END;
        ELSE
          xsroot:=ExecL.AllocMem(SIZE(xsd),ExecD.MemReqSet{});
          IF xsroot=NIL THEN
            MyError("","not enough memory","",TRUE);
          END;
          cxsd:=xsroot;
          ccxsd:=cxsd;
          first:=FALSE;
        END;
        cxsd^:=xsd;
      UNTIL NOT Get(s,t);
    ELSE
      MyError("in XStat.data",
              s1,"no host name found in N-line",TRUE);
    END;
  END ParseXSDataNEntry;



(*---------------------------------------------------------------------------
  ParseXSDataCEntry: Parse a "cost info" line from XStat.data
  ---------------------------------------------------------------------------*)
PROCEDURE ParseXSDataCEntry(VAR s,s1: StringT; VAR cxsd: XSDataPtr);
  VAR
    t,t1: StringT;
    cost: Cost;
    ccp: CostPtr;
    temp: INTEGER;
    c0{R.D7},c1{R.D6}: CHAR;
  BEGIN
    IF cxsd=NIL THEN
      MyError("in XStat.data",s1,"C-line without corresponding N-line",TRUE);
    END;

    cost.next:=NIL;
    cost.stop:=7*dmult-1;

    IF NOT Get(s,t) THEN
      MyError("in XStat.data",s1,"illegal or missing start time in C-line",TRUE);
    END;

    IF String.Compare(t,"LOCAL")=0 THEN
      IF cxsd^.costlist#NIL THEN
        MyError("in XStat.data",s1,"LOCAL must be the only C-line for the host",TRUE);
      ELSIF cxsd^.local#FALSE THEN
        MyError("in XStat.data",s1,"duplicate LOCAL after N-line",TRUE);
      ELSIF cxsd^.ignore#FALSE THEN
        MyError("in XStat.data",s1,"LOCAL and IGNORE are mutually exclusive",TRUE);
      ELSE
        cxsd^.local:=TRUE;
      END;

    ELSIF String.Compare(t,"IGNORE")=0 THEN
      IF cxsd^.costlist#NIL THEN
        MyError("in XStat.data",s1,"IGNORE must be the only C-line for the host",TRUE);
      ELSIF cxsd^.ignore#FALSE THEN
        MyError("in XStat.data",s1,"duplicate IGNORE after N-line",TRUE);
      ELSIF cxsd^.local#FALSE THEN
        MyError("in XStat.data",s1,"LOCAL and IGNORE are mutually exclusive",TRUE);
      ELSE
        cxsd^.ignore:=TRUE;
      END;

    ELSIF (t[2]="-") AND (t[5]=":") AND (t[8]=":") AND (String.Length(t)=11) THEN
      c0:=t[0]; c1:=t[1];
      IF (c0="S") AND (c1="U") THEN
        cost.start:=dsun;
      ELSIF (c0="M") AND (c1="O") THEN
        cost.start:=dmon;
      ELSIF (c0="T") AND (c1="U") THEN
        cost.start:=dtue;
      ELSIF (c0="W") AND (c1="E") THEN
        cost.start:=dwed;
      ELSIF (c0="T") AND (c1="H") THEN
        cost.start:=dthu;
      ELSIF (c0="F") AND (c1="R") THEN
        cost.start:=dfri;
      ELSIF (c0="S") AND (c1="A") THEN
        cost.start:=dsat;
      ELSE
        MyError("in XStat.data",s1,"illegal day of week in start time in C-line",TRUE);
      END;

      t[0]:=" "; t[1]:=" "; t[2]:=" "; t[5]:=" "; t[8]:=" ";

      IF NOT GetNum(t,temp) THEN
        MyError("in XStat.data",s1,"illegal 'hours' part in start time in C-line",TRUE);
      END;
      cost.start:=cost.start+hmult*LONGINT(temp);

      IF NOT GetNum(t,temp) THEN
        MyError("in XStat.data",s1,"illegal 'minutes' part in start time in C-line",TRUE);
      END;
      cost.start:=cost.start+mmult*LONGINT(temp);

      IF NOT GetNum(t,temp) THEN
        MyError("in XStat.data",s1,"illegal 'seconds' part in start time in C-line",TRUE);
      END;
      cost.start:=cost.start+smult*LONGINT(temp);

      IF NOT GetReal(s,cost.unit) THEN
        MyError("in XStat.data",s1,"illegal 'seconds per unit' in C-line",TRUE);
      END;

      IF NOT GetReal(s,cost.mpu) THEN
        MyError("in XStat.data",s1,"illegal 'money per unit' in C-line",TRUE);
      END;

      IF (cxsd^.local) OR (cxsd^.ignore) THEN
        MyError("in XStat.data",s1,"no more C-lines allowed after LOCAL or IGNORE",TRUE);
      END;

      ccp:=cxsd^.costlist;
      IF ccp#NIL THEN
        WHILE ccp^.next#NIL DO
          Break.TestBreak();
          ccp:=ccp^.next;
        END;
        ccp^.next:=ExecL.AllocMem(SIZE(Cost),ExecD.MemReqSet{});
        IF ccp^.next=NIL THEN
          MyError("","not enough memory","",TRUE);
        END;
        ccp^.next^:=cost;
        ccp^.stop:=cost.start-1;
        IF ccp^.stop<ccp^.start THEN
          MyError("in XStat.data","connect start time sequence error",
                  "(you probably swapped some C-lines, or you left out an N-line)",TRUE);
        END;
      ELSE
        cxsd^.costlist:=ExecL.AllocMem(SIZE(Cost),ExecD.MemReqSet{});
        IF cxsd^.costlist=NIL THEN
          MyError("","not enough memory","",TRUE);
        END;
        cxsd^.costlist^:=cost;
        IF cost.start#0 THEN
          MyError("in XStat.data",s1,"start time in first C-line must be SU-00:00:00",TRUE);
        END;
      END;
    ELSE
      MyError("in XStat.data",s1,"illegal or missing start time in C-line",TRUE);
    END;

    (* cxsd will always be #NIL here... *)
    WHILE cxsd^.next#NIL DO
      (* copy costlistPtr to next^.costlistPtr *)
      cxsd^.next^.costlist:=cxsd^.costlist;
      cxsd^.next^.local:=cxsd^.local;
      cxsd^.next^.ignore:=cxsd^.ignore;
      (* advance cxsd to cxsd#.next; note: cxsd is a VAR parameter, so this *)
      (* will only happen for the first C-record found... from then on,     *)
      (* this costlistPtr copying will not be done again.                   *)
      cxsd:=cxsd^.next;
    END;
  END ParseXSDataCEntry;



(*---------------------------------------------------------------------------
  ParseXSDataSEntry: Parse a "currency sign info" line from XStat.data
  ---------------------------------------------------------------------------*)
PROCEDURE ParseXSDataSEntry(VAR s,s1: StringT; VAR cxsd: XSDataPtr);
 VAR
  t: StringT;
 BEGIN
  IF (cxsd#NIL) THEN
   MyError("in XStat.data",
           s1,"S-line after the first connection cost record",TRUE);
  END;

  IF Get(s,t) THEN
   IF currency[0]=ASCII.nul THEN
    currency:=t;
    IF currency[0]#" " THEN
     String.Insert(currency,0," \o");
    END;
   ELSE
    MyError("in XStat.data",
            s1,"duplicate S-line found",TRUE);
   END;
  ELSE
   MyError("in XStat.data",
           s1,"missing currency sign in S-line",TRUE);
  END;
 END ParseXSDataSEntry;



(*---------------------------------------------------------------------------
  GetXStatData: Read and parse the XStat.data file
  ---------------------------------------------------------------------------*)
PROCEDURE GetXStatData();
 VAR
  s1,s: StringT;
  c: CHAR;
  cxsd: XSDataPtr;
 BEGIN
  cxsd:=NIL;
  TestXStatHeader();
  REPEAT
   Break.TestBreak();
   GetLine(s);
   s1:=s;
   GetHead(s,c);
   CASE c OF
    |"#": (* NOP *)
    |"N": ParseXSDataNEntry(s,s1,cxsd);
    |"C": ParseXSDataCEntry(s,s1,cxsd);
    |"S": ParseXSDataSEntry(s,s1,cxsd);
    |ELSE (* unknown: NOP *)
   END;
  UNTIL inf.eof;
 END GetXStatData;



(*---------------------------------------------------------------------------
  ParseLog: Parse a two-line connection data entry from Xferstat
  ---------------------------------------------------------------------------*)
(*$ CopyDyn:=FALSE *)
PROCEDURE ParseLog(line1,line2: StringT; VAR log: Connect): BOOLEAN;
 VAR
  c: CHAR;
  l1,l2: StringT;
  dummy: DosD.Date;
  i: INTEGER;
 BEGIN
  l1:=line1;
  l2:=line2;
  GetHead(l1,c);
  log.callout:=(c="<");

  IF Get(l1,log.host) THEN
(* ---------------------------------------------------
   IF (log.host[0]>="0") AND (log.host[0]<="9") THEN
    MyError("invalid log entry: can't find host name",line1,line2,FALSE);
    RETURN FALSE;
   END;
   --------------------------------------------------- *)
   IF line1[2]=" " THEN
     log.host:=".illegal.";
     MyError("invalid log entry: can't find host name (substituting .illegal.)",line1,line2,FALSE);
     l1:=line1;
     GetHead(l1,c);
   END;
   IF GetTime(l1,log.start,log.sdate) THEN
    Skip(l1);
    IF GetTime(l1,log.stop,dummy) THEN
     GetHead(l2,c);
     IF c="|" THEN
      FOR i:=1 TO 9 DO
        GetHead(l2,c); (* skip the initial "|" and the next 9 chars *)
      END;
      IF GetLNum(l2,log.inbb) THEN
       IF GetLNum(l2,log.outbb) THEN
        Skip(l2);
        IF GetLNum(l2,log.inbn) THEN
         IF GetLNum(l2,log.outbn) THEN
          RETURN TRUE;
         ELSE
          MyError("invalid log entry: can't get net send bytes",line1,line2,FALSE);
         END;
        ELSE
         MyError("invalid log entry: can't get net receive bytes",line1,line2,FALSE);
        END;
       ELSE
        MyError("invalid log entry: can't get gross send bytes",line1,line2,FALSE);
       END;
      ELSE
       MyError("invalid log entry: can't get gross receive bytes",line1,line2,FALSE);
      END;
     ELSE
      MyError("invalid log entry: illegal continuation line header ",line1,line2,FALSE);
     END;
    ELSE
     MyError("invalid log entry: can't compute end time",line1,line2,FALSE);
    END;
   ELSE
    MyError("invalid log entry: can't compute start time",line1,line2,FALSE);
   END;
  ELSE
   MyError("invalid log entry: can't find host name",line1,line2,FALSE);
  END;
  RETURN FALSE;
 END ParseLog;



(*---------------------------------------------------------------------------
  ComputeCost: Compute the cost for a single connect
  ---------------------------------------------------------------------------*)
PROCEDURE ComputeCost(VAR log: Connect): BOOLEAN;
 VAR
  cxsd: XSDataPtr;
  ccost: CostPtr;
  time: REAL;
 BEGIN
  log.seconds:=log.stop-log.start+1;
  IF log.seconds<0 THEN
    log.seconds:=log.seconds+7*dmult;
  END;

  cxsd:=xsroot;
  WHILE (cxsd#NIL) AND (String.Compare(cxsd^.host,log.host)#0) DO
    Break.TestBreak();
    cxsd:=cxsd^.next;
  END;
  IF cxsd=NIL THEN
    cxsd:=xsroot;
    WHILE (cxsd#NIL) AND (String.Compare(cxsd^.host,".default.")#0) DO
      Break.TestBreak();
      cxsd:=cxsd^.next;
    END;
    IF cxsd=NIL THEN
      MyError("ERROR: no connection cost data found for host",log.host,
              "extend your XStat.data file or use '.default.'!",FALSE);
      RETURN FALSE;
    END;
  END;

  IF NOT (cxsd^.local OR cxsd^.ignore) THEN
    ccost:=cxsd^.costlist;
    WHILE log.start>ccost^.stop DO
      Break.TestBreak();
      ccost:=ccost^.next;
    END;
  END;

  IF log.stop<log.start THEN
    log.stop:=log.stop+7*dmult;
  END;
  log.cost:=0.0;
  log.units:=0;
  IF NOT (cxsd^.local OR cxsd^.ignore) THEN
    time:=REAL(log.start);
    REPEAT
      Break.TestBreak();
      log.cost:=log.cost+ccost^.mpu;
      time:=time+ccost^.unit;
      INC(log.units);

      IF LONGINT(time)>ccost^.stop THEN
        ccost:=ccost^.next;
        IF ccost=NIL THEN
          ccost:=cxsd^.costlist;
          time:=time-REAL(7*dmult);
          log.stop:=log.stop-7*dmult;
        END;
      END;
    UNTIL LONGINT(time)>log.stop;
  END;
  log.local:=cxsd^.local;
  log.ignore:=cxsd^.ignore;
  RETURN TRUE;
 END ComputeCost;



(*---------------------------------------------------------------------------
  Date2Str: Convert a datestamp to a string
  ---------------------------------------------------------------------------*)
PROCEDURE Date2Str(date: DosD.Date; VAR strdate,strtime: ARRAY OF CHAR);
  VAR
    dt: DosD.DateTime;
  BEGIN
    dt.date:=date;
    dt.format:=DosD.formatDOS;
    dt.flags:=DosD.DateTimeFlagSet{};
    dt.strDay:=NIL;
    dt.strDate:=SYSTEM.ADR(strdate);
    dt.strTime:=SYSTEM.ADR(strtime);
    IF DosL.DateToStr(SYSTEM.ADR(dt))=0 THEN
      String.Copy(strdate,"??-???-??");
      String.Copy(strtime,"??:??:??");
    END;
  END Date2Str;


(*---------------------------------------------------------------------------
  DisplayConnect: Display the statistics for a single connect
  ---------------------------------------------------------------------------*)
PROCEDURE DisplayConnect(log: Connect);
 VAR
  name: StringT;
  date: StringT;
  time: StringT;
 BEGIN
  name:=log.host;
  WHILE String.Length(name)<8 DO
   Break.TestBreak();
   String.Insert(name,0," \o");
  END;

(* ----------
   can't do this now, would have to change Connect record definition
   ----------
  InOut.WriteString("\nxfer statistics: connected from ");
  Date2Str(log.start,date,time);
  InOut.WriteString(date); InOut.Write(" "); InOut.WriteString(time);
  InOut.WriteString(" to ");
  Date2Str(log.stop,date,time);
  InOut.WriteString(date); InOut.Write(" "); InOut.WriteString(time);
  InOut.WriteLn;
   ---------- *)

  InOut.WriteString("\nhost name\t"); InOut.WriteString(name);
  InOut.WriteString("\ndirection\t     "); IF log.callout THEN
                                            InOut.WriteString("out");
                                           ELSE
                                            InOut.WriteString(" in");
                                           END;
  InOut.WriteString("\nonline time\t"); InOut.WriteInt(log.seconds,8); InOut.WriteString(" seconds");
  InOut.WriteString("\nunits\t\t");
  IF log.local THEN
    InOut.WriteString("LOCAL");
  ELSIF log.ignore THEN
    InOut.WriteString("IGNORE");
  ELSE
    InOut.WriteInt(log.units,8); InOut.WriteString(" units");
  END;
  InOut.WriteString("\ncost\t\t");
  IF log.local THEN
    InOut.WriteString("LOCAL");
  ELSIF log.ignore THEN
    InOut.WriteString("IGNORE");
  ELSE
    RealInOut.WriteReal(log.cost,8,2); InOut.Write(" "); InOut.WriteString(currency);
  END;
  InOut.WriteString("\n\n");
 END DisplayConnect;



(*---------------------------------------------------------------------------
  DisplayStats: Display the overall statistics
  ---------------------------------------------------------------------------*)
PROCEDURE DisplayStats(tot: Statistics);
  VAR
    date: StringT;
    time: StringT;
  BEGIN
    InOut.WriteString("Connection statistics for ");
    IF NOT tot.in THEN
      InOut.WriteString("outgoing");
    ELSE
      InOut.WriteString("incoming");
    END;
    IF tot.name[0]="\o" THEN
      InOut.WriteString(" calls:\n");
      InOut.WriteString("-----------------------------------------\n");
    ELSE
      InOut.WriteString(" calls ");
      IF NOT tot.in THEN
        InOut.WriteString("to");
      ELSE
        InOut.WriteString("from");
      END;
      InOut.WriteString(' host "');
      InOut.WriteString(tot.name);
      InOut.WriteString('":\n');
    END;

    InOut.WriteString("\nxfer statistics from ");
    Date2Str(args.from,date,time);
    InOut.WriteString(date); InOut.Write(" "); InOut.WriteString(time);
    InOut.WriteString(" to ");
    Date2Str(args.to,date,time);
    InOut.WriteString(date); InOut.Write(" "); InOut.WriteString(time);
    InOut.WriteLn;

    IF tot.connects=0 THEN
      InOut.WriteString("\nno connects recorded.\n\n\n");
    ELSE
      InOut.WriteString("\nconnects        "); InOut.WriteInt(tot.connects,8); InOut.WriteString("  \t  ("); InOut.WriteInt(tot.localconnects,0); InOut.WriteString(" of them were local)");
      IF tot.name[0]="\o" THEN
        InOut.WriteString("\nignored connects"); InOut.WriteInt(tot.ignoreconnects,8);
      END;
      InOut.WriteString("\nonline time     "); InOut.WriteInt(tot.seconds,8); InOut.WriteString(" sec\t  (");
      InOut.WriteInt((tot.seconds+tot.connects-1) DIV tot.connects,8); InOut.WriteString(" sec/connect)");

      InOut.WriteString("\nunits           "); InOut.WriteInt(tot.units,8); InOut.WriteString(" units\t  (");
      RealInOut.WriteReal(REAL(tot.units)/REAL(tot.connects),8,3); InOut.WriteString(" units/connect)");

      InOut.WriteString("\ncost            "); RealInOut.WriteReal(tot.cost,8,2); InOut.WriteString(currency); InOut.WriteString("\t  (");
      RealInOut.WriteReal(tot.cost/REAL(tot.connects),8,3); InOut.WriteString(currency); InOut.WriteString("/connect)");

      InOut.WriteString("\n\ngross read      "); InOut.WriteInt(tot.inbb,8); InOut.WriteString(" bytes\t  (");
      InOut.WriteInt((tot.inbb+tot.connects-1) DIV tot.connects,8); InOut.WriteString(" bytes/connect)");

      InOut.WriteString("\ngross send      "); InOut.WriteInt(tot.outbb,8); InOut.WriteString(" bytes\t  (");
      InOut.WriteInt((tot.outbb+tot.connects-1) DIV tot.connects,8); InOut.WriteString(" bytes/connect)");

      InOut.WriteString("\nnet read        "); InOut.WriteInt(tot.inbn,8); InOut.WriteString(" bytes\t  (");
      InOut.WriteInt((tot.inbn+tot.connects-1) DIV tot.connects,8); InOut.WriteString(" bytes/connect)");

      InOut.WriteString("\nnet send        "); InOut.WriteInt(tot.outbn,8); InOut.WriteString(" bytes\t  (");
      InOut.WriteInt((tot.outbn+tot.connects-1) DIV tot.connects,8); InOut.WriteString(" bytes/connect)\n");

      InOut.WriteString("\nav. gross speed "); InOut.WriteInt((tot.inbb+tot.outbb) DIV tot.seconds,8); InOut.WriteString(" cps\t  (");
      InOut.WriteInt(tot.bpeak,8); InOut.WriteString(" cps peak)");

      InOut.WriteString("\nav. net speed   "); InOut.WriteInt((tot.inbn+tot.outbn) DIV tot.seconds,8); InOut.WriteString(" cps\t  (");
      InOut.WriteInt(tot.npeak,8); InOut.WriteString(" cps peak)");

      InOut.WriteString("\nav. gross cost  ");

      IF tot.inbb+tot.outbb=0 THEN
        InOut.WriteString("       -");
      ELSE
        RealInOut.WriteReal(tot.cost/REAL(tot.inbb+tot.outbb)*1048576.0,8,3);
      END;
      InOut.WriteString(currency); InOut.WriteString("/MB");

      InOut.WriteString("\nav. net cost    ");
      IF tot.inbn+tot.outbn=0 THEN
        InOut.WriteString("       -");
      ELSE
        RealInOut.WriteReal(tot.cost/REAL(tot.inbn+tot.outbn)*1048576.0,8,3);
      END;
      InOut.WriteString(currency); InOut.WriteString("/MB\n\n\n");
    END;
  END DisplayStats;



(*---------------------------------------------------------------------------
  UpdateStat: Update the totali/totalo records
  ---------------------------------------------------------------------------*)
PROCEDURE UpdateStat(VAR tot: Statistics; VAR log: Connect);
  VAR
    tempspeed: LONGINT;
  BEGIN
    IF NOT log.ignore THEN
      INC(tot.connects);
      tot.seconds:=tot.seconds+log.seconds;
      tot.inbb:=tot.inbb+log.inbb;
      tot.outbb:=tot.outbb+log.outbb;
      tot.inbn:=tot.inbn+log.inbn;
      tot.outbn:=tot.outbn+log.outbn;
      tot.units:=tot.units+log.units;
      tot.cost:=tot.cost+log.cost;

      IF log.seconds#0 THEN
        tempspeed:=(log.inbb+log.outbb) DIV log.seconds;
        IF tempspeed>tot.bpeak THEN
          tot.bpeak:=tempspeed
        END;

        tempspeed:=(log.inbn+log.outbn) DIV log.seconds;
        IF tempspeed>tot.npeak THEN
          tot.npeak:=tempspeed
        END;
      END;
    END;
    IF log.ignore THEN
      INC(tot.ignoreconnects);
    END;
    IF log.local THEN
      INC(tot.localconnects);
    END;
  END UpdateStat;



(*---------------------------------------------------------------------------
  ClearStat: Preset a Statistics-record with zeros
  ---------------------------------------------------------------------------*)
PROCEDURE ClearStat(VAR stat: Statistics);
  BEGIN
    stat.next:=NIL;
    stat.name:="\o\o";
    stat.in:=FALSE;
    stat.connects:=0;
    stat.inbb:=0;
    stat.outbb:=0;
    stat.inbn:=0;
    stat.outbn:=0;
    stat.cost:=0.0;
    stat.units:=0;
    stat.seconds:=0;
    stat.bpeak:=0;
    stat.npeak:=0;
    stat.localconnects:=0;
    stat.ignoreconnects:=0;
  END ClearStat;



(*---------------------------------------------------------------------------
  InsertStat: Insert a Statistics-record at the proper position
              into the linked list
  ---------------------------------------------------------------------------*)
PROCEDURE InsertStat(stat: StatisticsPtr);
  VAR
    cur,prev: StatisticsPtr;
    done: BOOLEAN;
  BEGIN
    done:=FALSE;
    IF statroot=NIL THEN
      statroot:=stat;
    ELSE
      cur:=statroot;
      prev:=NIL;
      REPEAT
        IF String.Compare(stat^.name,cur^.name)=0 THEN
          IF stat^.in THEN
            (* insert before current *)
            IF prev=NIL THEN
              stat^.next:=statroot;
              statroot:=stat;
              done:=TRUE;
            ELSE
              stat^.next:=prev^.next;
              prev^.next:=stat;
              done:=TRUE;
            END;
          ELSE
            (* insert after current *)
            stat^.next:=cur^.next;
            cur^.next:=stat;
            done:=TRUE;
          END;
        ELSIF String.Compare(stat^.name,cur^.name)<0 THEN
          (* insert before current *)
          IF prev=NIL THEN
            stat^.next:=statroot;
            statroot:=stat;
            done:=TRUE;
          ELSE
            stat^.next:=prev^.next;
            prev^.next:=stat;
            done:=TRUE;
          END;
        ELSE
          prev:=cur;
          cur:=cur^.next;
        END;
        Break.TestBreak();
      UNTIL (cur=NIL) OR done;
      IF cur=NIL THEN
        prev^.next:=stat;
      END;
    END;
  END InsertStat;



(*---------------------------------------------------------------------------
  FindStat: Search linked list for a specific Statistics-record
            Create that record if not found
  ---------------------------------------------------------------------------*)
PROCEDURE FindStat(name: StringT; in: BOOLEAN): StatisticsPtr;
  VAR
    cur,next: StatisticsPtr;
    found: BOOLEAN;
  BEGIN
    next:=statroot;
    IF next#NIL THEN
      REPEAT
        cur:=next;
        next:=cur^.next;
        found:=(in=cur^.in) AND (String.Compare(name,cur^.name)=0);
        Break.TestBreak();
      UNTIL found OR (next=NIL);
    ELSE
      found:=FALSE;
    END;
    IF found THEN
      RETURN cur;
    ELSE
      next:=ExecL.AllocMem(SIZE(Statistics),ExecD.MemReqSet{});
      IF next=NIL THEN
        MyError("","not enough memory","",TRUE);
      END;
      ClearStat(next^);
      next^.name:=name;
      next^.in:=in;
      InsertStat(next);
      RETURN next;
    END;
  END FindStat;



(*---------------------------------------------------------------------------
  DisplayStatList: Display the entire list of Statistics-records
  ---------------------------------------------------------------------------*)
PROCEDURE DisplayStatList;
  VAR
    cur: StatisticsPtr;
  BEGIN
    cur:=statroot;
    WHILE cur#NIL DO
      IF (cur^.in AND args.in) OR ((NOT cur^.in) AND args.out) THEN
        DisplayStats(cur^);
      END;
      cur:=cur^.next;
    END;
  END DisplayStatList;



BEGIN
 SYSTEM.SETREG(11,SYSTEM.ADR(version));
 rdargsPtr:=NIL;
 xsroot:=NIL;
 statroot:=NIL;
 inf.fh:=NIL;
 stdErr:=NIL;

 IF (Arts.kickVersion<37) OR (DosL.dosVersion<37) THEN
  MyError("","You need OS 2.04 or later (dos.library V37 or later)",
          "",TRUE);
 END;

 Break.InstallException;
 PresetArgs();

 InOut.WriteString("\n\n"+vers+"\n");
 InOut.WriteString("© Copyright 1992,1993 by Jürgen Weinelt\n");
(*$ IF BETA *)
 InOut.WriteString("THIS IS A BETA VERSION! DO NOT DISTRIBUTE!\n\n");
(*$ ELSE *)
 InOut.WriteString("XStat is Freeware - read the docs for details.\n\n");
(*$ ENDIF *)

 IF NOT Arts.wbStarted THEN
  IF Arts.dosCmdLen>0 THEN
   temp:=SYSTEM.CAST(StringTPtr,Arts.dosCmdBuf)^;
   IF temp[0]="?" THEN
    InOut.WriteString(extendedhelp);
    Arts.Exit(5);
   END;
  END;
 END;

 IF DosL.GetVar(SYSTEM.ADR("XSTATARGS"),SYSTEM.ADR(temp),
                strlen,DosD.VarFlagSet{})#-1 THEN
  index:=String.Length(temp);
  IF temp[index-1]#"\n" THEN
    temp[index]:="\n"; temp[index+1]:="\o";
  END;
  ParseDosArgs(temp);
 END;

 IF NOT Arts.wbStarted THEN
  IF Arts.dosCmdLen>0 THEN
   temp:=SYSTEM.CAST(StringTPtr,Arts.dosCmdBuf)^;
   ParseDosArgs(temp);
  END;
 END;

 IF DosL.CompareDates(SYSTEM.ADR(args.from),SYSTEM.ADR(args.to))<0 THEN
  MyError("","illegal combination of parameters",
          "FROMDATE is later than TODATE",TRUE);
 END;
 IF NOT (args.in OR args.out) THEN
  MyError("","illegal combination of parameters",
          "you can't specify NOINCOM and NOOUTGO simultaneously",TRUE);
 END;

 IF args.stderr THEN
   stdErr:=DosL.Open(SYSTEM.ADR("CONSOLE:"),DosD.newFile);
   IF stdErr=NIL THEN
     MyError("ERROR:","can't open CONSOLE: for StdErr output",
             "guess you'll have to live without it :-)",FALSE);
   END;
 END;

 ClearStat(totali);
 totali.in:=TRUE;
 ClearStat(totalo);
 totalo.in:=FALSE;

 inf.fh:=DosL.Open(SYSTEM.ADR(args.xstatdata),DosD.readOnly);
 IF inf.fh=NIL THEN
  MyError("Can't open XStat.data!","filename was",args.xstatdata,TRUE);
 END;
 GetXStatData();
 DosL.Close(inf.fh);
 IF currency[0]=ASCII.nul THEN
  MyError("WARNING","no currency definition found in XStat.data",
          "using 'DM' as default",FALSE);
  currency:=" DM";
 END;

 inf.fh:=DosL.Open(SYSTEM.ADR(args.xferstat),DosD.readOnly);
 IF inf.fh=NIL THEN
  MyError("Can't open Xferstat!","filename was",args.xferstat,TRUE);
 END;

 REPEAT
  Break.TestBreak();
  GetLog(ln1,ln2);
  IF String.Length(ln1)+String.Length(ln2)>0 THEN
   IF ParseLog(ln1,ln2,log) THEN
    IF (args.host[0]=ASCII.nul) OR
       (DosL.MatchPatternNoCase(SYSTEM.ADR(args.host),SYSTEM.ADR(log.host))) THEN
     IF (DosL.CompareDates(SYSTEM.ADR(args.from),SYSTEM.ADR(log.sdate))>=0) AND
        (DosL.CompareDates(SYSTEM.ADR(log.sdate),SYSTEM.ADR(args.to))>=0) THEN
      IF ComputeCost(log) THEN
       IF args.verbose THEN
        DisplayConnect(log);
       END;

       IF args.perhost THEN
         IF NOT log.ignore THEN
           UpdateStat(FindStat(log.host,NOT log.callout)^,log);
         END;
       END;
       IF log.callout THEN
         UpdateStat(totalo,log);
       ELSE
         UpdateStat(totali,log);
       END;

      ELSE
       IF NOT args.quiet THEN
        InOut.WriteString("(ignoring this one)\n\n");
       END;
      END;
     END;
    END;
   ELSE
    IF NOT args.quiet THEN
     InOut.WriteString("(ignoring this one)\n\n");
    END;
   END;
  END;
 UNTIL inf.eof;

 IF args.perhost THEN
   DisplayStatList;
 END;
 IF args.in THEN
  DisplayStats(totali);
 END;
 IF args.out THEN
  DisplayStats(totalo);
 END;
CLOSE
  IF rdargsPtr#NIL THEN
    DosL.FreeArgs(rdargsPtr);
    DosL.FreeDosObject(DosD.dosRdArgs,rdargsPtr);
    rdargsPtr:=NIL;
  END;
  IF xsroot#NIL THEN
    FreeXData(xsroot);
    xsroot:=NIL;
  END;
  IF statroot#NIL THEN
    FreeStat(statroot);
    statroot:=NIL;
  END;
  IF inf.fh#NIL THEN
    DosL.Close(inf.fh);
    inf.fh:=NIL;
  END;
  IF stdErr#NIL THEN
    DosL.Close(stdErr);
    stdErr:=NIL;
  END;
END XStat.
