PROGRAM castre;
 {$nomain}

{ 
  File:[22,310]CASTRE.PAS
  Author: Phil Hannay 9-May-89  (patterned after CASTIN.PAS)

  Last Edit: 14-AUG-1989 08:55:44 

  History:

}

 {[a+,b+,l-,k+,r+] Pasmat }

%include pas$ext:general.typ;

%include pas$ext:slen.ext;


  PROCEDURE Castre(VAR asc: PACKED ARRAY [lo..hi: integer] OF char;
                   VAR rea: real;
                   VAR pos: integer);
    EXTERNAL;

{*USER*

CASTRE converts an input string of ASCII characters in ASC
to a real number using a decimal (base 10) radix.  The compiler determines
whether this is single (4bytes) or double (8 bytes) precision real.  At
this time, we are compiling all Pascal-2 with the /DOUBLE switch, making
all reals double precision.

The real number is returned in REA.  ASC must be a valid type0 or type1
string.  POS specifies starting point in the string (array).  Upon exit from 
this procedure, POS will be left pointing to the position of the terminating
character or the end of the string, whichever comes first.
Leading space or tab character(s)
are ignored.  The number will be considered negative if a leading "-" minus
sign is encountered, and the number will be considered positive is a leading
"+" plus sign or no sign at all is encountered.  The plus or minus sign
needs to precede the first ascii digit, but can be separated from that first
digit by any number of (ignored) space or tab characters.  

Commas can be used in the conventional locations for thousands, millions, etc.
If commas are used in the number, they must be in the conventional locations,
or they will result in a conversion error.

Conversion of the ascii string will continue until a terminator
character is encountered or the end of the string is encountered.  
A terminator character will be any non digit that occurs
after the first valid digit (0-9, or decimal point) character is encountered.
Conventional commas imbedded in the number after the first valid digit and
before the decimal point will not be considered terminator characters and
will be ignored.  

A decimal point can be either explicit or implied.  A decimal point will
be implied if a terminator character is encountered after a valid digit,
or the end of the string is encountered after a valid digit.  
IMPORTANT note - since we ignore commas embeeded in numbers preceding a 
decimal point, commas are not always terminator characters.  Keep that in 
mind when more than one real number is in the ascii string.  
In cases where you will have multiple numbers in a string, and you wish to
allow commas embedded in the number, it is recommended that you use
explicit decimal points for all numbers, or use seperator (terminator)
characters other than commas to eliminate any ambiguity.

POS will be left pointing at the terminating character (or end of string + 1),
so you may use it to go on to the next number.  Thus you can parse
strings like "34.5,+538.,456.334,891.345  -.884,,3.,45.4".  
Where no valid number is found before the next terminator 
(such as the ",," in the example),
a value of zero will be returned.  

Remember that this routine handles arrays using the string conventions.
The length of a type 0 string is determined by the 0 element of the array,
and the length of a type 1 string is determined by the first null character
in the string.  Be careful if you use strings made up of left justified
numbers with extra blank padding to the right of the number.  The first
blank encountered will act as the terminator.  If you keep POS where it
is and call CASTRE again, the remaining blanks will be read as another
number and give you a zero.  This is nice in that an all blank string
like "    " will return a value of zero.  It does also mean that a
string like "34.5    " will return a value of 34.5, the first time you call
CASTRE, and if you do not alter POS, will return a 0, the second time you
call CASTRE.

Real numbers can vary from 1E-38 to 1E+38.  We do check for overflow/underflow,
and will signal that overflow/underflow would occur if the conversion 
were attempted
by returning negative value of the terminator character position in POS.  

If misplaced commas or numbers without decimal points are used, a negative
value will be returned in POS.  Because the procedure cannot guess what
was intended, the value of POS will be the negative value of the 
character position following the initial POS position supplied.

Remember that single precision
real numbers (4 bytes) will give you about 7 digits of precision
while double precision real numbers (8 bytes) will give you about
15 digits of precision.

*ERROR CODES*

Normal errors can be detected by the returned
value of POS.  You can check the value of POS
against the value you expected to determine
if the conversion was complete.  For example,
if a string contains a single number to be 
converted, and there are no trailing blanks,
POS should be returned with the same value
as SLEN of that string.  Likewise, if there
are multiple numbers separated by commas
in the string, you should get the expected
number of values back by repeatedly calling
CASTRE until POS is equal to SLEN of the
string.  

POS must always advance by 2 or more for
any reasonable number, since 1 digit plus
a decimal point is a reasonable number.

Use POS as the subscript into the string
to verify that the desired separator
character (like a comma or a space) 
was used.  If POS points to something
else, the proper separator was not
used.

If POS is negative, overflow or underflow 
occurred in the conversion, or misplaced
commas or omitted decimal point made it
impossible to interpret the number for
a conversion.

The following error messages can appear when using
CASTRE.  They normally indicate a programming error.
SLEN is used to validate a type0 or type1 string.
See SLEN for those errors.

CASTRE -- Not type0/type1 or illegal postition: n
  (string in ASC not a valid type0 or type1 string, 
   or string is an empty string, or value in POS
   in not within string lower and upper subscripts)
  
}
{*WIZARD*

This routine handles only type0 and type1 compatible strings.
SLEN is used to validate these strings.

}


  PROCEDURE Castre;

    LABEL
      999; { used for premature exit on error }

    VAR
      Bpos, Fpos, Dpos, Epos, i, count, Limit : integer;
      Positive, err: boolean; 
      mult: real;

    BEGIN
      {Initialize some stuff.}
      Positive := true; {assume positve number}
      Rea := 0.0; {assume zero - if no ascii digits found, will return zero}

      { use SLEN to get string length plus validate that string is indeed
        a type0 or type1 string }
      limit:= slen(asc);

      { make sure POS falls within string }
      IF (limit = 0) or (pos < 1) or (pos > limit) then
        BEGIN
        { write a quick diagnostic message to help programmer }
        writeln('CASTRE -- Not type0/type1 or illegal postition: ', pos:1);
        GOTO 999
        END;

      { skip leading spaces,tabs,zeros,plus sign or minus signs to find
        first meaningful digit - 1-9 or decimal point }
      Bpos := pos;
      WHILE (Bpos < Limit) AND
            (ord(asc[Bpos]) IN [40B, 11B, 60B, 53B, 55B]) DO
        BEGIN
        { If minus sign encountered, set postive false to indicate 
          negative number }
        IF ord(asc[Bpos]) IN [55B] THEN Positive := false;
        Bpos := Bpos + 1;
        END;

      if not(asc[bpos] in ['0'..'9','.'])
        then begin
          { nothing to convert }
          pos:= bpos;
          GOTO 999;
          END;

      { Save the first character position in FPOS, now find the decimal point.
        Remember that a decimal point can be explicit or implied by
        the context.}
      fpos:= bpos;
      WHILE (Bpos < Limit) AND (asc[Bpos] IN ['0'..'9',',']) 
        DO bpos:= bpos +1;

      { Verify the decimal point, and mark with DPOS, then find
        end of number and mark with EPOS }
      if asc[bpos] = '.' 
        then begin
          { explicit decimal found, now only 0-9 are valid }
          dpos:= bpos;
          if bpos < limit then bpos:= bpos + 1;
          while (bpos < limit) and (asc[bpos] in ['0'..'9']) 
            do bpos:= bpos + 1;
          if asc[bpos] in ['0'..'9'] then epos:= bpos else epos:= bpos - 1;
          end
        else begin
          { no decimal point found - must be implict since we encountered
            a terminating character or the end of string.  }
          if bpos >= limit then dpos:= bpos+1 else dpos:= bpos;
          epos:= dpos-1;
          end;

      { FPOS points to first digit, DPOS to decimal point (explict or
        implicit), and EPOS points to the last 0-9 digit of the number }

      { now check out comma positions, must be every 4th character back
        from DPOS to FPOS, also count commas in COUNT }
      count:= 0;
      err:= false;
      for i:= dpos-1 downto fpos
        do begin
          if (asc[i] = ',') 
            then begin
              { Comma found.  If not in correct place, declare an error. }
              count:= count+1;
              if ((dpos-i) mod 4 <> 0) then err:= true;
              end;
          end; 
      { exit if commas not right, POS left pointing where it was, 
        and POS is negated to signal error }
      if err 
        then begin
          pos:= -(pos);
          GOTO 999;
          end;
          
      { screen out overflow or under flow }
      err:= false;
      if dpos-fpos-count > 37 
        then begin
          { overflow }
          err:= true;
          end;
      if (dpos = fpos) and (epos - dpos > 37) 
        then begin
          { underflow }
          err:= true;
          end;
      if err 
        then begin
          pos:= -bpos;
          goto 999;
          end;

      { Finally, we have checked it all out, and we can now generate
        the real number.  First the portion in front of the decimal... }
      mult:= 1.0;
      for i:= dpos-1 downto fpos do
        begin
        if asc[i] <> ',' 
          then begin
            { convert ascii digit, multiply by current multiplier, and sum }
            rea:= rea + ((ord(asc[i])-ord('0'))*mult);
            mult:= mult * 10.0;
            end;
        end;
      { and the portion after the decimal (if epos > dpos)... }       
      mult:= 0.1;
      for i:= dpos+1 to epos do
        begin
        rea:= rea + ((ord(asc[i])-ord('0'))*mult);
        mult:= mult / 10.0;
        end;

      { set the sign }
      if not(positive) then rea:= -rea;

      { done - ending position is BPOS }
      Pos:= bpos;
      
    999:
    END;
