Introduction
As of .NET 4.0, the native library does offer a complex class in the namespace System.Numerics. The ideal situation had been to write the formulas in a way that they are written in for instance Matlab or Matematica.
The solution I have tried is to use Regular Expressions, and write the formula as a simple string
, convert the string
to a series of complex numbers, perform calculations and give out the calculated result.
The evaluator is more general than to just do calculations on complex numbers however. It can also function as a normal calculator with just real numbers.
RegEx Pattern for Complex and Real Numbers
The pattern to recognize the use input is always on the form a+bi and is defined as a complex pattern which we need to recognize. There are basically three main ways to write a complex number:
- a+bi Containing both a real part and an imaginary part
- a Only a real part
- bi Only a real imaginary
Both a and b are real numbers (in native .NET, they can have the form of Double, Decimal or Integer). The Regular Expression for a double number can be found on the web, but they rarely show a general RegEx for finding a general computer number in succession, separated by mathematical operators. A simple example of an input gives an idea of what is needed in recognizing the main pattern of numbers of computer format:
3.567E-10+4.89E+5i-.1
The correct interpretation of this input should be:
- 0.00000000003567
- 489000i
- -0.1
The general expression for a number that can either be a real or Imaginary can be written as follows:
Dim Real As String = "(?<!([E][+-][0-9]+))([-]?\d+\.?\d*([E][+-]" & _
"[0-9]+)?(?!([i0-9.E]))|[-]?\d*\.?\d+([E][+-][0-9]+)?)(?![i0-9.E])"
Dim Img As String = "(?<!([E][+-][0-9]+))([-]?\d+\.?\d*([E][+-]" & _
"[0-9]+)?(?![0-9.E])(?:i)|([-]?\d*\.?\d+)?([E][+-][0-9]+)?\s*(?:i)(?![0-9.E]))"
The two regular expressions can be broken down with the following assumptions:
- A mathematical operator (+*/^) will preside and operators (-+*/^) follow the actual number.
- A number can start with the operator "–" (we don't need the "+" operator as a number is positive by default
- It is not a standalone number if it is preceded with the letter E or if it is immediately followed by E.
To separate the Real and imaginary numbers is just a small difference at the end. It is Real if the number is not preceded with the letter “i” and imaginary if it is. All the other code is taken in to force the regular expression to include the full number.
Complex numbers on the other hand usually come in pairs of real and imaginary, so we need to write a RegEx that understands this, and only parses the number as a standalone real or imaginary if it can’t find the pairs. The match will occur in the following pair with matches would be returned in this order:
- a+bi, called Both
- bi+a, called Both
- a, called Real
- bi, called Imag
The regular expression for defining a number type is given below:
Dim NumType As String = "((?<Both>((" & Real & "\s*([+])*\s*" & Img & _
")|(" & Img & "\s*([+])*\s*" & Real & ")))|(?<Real>(" & _
Real & "))|(?<Imag>(" & Img & ")))"
Evaluator
The original evaluator is written by Francesco Balena in the book “Programming Microsoft Visual Basic .NET” (2003) Pages 505 – 509. It was basically a calculator that dealt with real numbers, and this code is altered to take complex numbers. The architecture is basically the same.
We begin with defining the different numbers that we will encounter:
Dim NumType As String = "((?<Both>((" & Real & "\s*([+])*\s*" & Img & _
")|(" & Img & "\s*([+])*\s*" & Real & ")))|(?<Real>(" & _
Real & "))|(?<Imag>(" & Img & ")))"
Dim NumTypeSingle As String = "((?<Real>(" & Real & "))|(?<Imag>(" & Img & ")))"
There are two different kinds, as we might encounter numbers that are written in the following way: 5+8i^2. This means that the NumType
will read it as (5+8i)^2 with is wrong, therefore the need for NumTypeSingle
.
Next, we define all the functions and operators that we will support with this evaluator:
Const Func1 As String = "(exp|log|log10|abs|sqr|sqrt|sin|cos|tan|asin|acos|atan)"
Const Func2 As String = "(atan2)"
Const FuncN As String = "(min|max)"
Const Constants As String = "(e|pi)"
Dim rePower As New Regex("\(?(?<No1>" & NumType & ")\)?" & _
"\s*(?<Operator>(\^))\s*\(?(?<No2>" & NumType & ")\)?")
Dim rePower2 As New Regex("\(?(?<No1>" & NumType & ")\)?" & _
"\s*(?<Operator>(\^))\s*(?<No2>" & NumTypeSingle & ")")
Dim rePowerSingle As New Regex("(?<No1>" & NumTypeSingle & ")" & _
"\s*(?<Operator>(\^))\s*(?<No2>" & NumTypeSingle & ")")
Dim rePowerSingle2 As New Regex("(?<No1>" & NumTypeSingle & ")" & _
"\s*(?<Operator>(\^))\s*\(?(?<No2>" & NumType & ")\)?")
Dim reMulDiv As New Regex("\(?\s*(?<No1>" & NumType & ")\)?" & _
"\s*(?<Operator>([*/]))\s*\(?(?<No2>" & NumType & ")\s*\)?\)?")
Dim reMulDiv2 As New Regex("\(?\s*(?<No1>" & NumType & ")\)?" & _
"\s*(?<Operator>([*/]))\s*(?<No2>" & NumTypeSingle & ")")
Dim reMulDivSingle As New Regex("\(?\s*(?<No1>" & NumTypeSingle & ")" & _
"\s*(?<Operator>([*/]))\s*(?<No2>" & NumTypeSingle & ")\s*\)?\)?")
Dim reMulDivSingle2 As New Regex("\(?\s*(?<No1>" & NumTypeSingle & ")" & _
"\s*(?<Operator>([*/]))\s*\(?(?<No2>" & NumType & ")\s*\)?")
Dim reAddSub As New Regex("\(?(?<No1>" & NumType & ")\)?" & -
"\s*(?<Operator>([-+]))\s*\(?(?<No2>" & NumType & ")\)?")
Dim reFunc1 As New Regex("\s*(?<Function>" & Func1 & ")\(?\s*" & _
"(?<No1>" & NumType & ")" & "\s*\)?", RegexOptions.IgnoreCase)
Dim reFunc2 As New Regex("\s*(?<Function>" & Func2 & ")\(\s*" & "(?<No1>" & _
NumType & ")" & "\s*,\s*" & "(?<No2>" & _
NumType & ")" & "\s*\)", RegexOptions.IgnoreCase)
Dim reFuncN As New Regex("\s*(?<Function>" & FuncN & ")\((?<Numbers>(\s*" & _
NumType & "\s*,)+\s*" & NumType & ")\s*\)", RegexOptions.IgnoreCase)
Dim reSign1 As New Regex("([-+/*^])\s*\+")
Dim reSign2 As New Regex("\-\s*\-")
In a normal calculation with numbers, the Operators *,/,+,- ,( )and ^ have to be given different priorities in order to function properly.
- ( ) This means calculate everything inside the () first, and when it can be defined as a "(" , complex number and ")", then move on. We will leave this out of the main evaluator for now.
- Replace all constants in the input.
- ^ If you have a match for "(" Complex number ")" ^ "(" Complex number ")", perform this task.
- * and / Do * or / if you find "(" Complex number ")" ( * or /) "(" Complex number ")".
- + and - Do + or - if you find "(" Complex number ")" ( + or -) "(" Complex number ")".
First, we replace all the constants with the actual numerical value (this only supports e and pi as the code is written):
Dim reConst As New Regex("\s*(?<Const>" & Constants & ")\s*")
Input = reConst.Replace(Input, AddressOf DoConstants)
The actual calculation should be preformed as long as the input string cannot be recognized as a complex or real number.
The actual functions that do the arithmetic operations are written as follows:
Function DoAddSub(ByVal m As Match) As String
Dim n1, n2 As New Complex()
n1 = GenerateComplexNumberFromString(m.Groups("No1").Value)
n2 = GenerateComplexNumberFromString(m.Groups("No2").Value)
Select Case m.Groups("Operator").Value
Case "+"
Dim f As New Complex
f = n1 + n2
Return String.Format(New ComplexFormatter(), "{0:I0}", f)
Case "-"
Dim f As New Complex
f = n1 - n2
Return String.Format(New ComplexFormatter(), "{0:I0}", f)
Case Else
Return 1
End Select
End Function
Function DoMulDiv(ByVal m As Match) As String
Dim n1, n2 As New Complex()
n1 = GenerateComplexNumberFromString(m.Groups("No1").Value)
n2 = GenerateComplexNumberFromString(m.Groups("No2").Value)
Select Case m.Groups("Operator").Value
Case "/"
Return String.Format(New ComplexFormatter(), "{0:I0}", (n1 / n2))
Case "*"
Return String.Format(New ComplexFormatter(), "{0:I0}", (n1 * n2))
Case Else
Return 1
End Select
End Function
Function DoPower(ByVal m As Match) As String
Dim n1, n2, n3 As New Complex()
n1 = GenerateComplexNumberFromString(m.Groups("No1").Value)
n2 = GenerateComplexNumberFromString(m.Groups("No2").Value)
n3 = Complex.Pow(n1, n2)
Dim s As String = String.Format(New ComplexFormatter(), "{0:I0}", n3)
Return "(" & s & ")"
End Function
Function DoFunc1(ByVal m As Match) As String
Dim n1 As New Complex
n1 = GenerateComplexNumberFromString(m.Groups("No1").Value)
Select Case m.Groups("Function").Value.ToUpper
Case "EXP"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Exp(n1))
Case "LOG"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Log(n1))
Case "LOG10"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Log10(n1))
Case "ABS"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Abs(n1))
Case "SQR", "SQRT"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Sqrt(n1))
Case "SIN"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Sin(n1))
Case "COS"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Cos(n1))
Case "TAN"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Tan(n1))
Case "ASIN"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Asin(n1))
Case "ACOS"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Acos(n1))
Case "ATAN"
Return String.Format(New ComplexFormatter(), "{0:I0}", Complex.Atan(n1))
Case Else
Return 1
End Select
End Function
Function DoFuncN(ByVal m As Match) As String
Dim args As String()
Dim args2 As New ArrayList
Dim i As Integer = 2
For Each h As Capture In m.Groups("Numbers").Captures
args = h.ToString.Split(",")
Next
For Each Str As String In args
args2.Add(GenerateComplexNumberFromString(Str.Replace(","c, " "c)))
Next
Select Case m.Groups("Function").Value.ToUpper
Case "MIN"
args2.Sort()
Return String.Format(New ComplexFormatter(), "{0:I0}", args(0))
Case "MAX"
args2.Sort()
Return String.Format(New ComplexFormatter(), "{0:I0}", _
args(args.Count - 1))
Case Else
Return 1
End Select
End Function
There are two things in the code that I did not mention yet. We need to cast the string
to an actual complex number, and by default the System.Numerics.Complex.ToString
returns (Real,Imaginary), and we don’t want it in that form. And second, we actually have to cast the matched string
as a Complex number.
Private Function GenerateComplexNumberFromString(ByVal input As String) As Complex
input = input.Replace(" ", "")
Dim Number As String = "((?<Real>(" & Real & "))|(?<Imag>(" & Img & ")))"
Dim Re, Im As Double
Re = 0
Im = 0
For Each Match As Match In Regex.Matches(input, Number)
If Not Match.Groups("Real").Value = String.Empty Then
Re = Double.Parse(Match.Groups("Real").Value, CultureInfo.InvariantCulture)
End If
If Not Match.Groups("Imag").Value = String.Empty Then
If Match.Groups("Imag").Value.ToString.Replace(" ", "") = "-i" Then
Im = Double.Parse("-1", CultureInfo.InvariantCulture)
ElseIf Match.Groups("Imag").Value.ToString.Replace(" ", "") = "i" Then
Im = Double.Parse("1", CultureInfo.InvariantCulture)
Else
Im = Double.Parse(Match.Groups("Imag").Value.ToString.Replace("i", ""), _
CultureInfo.InvariantCulture)
End If
End If
Next
Dim result As New Complex(Re, Im)
Return result
End Function
The default complex ToString
is overwritten, after an example from the Microsoft documentation:
Public Function Format(ByVal fmt As String, ByVal arg As Object,
ByVal provider As IFormatProvider) As String _
Implements ICustomFormatter.Format
If TypeOf arg Is Complex Then
Dim c1 As Complex = DirectCast(arg, Complex)
Dim precision As Integer
Dim fmtString As String = String.Empty
If fmt.Length > 1 Then
Try
precision = Int32.Parse(fmt.Substring(1))
Catch e As FormatException
precision = 0
End Try
fmtString = "N" + precision.ToString()
End If
If fmt.Substring(0, 1).Equals("I", StringComparison.OrdinalIgnoreCase) Then
Dim s As String = ""
If c1.Imaginary = 0 And c1.Real = 0 Then
s = "0"
ElseIf c1.Imaginary = 0 Then
s = c1.Real.ToString("r")
ElseIf c1.Real = 0 Then
s = c1.Imaginary.ToString("r") & "i"
Else
If c1.Imaginary >= 0 Then
s = [String].Format("{0}+{1}i", _
c1.Real.ToString("r"), _
c1.Imaginary.ToString("r"))
Else
s = [String].Format("{0}-{1}i", _
c1.Real.ToString("r"), _
Math.Abs(c1.Imaginary).ToString("r"))
End If
End If
Return s.Replace(",", ".")
ElseIf fmt.Substring(0, 1).Equals("J", _
StringComparison.OrdinalIgnoreCase) Then
Return c1.Real.ToString(fmtString) + " + " + _
c1.Imaginary.ToString(fmtString) + "j"
Else
Return c1.ToString(fmt, provider)
End If
Else
If TypeOf arg Is IFormattable Then
Return DirectCast(arg, IFormattable).ToString(fmt, provider)
ElseIf arg IsNot Nothing Then
Return arg.ToString()
Else
Return String.Empty
End If
End If
End Function
End Class
Bracket Evaluation
Evaluation should be done by calculating the inner most brackets first, replace the brackets with the evaluated result, and then evaluate the next bracket:
Function EvaluateBrackets(ByVal input As String) As String
input = "(" & input & ")"
Dim pattern As String = "(?>\( (?<LEVEL>)(?<CURRENT>)| (?=\))(?" & _
"<LAST-CURRENT>)(?(?<=\(\k<LAST>)(?<-LEVEL> \)))|\[ (?<LEVEL>)(?" & _
"<CURRENT>)|(?=\])(?<LAST-CURRENT>)(?(?<=\[\k<LAST>)" & _
"(?<-LEVEL> \] ))|[^()\[\]]*)+(?(LEVEL)(?!))"
Dim MAtchBracets As MatchCollection = _
Regex.Matches(input, pattern, RegexOptions.IgnorePatternWhitespace)
Dim captures As CaptureCollection = MAtchBracets(0).Groups("LAST").Captures
Dim ListOfPara As New List(Of String)
For Each c As Capture In captures
ListOfPara.Add(c.Value)
Next
Dim result As String = input
Dim CalcList As New List(Of String)
For i As Integer = 0 To ListOfPara.Count - 1
If i = 0 Then
CalcList.Add(Evaluate(ListOfPara(i)))
result = CalcList(i)
Else
For j As Integer = i To ListOfPara.Count - 1
ListOfPara(j) = ListOfPara(j).Replace(ListOfPara(i - 1), _
CalcList(i - 1)).Replace(" ", "")
Next
result = Evaluate(ListOfPara(i)).Replace(" ", "")
CalcList.Add(result)
End If
Next
result = Evaluate(ListOfPara(ListOfPara.Count - 1))
Return result
End Function
The Regular Expression is originally written by Morten Holk Maate, and it is an example of Balanced grouping, one of the more difficult aspects in RegEx.
History
This evaluator is basically a modified version of the real number evaluator in: Programming Microsoft Visual Basic .NET (2003) - Francesco Balena (pages 505 - 509.)
With thanks to the publishers for permission to publish the modified source code from the book.
The balanced group regex is from Morten Holk Maate.