Evaluate and process math expressions

Share your advanced PureBasic knowledge/code with the community.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Evaluate and process math expressions

Post by Little John »

Hi,

this is just one more recursive descent parser. :-)
In contrast to the code in the following post, this one evaluates mathematical expressions.
For more details see comments in the code.

//edit 2015-04-23:
Yesterday I had forgotten to translate some error messages into English. Now fixed.

//edit 2015-04-26:
  • added functions Ceil(), Trunc(), and Floor()
  • some cosmetic changes

Code: Select all

; -- Recursive descent parser for evaluating mathematical expressions.
; works in ASCII mode and in Unicode mode
; <http://www.purebasic.fr/english/viewtopic.php?f=12&t=62125>

; o First   version: 2008-07-30
; o Current version: 2015-04-26 (tested with PB 5.31)
; by Little John

; -- Notes
; o The syntax is checked rather strictly.
; o All built-in PB constants as well as self-defined constants
;   can be evaluated (examples are given).
;   For doing so, add the appropriate code to the procedure Const().
; o All built-in PB functions as well as self-written functions
;   can be evaluated (examples are given).
;   For doing so, add the appropriate code to the procedure Func().
; o The power operator "^" is handled correctly (right associative).
; o 0^0 yields 1 here, as with PB and many other programming languages.
;   In mathematics, it has been discussed for a long time about the
;   value of 0^0, see e.g.:
;   <http://mathworld.wolfram.com/Zero.html>
;   <http://mathforum.org/dr.math/faq/faq.0.to.0.power.html>
;   <http://en.wikipedia.org/wiki/Exponentiation#Zero_to_the_power_of_zero>


DeclareModule Eval
   ;-- Error messages
   #Err_BadSyntax = "Syntax error"
   #Err_MissingOperator = "Missing operator"
   #Err_NoClosingBracket = "Missing )"
   #Err_NoOpeningBracket = "Missing ("
   #Err_UnknownOperator = "Unknown operator: "
   #Err_InvalidDecNumber = "Invalid decimal number"
   #Err_InvalidHexNumber = "Invalid hex number"
   #Err_ParameterMismatch = "(): Parameter count mismatch"
   
   #Err_UnknownFunction = "Unknown function: "
   #Err_UnknownConstant = "Unknown constant: "
   #Err_NegativeBase = "Negative base with non-integer exponent"
   #Err_DivisionByZero = "Division by zero"
   #Err_Sqr = "Square root of negative number"
   #Err_Log = "Logarithm of number <= 0"
   #Err_LogBase = "Logarithm to base <= 0 or 1"
   
   ;-- Main function
   Declare.s Calc(expr.s)
EndDeclareModule


Module Eval
   EnableExplicit
   
   ;-- Token types
   Enumeration
      #Unknown
      #Operator
      #DecNumber
      #HexNumber
      #Identifier
   EndEnumeration
   
   ;-- Token definitions
   #OpRel = "<=<>="
   #OpChars = #OpRel + "+-*/^),"
   #Letters = "abcdefghijklmnopqrstuvwxyz_"
   #DecDigits = "0123456789"
   #HexDigits = #DecDigits + "abcdef"
   #AlphaNum = #Letters + #DecDigits
   
   ;-- Constants
   #E = 2.7182818284590452
   
   Structure Tokens
      Str.s
      Typ.i
   EndStructure
   
   Global Dim TokenArray.Tokens(0)
   
   Global Token.s, Error.s
   Global TokenType.i
   
   ;-----------------------------------------------------------------------
   
   Procedure NextToken (expr.s="")
      ; in : expr     : expression to be evaluated
      ; out: Token    : current token
      ;      TokenType: type of current token
      Static formula.s, fLen.i, posn.i
      Protected left.i
      
      If expr <> ""           ; only on first call
         formula = expr
         fLen = Len(formula)
         posn = 1
      EndIf
      
      While posn <= fLen And Mid(formula, posn, 1) = " "
         posn + 1
      Wend
      
      Token = Mid(formula, posn, 1)
      posn + 1
      left = posn
      
      If FindString(#OpChars, Token)
         If posn <= fLen And FindString(#OpRel, Token) <> 0 And
            FindString(#OpRel, Mid(formula, posn, 1)) <> 0
            Token + Mid(formula, posn, 1)
            posn + 1
         EndIf
         TokenType = #Operator
         
      ElseIf FindString(#Letters, Token)
         While posn <= fLen And FindString(#AlphaNum, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #Identifier
         
      ElseIf FindString(#DecDigits, Token)
         While posn <= fLen And FindString(#DecDigits, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #DecNumber
         
      ElseIf Token = "$"
         While posn <= fLen And FindString(#HexDigits, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #HexNumber
         
      Else
         TokenType = #Unknown
      EndIf
   EndProcedure
   
   
   Procedure Scan (expr.s)
      ;-- Split the whole expression in advance into separate tokens,
      ;   and store them in TokenArray().
      Protected index
      
      Dim TokenArray.Tokens(Len(expr)*2)  ; so the array is always big enough
      
      index = 0
      NextToken(expr)
      While Token <> ""
         TokenArray(index)\Str = Token
         TokenArray(index)\Typ = TokenType
         index + 1
         NextToken()
      Wend
   EndProcedure
   
   
   Procedure GetToken (idx=-1)
      ;-- get the next token from TokenArray()
      Static index
      
      If idx <> -1        ; only on first call
         index = idx
      EndIf
      
      Token = TokenArray(index)\Str
      TokenType = TokenArray(index)\Typ
      index + 1
   EndProcedure
   
   ;-----------------------------------------------------------------------
   
   Procedure.d Arith (op.s, v.d, r.d)
      Protected ret.d = #False
      
      If Error <> ""
         ProcedureReturn ret
      EndIf
      
      Select op
         Case "<"
            If v < r
               ret = #True
            EndIf
         Case ">"
            If v > r
               ret = #True
            EndIf
         Case "="
            If v = r
               ret = #True
            EndIf
         Case "<>"
            If v <> r
               ret = #True
            EndIf
         Case "<="
            If v <= r
               ret = #True
            EndIf
         Case ">="
            If v >= r
               ret = #True
            EndIf
         Case "+"
            ret = v + r
         Case "-"
            ret = v - r
         Case "*"
            ret = v * r
         Case "/"
            If r = 0
               Error = #Err_DivisionByZero
            Else
               ret = v / r
            EndIf
         Case "^"
            If v < 0 And r <> Int(r)
               Error = #Err_NegativeBase
            ElseIf v = 0 And r < 0
               Error = #Err_DivisionByZero
            Else
               ret = Pow(v, r)
            EndIf
         Default
            Error = #Err_UnknownOperator + op
      EndSelect
      
      ProcedureReturn ret
   EndProcedure
   
   
   ;-- self-written functions (with as many arguments as appropriate)
   
   Macro Ceil (_x_)
      ; -- round towards positive infinity
      ; gives the smallest integer greater than or equal to x
      Round(_x_, #PB_Round_Up)
   EndMacro
   
   
   Macro Trunc (_x_)
      ; -- round towards zero
      ; discards the noninteger part of x
      IntQ(_x_)
   EndMacro
   
   
   Macro Floor (_x_)
      ; -- round towards negative infinity
      ; gives the greatest integer less than or equal to x
      Round(_x_, #PB_Round_Down)
   EndMacro
   
   
   Procedure.d Min (List args.d())
      ; in : List of real numbers
      ; out: Smallest number in the list
      Protected ret.d
      
      ret = args()
      While NextElement(args())
         If ret > args()
            ret = args()
         EndIf
      Wend
      ProcedureReturn ret
   EndProcedure
   
   
   Procedure.d Max (List args.d())
      ; in : List of real numbers
      ; out: Biggest number in the list
      Protected ret.d
      
      ret = args()
      While NextElement(args())
         If ret < args()
            ret = args()
         EndIf
      Wend
      ProcedureReturn ret
   EndProcedure
   
   
   Procedure.d Sum (List args.d())
      ; in : List of real numbers
      ; out: Sum of all numbers in the list
      Protected ret.d
      
      ret = args()
      While NextElement(args())
         ret + args()
      Wend
      ProcedureReturn ret
   EndProcedure
   
   ;-----------------------------------------------------------------------
   
   Procedure.d Const (name.s)
      Protected ret.d
      
      Select name
         Case "pi"
            ret = #PI
         Case "e"
            ret = #E
         Default
            Error = #Err_UnknownConstant + name
      EndSelect
      
      ProcedureReturn ret
   EndProcedure
   
   
   Procedure.d Func (name.s, List args.d())
      ; Add here the appropriate code for all functions that shall be evaluated
      ; (built-in PB functions or self-written functions).
      ; It is not necessary to check for an empty list of arguments here,
      ; since that will raise a syntax error anyway.
      Protected ret.d
      
      FirstElement(args())
      
      Select name
         Case "sqr"
            If ListSize(args()) > 1
               Error = name + #Err_ParameterMismatch
            ElseIf args() < 0
               Error = #Err_Sqr
            Else
               ret = Sqr(args())
            EndIf
            
         Case "log"
            If ListSize(args()) > 2
               Error = name + #Err_ParameterMismatch
            ElseIf args() <= 0
               Error = #Err_Log
            Else
               ret = Log(args())                  ; Default base is 'e'.
               If ListSize(args()) = 2            ; If a different base is given ...
                  NextElement(args())
                  If args() <= 0 Or args() = 1
                     Error = #Err_LogBase
                  Else
                     ret / Log(args())
                  EndIf
               EndIf
            EndIf
            
         Case "ceil"
            If ListSize(args()) > 1
               Error = name + #Err_ParameterMismatch
            Else
               ret = Ceil(args())
            EndIf
            
         Case "trunc"
            If ListSize(args()) > 1
               Error = name + #Err_ParameterMismatch
            Else
               ret = Trunc(args())
            EndIf
            
         Case "floor"
            If ListSize(args()) > 1
               Error = name + #Err_ParameterMismatch
            Else
               ret = Floor(args())
            EndIf
            
         Case "min"
            ret = Min(args())
            
         Case "max"
            ret = Max(args())
            
         Case "sum"
            ret = Sum(args())
            
         Default
            Error = #Err_UnknownFunction + name
      EndSelect
      
      ProcedureReturn ret
   EndProcedure
   
   ;-----------------------------------------------------------------------
   ;-- recursive procedures
   
   Declare.d Expression()
   
   
   Procedure.d Factor()
      Protected op.s
      Protected v.d
      
      If TokenType = #DecNumber
         op = Token
         GetToken()
         If Token <> "."
            v = Val(op)
         Else
            GetToken()
            If TokenType = #DecNumber
               v = ValD(op + "." + Token)
            Else
               Error = #Err_InvalidDecNumber
            EndIf
            GetToken()
         EndIf
         
      ElseIf TokenType = #HexNumber
         If Len(Token) > 1
            v = Val(Token)
         Else
            Error = #Err_InvalidHexNumber
         EndIf
         GetToken()
         
      ElseIf TokenType = #Identifier
         op = Token
         GetToken()
         If Token = "("
            ;-- get list of function arguments
            Protected NewList args.d()
            Repeat
               GetToken()
               AddElement(args())
               args() = Expression()
            Until Token <> ","
            If Token <> ")" And Error = ""
               Error = #Err_NoClosingBracket
            EndIf
            v = Func(op, args())
            GetToken()
         Else
            v = Const(op)
         EndIf
         
      ElseIf Token = "("
         GetToken()
         v = Expression()
         If Token <> ")" And Error = ""
            Error = #Err_NoClosingBracket
         EndIf
         GetToken()
         
      Else
         Error = #Err_BadSyntax
      EndIf
      
      If Token <> "" And FindString(#OpChars, Token) = 0 And Error = ""
         If TokenType = #DecNumber Or TokenType = #HexNumber Or
            TokenType = #Identifier Or Token = "("
            Error = #Err_MissingOperator
         Else
            Error = #Err_UnknownOperator + Token
         EndIf
      EndIf
      
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.d Power()
      Protected v.d, r.d
      
      v = Factor()
      If Token = "^"
         GetToken()
         r = Power()
         v = Arith("^", v, r)
      EndIf
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.d Term()
      Protected op.s
      Protected v.d, r.d
      
      v = Power()
      op = Token
      While op = "*" Or op = "/"
         GetToken()
         r = Power()
         v = Arith(op, v, r)
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.d Signed()
      Protected op.s
      Protected v.d
      
      op = ""
      If Token = "+" Or Token = "-"
         op = Token
         GetToken()
      EndIf
      v = Term()
      If op = "-"
         v = -v
      EndIf
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.d SingleExpression()
      Protected op.s
      Protected v.d, r.d
      
      v = Signed()
      op = Token
      While op = "+" Or op = "-"
         GetToken()
         r = Term()
         v = Arith(op, v, r)
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.d Expression()
      Protected op.s
      Protected v.d, r.d
      
      v = SingleExpression()
      op = Token
      While FindString(#OpRel, op) <> 0
         GetToken()
         r = SingleExpression()
         v = Arith(op, v, r)
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   ;-----------------------------------------------------------------------
   
   Procedure.s Calc (expr.s)
      ; * Main function *
      ; in : expression to be evaluated
      ; out: result (1st character is '-' or a decimal digit)
      ;            or
      ;      error message (1st character is a letter)
      Protected v.d
      
      Scan(LCase(expr))
      
      Error = ""
      GetToken(0)
      v = Expression()
      
      If Error <> ""
         ProcedureReturn Error
      ElseIf Token = ")"
         ProcedureReturn #Err_NoOpeningBracket
      ElseIf Token = ","
         ProcedureReturn #Err_BadSyntax
      Else                               ; no error
         ProcedureReturn StrD(v)         ; return result
      EndIf
   EndProcedure
EndModule


CompilerIf #PB_Compiler_IsMainFile
   ; -- Module demo
   
   Debug Eval::Calc("-1 <  7")
   Debug Eval::Calc(" 1 > -7") 
   Debug Eval::Calc(" 4 =  5")
   Debug Eval::Calc(" 4 <> 5")
   Debug Eval::Calc(" 4 <= 5") 
   Debug Eval::Calc(" 4 >= 5") 
   Debug Eval::Calc("(4+2) = 6")
   Debug Eval::Calc(" 4+2  = 6")
   Debug Eval::Calc("6 = (4+2)")
   Debug Eval::Calc("6 =  4+2") 
   Debug ""
   Debug Eval::Calc("  2+3+4")
   Debug Eval::Calc("  2-3*4")
   Debug Eval::Calc(" +2+3*4")
   Debug Eval::Calc(" -2+3*4")
   Debug Eval::Calc("   12*2")
   Debug Eval::Calc("   -3*4")
   Debug Eval::Calc("(2+3)*4")
   Debug ""
   Debug Eval::Calc("4^3^2")  
   Debug Eval::Calc("4^(3^2)")
   Debug Eval::Calc("(4^3)^2")
   Debug ""
   Debug Eval::Calc("Ceil ( 3.2)")
   Debug Eval::Calc("Ceil (-3.8)")
   Debug Eval::Calc("Trunc( 4.7)")
   Debug Eval::Calc("Trunc(-4.7)")
   Debug Eval::Calc("Floor( 3.8)")
   Debug Eval::Calc("Floor(-3.2)")
CompilerEndIf

-------------------------------------------------

My best tricks & tips from 15+ years
Create arrays elegantly
Extended date library
Save JSON data with object members well-arranged
Evaluate and process math expressions
Functions for sets
Statistics with R
Thue-Morse sequence
Natural sorting
Sort array indexes and parallel arrays
Time profiling
VectorIcons
Generate focus events
Last edited by Little John on Thu Jul 03, 2025 5:09 pm, edited 4 times in total.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

This code is derived from the previous one.
It's purpose is to convert a mathematical expression, so that it's compatible with PB syntax.
There was a discussion that showed need for code like this.

//edit 2015-04-26:
  • New: Functions "Ceil()", "Trunc()", and "Floor()" can be replaced with the equivalent PB functions.
    More functions can be added easily.
  • New: Any boolean expression can be enclosed in a "Bool()" function.
  • Flags are used for controlling what the module will replace.
  • The module has been renamed.
  • Some cosmetic changes.

Code: Select all

; -- Special recursive descent parser for mathematical expressions:
;    For making expressions compatible with PureBasic syntax, it
;    works as pre-processor that can
;    o replace functions "Ceil()", "Trunc()", and "Floor()" with the equivalent PB functions
;    o replace any "a^b" with "Pow(a,b)" in a mathematically correct way (right associative)
;    o enclose any boolean expressions in a "Bool()" function
; works in ASCII mode and in Unicode mode
; <http://www.purebasic.fr/english/viewtopic.php?f=12&t=62125>

; o First   version: 2015-04-21
; o Current version: 2015-04-26 (tested with PB 5.31)
; by Little John


DeclareModule M2PB
   ;-- Possible replace modes
   #Func = 1
   #Pow  = 2
   #Bool = 4
   #All  = 7
   
   ;-- Error messages
   #Err_BadSyntax = "Syntax error"
   #Err_MissingOperator = "Missing operator"
   #Err_NoClosingBracket = "Missing )"
   #Err_NoOpeningBracket = "Missing ("
   #Err_UnknownOperator = "Unknown operator: "
   #Err_InvalidDecNumber = "Invalid decimal number"
   #Err_InvalidHexNumber = "Invalid hex number"
   #Err_ParameterMismatch = "(): Parameter count mismatch"
   
   ;-- Main function
   Declare.s Calc(expr.s, mode.i=#Func|#Pow)
EndDeclareModule


Module M2PB
   EnableExplicit
   
   ;-- Token types
   Enumeration
      #Unknown
      #Operator
      #DecNumber
      #HexNumber
      #Identifier
   EndEnumeration
   
   ;-- Token definitions
   #OpRel = "<=<>="
   #OpChars = #OpRel + "+-*/^),"
   #Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
   #DecDigits = "0123456789"
   #HexDigits = #DecDigits + "ABCDEFabcdef"
   #AlphaNum = #Letters + #DecDigits
   
   Structure Tokens
      Str.s
      Typ.i
   EndStructure
   
   Global Dim TokenArray.Tokens(0)
   
   Global Token.s, Error.s
   Global TokenType.i
   Global Replace.i
   
   Macro Strip (_string_, _firstChar_)
      If Left(_string_, 1) = _firstChar_
         _string_ = Mid(_string_, 2, Len(_string_)-2)
      EndIf
   EndMacro
   
   ;-----------------------------------------------------------------------
   
   Procedure NextToken (expr.s="")
      ; in : expr     : expression to be evaluated
      ; out: Token    : current token
      ;      TokenType: type of current token
      Static formula.s, fLen.i, posn.i
      Protected left.i
      
      If expr <> ""      ; only on first call
         formula = expr
         fLen = Len(formula)
         posn = 1
      EndIf
      
      While posn <= fLen And Mid(formula, posn, 1) = " "
         posn + 1
      Wend
      
      Token = Mid(formula, posn, 1)
      posn + 1
      left = posn
      
      If FindString(#OpChars, Token)
         If posn <= fLen And FindString(#OpRel, Token) <> 0 And
            FindString(#OpRel, Mid(formula, posn, 1)) <> 0
            Token + Mid(formula, posn, 1)
            posn + 1
         EndIf
         TokenType = #Operator
         
      ElseIf FindString(#Letters, Token)
         While posn <= fLen And FindString(#AlphaNum, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #Identifier
         
      ElseIf FindString(#DecDigits, Token)
         While posn <= fLen And FindString(#DecDigits, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #DecNumber
         
      ElseIf Token = "$"
         While posn <= fLen And FindString(#HexDigits, Mid(formula, posn, 1)) <> 0
            posn + 1
         Wend
         Token + Mid(formula, left, posn-left)
         TokenType = #HexNumber
         
      Else
         TokenType = #Unknown
      EndIf
   EndProcedure
   
   
   Procedure Scan (expr.s)
      ;-- Split the whole expression in advance into separate tokens,
      ;   and store them in TokenArray().
      Protected index
      
      Dim TokenArray.Tokens(Len(expr)*2)  ; so the array is always big enough
      
      index = 0
      NextToken(expr)
      While Token <> ""
         TokenArray(index)\Str = Token
         TokenArray(index)\Typ = TokenType
         index + 1
         NextToken()
      Wend
   EndProcedure
   
   
   Procedure GetToken (idx=-1)
      ;-- get the next token from TokenArray()
      Static index
      
      If idx <> -1        ; only on first call
         index = idx
      EndIf
      
      Token = TokenArray(index)\Str
      TokenType = TokenArray(index)\Typ
      index + 1
   EndProcedure
   
   ;-----------------------------------------------------------------------
   
   Procedure.s Func (name.s, args.s)
      ; Add here the appropriate code for all functions that shall be converted.
      Protected ret.s=""
      
      Select LCase(name)
         Case "ceil"
            If CountString(args, ",") > 0
               Error = name + #Err_ParameterMismatch
            Else
               ret = "Round(" + args + ", #PB_Round_Up)"
            EndIf
            
         Case "trunc"
            If CountString(args, ",") > 0
               Error = name + #Err_ParameterMismatch
            Else
               ret = "IntQ(" + args + ")"
            EndIf
            
         Case "floor"
            If CountString(args, ",") > 0
               Error = name + #Err_ParameterMismatch
            Else
               ret = "Round(" + args + ", #PB_Round_Down)"
            EndIf
      EndSelect
      
      ProcedureReturn ret
   EndProcedure
   
   ;-----------------------------------------------------------------------
   ;-- recursive procedures
   
   Declare.s Expression()
   
   
   Procedure.s Factor()
      Protected op.s
      Protected args.s, v.s=""
      
      If TokenType = #DecNumber
         op = Token
         GetToken()
         If Token <> "."
            v = op
         Else
            GetToken()
            If TokenType = #DecNumber
               v = op + "." + Token
            Else
               Error = #Err_InvalidDecNumber
            EndIf
            GetToken()
         EndIf
         
      ElseIf TokenType = #HexNumber
         If Len(Token) > 1
            v = Token
         Else
            Error = #Err_InvalidHexNumber
         EndIf
         GetToken()
         
      ElseIf TokenType = #Identifier
         op = Token
         GetToken()
         If Token = "("
            ;-- get list of function arguments
            args = ""
            Repeat
               GetToken()
               args + Expression()
               If Token <> ","
                  Break
               EndIf
               args + ","
            ForEver
            If Token <> ")" And Error = ""
               Error = #Err_NoClosingBracket
            EndIf
            If Replace & #Func
               v = Func(op, args)
            EndIf
            If v = ""
               v = op + "(" + args + ")"
            EndIf
            GetToken()
         Else
            v = op               ; op is a constant
         EndIf
         
      ElseIf Token = "("
         GetToken()
         v = "(" + Expression()
         If Token = ")"
            v + ")"
         ElseIf Error = ""
            Error = #Err_NoClosingBracket
         EndIf
         GetToken()
         
      Else
         Error = #Err_BadSyntax
      EndIf
      
      If Token <> "" And FindString(#OpChars, Token) = 0 And Error = ""
         If TokenType = #DecNumber Or TokenType = #HexNumber Or
            TokenType = #Identifier Or Token = "("
            Error = #Err_MissingOperator
         Else
            Error = #Err_UnknownOperator + Token
         EndIf
      EndIf
      
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.s Power()
      Protected v.s, r.s
      
      v = Factor()
      If Token = "^"
         GetToken()
         r = Power()
         If Replace & #Pow
            Strip(v, "(")
            Strip(r, "(")
            v = "Pow(" + v + "," + r + ")"
         Else
            v + "^" + r
         EndIf
      EndIf
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.s Term()
      Protected op.s
      Protected v.s, r.s
      
      v = Power()
      op = Token
      While op = "*" Or op = "/"
         GetToken()
         v + op + Power()
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.s Signed()
      Protected op.s
      Protected v.s
      
      op = ""
      If Token = "+" Or Token = "-"
         op = Token
         GetToken()
      EndIf
      v = op + Term()
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.s SingleExpression()
      Protected op.s
      Protected v.s, r.s
      
      v = Signed()
      op = Token
      While op = "+" Or op = "-"
         GetToken()
         v + op + Term()
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   
   Procedure.s Expression()
      Protected op.s
      Protected v.s, r.s
      
      v = SingleExpression()
      op = Token
      While FindString(#OpRel, op) <> 0
         GetToken()
         If Replace & #Bool
            v = "Bool(" + v + op + SingleExpression() + ")"
         Else
            v + op + SingleExpression()
         EndIf
         op = Token
      Wend
      ProcedureReturn v
   EndProcedure
   
   ;-----------------------------------------------------------------------
   
   Procedure.s Calc (expr.s, mode.i=#Func|#Pow)
      ; * Main function *
      ; in : expression to be processed
      ;      mode: #Func: Functions "Ceil()", "Trunc()", and "Floor()" are replaced
      ;                   with the equivalent PB functions.
      ;            #Pow : Any "a^b" is replaced with "Pow(a,b)".
      ;            #Bool: Any boolean expression is enclosed in a "Bool()" function.
      ;                   Cave: Assignments are interpreted as boolean expressions, too.
      ;         The flags can be combined with '|'.
      ; out: converted expression
      ;            or
      ;      error message
      Protected v.s
      
      Scan(expr)
      
      Replace = mode                     ; copy parameter to global variable
      Error = ""
      GetToken(0)
      v = Expression()
      
      If Error <> ""
         ProcedureReturn Error
      ElseIf Token = ")"
         ProcedureReturn #Err_NoOpeningBracket
      ElseIf Token = ","
         ProcedureReturn #Err_BadSyntax
      Else                               ; no error
         ProcedureReturn v               ; return result
      EndIf
   EndProcedure
EndModule


CompilerIf #PB_Compiler_IsMainFile
   ; -- Module demo
   
   Debug M2PB::Calc("-1 <  7"  , M2PB::#Bool)
   Debug M2PB::Calc(" 1 > -7"  , M2PB::#Bool)
   Debug M2PB::Calc(" 4 =  5"  , M2PB::#Bool)
   Debug M2PB::Calc(" 4 <> 5"  , M2PB::#Bool)
   Debug M2PB::Calc(" 4 <= 5"  , M2PB::#Bool)
   Debug M2PB::Calc(" 4 >= 5"  , M2PB::#Bool)
   Debug M2PB::Calc("(4+2) = 6", M2PB::#Bool)
   Debug M2PB::Calc(" 4+2  = 6", M2PB::#Bool)
   Debug M2PB::Calc("6 = (4+2)", M2PB::#Bool)
   Debug M2PB::Calc("6 =  4+2" , M2PB::#Bool)
   Debug ""
   Debug M2PB::Calc("  2+3+4")
   Debug M2PB::Calc(" -2+3*4")
   Debug M2PB::Calc("   12*2")
   Debug M2PB::Calc("   -3*4")
   Debug M2PB::Calc("(2+3)*4")
   Debug ""
   Debug M2PB::Calc("4^3^2")
   Debug M2PB::Calc("4^(3^2)")
   Debug M2PB::Calc("(4^3)^2")
   Debug ""
   Debug M2PB::Calc("4+3^2")
   Debug M2PB::Calc("4^3-2")
   Debug M2PB::Calc("5-4^3^2+1")
   Debug M2PB::Calc("MyFunc(4^(3*5)^2, 6^Sin(2+7)^(4+1), 5^3^4)")
   Debug M2PB::Calc("y = (0.4^2-(0.6-(x^2+y^2)^0.5)^2)^0.5")
   Debug ""
   Debug M2PB::Calc("Ceil ( 3.2)")
   Debug M2PB::Calc("Ceil (-3.8)")
   Debug M2PB::Calc("Trunc( 4.7)")
   Debug M2PB::Calc("Trunc(-4.7)")
   Debug M2PB::Calc("Floor( 3.8)")
   Debug M2PB::Calc("Floor(-3.2)")
   Debug ""
   ; -- Errors
   Debug M2PB::Calc("4+3*")
   Debug M2PB::Calc("(4+3)5")
   Debug M2PB::Calc("((4+3")
   Debug M2PB::Calc("4+3)")
   Debug M2PB::Calc("4~3")
   Debug M2PB::Calc("1.A")
   Debug M2PB::Calc("$")
CompilerEndIf
Last edited by Little John on Sun Apr 26, 2015 10:37 pm, edited 1 time in total.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

The codes in the first and second messages in this thread are written in a way, so that the scanner (tokenizer) and the parser are independent from each other. So the used scanner can be easily replaced with another one.

For those who like Regular Expressions, here is a scanner that is based on them.
For using it, in the code in the first or second post here
  • Remove the procedure NextToken().
  • Replace the procedure Scan() with the following one.

Code: Select all

   Procedure Scan (expr.s)
      ;-- Split the whole expression in advance into separate tokens,
      ;   and store them in TokenArray().
      Protected firstChar.s
      Protected rex.i, n.i, k.i
      Protected Dim result.s(0)

      rex = CreateRegularExpression(#PB_Any, "[-+*/^().,]|[<>=]+|[a-z_][a-z_\d]*|\d+|\$[a-f\d]+|\S+?")
      If rex = 0
         Debug "Error creating Regular Expression: " + RegularExpressionError()
         End
      EndIf

      n = ExtractRegularExpression(rex, expr, result())
      Dim TokenArray.Tokens(n+1)

      For k = 0 To n-1
         TokenArray(k)\Str = result(k)

         firstChar = Left(result(k), 1)
         If FindString(#OpChars, firstChar)
            TokenArray(k)\Typ = #Operator
         ElseIf FindString(#Letters, firstChar)
            TokenArray(k)\Typ = #Identifier
         ElseIf FindString(#DecDigits, firstChar)
            TokenArray(k)\Typ = #DecNumber
         ElseIf firstChar = "$"
            TokenArray(k)\Typ = #HexNumber
         Else
            TokenArray(k)\Typ = #Unknown
         EndIf
      Next

      FreeRegularExpression(rex)
   EndProcedure
Last edited by Little John on Fri Apr 24, 2015 8:25 am, edited 1 time in total.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

For code such as a math expression evaluator, thorough testing is important.
Here are tests for the evaluator in the first message of this thread, to be used
with the tool "PureUnit" that comes with PureBasic (see <PB folder>\SDK\PureUnit\).
The "normal" version of the evaluator as well as the RegEx version passes all tests provided here.

If you do some other tests, and you'll encounter any bug, please let me know. Thanks!

//edit 2015-04-24:
Added a few more test cases.

//edit 2015-04-26:
Added test cases for Ceil(), Trunc(), and Floor().

Code: Select all

; PB 5.31
; <http://www.purebasic.fr/english/viewtopic.php?f=12&t=62125>

PureUnitOptions(Thread)
EnableExplicit

XIncludeFile "calc.pbi"


Procedure Check (expr$, result$)
   ; This procedure takes an expression and its expected result as parameters.
   ; It checks whether Calc() actually yields this result.
   Protected test$
   
   test$ = Eval::Calc(expr$)
   Assert(test$ = result$, expr$ + " <> " + test$)
EndProcedure


;* Valid expressions *

ProcedureUnit compare()
   Check("-1 <  7",   "1")
   Check(" 1 > -7",   "1")
   Check(" 4 =  5",   "0")
   Check(" 4 <> 5",   "1")
   Check(" 4 <= 5",   "1")
   Check(" 4 >= 5",   "0")
   Check("(4+2) = 6", "1")
   Check(" 4+2  = 6", "1")
   Check("6 = (4+2)", "1")
   Check("6 =  4+2",  "1")
EndProcedureUnit

ProcedureUnit basic_arithmetics()
   Check(" 2+3+4",     "9")
   Check(" 2-3*4",   "-10")
   Check("+2+3*4",    "14")
   Check("-2+3*4",    "10")
   Check("  12*2",    "24")
   Check("  -3*4",   "-12")
   Check(" (2+3)*4",  "20")
   Check("+(2+3)*4",  "20")
   Check("-(2+3)*4", "-20")
   Check("(-2+3)*4",   "4")
   Check("1.27+4.73", "6")
   Check("7/(10-1)", "0.7777777778")
   Check("6-(2+7)*4+5", "-25")
   Check("2*(3+4)/((5-6)*7)", "-2")
   Check("(7*(5-6))/2*(3+4)", "-24.5")
EndProcedureUnit

ProcedureUnit powr()
   Check("(2^3)^2",   "64")
   Check("2^(3^2)",  "512")
   Check("2^3^2",    "512")
   Check("0^0.5",      "0")
   Check("-9^0.5",    "-3")
   Check("0^2",        "0")
   Check("2^0",        "1")
   Check("2^(-3)", "0.125")
   Check("0^0",        "1")
EndProcedureUnit

ProcedureUnit nested()
   Check("min(1,2,max(3,4,sum(5,6,7,log(max(60,64),sqr(4)),8),9,10),9,10,11,12)", "1")
   Check("1+(2+(3+(4+(5+(6+(7+(8+(9-(10+(11+(12+(13+(14+(15+(16-(17+(18+(19+(20)))))))))))))))))))", "28")
EndProcedureUnit

ProcedureUnit hexadecimal()
   Check("$FF",  "255")
   Check("-$1C", "-28")
EndProcedureUnit

ProcedureUnit const_func()
   Check("pi", "3.1415926536")
   Check("e",  "2.7182818285")
   Check(" sqr(9)", "3")
   Check(" sqr(9)+4-5", "2")
   Check("-sqr(9)+4*5", "17")
   Check("6-sqr(2+7)*4+5", "-1")
   Check("2+3*(sqr(4)+5)", "23")
   Check("2+3/(sqr(4)*5)", "2.3")
   Check("2/3*(sqr((1+4)/5))", "0.6666666667")
   Check("log(e)", "1")             ; function with default parameter
   Check("log(32, 2)", "5")
   Check("ceil ( 3.2)",  "4")
   Check("ceil (-3.8)", "-3")
   Check("trunc( 4.7)",  "4")
   Check("trunc(-4.7)", "-4")
   Check("floor( 3.8)",  "3")
   Check("floor(-3.2)", "-4")
   Check("min(1,2,3,4,5,6,7,-3,8)", "-3")
   Check("max(1,2,3,4,5,6,7,-3,8)",  "8")
   Check("sum(1,2,3,4,5,6,7,-3,8)", "33")
EndProcedureUnit


;* Errors *

ProcedureUnit err_const_func()
   Check("sqr",       Eval::#Err_UnknownConstant + "sqr")
   Check("sqr 9",     Eval::#Err_UnknownConstant + "sqr")
   Check("sqr9",      Eval::#Err_UnknownConstant + "sqr9")
   Check("sqr_9()",   Eval::#Err_UnknownFunction + "sqr_9")
   Check("sin()",     Eval::#Err_UnknownFunction + "sin")
   Check("sqr(9,10)", "sqr" + Eval::#Err_ParameterMismatch)
EndProcedureUnit

ProcedureUnit err_arith()
   Check("(-9)^0.5",   Eval::#Err_NegativeBase)
   Check("7/0",        Eval::#Err_DivisionByZero)
   Check("0^(-2)",     Eval::#Err_DivisionByZero)
   Check("sqr(2-7)",   Eval::#Err_Sqr)
   Check("log(-2)",    Eval::#Err_Log)
   Check("log(8, -3)", Eval::#Err_LogBase)
   Check("log(8, 1)",  Eval::#Err_LogBase)
EndProcedureUnit

ProcedureUnit err_missing_bracket()
   Check("2)3",      Eval::#Err_NoOpeningBracket)
   Check("3*(2+5))", Eval::#Err_NoOpeningBracket)
   Check("3*(2+5",   Eval::#Err_NoClosingBracket)
EndProcedureUnit

ProcedureUnit err_missing_op()
   Check("1 2",        Eval::#Err_MissingOperator)
   Check("27$A",       Eval::#Err_MissingOperator)
   Check("(27+3)$A",   Eval::#Err_MissingOperator)
   Check("pi 9",       Eval::#Err_MissingOperator)
   Check("3sqr",       Eval::#Err_MissingOperator)
   Check("(2)sqr",     Eval::#Err_MissingOperator)
   Check("(2)3",       Eval::#Err_MissingOperator)
   Check("(3+4)5",     Eval::#Err_MissingOperator)
   Check("2(3",        Eval::#Err_MissingOperator)
   Check("(2)(3)",     Eval::#Err_MissingOperator)
   Check("(3+2)(7-5)", Eval::#Err_MissingOperator)
   Check("5(3+4)",     Eval::#Err_MissingOperator)
EndProcedureUnit

ProcedureUnit err_number()
   Check("1+4.", Eval::#Err_InvalidDecNumber)
   Check("1.+4", Eval::#Err_InvalidDecNumber)
   Check("$-3",  Eval::#Err_InvalidHexNumber)
   Check("$",    Eval::#Err_InvalidHexNumber)
EndProcedureUnit

ProcedureUnit err_unknown_op()
   Check("4 =< 5", Eval::#Err_UnknownOperator + "=<")
   Check("4 => 5", Eval::#Err_UnknownOperator + "=>")
   Check("4 == 5", Eval::#Err_UnknownOperator + "==")
   Check("4 != 5", Eval::#Err_UnknownOperator + "!")
   Check("4 ! 5",  Eval::#Err_UnknownOperator + "!")
   Check("$4.3",   Eval::#Err_UnknownOperator + ".")
   Check("7.3.4",  Eval::#Err_UnknownOperator + ".")
EndProcedureUnit

ProcedureUnit err_syntax()
   Check("4 =! 5",   Eval::#Err_BadSyntax)
   Check("2+)3",     Eval::#Err_BadSyntax)
   Check("(2+-3",    Eval::#Err_BadSyntax)
   Check("sqr(2+-3", Eval::#Err_BadSyntax)
   Check("sqr()",    Eval::#Err_BadSyntax)
   Check("3+-2",     Eval::#Err_BadSyntax)
   Check("3*-2",     Eval::#Err_BadSyntax)
   Check("3+*2",     Eval::#Err_BadSyntax)
   Check("3*/2",     Eval::#Err_BadSyntax)
   Check("0-^2",     Eval::#Err_BadSyntax)
   Check("0^-2",     Eval::#Err_BadSyntax)
   Check("2^-3",     Eval::#Err_BadSyntax)
   Check("2+.3",     Eval::#Err_BadSyntax)
   Check("2+,3",     Eval::#Err_BadSyntax)
   Check("3+",       Eval::#Err_BadSyntax)
   Check("3*",       Eval::#Err_BadSyntax)
   Check("*58+6",    Eval::#Err_BadSyntax)
   Check("/62-9",    Eval::#Err_BadSyntax)
   Check("^4+3",     Eval::#Err_BadSyntax)
   Check("/",        Eval::#Err_BadSyntax)
   Check("()",       Eval::#Err_BadSyntax)
   Check("2,3",      Eval::#Err_BadSyntax)
EndProcedureUnit
Last edited by Little John on Sun Apr 26, 2015 10:44 pm, edited 3 times in total.
User avatar
Thunder93
Addict
Addict
Posts: 1788
Joined: Tue Mar 21, 2006 12:31 am
Location: Canada

Re: Evaluate and process math expressions

Post by Thunder93 »

Interesting stuff.

Your first code has a dependency that wasn't included.

[19:23:37] [COMPILER] Line 530: Assert() is not a function, array, list, map or macro.
ʽʽSuccess is almost totally dependent upon drive and persistence. The extra energy required to make another effort or try another approach is the secret of winning.ʾʾ --Dennis Waitley
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

Sorry, I'm currently a bit lazy. :-)
So I didn't write an explanation for that.

The Assert() function comes with PB (and it is very useful IMHO),
but for unknown reasons it is not active by default. So each user
has to take some steps her/himself.
For details see <PB folder>\SDK\PureUnit\.

AFAIR, copying the file "PureUnit.res" to the folder <PB folder>\Residents\
is the decisive step here.
User avatar
Thunder93
Addict
Addict
Posts: 1788
Joined: Tue Mar 21, 2006 12:31 am
Location: Canada

Re: Evaluate and process math expressions

Post by Thunder93 »

Very useful indeed. Thanks.

Continue the superb work. :P
ʽʽSuccess is almost totally dependent upon drive and persistence. The extra energy required to make another effort or try another approach is the secret of winning.ʾʾ --Dennis Waitley
Dude
Addict
Addict
Posts: 1907
Joined: Mon Feb 16, 2015 2:49 pm

Re: Evaluate and process math expressions

Post by Dude »

Little John wrote:Reserved. :-)
For what?
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

Dude wrote:
Little John wrote:Reserved. :-)
For what?
For something that you'll see in the near future. :-)
User avatar
Thunder93
Addict
Addict
Posts: 1788
Joined: Tue Mar 21, 2006 12:31 am
Location: Canada

Re: Evaluate and process math expressions

Post by Thunder93 »

:wink:
Last edited by Thunder93 on Thu Apr 23, 2015 4:04 pm, edited 1 time in total.
ʽʽSuccess is almost totally dependent upon drive and persistence. The extra energy required to make another effort or try another approach is the secret of winning.ʾʾ --Dennis Waitley
User avatar
Tenaja
Addict
Addict
Posts: 1959
Joined: Tue Nov 09, 2010 10:15 pm

Re: Evaluate and process math expressions

Post by Tenaja »

Neat tool, Little John. Thanks for sharing!
applePi
Addict
Addict
Posts: 1404
Joined: Sun Jun 25, 2006 7:28 pm

Re: Evaluate and process math expressions

Post by applePi »

Little John wrote:
Reserved

so i will keep an eye on this thread, since filling the reserved will not pop up the subject to the front.
thanks Little John
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

No worries. :-)
When I'll fill the reserved posts or change something else significantly, I'll also post a new message here.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

I've updated the code in the first post:
Yesterday I had forgotten to translate some error messages into English. Now fixed.

One "reserved" easter egg :-) is disclosed now:
You'll find code for testing the module in the first post.
Little John
Addict
Addict
Posts: 4775
Joined: Thu Jun 07, 2007 3:25 pm
Location: Berlin, Germany

Re: Evaluate and process math expressions

Post by Little John »

The remaining empty post is now filled with content:
It contains an alternative scanner, which is based on Regular Expressions.

Also, I've added a few more test cases to the fourth post.
Post Reply