7. Error Handling

So far, we have forgotten one very important thing: error handling. At the moment the calculator we have made can't react on syntax errors and other types of errors. If you enter "2+*3" the resulting answer is 2, while you would like to get an message that something is wrong. For expression parsers it is absolutely necessary that they either give the right answer, or give an error message if there is something that the parser can't handle. Errors can occur when:
To handle errors, each function needs the possibility to fill in an error message and send that back. At the end of the calculation there will be checked if an error occured somewhere. If so, the error will be printed on screen instead of the wrong answer. This will be done by the funcion evalLine.

SUB evalLine(expression:STRING), STRING
    'this sub evaluates the expression and returns a string that
    'can contain the answer or an (error) message. The expression
    'may contain values, operators + - * / ^ E and parentheses ( )
    DOUBLE ans
    MESSAGE msg

    'make the error message empty (ready for use)
    errClear(msg)

    'evaluate the expression
    ans = eval(expression, msg)

    'return (error) message if not empty
    IF msg.t<>MSG_EMPTY THEN RETURN msg.desc

    'if no error occured, return the answer
    RETURN "Ans = " + USING("#.#####", ans)
ENDSUB

Error type and constants
To store and fill in errors we will create an special Type that can contain an error id and an error description. Furthermore we define some constants that represent the types for errors. For example a syntax error like "2+*3" and a domain error like "2.5 / 0".

TYPE MESSAGE        'this type will store error messages
    INT t           'type of the error
    STRING desc     'description of the error
ENDTYPE

'the following error types are available for error messages
CONST MSG_EMPTY = 0
CONST MSG_SYNTAXERROR = 1
CONST MSG_DOMAINERROR = 2
CONST MSG_INFO = 3
CONST MSG_MEMERROR = 4
CONST MSG_SYSTEMERROR = 5
CONST MSG_OVERFLOW = 6

Error messages
The error message will be manipulated by two functions: errSet and errClear. Before using an error message, the message must be cleared. That is done with the function errClear. And when an error message needs to be filled in, the function errSet is used. This function does fill in a new message only if the current message is empty. So, when a certain expression causes multiple errors, only the first will be returned. This gives the most logical response for the user.

SUB errSet(msg:MESSAGE, newType:INT, newMessage:STRING)
    'if msg is empty, then fill in the new type and message.
    'don't overwrite an existing message
    IF msg.t = MSG_EMPTY
        msg.t = newType
        msg.desc = newMessage
    ENDIF
    RETURN
ENDSUB

SUB errClear(msg:MESSAGE)
    'clear the given message
    msg.t = MSG_EMPTY
    msg.desc = ""
    RETURN
ENDSUB

Value check
So far the function Eval does search for an operator and, if found, executes the calculation. If no operator is found the function assumes that an value will be left and returns this value. But it is possible that the remaining expression is no legal value, for example because of an syntax error or unsupported operators or functions. Therefore the function has to check whether the remaining expression is a value and if not return an error message. To check if an expression consists of a legal value the function isValue is created.

SUB isValue(value:STRING), INT
    'This sub checks if value is a legal value. if so, returns True.
    'If not, returns False
    INT i
    STRING sign

    FOR i=1 TO LEN(value)
        'check each character one by one
        sign = UCASE$(MID$(value,i,1))

        'check for legal signs in the string
        IF INSTR("1234567890.-", sign)=0 THEN RETURN FALSE

        'check if there is max. 1 point in the string
        IF sign="." THEN IF INSTR(value,".",i+1)>0 THEN RETURN FALSE

        'check for correct use of minus: only at position 1
        IF sign="-" THEN IF i<>1 THEN RETURN FALSE
    NEXT i

    RETURN TRUE
ENDSUB

Example 4
In the following example the error handling is added. There are some things changed in the function Eval, and the functions evalLine, isValue, errSet and errClear are added.

Example 4
/*
Recursive expression parser, Example 4.
Written with IBasic Professional
By Jos de Jong, may 2006

Supports expressions with operators + - * / ^ E and parentheses ( )
Error handling is included.
*/
AUTODEFINE "Off"

TYPE MESSAGE        'this type can store an error message
    INT t           'type of the error
    STRING desc     'description of the error
ENDTYPE

'the following error types are available for error messages
CONST MSG_EMPTY = 0
CONST MSG_SYNTAXERROR = 1
CONST MSG_DOMAINERROR = 2
CONST MSG_INFO = 3
CONST MSG_MEMERROR = 4
CONST MSG_SYSTEMERROR = 5
CONST MSG_OVERFLOW = 6


OPENCONSOLE

'print some examples of expressions with their answers
PRINT "Expression Calculator"
PRINT ""
PRINT "You can use + - * / ^ E and parentheses ( )"
PRINT "Enter an expression and press Enter to calculate it."
PRINT "Enter an empty expression to quit"
PRINT ""
PRINT "Examples:"
PRINT "2 + 3 * -4 = ", evalLine("2 + 3 * -4")
PRINT "2 + 3 / 4 - 5= ", evalLine("2 + 3 / 4 - 5")
PRINT "2.5 ^ 3.25 - 5E2 / 100 = ", evalLine("2.5 ^ 3.25 - 5E2 / 100 ")
PRINT "(1.5+2.5) * (3+4) = ", evalLine("(1.5+2.5) * (3+4)")
PRINT ""

DEF inputexpr:STRING
DO
    'ask the user to enter an expression. After this the expression
    'will be evaluated and the answer will be printed
    INPUT ">>", inputexpr
    IF RTRIM$(inputexpr)<>""
        PRINT "    ", evalLine(inputexpr)
        PRINT ""
    ENDIF
UNTIL RTRIM$(inputexpr)=""

'end the program
CLOSECONSOLE
END

'_______________________________________________________________
SUB evalLine(expression:STRING), STRING
    'this sub evaluates the expression and returns a string that
    'can contain the answer or an (error) message. The expression can
    'have values, operators + - * / ^ E and parentheses ( )
    DOUBLE ans
    MESSAGE msg
    
    'make the error message empty (ready for use)
    errClear(msg)
    
    'evaluate the expression
    ans = eval(expression, msg)
    
    'return (error) message if not empty
    IF msg.t<>MSG_EMPTY THEN RETURN msg.desc
    
    'if no error occured, return the answer
    RETURN "Ans = " + USING("#.#####", ans)
ENDSUB

'_____________________________________________________________________
SUB Eval(Expression:STRING, msg:MESSAGE), DOUBLE
    DEF pos, n:INT
    DEF expr:STRING
    DEF part1, part2:STRING
    DEF value1, value2:DOUBLE
    DEF operator:STRING
    
    'remove spaces at start and end of the expression
    expr = LTRIM$(RTRIM$(Expression))
    
    FOR n=1 TO 6
        'choose an operator
        '(for each loop choose the following of the five operators)
        operator = MID$("-+/*^E", n, 1)
        
        'search for the last occurance of this operator outside
        'parentheses
        pos = InstrP(UCASE$(expr), operator, 255)
        WHILE pos>0
            'Check if the found operator realy is an operator
            '(and not a unary plus or minus)
            IF isOperator(expr, operator, pos)=TRUE
                'there is an operator found at position pos. Split up
                'the expression in two parts and calculate the result
                'of that two parts. Then calculate the result for the
                'complete expression.
                part1 = LEFT$(expr, pos-1)
                part2 = RIGHT$(expr, LEN(expr)-pos)
                
                'If part1 and part2 are empty fill in an error message and return
                IF RTRIM$(part1)=""
                    errSet(msg, MSG_SYNTAXERROR, "Error: Missing value before operator " + operator)
                    RETURN 0
                ENDIF
                IF RTRIM$(part2)=""
                    errSet(msg, MSG_SYNTAXERROR, "Error: Missing value after operator " + operator)
                    RETURN 0
                ENDIF
                
                'calculate the left and the right part
                value1 = Eval(part1, msg)
                value2 = Eval(part2, msg)
                
                'now execute the operation between left and right
                'part, and return the answer
                SELECT operator
                    CASE "-"
                        RETURN value1 - value2
                    CASE "+"
                        RETURN value1 + value2
                    CASE "/"
                        'If value2 is zero, return an error message
                        IF value2=0
                            errSet(msg, MSG_DOMAINERROR, "Error: Divide by zero")
                            RETURN 0
                        ENDIF
                        RETURN value1 / value2
                    CASE "*"
                        RETURN value1 * value2
                    CASE "^"
                        RETURN value1 ^ value2
                    CASE "E"
                        RETURN value1 * 10 ^ value2
                ENDSELECT
            ENDIF
            
            'the found operator was no operator,
            'search if there is another operator before this one
            IF pos>0 THEN pos = instrP(UCASE$(expr), operator, pos-1)
        WEND
    NEXT n
    
    'check if the expression starts and ends with parentheses
    IF LEFT$(expr,1)="(" AND RIGHT$(expr,1)=")"
        'remove parentheses at start and end of the expression,
        'for example "(2+3)" -> "2+3"
        expr = MID$(expr,2,LEN(expr)-2)
        RETURN Eval(expr, msg)
    ENDIF
    
    'there are no more operators left in the expression, and the
    'expression is not solved yet so the expression should be a value
    'If expr is indeed a legal value, return the value
    IF isValue(expr) THEN RETURN VAL(expr)
    
    'something unsupported happend. return an error message
    errSet(msg, MSG_SYNTAXERROR, "Error: Syntax error in part \"" + expr + "\"")
    RETURN 0
ENDSUB

'_____________________________________________________________________
SUB instrP(source:STRING, search:STRING, start:INT), INT
    'this sub searches for the LAST string search in the string
    'source, reverse of the function instr(). The sub returns 0 if
    'the search could not be found. Also this function neglects
    'content between parenthesis () in source
    INT n, bopen, bclose
    STRING sign
    
    n=start
    IF n>LEN(source) THEN n=LEN(source)-LEN(search)+1
    bopen=0     :'number of parenthesis open (
    bclose=0    :'number of parenthesis close )
    DO
        sign = MID$(source, n, LEN(search))
        'if search is found and outside parentheses then return n
        IF (sign=search) AND (bopen=bclose) THEN RETURN n
        IF LEFT$(sign,1)="(" THEN bopen++
        IF LEFT$(sign,1)=")" THEN bclose++
        n--
    UNTIL (n <= 0)
    
    'if the string search is not found, then return 0
    RETURN 0
ENDSUB

'_______________________________________________________________
SUB isOperator(expr:STRING, op:CHAR, n:INT), INT
    'This sub checks of the operator at postion n in expr is a
    'legal operator. For example the "+" in "2.3E+3" is no operator,
    'and the "-" in "3 * -2.5" is no operator but a unary minus
    STRING sign
    
    IF op="+"
        IF UCASE$(MID$(expr,n-1,1))="E"
            IF n>2
                IF INSTR("1234567890.", MID$(expr,n-2,1))>0
                    'for example "2.52E+4"
                    RETURN FALSE
                ENDIF
            ENDIF
        ENDIF
        RETURN TRUE
    ENDIF
    
    IF op="-"
        IF n=1
            'this is an unary minus, at the first position of the expr
            'for example "-3.4 / 6"
            RETURN FALSE
        ELSE
            'check for an unary minus (for example 2*-3  or  2.5E-6)
            'check the characters before the minus
            sign = LEFT$(expr,n-1)
            sign = RIGHT$(RTRIM$(sign),1)
            IF INSTR("+-/*^", sign)>0
                'For example "3.5 * -2"
                RETURN FALSE
            ENDIF
            IF UCASE$(MID$(expr,n-1,1))="E" AND n>2
                IF INSTR("1234567890.", MID$(expr,n-2,1))>0
                    'for example "2.3E-4"
                    RETURN FALSE
                ENDIF
            ENDIF
        ENDIF
        RETURN TRUE
    ENDIF
    
    'no problems found, the operator is indeed an operator.
    RETURN TRUE
ENDSUB

'_______________________________________________________________
SUB isValue(value:STRING), INT
    'This sub checks if value is a legal value. if so, returns True.
    'If not, returns False
    INT i
    STRING sign
    
    FOR i=1 TO LEN(value)
        'check each character one by one
        sign = UCASE$(MID$(value,i,1))
        
        'check for legal signs in the string
        IF INSTR("1234567890.-", sign)=0 THEN RETURN FALSE
        
        'check if there is max. 1 point in the string
        IF sign="." THEN IF INSTR(value,".",i+1)>0 THEN RETURN FALSE
        
        'check for correct use of minus: only at position 1
        IF sign="-" THEN IF i<>1 THEN RETURN FALSE
    NEXT i
    
    RETURN TRUE
ENDSUB

'_______________________________________________________________
SUB errSet(msg:MESSAGE, newType:INT, newMessage:STRING)
    'if msg is empty, then fill in the new type and message.
    'don't overwrite an existing message
    IF msg.t = MSG_EMPTY
        msg.t = newType
        msg.desc = newMessage
    ENDIF
    RETURN
ENDSUB

'_______________________________________________________________
SUB errClear(msg:MESSAGE)
    'clear the given message
    msg.t = MSG_EMPTY
    msg.desc = ""
    RETURN
ENDSUB


Copyright 2006 SpeQ Mathematics