'=========================================================================== ' Subject: INFIX EXPRESSION EVALUATOR Date: 08-17-97 (02:58) ' Author: J. W. Dare Code: QB, QBasic, PDS ' Origin: jwdare@novia.net Packet: ALGOR.ABC '=========================================================================== 'Filename: MATH1.BAS 'QBasic/QB4.5 'jwdare@novia.net ' 'MATH1.BAS is a mathematical expression evaluator which converts an Infix '(standard calculator) expression to a Postfix expression and then solves 'the equation. The program is capable of handling the main arithmetic 'operations along with exponents, basic trig functions, the Pi constant, 'and one variable (X). The input is case insensitive. 'Valid operators are: ' ' +, -, * , /, ^, SIN, COS, TAN, PI, and X for variable ' 'Negative numbers must be enclosed in parentheses, for example (-5) or '(-(2+X)). Commas are allowed in numeric expressions, for example '987,654,321 is valid. White space in input expression is ignored. ' 'The program uses MS Basic conventions such as disallowing raising any 'negative number to a non-integer power. The program may be modified to 'alter these conventions. ' 'Math1.bas takes an input mathematical expression string, Math.in$. The 'Subroutine Prep then "standardizes" the string. The standardized Math.in$ 'is then passed to the Subroutine Postfix which converts Math.in$ to the 'Postfix expression Math.fix$. Math.fix$ is then passed to the Subroutine 'Calc which evaluates the Postfix expression. ' 'Most of the following routines could be optimized by using the ASC function 'to convert ASCII characters to their numeric value and then doing integer 'comparisons. ' '--------------- Program error codes ' ' ERROR.CODE% = '0 = NO DETECTED ERRORS '1 = SYNTAX ERROR '2 = PARENTHESES MISCOUNT '3 = DIVISION BY 0 ERROR '4 = RAISE NEG NUMBER TO NON-INTEGER POWER (SQR ROOT OF NEG NUMBER, ECT..) '5 = DISALLOWED CHARACTER '6 = DECIMAL POINT ERROR '----------------------------------------- 'The main module variables are: ' Math.in$ = The input expression ' Math.fix$ = The Mathin$ expression converted to Postfix form ' X = The user defined variable ' Error.code% = 0 if no errors, otherwise contains error number ' PFix.Stack$() = String array used in Sub Postfix ' Val.Stack() = Single precision array used in Sub Calc ' Pi = 4 * ATN(1), the value of Pi ' Deg.flg% = -1 if input angles are in degrees, 0 if in radians '----------------------------------------- DECLARE FUNCTION PrepString$ (Pstr$) 'Used in SUB Prep 'Converts lcase to ucase, removes spaces and commas 'Converts negation to CHR$(22) DECLARE SUB Functnams (Math.In$, Math.test$, Fun$, Char$) 'Used in SUB Prep 'Replaces multiple character function names with 'single representitive character and creates string for 'illegal character detection. DECLARE FUNCTION Parenth.Cnt% (Cnt.str$) '0 if "(" and ")" count equal DECLARE FUNCTION Legal.Char% (Strin$) 'Used in SUB prep to check for invalid characters. DECLARE FUNCTION String.Priority% (Test.Char$) 'Used in SUB Postfix 'Determines currently scanned character's priority value 'Test.Char$ = Current Math.fix$ character DECLARE FUNCTION Stack.Priority% (Test.Char$) 'Used in SUB Postfix 'Determines the priority value of operators stored in the stack 'Test.Char$ = Top stack character DECLARE FUNCTION Decim.Cnt% (Strin$) 'Used in SUB Calc to test for correct number (1) of 'decimal points in a numaric string (Val.tmp$). DECLARE SUB Prep (Strin$, Error.Code%) DECLARE SUB Postfix (Math.In$, Math.Fix$, PFix.Stack$()) DECLARE SUB Calc (Math.Fix$, Val.Stack(), X, Final.val, Error.Code%, Deg.flg%, Pi) DIM Val.Stack(50) 'picked 50 arbitrarily DIM PFix.Stack$(50) Pi = 4 * ATN(1) Error.Code% = 0 '--------- Test input Deg.flg% = -1 '-1 for degree calculations, 0 for radian calculations. X = 5 'Set to any value for testing use of a variable. LINE INPUT "Enter an expression: "; Math.In$ '-------------- Main module IF LEN(Math.In$) THEN CALL Prep(Math.In$, Error.Code%) IF Error.Code% = 0 THEN CALL Postfix(Math.In$, Math.Fix$, PFix.Stack$()) 'The Sub Calc may be placed in a loop to solve for 'multiple values of X CALL Calc(Math.Fix$, Val.Stack(), X, Final.val, Error.Code%, Deg.flg%, Pi) END IF ELSE Error.Code% = 5 END IF '-------------- Test output PRINT PRINT "Math.In$ = "; Math.In$ PRINT "Math.fix$ = "; Math.Fix$ PRINT IF Error.Code% = 0 THEN PRINT "Final.val ="; Final.val ELSE PRINT "Error code #"; LTRIM$(STR$(Error.Code%)) END IF PRINT END ' The Calc subroutine is used to solve the Postfix expression 'Math.Fix$. This is accomplished by scanning Math.Fix$ one character at a 'time, from left to right. Operands and temporary results are store in the 'single precision array, Val.Stack(), which is a stack addressed by 'Valstk.pntr%. ' Numeric string characters are stored in a temporary variable '(Val.Temp$) until a comma is encountered. The comma signals the program 'to increment the pointer, convert the numeric string to a single 'precision numeric value, then store the numeric value in the top position 'of Val.Stack(). Likewise, constants and the value contained in the 'variable (X) are stored on the top of the stack when encountered. ' If an operator that only requires one operand such as trig 'functions or negation, is scanned, the function/operation is applied to 'the top value in the stack. If an operator requiring two operators such 'as addition, subtraction, multiplication, or division, is the current 'scanned character, the top value is pulled from the stack, the stack 'pointer is decremented by one, and that value is also pulled from the stack. 'The operation is performed on these two operands and then the results of 'the operation is stored in the current stack position. ' SUB Calc (Math.Fix$, Val.Stack(), X, Final.val, Error.Code%, Deg.flg%, Pi) Valstk.pntr% = 0 Val.tmp$ = "" Co.ef1 = 0 Co.ef2 = 0 Test.str1$ = "0123456789." 'numerics Test.str2$ = "SCT" 'functions FOR i% = 1 TO LEN(Math.Fix$) Crnt.Char$ = MID$(Math.Fix$, i%, 1) IF INSTR(Test.str1$, Crnt.Char$) THEN Val.tmp$ = Val.tmp$ + Crnt.Char$ ELSEIF INSTR(Test.str2$, Crnt.Char$) THEN IF Deg.flg% THEN 'Convert degree input to radian Val.Stack(Valstk.pntr%) = Val.Stack(Valstk.pntr%) * Pi / 180 END IF IF Crnt.Char$ = "S" THEN Val.Stack(Valstk.pntr%) = SIN(Val.Stack(Valstk.pntr%)) ELSEIF Crnt.Char$ = "C" THEN Val.Stack(Valstk.pntr%) = COS(Val.Stack(Valstk.pntr%)) ELSEIF Crnt.Char$ = "T" THEN Val.Stack(Valstk.pntr%) = TAN(Val.Stack(Valstk.pntr%)) END IF ELSE IF Crnt.Char$ = "," THEN Error.Code% = Decim.Cnt%(Val.tmp$) 'Test for multiple decimal points IF Error.Code% = 0 THEN Valstk.pntr% = Valstk.pntr% + 1 Val.Stack(Valstk.pntr%) = VAL(Val.tmp$) 'Add number to stack Val.tmp$ = "" ELSE EXIT FOR END IF ELSEIF Crnt.Char$ = "P" THEN Valstk.pntr% = Valstk.pntr% + 1 Val.Stack(Valstk.pntr%) = Pi ELSEIF Crnt.Char$ = "X" THEN Valstk.pntr% = Valstk.pntr% + 1 Val.Stack(Valstk.pntr%) = X ELSEIF Crnt.Char$ = CHR$(22) THEN Val.Stack(Valstk.pntr%) = -Val.Stack(Valstk.pntr%) ELSEIF Crnt.Char$ = "*" THEN Co.ef2 = Val.Stack(Valstk.pntr%) Valstk.pntr% = Valstk.pntr% - 1 Co.ef1 = Val.Stack(Valstk.pntr%) Val.Stack(Valstk.pntr%) = Co.ef1 * Co.ef2 ELSEIF Crnt.Char$ = "/" THEN Co.ef2 = Val.Stack(Valstk.pntr%) IF Co.ef2 <> 0 THEN 'Test for divide by 0 Valstk.pntr% = Valstk.pntr% - 1 Co.ef1 = Val.Stack(Valstk.pntr%) Val.Stack(Valstk.pntr%) = Co.ef1 / Co.ef2 ELSE Error.Code% = 3 EXIT FOR END IF ELSEIF Crnt.Char$ = "+" THEN Co.ef2 = Val.Stack(Valstk.pntr%) Valstk.pntr% = Valstk.pntr% - 1 Co.ef1 = Val.Stack(Valstk.pntr%) Val.Stack(Valstk.pntr%) = Co.ef1 + Co.ef2 ELSEIF Crnt.Char$ = "-" THEN Co.ef2 = Val.Stack(Valstk.pntr%) Valstk.pntr% = Valstk.pntr% - 1 Co.ef1 = Val.Stack(Valstk.pntr%) Val.Stack(Valstk.pntr%) = Co.ef1 - Co.ef2 ELSEIF Crnt.Char$ = "^" THEN Co.ef2 = Val.Stack(Valstk.pntr%) Valstk.pntr% = Valstk.pntr% - 1 Co.ef1 = Val.Stack(Valstk.pntr%) Pwr.tmp = Co.ef2 - FIX(Co.ef2) 'test for non-integer value IF Co.ef1 < 0 AND Pwr.tmp <> 0 THEN Error.Code% = 4 EXIT FOR ELSE Val.Stack(Valstk.pntr%) = Co.ef1 ^ Co.ef2 END IF END IF END IF NEXT i% IF Error.Code% = 0 THEN IF Valstk.pntr% = 1 THEN 'Program has reached bottom of stack Final.val = Val.Stack(Valstk.pntr%) ELSE Error.Code% = 1 'Probable input syntax error END IF END IF END SUB 'Makes sure there's only one decimal point in Strin$, returns error #6 if 'multiple decimal points exits in a numeric exprssion (input error checking.) ' FUNCTION Decim.Cnt% (Strin$) flag% = 0 Position% = 1 Test% = 0 Decim.Cnt% = 0 DO Test% = INSTR(Position%, Strin$, ".") IF Test% THEN IF flag% THEN Decim.Cnt% = 6 EXIT DO ELSE Position% = Test% + 1 flag% = NOT flag% END IF END IF LOOP UNTIL Test% = 0 END FUNCTION 'Replaces Math.in$ function names with single representitive character. 'Math.test$ is a control used by the Legal.Char%() function to check for 'invalid characters later in Sub Prep ' SUB Functnams (Math.In$, Math.test$, Fun$, Char$) DO Place% = INSTR(Math.In$, Fun$) IF Place% THEN Str.1$ = LEFT$(Math.In$, (Place% - 1)) Str.2$ = RIGHT$(Math.In$, (LEN(Math.In$) - (LEN(Str.1$) + LEN(Fun$)))) Math.In$ = Str.1$ + Char$ + Str.2$ 'Used by function Legal.Char%() for input error testing Str.1test$ = LEFT$(Math.test$, (Place% - 1)) Str.2test$ = RIGHT$(Math.test$, (LEN(Math.test$) - (LEN(Str.1$) + LEN(Fun$)))) Math.test$ = Str.1test$ + SPACE$(1) + Str.2test$ END IF LOOP WHILE Place% END SUB 'Tests a control string for invalid characters (input error checking.) 'Returns error #5 if error detected. ' FUNCTION Legal.Char% (Strin$) Test.str$ = "01234567890.X()+-*/^ " + CHR$(22) flag% = 0 Cnt% = 0 DO Cnt% = Cnt% + 1 IF INSTR(Test.str$, MID$(Strin$, Cnt%, 1)) = 0 THEN flag% = 1 LOOP UNTIL Cnt% = LEN(Strin$) OR flag% IF flag% THEN Legal.Char% = 5 ELSE Legal.Char% = 0 END IF END FUNCTION 'Counts parentheses, returns an error #2 if parentheses miscount occurs (input 'error checking.) ' FUNCTION Parenth.Cnt% (Cnt.str$) Open.Parcnt% = 0 Close.Parcnt% = 0 FOR i% = 1 TO LEN(Cnt.str$) IF MID$(Cnt.str$, i%, 1) = "(" THEN Open.Parcnt% = Open.Parcnt% + 1 ELSEIF MID$(Cnt.str$, i%, 1) = ")" THEN Close.Parcnt% = Close.Parcnt% + 1 END IF IF Close.Parcnt% > Open.Parcnt% THEN EXIT FOR NEXT i% IF (Open.Parcnt% - Close.Parcnt%) THEN Parenth.Cnt% = 2 ELSE Parenth.Cnt% = 0 END IF END FUNCTION 'Sub for converting infix (Math.In$) to postfix (Matfix$) ' The Postfix subroutine contains the code that scans the Infix 'expression, Math.In$, and creates the Postfix (polish notation) expression 'Math.Fix$. Postfix expressions differ from standard Infix expressions in 'that the math operator follows the two operands instead of being placed 'between them, and also, parentheses are not needed. The use of comma 'delimiters is my own embellishment. 'For example: ' ' Infix: Postfix: ' ----------- ------------- ' 3+2 3,2,+ ' (3+2)*(6-5) 3,2,+6,5,-* ' 2^8/256 2,8,^256,/ ' ' This is done with the use of two functions, String.Priority%() and 'Stack.Priority%(), and the PFix.Stack$ array which serves as a stack to 'store mathematical operators. ' The Math.In$ string is scanned, left to right. Each scanned 'number is added directly to the right end of Math.Fix$, with a comma added 'to the end of each complete numeric expression. Constants and variables are 'added directly to the right end of Math.Fix$ also, but no trailing comma is 'added. The trailing comma after each numeric expression is used by the 'Calc subroutine as an "end of numeric string" flag. ' The two functions are used to determine the "priority" of scanned 'operators or parentheses. String.Priority%() assigns a priority value to 'the currently scanned Math.In$ operator. Stack.Priority%() assigns a 'value to operators stored in the PFix.Stack$ array. Their priority 'values are: ' ' Operator String.Priority%() Stack.Priority%() ' -------- ------------------ ----------------- ' ( 0 6 ' Trig Function 1 1 ' Negative number 2 2 ' ^ 3 3 ' * or / 4 4 ' + or - 5 5 ' ' If the scanned character is an operator or opening parenthesis 'then the priority of the top operator on the stack is checked. If the 'stack operator's Stack.Priority% is equal or less than the currently 'scanned operator's String.Priority% then the stack operator is added to 'the right end of Math.Fix$, the stack pointer (PfixStk.Pntr%) is decremented 'by one, and the next operator on the stack is compared to the currently 'scanned operator. When the Stack.Priority% value of the top stack 'operator is greater than the String.Priority% of the currently scanned 'operator, then the stack pointer is incremented by one and the currently 'scanned operator is added to the stack. ' If the scanned operator is a closing parenthesis, then each stack 'operator is pulled off the stack and added to the right of Math.Fix$ until 'an opening parenthesis is encountered in the stack. After the last 'Math.In$ character is scanned and processed, any remaining operators in 'the stack are pulled one at a time and added to the right end of Math.Fix$. ' 'note, CHR$(22) denotes a negative number ' SUB Postfix (Math.In$, Math.Fix$, PFix.Stack$()) true% = -1 false% = NOT true% Crnt.Char$ = "" Test.str1$ = "0123456789." Test.str2$ = "+-*/^SCT()XP" + CHR$(22) Begin.num% = true% PfixStk.Pntr% = 0 Math.Fix$ = "" FOR i% = 1 TO LEN(Math.In$) Crnt.Char$ = MID$(Math.In$, i%, 1) IF INSTR(Test.str1$, Crnt.Char$) THEN 'Process numerics Math.Fix$ = Math.Fix$ + Crnt.Char$ Begin.num% = false% ELSEIF INSTR(Test.str2$, Crnt.Char$) THEN IF NOT Begin.num% THEN Math.Fix$ = Math.Fix$ + "," Begin.num% = true% END IF IF Crnt.Char$ = "X" OR Crnt.Char$ = "P" THEN 'Process constants/variables Math.Fix$ = Math.Fix$ + Crnt.Char$ ELSE 'Process operators repeat% = true% DO IF Crnt.Char$ = ")" THEN IF PFix.Stack$(PfixStk.Pntr%) = "(" THEN repeat% = false% ELSE Math.Fix$ = Math.Fix$ + PFix.Stack$(PfixStk.Pntr%) END IF PfixStk.Pntr% = PfixStk.Pntr% - 1 ELSE IF Stack.Priority%(PFix.Stack$(PfixStk.Pntr%)) <= String.Priority%(Crnt.Char$) AND PfixStk.Pntr% > 0 THEN Math.Fix$ = Math.Fix$ + PFix.Stack$(PfixStk.Pntr%) PfixStk.Pntr% = PfixStk.Pntr% - 1 ELSE PfixStk.Pntr% = PfixStk.Pntr% + 1 PFix.Stack$(PfixStk.Pntr%) = Crnt.Char$ repeat% = false% END IF END IF LOOP WHILE repeat% END IF END IF NEXT i% IF NOT Begin.num% THEN Math.Fix$ = Math.Fix$ + "," 'Add any remaining operators to Math.Fix$ DO UNTIL PfixStk.Pntr% = 0 Math.Fix$ = Math.Fix$ + PFix.Stack$(PfixStk.Pntr%) PfixStk.Pntr% = PfixStk.Pntr% - 1 LOOP END SUB ' The Prep subroutine prepares the Math.In$ string for later 'evaluation by the Postfix subroutine. ' First, Math.In$ is passed to the Prepstring$ function which 'removes spaces and commas, and assigns CHR$(22) to denote negative 'numbers. It also converts all letters to upper case. Math.In$ is then 'checked for proper parentheses count by the Parenth.Cnt% function. ' Next, Math.In$ is passed to a series of the subroutine Functnams 'which replaces function names such a "SIN" with a single representative 'character, in this case "S". The sub Functnams also creates a test string, 'Math.Test$, which is scanned by the Legal.Char% function for remaining 'invalid characters. ' SUB Prep (Math.In$, Error.Code%) Math.In$ = PrepString$(Math.In$) Error.Code% = Parenth.Cnt%(Math.In$) IF Error.Code% = 0 THEN Math.test$ = Math.In$ CALL Functnams(Math.In$, Math.test$, "SIN", "S") CALL Functnams(Math.In$, Math.test$, "COS", "C") CALL Functnams(Math.In$, Math.test$, "TAN", "T") CALL Functnams(Math.In$, Math.test$, "PI", "P") Error.Code% = Legal.Char%(Math.test$) END IF END SUB 'Removes spaces and commas from input string, converts negation to a CHR$(22), 'and converts all letters to upper case. ' FUNCTION PrepString$ (Pstr$) Pstr.tmp$ = "" Prev.b$ = "" FOR i% = 1 TO LEN(Pstr$) b$ = MID$(Pstr$, i%, 1) IF b$ <> " " AND b$ <> "," THEN IF b$ = "-" AND Prev.b$ = "(" THEN b$ = CHR$(22) Pstr.tmp$ = Pstr.tmp$ + b$ END IF Prev.b$ = b$ NEXT i% PrepString$ = UCASE$(Pstr.tmp$) END FUNCTION 'Determines the priority value of the current operator from the stack. ' FUNCTION Stack.Priority% (Test.Char$) IF Test.Char$ = "(" THEN Stack.Priority% = 6 ELSEIF Test.Char$ = "S" OR Test.Char$ = "C" OR Test.Char$ = "T" THEN Stack.Priority% = 1 ELSEIF Test.Char$ = CHR$(22) THEN Stack.Priority% = 2 ELSEIF Test.Char$ = "^" THEN Stack.Priority% = 3 ELSEIF Test.Char$ = "*" OR Test.Char$ = "/" THEN Stack.Priority% = 4 ELSEIF Test.Char$ = "+" OR Test.Char$ = "-" THEN Stack.Priority% = 5 END IF END FUNCTION 'Determines the priority of the current Math.Fix$ test character. ' FUNCTION String.Priority% (Test.Char$) IF Test.Char$ = "(" THEN String.Priority% = 0 ELSEIF Test.Char$ = "S" OR Test.Char$ = "C" OR Test.Char$ = "T" THEN String.Priority% = 1 ELSEIF Test.Char$ = CHR$(22) THEN String.Priority% = 2 ELSEIF Test.Char$ = "^" THEN String.Priority% = 3 ELSEIF Test.Char$ = "*" OR Test.Char$ = "/" THEN String.Priority% = 4 ELSEIF Test.Char$ = "+" OR Test.Char$ = "-" THEN String.Priority% = 5 END IF END FUNCTION