ComplexEvaluator.png

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 then 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:

  1. a+bi Containing both a real part and an imaginary part
  2. a Only a real part
  3. 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:
1. 0.00000000003567
2. 489000i
3. -0.1

The general expression for a number that can either be a real or Imaginary can be written as follows:

'RegEx for Real and Complex numbers
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:

  1. A mathematical operator (+*/^) will preside and operators (-+*/^) follow the actual number.
  2. A number can start with the operator "–" (we dont need the "+" operator as a number is positive by default
  3. It is not a standalone number if it is presided with the letter E or if it 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 presided 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 comes in pairs of a 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:

  1. a+bi, called Both
  2. bi+a, called Both
  3. a, called Real
  4. 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 is 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)"
' List of 2-operand functions.
Const Func2 As String = "(atan2)"
' List of N-operand functions.
Const FuncN As String = "(min|max)"

' List of predefined constants.
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*\+")

' This Regex object converts a double minus into a plus.
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.

  1. ( ) 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.
  2. Replace all constants in the input.
  3. ^ If you have a match for "(" Complex number ")" ^ "(" Complex number ")" perform this task.
  4. * and / Do * or / if you find "(" Complex number ")" ( * or /) "(" Complex number ")".
  5. + 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):

 ' The Regex object deals with constants. (Requires case insensitivity.)
Dim reConst As New Regex("\s*(?<Const>" & Constants & ")\s*")
' This resolves predefined constants. (Can be kept out of the loop.)
Input = reConst.Replace(Input, AddressOf DoConstants)

The actual calculation should be preformed as long as the input string cannott be recognized as a complex or real number.

The actual functions that do the arithmetic operations are written as the 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
    ' function argument is 2nd group.
    Dim n1 As New Complex
    n1 = GenerateComplexNumberFromString(m.Groups("No1").Value)
    ' function name is 1st group.
    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
    ' function arguments are from group 2 onward.
    Dim args As String() '
    Dim args2 As New ArrayList
    Dim i As Integer = 2
    ' Load all the arguments into the array.

    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

    'I cant sort complex numbers, you have a go ;)
    ' function name is 1st group.
    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)) 'args(args.Count - 1).ToString
        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 on 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)
            ' Check if the format string has a precision specifier.
            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 original 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 the permission to publish the modified source code from the book.

The balanced group regex is from Morten Holk Maate.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"