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;