Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB10

Evaluate Complex and Real Math Calculator

4.65/5 (15 votes)
22 May 2011Ms-PL5 min read 34.2K   803  
Evaluation of complex and real numbers from string
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 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:

  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:

VB.NET
'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 don't need the "+" operator as a number is positive by default
  3. 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:

  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:

VB.NET
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:

VB.NET
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:

VB.NET
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):

VB.NET
 ' 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 cannot be recognized as a complex or real number.

The actual functions that do the arithmetic operations are written as follows:

VB.NET
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 in that form. And second, we actually have to cast the matched string as a Complex number.

VB.NET
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:

VB.NET
    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:

VB.NET
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.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)