Fixed this now, at least for the mentioned test expression.Just trying this out:
[B4X] Eval (expressions evaluator)
The attached class allows you to evaluate mathematical expressions with support for custom functions. It is compatible with B4A, B4J and B4i. Example: Sub AppStart (Args() As String) Dim e As B4XEval e.Initialize(Me, "Eval") Log(e.Eval("1 + Min(2, Max(-4, 1), 6)"))...www.b4x.com
And trying to make it so that it will handle any expression, so also faulty expressions.
For now I am just testing with this simple but faulty expression:
6/2(2+1)
Added lots of Try Catch and made most return values of type Object (so it can return "error"), but sofar no success
in making Eval return eg "error". I managed to make it return "error" but that was only in debug mode.
Any suggestions how this should be done?
RBS
'version 2.01
#Event: Function(Name As String, Values As List) As Double
Sub Class_Globals
Private Const NUMBER_TYPE = 1, OPERATOR_TYPE = 2 As Int
Type ParsedNode (Operator As String, Left As ParsedNode, Right As ParsedNode, _
NodeType As Int, Value As Double)
Type OrderData (Index As Int, Level As Int, Added As Int)
Private root As ParsedNode
Private ParseIndex As Int
Private Nodes As List
Private OperatorLevel As Map
Public Error As Boolean
Private mCallback As Object
Private mEventName As String
End Sub
Public Sub Initialize (Callback As Object, EventName As String)
mCallback = Callback
mEventName = EventName
OperatorLevel = CreateMap("+": 1, "-": 1, "*":2, "/": 2)
End Sub
Public Sub Eval(Expression As String) As Object
Dim d As Double
Error = False
Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(")
d = EvalHelper(Expression)
If Error Then
Return "error"
Else
Return d
End If
End Sub
Private Sub PrepareExpression(expr As String) As String
If Error Then Return "error"
Dim m As Matcher = Regex.Matcher("(\w*)\(", expr)
Dim sb As StringBuilder
Dim lastMatchEnd As Int = 0
sb.Initialize
Do While m.Find
Dim currentStart As Int = m.GetStart(0)
If currentStart < lastMatchEnd Then Continue
sb.Append(expr.SubString2(lastMatchEnd, currentStart))
Dim functionCall As Boolean
Dim args As List
If m.Match.Length > 1 Then
args.Initialize
functionCall = True
End If
Dim level As Int
Dim start As Int = m.GetEnd(0)
For i = start To expr.Length - 1
If expr.CharAt(i) = "(" Then
level = level + 1
Else if expr.CharAt(i) = "," And level = 0 Then
args.Add(CalcSubExpression(expr.SubString2(start, i)))
start = i + 1
Else if expr.CharAt(i) = ")" Then
level = level - 1
If level = -1 Then
Dim d As Double = CalcSubExpression(expr.SubString2(start, i))
If functionCall Then
args.Add(d)
Try
d = CallSub3(mCallback, mEventName & "_Function", m.Match.SubString2(0, m.Match.Length - 1), args)
Catch
Error = True
Return "error"
End Try
End If
sb.Append(NumberFormat2(d, 1, 15, 0, False))
lastMatchEnd = i + 1
Exit
End If
End If
Next
Loop
If lastMatchEnd < expr.Length Then sb.Append(expr.SubString(lastMatchEnd))
Return sb.ToString
End Sub
Private Sub CalcSubExpression (expr As String) As Double
If Error Then Return 0
Dim be As B4XEval
be.Initialize (mCallback, mEventName)
Dim d As Double = be.EvalHelper(expr)
If be.Error Then
Error = True
Return 0
End If
Return d
End Sub
'only evaluates numbers and operators. No functions or parenthesis here.
Private Sub EvalHelper (expr As String) As Double
If Error Then Return 0
ParseIndex = 0
Dim root As ParsedNode
root.Initialize
expr = PrepareExpression(expr)
'If Error Then Return 0
Dim m As Matcher = Regex.Matcher("[\.\d]+", expr)
Nodes.Initialize
Dim lastIndex As Int = 0
Dim currentOrderData As OrderData
currentOrderData.Initialize
Nodes.Add(CreateOperatorNode("("))
Do While m.Find
Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0))
Dim rawNumber As String = m.Match
If Operator.EndsWith("-") Then
Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1)
If lastNode.Operator = "(" Or Operator.Length > 1 Then
'handle negative numbers: (-2 + 1, 1/-2
Operator = Operator.SubString2(0, Operator.Length - 1)
rawNumber = "-" & rawNumber
End If
End If
lastIndex = m.GetEnd(0)
If Operator.Length > 0 Then
Dim level As Int = OperatorLevel.Get(Operator)
If currentOrderData.Level > 0 Then
If currentOrderData.Level < level Then
Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("("))
currentOrderData.Added = currentOrderData.Added + 1
Else if currentOrderData.Level > level Then
If currentOrderData.Added > 0 Then
Nodes.Add(CreateOperatorNode(")"))
currentOrderData.Added = currentOrderData.Added - 1
End If
End If
End If
currentOrderData.Level = level
currentOrderData.Index = Nodes.Size + 1
Nodes.Add(CreateOperatorNode(Operator))
End If
Dim d As Double = rawNumber
Nodes.Add(CreateNumberNode(d))
Loop
For i = 1 To currentOrderData.Added
Nodes.Add(CreateOperatorNode(")"))
Next
Nodes.Add(CreateOperatorNode(")"))
root = BuildTree
Return EvalNode(root)
End Sub
private Sub BuildTree As ParsedNode
If Error Then Return Null
Dim rt As ParsedNode
Do While ParseIndex < Nodes.Size
Dim pn As ParsedNode = TakeNextNode
Dim built As Boolean
If pn.Operator = ")" Then
Exit
Else If pn.Operator = "(" Then
pn = BuildTree
built = True
End If
If pn.NodeType = NUMBER_TYPE Or built Then
If rt.IsInitialized Then
rt.Right = pn
Else
rt = pn
End If
Else if pn.NodeType = OPERATOR_TYPE Then
pn.Left = rt
rt = pn
End If
Loop
If rt.IsInitialized = False Then rt = pn
Return rt
End Sub
Private Sub EvalNode (pn As ParsedNode) As Double
If Error Then Return 0
If pn.NodeType = NUMBER_TYPE Then Return pn.Value
Dim left As Double = EvalNode(pn.Left)
Dim right As Double = EvalNode(pn.Right)
Select pn.Operator
Case "+"
Return left + right
Case "-"
Return left - right
Case "*"
Return left * right
Case "/"
Return left / right
Case Else
Log("Syntax error: " & pn.Operator)
Error = True
Return "error"
End Select
End Sub
private Sub TakeNextNode As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode = Nodes.Get(ParseIndex)
ParseIndex = ParseIndex + 1
Return pn
End Sub
Private Sub CreateOperatorNode(operator As String) As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode
pn.Initialize
pn.NodeType = OPERATOR_TYPE
pn.Operator = operator
Return pn
End Sub
Private Sub CreateNumberNode (d As Double) As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode
pn.Initialize
pn.NodeType = NUMBER_TYPE
pn.Value = d
Return pn
End Sub
Added some further error handling and I think this may cover all now:Fixed this now, at least for the mentioned test expression.
This is the text of the class B4XEval:
B4X:'version 2.01 #Event: Function(Name As String, Values As List) As Double Sub Class_Globals Private Const NUMBER_TYPE = 1, OPERATOR_TYPE = 2 As Int Type ParsedNode (Operator As String, Left As ParsedNode, Right As ParsedNode, _ NodeType As Int, Value As Double) Type OrderData (Index As Int, Level As Int, Added As Int) Private root As ParsedNode Private ParseIndex As Int Private Nodes As List Private OperatorLevel As Map Public Error As Boolean Private mCallback As Object Private mEventName As String End Sub Public Sub Initialize (Callback As Object, EventName As String) mCallback = Callback mEventName = EventName OperatorLevel = CreateMap("+": 1, "-": 1, "*":2, "/": 2) End Sub Public Sub Eval(Expression As String) As Object Dim d As Double Error = False Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(") d = EvalHelper(Expression) If Error Then Return "error" Else Return d End If End Sub Private Sub PrepareExpression(expr As String) As String If Error Then Return "error" Dim m As Matcher = Regex.Matcher("(\w*)\(", expr) Dim sb As StringBuilder Dim lastMatchEnd As Int = 0 sb.Initialize Do While m.Find Dim currentStart As Int = m.GetStart(0) If currentStart < lastMatchEnd Then Continue sb.Append(expr.SubString2(lastMatchEnd, currentStart)) Dim functionCall As Boolean Dim args As List If m.Match.Length > 1 Then args.Initialize functionCall = True End If Dim level As Int Dim start As Int = m.GetEnd(0) For i = start To expr.Length - 1 If expr.CharAt(i) = "(" Then level = level + 1 Else if expr.CharAt(i) = "," And level = 0 Then args.Add(CalcSubExpression(expr.SubString2(start, i))) start = i + 1 Else if expr.CharAt(i) = ")" Then level = level - 1 If level = -1 Then Dim d As Double = CalcSubExpression(expr.SubString2(start, i)) If functionCall Then args.Add(d) Try d = CallSub3(mCallback, mEventName & "_Function", m.Match.SubString2(0, m.Match.Length - 1), args) Catch Error = True Return "error" End Try End If sb.Append(NumberFormat2(d, 1, 15, 0, False)) lastMatchEnd = i + 1 Exit End If End If Next Loop If lastMatchEnd < expr.Length Then sb.Append(expr.SubString(lastMatchEnd)) Return sb.ToString End Sub Private Sub CalcSubExpression (expr As String) As Double If Error Then Return 0 Dim be As B4XEval be.Initialize (mCallback, mEventName) Dim d As Double = be.EvalHelper(expr) If be.Error Then Error = True Return 0 End If Return d End Sub 'only evaluates numbers and operators. No functions or parenthesis here. Private Sub EvalHelper (expr As String) As Double If Error Then Return 0 ParseIndex = 0 Dim root As ParsedNode root.Initialize expr = PrepareExpression(expr) 'If Error Then Return 0 Dim m As Matcher = Regex.Matcher("[\.\d]+", expr) Nodes.Initialize Dim lastIndex As Int = 0 Dim currentOrderData As OrderData currentOrderData.Initialize Nodes.Add(CreateOperatorNode("(")) Do While m.Find Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0)) Dim rawNumber As String = m.Match If Operator.EndsWith("-") Then Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1) If lastNode.Operator = "(" Or Operator.Length > 1 Then 'handle negative numbers: (-2 + 1, 1/-2 Operator = Operator.SubString2(0, Operator.Length - 1) rawNumber = "-" & rawNumber End If End If lastIndex = m.GetEnd(0) If Operator.Length > 0 Then Dim level As Int = OperatorLevel.Get(Operator) If currentOrderData.Level > 0 Then If currentOrderData.Level < level Then Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("(")) currentOrderData.Added = currentOrderData.Added + 1 Else if currentOrderData.Level > level Then If currentOrderData.Added > 0 Then Nodes.Add(CreateOperatorNode(")")) currentOrderData.Added = currentOrderData.Added - 1 End If End If End If currentOrderData.Level = level currentOrderData.Index = Nodes.Size + 1 Nodes.Add(CreateOperatorNode(Operator)) End If Dim d As Double = rawNumber Nodes.Add(CreateNumberNode(d)) Loop For i = 1 To currentOrderData.Added Nodes.Add(CreateOperatorNode(")")) Next Nodes.Add(CreateOperatorNode(")")) root = BuildTree Return EvalNode(root) End Sub private Sub BuildTree As ParsedNode If Error Then Return Null Dim rt As ParsedNode Do While ParseIndex < Nodes.Size Dim pn As ParsedNode = TakeNextNode Dim built As Boolean If pn.Operator = ")" Then Exit Else If pn.Operator = "(" Then pn = BuildTree built = True End If If pn.NodeType = NUMBER_TYPE Or built Then If rt.IsInitialized Then rt.Right = pn Else rt = pn End If Else if pn.NodeType = OPERATOR_TYPE Then pn.Left = rt rt = pn End If Loop If rt.IsInitialized = False Then rt = pn Return rt End Sub Private Sub EvalNode (pn As ParsedNode) As Double If Error Then Return 0 If pn.NodeType = NUMBER_TYPE Then Return pn.Value Dim left As Double = EvalNode(pn.Left) Dim right As Double = EvalNode(pn.Right) Select pn.Operator Case "+" Return left + right Case "-" Return left - right Case "*" Return left * right Case "/" Return left / right Case Else Log("Syntax error: " & pn.Operator) Error = True Return "error" End Select End Sub private Sub TakeNextNode As ParsedNode If Error Then Return Null Dim pn As ParsedNode = Nodes.Get(ParseIndex) ParseIndex = ParseIndex + 1 Return pn End Sub Private Sub CreateOperatorNode(operator As String) As ParsedNode If Error Then Return Null Dim pn As ParsedNode pn.Initialize pn.NodeType = OPERATOR_TYPE pn.Operator = operator Return pn End Sub Private Sub CreateNumberNode (d As Double) As ParsedNode If Error Then Return Null Dim pn As ParsedNode pn.Initialize pn.NodeType = NUMBER_TYPE pn.Value = d Return pn End Sub
RBS
'version 2.01
#Event: Function(Name As String, Values As List) As Double
Sub Class_Globals
Private Const NUMBER_TYPE = 1, OPERATOR_TYPE = 2 As Int
Type ParsedNode (Operator As String, Left As ParsedNode, Right As ParsedNode, _
NodeType As Int, Value As Double)
Type OrderData (Index As Int, Level As Int, Added As Int)
Private root As ParsedNode
Private ParseIndex As Int
Private Nodes As List
Private OperatorLevel As Map
Public Error As Boolean
Private mCallback As Object
Private mEventName As String
End Sub
Public Sub Initialize (Callback As Object, EventName As String)
mCallback = Callback
mEventName = EventName
OperatorLevel = CreateMap("+": 1, "-": 1, "*":2, "/": 2)
End Sub
Public Sub Eval(Expression As String) As Object
Dim d As Double
Error = False
Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(")
d = EvalHelper(Expression)
If Error Then
Return "error"
Else
Return d
End If
End Sub
Private Sub PrepareExpression(expr As String) As String
If Error Then Return "error"
Dim m As Matcher = Regex.Matcher("(\w*)\(", expr)
Dim sb As StringBuilder
Dim lastMatchEnd As Int = 0
sb.Initialize
Do While m.Find
Dim currentStart As Int = m.GetStart(0)
If currentStart < lastMatchEnd Then Continue
sb.Append(expr.SubString2(lastMatchEnd, currentStart))
Dim functionCall As Boolean
Dim args As List
If m.Match.Length > 1 Then
args.Initialize
functionCall = True
End If
Dim level As Int
Dim start As Int = m.GetEnd(0)
For i = start To expr.Length - 1
If expr.CharAt(i) = "(" Then
level = level + 1
Else if expr.CharAt(i) = "," And level = 0 Then
args.Add(CalcSubExpression(expr.SubString2(start, i)))
start = i + 1
Else if expr.CharAt(i) = ")" Then
level = level - 1
If level = -1 Then
Dim d As Double = CalcSubExpression(expr.SubString2(start, i))
If functionCall Then
args.Add(d)
Try
d = CallSub3(mCallback, mEventName & "_Function", m.Match.SubString2(0, m.Match.Length - 1), args)
Catch
Error = True
Return "error"
End Try
End If
sb.Append(NumberFormat2(d, 1, 15, 0, False))
lastMatchEnd = i + 1
Exit
End If
End If
Next
Loop
If lastMatchEnd < expr.Length Then sb.Append(expr.SubString(lastMatchEnd))
Return sb.ToString
End Sub
Private Sub CalcSubExpression (expr As String) As Double
If Error Then Return 0
Dim be As B4XEval
be.Initialize (mCallback, mEventName)
Dim d As Double = be.EvalHelper(expr)
If be.Error Then
Error = True
Return 0
End If
Return d
End Sub
'only evaluates numbers and operators. No functions or parenthesis here.
Private Sub EvalHelper (expr As String) As Double
If Error Then Return 0
ParseIndex = 0
Dim root As ParsedNode
root.Initialize
expr = PrepareExpression(expr)
If Error Then Return 0
Dim m As Matcher = Regex.Matcher("[\.\d]+", expr)
Nodes.Initialize
Dim lastIndex As Int = 0
Dim currentOrderData As OrderData
currentOrderData.Initialize
Nodes.Add(CreateOperatorNode("("))
Do While m.Find
Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0))
Dim rawNumber As String = m.Match
If Operator.EndsWith("-") Then
Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1)
If lastNode.Operator = "(" Or Operator.Length > 1 Then
'handle negative numbers: (-2 + 1, 1/-2
Operator = Operator.SubString2(0, Operator.Length - 1)
rawNumber = "-" & rawNumber
End If
End If
lastIndex = m.GetEnd(0)
If Operator.Length > 0 Then
Dim level As Int = OperatorLevel.Get(Operator)
If currentOrderData.Level > 0 Then
If currentOrderData.Level < level Then
Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("("))
currentOrderData.Added = currentOrderData.Added + 1
Else if currentOrderData.Level > level Then
If currentOrderData.Added > 0 Then
Nodes.Add(CreateOperatorNode(")"))
currentOrderData.Added = currentOrderData.Added - 1
End If
End If
End If
currentOrderData.Level = level
currentOrderData.Index = Nodes.Size + 1
Nodes.Add(CreateOperatorNode(Operator))
End If
Dim d As Double = rawNumber
Nodes.Add(CreateNumberNode(d))
Loop
For i = 1 To currentOrderData.Added
Nodes.Add(CreateOperatorNode(")"))
Next
Nodes.Add(CreateOperatorNode(")"))
root = BuildTree
Return EvalNode(root)
End Sub
private Sub BuildTree As ParsedNode
If Error Then Return Null
Dim rt As ParsedNode
Do While ParseIndex < Nodes.Size
Dim pn As ParsedNode = TakeNextNode
Dim built As Boolean
If pn.Operator = ")" Then
Exit
Else If pn.Operator = "(" Then
pn = BuildTree
built = True
End If
If pn.NodeType = NUMBER_TYPE Or built Then
If rt.IsInitialized Then
rt.Right = pn
Else
rt = pn
End If
Else if pn.NodeType = OPERATOR_TYPE Then
pn.Left = rt
rt = pn
End If
Loop
If rt.IsInitialized = False Then rt = pn
Return rt
End Sub
Private Sub EvalNode (pn As ParsedNode) As Double
If Error Then Return 0
If pn.IsInitialized = False Then
Error = True
Return 0
End If
If pn.Operator = Null Then
Error = True
Return 0
End If
Try
If pn.NodeType = NUMBER_TYPE Then Return pn.Value
Catch
Error = True
Return 0
End Try
Dim left As Double = EvalNode(pn.Left)
Dim right As Double = EvalNode(pn.Right)
Select pn.Operator
Case "+"
Return left + right
Case "-"
Return left - right
Case "*"
Return left * right
Case "/"
Return left / right
Case Else
Log("Syntax error: " & pn.Operator)
Error = True
Return 0
End Select
End Sub
private Sub TakeNextNode As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode = Nodes.Get(ParseIndex)
ParseIndex = ParseIndex + 1
Return pn
End Sub
Private Sub CreateOperatorNode(operator As String) As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode
pn.Initialize
pn.NodeType = OPERATOR_TYPE
pn.Operator = operator
Return pn
End Sub
Private Sub CreateNumberNode (d As Double) As ParsedNode
If Error Then Return Null
Dim pn As ParsedNode
pn.Initialize
pn.NodeType = NUMBER_TYPE
pn.Value = d
Return pn
End Sub
Not sure why I got the unhandled errors, but simply altering the Eval Sub to thisAdded some further error handling and I think this may cover all now:
B4X:'version 2.01 #Event: Function(Name As String, Values As List) As Double Sub Class_Globals Private Const NUMBER_TYPE = 1, OPERATOR_TYPE = 2 As Int Type ParsedNode (Operator As String, Left As ParsedNode, Right As ParsedNode, _ NodeType As Int, Value As Double) Type OrderData (Index As Int, Level As Int, Added As Int) Private root As ParsedNode Private ParseIndex As Int Private Nodes As List Private OperatorLevel As Map Public Error As Boolean Private mCallback As Object Private mEventName As String End Sub Public Sub Initialize (Callback As Object, EventName As String) mCallback = Callback mEventName = EventName OperatorLevel = CreateMap("+": 1, "-": 1, "*":2, "/": 2) End Sub Public Sub Eval(Expression As String) As Object Dim d As Double Error = False Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(") d = EvalHelper(Expression) If Error Then Return "error" Else Return d End If End Sub Private Sub PrepareExpression(expr As String) As String If Error Then Return "error" Dim m As Matcher = Regex.Matcher("(\w*)\(", expr) Dim sb As StringBuilder Dim lastMatchEnd As Int = 0 sb.Initialize Do While m.Find Dim currentStart As Int = m.GetStart(0) If currentStart < lastMatchEnd Then Continue sb.Append(expr.SubString2(lastMatchEnd, currentStart)) Dim functionCall As Boolean Dim args As List If m.Match.Length > 1 Then args.Initialize functionCall = True End If Dim level As Int Dim start As Int = m.GetEnd(0) For i = start To expr.Length - 1 If expr.CharAt(i) = "(" Then level = level + 1 Else if expr.CharAt(i) = "," And level = 0 Then args.Add(CalcSubExpression(expr.SubString2(start, i))) start = i + 1 Else if expr.CharAt(i) = ")" Then level = level - 1 If level = -1 Then Dim d As Double = CalcSubExpression(expr.SubString2(start, i)) If functionCall Then args.Add(d) Try d = CallSub3(mCallback, mEventName & "_Function", m.Match.SubString2(0, m.Match.Length - 1), args) Catch Error = True Return "error" End Try End If sb.Append(NumberFormat2(d, 1, 15, 0, False)) lastMatchEnd = i + 1 Exit End If End If Next Loop If lastMatchEnd < expr.Length Then sb.Append(expr.SubString(lastMatchEnd)) Return sb.ToString End Sub Private Sub CalcSubExpression (expr As String) As Double If Error Then Return 0 Dim be As B4XEval be.Initialize (mCallback, mEventName) Dim d As Double = be.EvalHelper(expr) If be.Error Then Error = True Return 0 End If Return d End Sub 'only evaluates numbers and operators. No functions or parenthesis here. Private Sub EvalHelper (expr As String) As Double If Error Then Return 0 ParseIndex = 0 Dim root As ParsedNode root.Initialize expr = PrepareExpression(expr) If Error Then Return 0 Dim m As Matcher = Regex.Matcher("[\.\d]+", expr) Nodes.Initialize Dim lastIndex As Int = 0 Dim currentOrderData As OrderData currentOrderData.Initialize Nodes.Add(CreateOperatorNode("(")) Do While m.Find Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0)) Dim rawNumber As String = m.Match If Operator.EndsWith("-") Then Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1) If lastNode.Operator = "(" Or Operator.Length > 1 Then 'handle negative numbers: (-2 + 1, 1/-2 Operator = Operator.SubString2(0, Operator.Length - 1) rawNumber = "-" & rawNumber End If End If lastIndex = m.GetEnd(0) If Operator.Length > 0 Then Dim level As Int = OperatorLevel.Get(Operator) If currentOrderData.Level > 0 Then If currentOrderData.Level < level Then Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("(")) currentOrderData.Added = currentOrderData.Added + 1 Else if currentOrderData.Level > level Then If currentOrderData.Added > 0 Then Nodes.Add(CreateOperatorNode(")")) currentOrderData.Added = currentOrderData.Added - 1 End If End If End If currentOrderData.Level = level currentOrderData.Index = Nodes.Size + 1 Nodes.Add(CreateOperatorNode(Operator)) End If Dim d As Double = rawNumber Nodes.Add(CreateNumberNode(d)) Loop For i = 1 To currentOrderData.Added Nodes.Add(CreateOperatorNode(")")) Next Nodes.Add(CreateOperatorNode(")")) root = BuildTree Return EvalNode(root) End Sub private Sub BuildTree As ParsedNode If Error Then Return Null Dim rt As ParsedNode Do While ParseIndex < Nodes.Size Dim pn As ParsedNode = TakeNextNode Dim built As Boolean If pn.Operator = ")" Then Exit Else If pn.Operator = "(" Then pn = BuildTree built = True End If If pn.NodeType = NUMBER_TYPE Or built Then If rt.IsInitialized Then rt.Right = pn Else rt = pn End If Else if pn.NodeType = OPERATOR_TYPE Then pn.Left = rt rt = pn End If Loop If rt.IsInitialized = False Then rt = pn Return rt End Sub Private Sub EvalNode (pn As ParsedNode) As Double If Error Then Return 0 If pn.IsInitialized = False Then Error = True Return 0 End If If pn.Operator = Null Then Error = True Return 0 End If Try If pn.NodeType = NUMBER_TYPE Then Return pn.Value Catch Error = True Return 0 End Try Dim left As Double = EvalNode(pn.Left) Dim right As Double = EvalNode(pn.Right) Select pn.Operator Case "+" Return left + right Case "-" Return left - right Case "*" Return left * right Case "/" Return left / right Case Else Log("Syntax error: " & pn.Operator) Error = True Return 0 End Select End Sub private Sub TakeNextNode As ParsedNode If Error Then Return Null Dim pn As ParsedNode = Nodes.Get(ParseIndex) ParseIndex = ParseIndex + 1 Return pn End Sub Private Sub CreateOperatorNode(operator As String) As ParsedNode If Error Then Return Null Dim pn As ParsedNode pn.Initialize pn.NodeType = OPERATOR_TYPE pn.Operator = operator Return pn End Sub Private Sub CreateNumberNode (d As Double) As ParsedNode If Error Then Return Null Dim pn As ParsedNode pn.Initialize pn.NodeType = NUMBER_TYPE pn.Value = d Return pn End Sub
RBS
' Type EvalReturn(dValue As Double, _
' bError As Boolean)
Public Sub Eval(Expression As String) As EvalReturn
Dim d As Double
Dim tET As EvalReturn
Error = False
Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(")
d = EvalHelper(Expression)
tET.dValue = d 'will be 0 if error, could return something like 123456 instead to indicate error and avoid the type return
tET.bError = Error
Return tET
End Sub
There needs to one more change for in case the expression has no numbers, brackets or operators:Not sure why I got the unhandled errors, but simply altering the Eval Sub to this
(and nil else to be altered from Erel's original code) will sort it all out:
B4X:' Type EvalReturn(dValue As Double, _ ' bError As Boolean) Public Sub Eval(Expression As String) As EvalReturn Dim d As Double Dim tET As EvalReturn Error = False Expression = Expression.Replace(" ", "").ToLowerCase.Replace("-(", "-1*(") d = EvalHelper(Expression) tET.dValue = d 'will be 0 if error, could return something like 123456 instead to indicate error and avoid the type return tET.bError = Error Return tET End Sub
RBS
Private Sub EvalNode (pn As ParsedNode) As Double
If pn.IsInitialized = False Then
Error = True
Return 0
End If
If pn.NodeType = NUMBER_TYPE Then Return pn.Value
Dim left As Double = EvalNode(pn.Left)
Dim right As Double = EvalNode(pn.Right)
Select pn.Operator
Case "+"
Return left + right
Case "-"
Return left - right
Case "*"
Return left * right
Case "/"
Return left / right
Case Else
Log("Syntax error: " & pn.Operator)
Error = True
Return 0
End Select
End Sub
One more alteration needed for in case the provided expression contained only dots:There needs to one more change for in case the expression has no numbers, brackets or operators:
B4X:Private Sub EvalNode (pn As ParsedNode) As Double If pn.IsInitialized = False Then Error = True Return 0 End If If pn.NodeType = NUMBER_TYPE Then Return pn.Value Dim left As Double = EvalNode(pn.Left) Dim right As Double = EvalNode(pn.Right) Select pn.Operator Case "+" Return left + right Case "-" Return left - right Case "*" Return left * right Case "/" Return left / right Case Else Log("Syntax error: " & pn.Operator) Error = True Return 0 End Select End Sub
RBS
'only evaluates numbers and operators. No functions or parenthesis here.
Private Sub EvalHelper (expr As String) As Double
'Log("Expr: " & expr)
ParseIndex = 0
Dim root As ParsedNode
root.Initialize
expr = PrepareExpression(expr)
If Error Then Return 0
Dim m As Matcher = Regex.Matcher("[\.\d]+", expr)
Nodes.Initialize
Dim lastIndex As Int = 0
Dim currentOrderData As OrderData
currentOrderData.Initialize
Nodes.Add(CreateOperatorNode("("))
Do While m.Find
Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0))
Dim rawNumber As String = m.Match
If Operator.EndsWith("-") Then
Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1)
If lastNode.Operator = "(" Or Operator.Length > 1 Then
'handle negative numbers: (-2 + 1, 1/-2
Operator = Operator.SubString2(0, Operator.Length - 1)
rawNumber = "-" & rawNumber
End If
End If
lastIndex = m.GetEnd(0)
If Operator.Length > 0 Then
Dim level As Int = OperatorLevel.Get(Operator)
If currentOrderData.Level > 0 Then
If currentOrderData.Level < level Then
Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("("))
currentOrderData.Added = currentOrderData.Added + 1
Else if currentOrderData.Level > level Then
If currentOrderData.Added > 0 Then
Nodes.Add(CreateOperatorNode(")"))
currentOrderData.Added = currentOrderData.Added - 1
End If
End If
End If
currentOrderData.Level = level
currentOrderData.Index = Nodes.Size + 1
Nodes.Add(CreateOperatorNode(Operator))
End If
'Log("rawNumber: " & rawNumber)
'otherwise there will be an unhandled error if provided expression has dots only
If StringHasCharXOnly(rawNumber, Asc(".")) Then
Error = True
Return 0
End If
Dim d As Double = rawNumber
Nodes.Add(CreateNumberNode(d))
Loop
For i = 1 To currentOrderData.Added
Nodes.Add(CreateOperatorNode(")"))
Next
Nodes.Add(CreateOperatorNode(")"))
root = BuildTree
Return EvalNode(root)
End Sub
Sub StringHasCharXOnly(strString As String, iByte As Int) As Boolean
Dim i As Int
Dim arrBytes() As Byte
arrBytes = strString.GetBytes("UTF8")
For i = 0 To arrBytes.Length - 1
If arrBytes(i) <> iByte Then
Return False
End If
Next
Return True
End Sub
Found some other expression strings that would cause an unhandled error and fixed these as well.One more alteration needed for in case the provided expression contained only dots:
B4X:'only evaluates numbers and operators. No functions or parenthesis here. Private Sub EvalHelper (expr As String) As Double 'Log("Expr: " & expr) ParseIndex = 0 Dim root As ParsedNode root.Initialize expr = PrepareExpression(expr) If Error Then Return 0 Dim m As Matcher = Regex.Matcher("[\.\d]+", expr) Nodes.Initialize Dim lastIndex As Int = 0 Dim currentOrderData As OrderData currentOrderData.Initialize Nodes.Add(CreateOperatorNode("(")) Do While m.Find Dim Operator As String = expr.SubString2(lastIndex, m.GetStart(0)) Dim rawNumber As String = m.Match If Operator.EndsWith("-") Then Dim lastNode As ParsedNode = Nodes.Get(Nodes.Size - 1) If lastNode.Operator = "(" Or Operator.Length > 1 Then 'handle negative numbers: (-2 + 1, 1/-2 Operator = Operator.SubString2(0, Operator.Length - 1) rawNumber = "-" & rawNumber End If End If lastIndex = m.GetEnd(0) If Operator.Length > 0 Then Dim level As Int = OperatorLevel.Get(Operator) If currentOrderData.Level > 0 Then If currentOrderData.Level < level Then Nodes.InsertAt(currentOrderData.Index, CreateOperatorNode("(")) currentOrderData.Added = currentOrderData.Added + 1 Else if currentOrderData.Level > level Then If currentOrderData.Added > 0 Then Nodes.Add(CreateOperatorNode(")")) currentOrderData.Added = currentOrderData.Added - 1 End If End If End If currentOrderData.Level = level currentOrderData.Index = Nodes.Size + 1 Nodes.Add(CreateOperatorNode(Operator)) End If 'Log("rawNumber: " & rawNumber) 'otherwise there will be an unhandled error if provided expression has dots only If StringHasCharXOnly(rawNumber, Asc(".")) Then Error = True Return 0 End If Dim d As Double = rawNumber Nodes.Add(CreateNumberNode(d)) Loop For i = 1 To currentOrderData.Added Nodes.Add(CreateOperatorNode(")")) Next Nodes.Add(CreateOperatorNode(")")) root = BuildTree Return EvalNode(root) End Sub Sub StringHasCharXOnly(strString As String, iByte As Int) As Boolean Dim i As Int Dim arrBytes() As Byte arrBytes = strString.GetBytes("UTF8") For i = 0 To arrBytes.Length - 1 If arrBytes(i) <> iByte Then Return False End If Next Return True End Sub
RBS
OK, will do.It might be useful to post this in the Libraries & Classes forum, with a usage example perhaps (given that your version is returning a custom type rather than a Double)
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?