Valutare una espressione algebrica passo dopo passo
a cura di Diego Cattaruzza (requisiti: conoscenza di base di .Net)

Premessa
Alcuni giorni fa, un membro della nostra Community ha proposto una Tip (attraverso il link posto in area Tips), che era, più che un pezzo di codice per svolgere una particolare e ristretta funzione, una procedura piuttosto complessa per eseguire un compito particolare: la valutazione di una espressione attraverso una progressiva risoluzione.

L'autore, che desidera restare anonimo e quindi lo chiamerò solo Anonimo, si proponeva di fornire al nipote un programma che illustrasse la risoluzione di una espressione, passaggio dopo passaggio.
Non era una vera e propria Tip, quindi, però il codice era davvero interessante e ho chiesto ad Anonimo di farci sopra un articolo di spiegazione, o di permettere a me di farlo.
Gli ho chiesto anche di modificare il codice rendendolo più moderno, ma non ha avuto tempo di dedicarcisi e quindi l'ho fatto io.

In questo articolo, mi propongo di illustrare il passaggio dal codice di Anonimo alla mia versione.

Analisi del codice e prime modifiche
La prima cosa che salta agli occhi, esaminando il codice, è la serie di etichette cui puntano vari GoTo.
E' brutta programmazione. Da evitare. Come? Vedremo nel seguito, perché per farlo bisogna comprendere bene la logica dell'algoritmo.
Un GoTo porta il controllo di flusso in un punto diverso del codice; se questo punto è successivo alla posizione del GoTo, il GoTo è di solito sostituibile da una If; se invece il punto è precedente la posizione del GoTo, il GoTo è si solito sostituibile da un ciclo. Inoltre, una procedura di questo tipo è tipicamente richiamabile da se stessa (cioè si presta alla cosiddetta chiamata ricorsiva)

La seconda cosa evidente è l'abuso poco funzionale della manipolazione della stringa di log. E' una procedura pressocché inevitabile in VB6, ma VB.Net fornisce (proprio per questo scopo) l'oggetto StringBuilder. Naturalmente, Anonimo stava programmando in VB2005 da neanche un mese, e non era al corrente di questa possibilità.
Ho quindi aggiunto una variabile di questo tipo a livello di modulo (LogStringBuilder) ed ho sostituito la variabile pubblica GetValueLogResolveExp con una proprietà a sola lettura ExpressionResolvingLog che restituisce il contenuto dello StringBuilder.
Naturalmente, questo oggetto viene istanziato dal costruttore della classe. En passant, ho anche cambiato il nome della classe.

Imports System.Text

Public Class StringEvaluator
  Implements IDisposable

  Private LogStringBuilder As StringBuilder

  Public Sub New()
    LogStringBuilder = New StringBuilder
  End Sub

  Public ReadOnly Property ExpressionResolvingLog() As String
    Get
      Return LogStringBuilder.ToString
    End Get
  End Property

Ho quindi proceduto a sostituire tutte le concatenazioni a GetValueLogResolveExp con l'uso dei metodi Append o AppendLine (a seconda della necessità di andare a capo oppure no) dello StringBuilder. Qui presento solo il primo esempio:

      LogStringBuilder.AppendLine("Risoluzione espressione: " & expressionToEval)
      LogStringBuilder.AppendLine( _
                "________________________________________________________________")
      LogStringBuilder.AppendLine()
      LogStringBuilder.AppendLine()

Ricominciando l'analisi del codice, mi sono imbattuto nella istruzione On Error Goto: l'ho sostituita con il costrutto Try-Catch implementando la cattura di un tipo specifico di Exception, ArgumentException, in base al significato del messaggio indicato.
Nei vari punti in cui ho cercato di prevenire gli errori, ho lanciato una ArgumentException, fornendo il messaggio più adatto.

    Throw New ArgumentException( _
          "Immettere un'espressione! Impossibile eseguire il metodo!")
    Throw New ArgumentException("Le parentesi non sono correttamente inserite!")
    Throw New ArgumentException("Ci sono più di un segno =!")
    Throw New ArgumentException( _
          "Risoluzione impossibile: numero negativo sotto radice " & n)

Queste eccezioni, poiché vengono sollevate da codice interno a una classe da istanziare, devono essere rigirate al chiamante, quindi la loro gestione diventa:

    Catch e As ArgumentException
      Throw New ArgumentException(e.Message)
    Catch e As DivideByZeroException
      Throw New DivideByZeroException("Errore di divisione per zero! Impossibile continuare")
    Catch e As OverflowException
      Throw New OverflowException("Errore di numero troppo grande! Impossibile continuare")
    Catch ex As Exception
      Throw New Exception(ex.Message, ex.InnerException)
    End Try

A questo punto, ho analizzato il problema delle parentesi. In effetti, c'è una mancanza: se l'utente digita una parentesi tonda prima di una grafa (il che è lecito) si ha un errore. Secondo la logica imparata a scuola, invece, si dovrebbe risolvere (prima del resto) le parti di espressione racchiuse tra le parentesi più interne.
Inoltre, il compito del metodo (rinominato DidacticEval) è risolvere, in ultima analisi, ogni espressione priva di parentesi. Questa considerazione porta a decidere di usare il metodo ricorsivamente. Infatti, risolvere una espressione come questa (v è il carattere adottato per indicare la radice quadrata, poiché far scrivere sqrt è noioso):

  15+(13+7^7)-(23+2-v49)

con

  DidacticEval("15+(13+7^7)-(23+2-v49)")

significa eseguire

  DidacticEval("15+ DidacticEval(13+7^7) - DidacticEval(23+2-v49)")

Ma occorre anche verificare che le parentesi siano correttamente inserite (appaiate e non accavallate).
Ho quindi implementato anche una verifica sulla validità delle parentesi, se ce ne sono.

  Private Function ParentesiValide(ByVal expressionToValidate As String) As Boolean
    Dim parsA As String = "{[(", parsC As String = "}])", p1 As Integer, p2 As Integer
    For i As Integer = 0 To 2
      Do While expressionToValidate.Contains(parsA.Chars(i))
        p1 = -1 : p2 = -1
        p1 = expressionToValidate.IndexOf(parsA.Chars(i))
        p2 = expressionToValidate.IndexOf(parsC.Chars(i), p1 + 1)
        If p1 > -1 AndAlso p2 > -1 AndAlso p2 > p1 Then
          expressionToValidate = expressionToValidate.Remove(p2, 1)
          expressionToValidate = expressionToValidate.Remove(p1, 1)
        ElseIf p1 <> p2 Then
          Return False
        End If
      Loop
    Next
    Return True
  End Function

In conseguenza di questo breve codice, posso saltare direttamente al codice STEP0, che nelle intenzioni deve essere l'inizio di un ciclo di ripetute sostituzioni di 'segni doppi', ma in realtà Replace sostituisce tutte le ricorrenze (forse si vuol prevenire la presenza di 'segni tripli', ma in questo caso sarebbe meglio segnalare la scorrettezza, piuttosto che correggerla).

Poi mi sono trovato ad analizzare il codice con il quale viene scandita la stringa dell'espressione per impostare tre vettori: uno per gli operandi, uno per gli operatori e uno per i valori.
Capito il procedimento, ho provveduto a implementare il codice secondo una logica che eviti il ricorso alle etichette e ai GoTo, mediante l'uso di cicli condizionati.
In tal modo ho potuto guidare la risoluzione dell'espressione secondo le priorità ben note ai tempi scolastici (prima le parentesi più interne, poi radici quadrate e potenze, poi moltiplicazioni e divisioni e infine addizioni e sottrazioni).

La nuova classe StringEvaluator
La prima cosa da fare è verificare la validità dell'espressione da valutare, che non deve essere una stringa vuota o nulla, avere un numero pari di parentesi correttamente appaiate, avere (eventualmente) un solo operatore di uguaglianza.

  Public Function DidacticEval(ByVal expressionToEval As String) As Decimal
    Try
      'prime verifiche
      If String.IsNullOrEmpty(expressionToEval) Then
        Throw New ArgumentException( _
          "Immettere un'espressione! Impossibile eseguire il metodo!")
      End If
      If Not ParentesiValide(expressionToEval) Then
        Throw New ArgumentException("Le parentesi non sono correttamente inserite!")
      End If
      If expressionToEval.Contains("=") Then
        If expressionToEval.IndexOf("=", expressionToEval.IndexOf("=") + 1) > 0 Then
          Throw New ArgumentException("Ci sono più di un segno =!")
        End If
      End If

Se l'espressione è una eguaglianza, si richiama ricorsivamente il metodo per il primo e per il secondo membro, si verifica l'uguaglianza e si restituisce l'esito facilmente convertibile in valore booleano.

      ' prima i membri dell'uguaglianza.
      ' se entra in questo if, non prosegue, perché esce con un Return o l'altro
      If expressionToEval.Contains("=") Then
        Dim primoMembro As Decimal = _
            DidacticEval(expressionToEval.Substring(0, expressionToEval.IndexOf("=") - 1))
        Dim secondoMembro As Decimal = DidacticEval(expressionToEval.IndexOf("=") + 1)
        If primoMembro = secondoMembro Then
          LogStringBuilder.AppendLine("L'uguaglianza è verificata")
          Return 1D
        Else
          LogStringBuilder.AppendLine("L'uguaglianza NON è verificata")
          Return 0D
        End If
      End If

Se l'espressione contiene parentesi, si entra in un ciclo che richiama ricorsivamente il metodo passando via via il contenuto delle parentesi più interne, finché ce ne sono.

      ' prima le parentesi
      Do While expressionToEval.Contains("(") OrElse _
                expressionToEval.Contains("[") OrElse _
                expressionToEval.Contains("{")

        LogStringBuilder.AppendLine("Ci sono parentesi. Risolvo quelle più interne")
        ' trovo le parentesi più interne
        Dim p1 As Integer = expressionToEval.LastIndexOf("(")
        Dim p2 As Integer = expressionToEval.LastIndexOf("[")
        Dim p3 As Integer = expressionToEval.LastIndexOf("{")
        If p1 < p2 Then p1 = p2
        If p1 < p3 Then p1 = p3
        Dim pi As Integer = p1
        p1 = expressionToEval.IndexOf(")", pi)
        p2 = expressionToEval.IndexOf("]", pi)
        p3 = expressionToEval.IndexOf("}", pi)
        If p1 = -1 Then p1 = expressionToEval.Length
        If p2 = -1 Then p2 = expressionToEval.Length
        If p3 = -1 Then p3 = expressionToEval.Length
        If p1 > p2 Then p1 = p2
        If p1 > p3 Then p1 = p3
        'estraggo la parte tra parentesi
        Dim subExpression As String = expressionToEval.Substring(pi + 1, p1 - pi - 1)
        'la sottopongo ricorsivamente al metodo
        Dim risultato As Decimal = DidacticEval(subExpression)
        'aggiorno l'espressione da valutare
        expressionToEval = expressionToEval.Remove(pi, p1 - pi + 1)
        expressionToEval = expressionToEval.Insert(pi, risultato.ToString)

      Loop ' finché parentesi

A questo punto non ci sono più parti di espressione da passare ricorsivamente al metodo, e posso passare a risolvere via via le varie operazioni, individuando operandi e operatori e sostituendo le parti risolte col rispettivo risultato, in un ciclo che si ripete finché ci sono operatori.
Poiché il primo carattere può essere un segno meno a indicare semplicemente un risultato negativo, l'indagine sulla presenza di operatori inizia dal secondo carattere. Questo comporta prevenire l'errore che si verificherebbe se tale secondo carattere fosse assente.

      'finché l'espressione è indagabile
      If expressionToEval.Length > 1 Then
        ' finché ci sono operazioni da fare
        Do While expressionToEval.Substring(1).Contains("-") _
                  OrElse expressionToEval.Substring(1).Contains("+") _
                  OrElse expressionToEval.Substring(1).Contains("*") _
                  OrElse expressionToEval.Substring(1).Contains("/") _
                  OrElse expressionToEval.Substring(1).Contains("^") _
                  OrElse expressionToEval.Contains("v")

Dapprima, sostituisco eventuali segni doppi:

          If expressionToEval.Contains("+-") Then
            LogStringBuilder.AppendLine( _
              "Esistono coppie di segni contigui + - ! Sostituisco con segno - !")
            LogStringBuilder.AppendLine("(Spiegazione: + * -  =  -  )")
            expressionToEval = expressionToEval.Replace("+-", "-")
          End If
          If expressionToEval.Contains("--") Then
            LogStringBuilder.AppendLine( _
              "Esistono coppie di segni contigui - -  ! Sostituisco con segno + !")
            LogStringBuilder.AppendLine("(Spiegazione:  -  *  -  =  +  )")
            expressionToEval = expressionToEval.Replace("--", "+")
          End If

Poi risolvo tutte le eventuali radici quadrate:

          Do While expressionToEval.Contains("v")
            LogStringBuilder.Append("Ci sono radici quadrate! Numero sotto radice : ")

            Dim v As Integer = expressionToEval.IndexOf("v")

            Dim n As String = ""
            For c As Integer = v + 1 To expressionToEval.Length - 1
              If Char.IsDigit(expressionToEval.Chars(c)) _
                  OrElse expressionToEval.Chars(c) = ","c Then
                n = n & expressionToEval.Chars(c)
              Else
                Exit For
              End If
            Next
            LogStringBuilder.AppendLine(n)

            If expressionToEval.Chars(v + 1) = "-"c Then
              Throw New ArgumentException( _
                "Risoluzione impossibile: numero negativo sotto radice " & n)
            End If

            Dim r As Decimal = Convert.ToDecimal(System.Math.Sqrt(n))
            LogStringBuilder.AppendLine("La radice vale: " & r.ToString)
            expressionToEval = expressionToEval.Remove(v, (v & n).Length)
            expressionToEval = expressionToEval.Insert(v, r.ToString)
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

E' quindi la volta di tutte le eventuali potenze, la scansione per evincere gli operandi è leggermente più laboriosa:

          Do While expressionToEval.Contains("^")
            LogStringBuilder.Append("Ci sono potenze! Potenza: ")
            Dim base As String = "", esponente As String = "", operation As String
            Dim v As Integer = expressionToEval.IndexOf("^")
            For c As Integer = v - 1 To 0 Step -1
              If Char.IsDigit(expressionToEval.Chars(c)) _
                  OrElse expressionToEval.Chars(c) Like "[,-]" Then
                base = expressionToEval.Chars(c) & base
              ElseIf expressionToEval.Chars(c) Like "[()]" Then
              Else
                Exit For
              End If
            Next

            For c As Integer = v + 1 To expressionToEval.Length - 1
              If Char.IsDigit(expressionToEval.Chars(c)) _
                  OrElse expressionToEval.Chars(c) = "," Then
                esponente = esponente & expressionToEval.Chars(c)
              Else
                Exit For
              End If
            Next
            operation = base & "^" & esponente
            Dim p As Integer = expressionToEval.IndexOf(operation)
            LogStringBuilder.AppendLine(operation)
            Dim r As Decimal = _
              Convert.ToDecimal(Decimal.Parse(base) ^ Decimal.Parse(esponente))
            LogStringBuilder.AppendLine("La potenza vale: " & r.ToString)
            expressionToEval = expressionToEval.Remove(p, operation.Length)
            expressionToEval = expressionToEval.Insert(p, r.ToString)
            If p > 0 AndAlso Char.IsDigit(expressionToEval.Chars(p - 1)) Then _
              expressionToEval = expressionToEval.Insert(p, "+")
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

A questo punto rimangono solo segni aritmetici, quindi scandisco ciò che rimane della stringa da valutare dimensionando tre vettori al numero di caratteri (non vale la pena cercare la precisione assoluta) e riempiendo i primi elementi con i risultati delle scansioni.
Si valuta, per ogni carattere, se è una cifra o un segno e si discrimina se il segno è un meno. Quindi, in capo alla valutazione, si ha una cifra, un operatore, un valore.

          ' scansione della stringa in tre vettori
          Dim nChars As Integer = expressionToEval.Length
          Dim cifre(nChars) As String
          Dim segni(nChars) As String
          Dim valori(nChars) As Decimal
          Dim indice As Integer = 0

          For c As Integer = 0 To nChars - 1
            cifre(c) = ""
            Static meno As Boolean
            If meno Then
              cifre(indice) = "-"
              meno = False
            End If
            If c = 0 Then
              If Char.IsDigit(expressionToEval.Chars(c)) _
                  OrElse expressionToEval.Chars(c) Like "[,-]" Then
                cifre(indice) = expressionToEval.Chars(c)
              Else
                segni(indice) = expressionToEval.Chars(c)
                indice += 1
              End If
            Else
              If Char.IsDigit(expressionToEval.Chars(c)) _
                  OrElse expressionToEval.Chars(c) = ","c Then
                cifre(indice) &= expressionToEval.Chars(c)
              Else
                If expressionToEval.Chars(c) = "-" Then
                  If expressionToEval.Chars(c - 1) Like "[+*/]" Then
                    cifre(indice) &= expressionToEval.Chars(c)
                  Else
                    segni(indice) = "+"
                    indice += 1
                    meno = True
                  End If
                Else
                  segni(indice) = expressionToEval.Chars(c)
                  indice += 1
                End If
              End If
            End If
            If cifre(indice) <> "" AndAlso c > 0 AndAlso Not meno Then _
                valori(indice) = Decimal.Parse(cifre(indice))
          Next

Si può infine procedere alle operazioni, cominciando da divisioni e moltiplicazioni, modificando via via la stringa dell'espressione e calcolando il risultato di ciascuna operazione nel primo elemento del vettore valori:

          ' esecuzione dei calcoli rimanenti
          If expressionToEval.Contains("/") OrElse expressionToEval.Contains("*") Then
            LogStringBuilder.AppendLine( _
              "Eseguo i calcoli. Prima le divisioni e le moltiplicazioni")
          End If

          Do While Array.IndexOf(segni, "/") > -1
            Dim s As Integer = Array.IndexOf(segni, "/") ' posizione del segno
            Dim operation As String = cifre(s) & "/" & cifre(s + 1)
            Dim p As Integer = expressionToEval.IndexOf(operation)
            LogStringBuilder.Append("Eseguo la divisione: ")
            LogStringBuilder.Append(operation & " => ")

            valori(s) = System.Math.Round(valori(s) / valori(s + 1), 5)
            cifre(s) = valori(s).ToString
            LogStringBuilder.AppendLine(cifre(s))
            expressionToEval = expressionToEval.Remove(p, operation.Length)
            expressionToEval = expressionToEval.Insert(p, cifre(s))
            cifre(s + 1) = ""
            segni(s) = ""
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

          Do While Array.IndexOf(segni, "*") > -1
            Dim s As Integer = Array.IndexOf(segni, "*") ' posizione del segno
            LogStringBuilder.Append("Eseguo la moltiplicazione: ")
            Dim operation As String = cifre(s) & "*" & cifre(s + 1)
            Dim p As Integer = expressionToEval.IndexOf(operation)
            LogStringBuilder.Append(operation & " => ")

            valori(s) = System.Math.Round(valori(s) * valori(s + 1), 5)
            cifre(s) = valori(s).ToString
            LogStringBuilder.AppendLine(cifre(s))
            expressionToEval = expressionToEval.Remove(p, operation.Length)
            expressionToEval = expressionToEval.Insert(p, cifre(s))
            cifre(s + 1) = ""
            segni(s) = ""
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

Questo frammento di codice contiene un bug, che ha reso necessaria una correzione. Non basta svuotare gli elementi che 'non servono più'. Bisogna rimuoverli. Poiché non è possibile farlo agevolmente con un vettore, riporto gli elementi significativi in altrettante liste. Di conseguenza, il frammento va sostituito con il seguente:

          'disposizione dei vettori in liste
          Dim lstCifre As New List(Of String)
          Dim lstSegni As New List(Of String)
          Dim lstValori As New List(Of Decimal)

          For c As Integer = 0 To nChars - 1
            If cifre(c) <> "" Then
              lstCifre.Add(cifre(c))
              lstValori.Add(Decimal.Parse(cifre(c)))
            End If
            If segni(c) <> "" Then lstSegni.Add(segni(c))
          Next
          ' esecuzione dei calcoli rimanenti
          If expressionToEval.Contains("/") OrElse expressionToEval.Contains("*") Then
            LogStringBuilder.AppendLine( _
              "Eseguo i calcoli. Prima le divisioni e le moltiplicazioni")
          End If

          Do While lstSegni.Contains("/") ' Array.IndexOf(segni, "/") > -1
            Dim s As Integer = lstSegni.IndexOf("/") 'Array.IndexOf(segni, "/") ' posizione del segno
            Dim operation As String = lstCifre(s) & "/" & lstCifre(s + 1) 'cifre(s) & "/" & cifre(s + 1)
            Dim p As Integer = expressionToEval.IndexOf(operation)
            LogStringBuilder.Append("Eseguo la divisione: ")
            LogStringBuilder.Append(operation & " => ")

            lstValori(s) = System.Math.Round(lstValori(s) / lstValori(s + 1), 5)
            lstCifre(s) = lstValori(s).ToString
            LogStringBuilder.AppendLine(lstCifre(s))
            expressionToEval = expressionToEval.Remove(p, operation.Length)
            expressionToEval = expressionToEval.Insert(p, lstCifre(s))

            lstCifre.RemoveAt(s + 1) 'cifre(s + 1) = ""
            lstValori.RemoveAt(s + 1)
            lstSegni.RemoveAt(s) 'segni(s) = ""
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

          Do While lstSegni.Contains("*") ' Array.IndexOf(segni, "/") > -1
            Dim s As Integer = lstSegni.IndexOf("*") 'Array.IndexOf(segni, "/") ' posizione del segno
            Dim operation As String = lstCifre(s) & "*" & lstCifre(s + 1) 'cifre(s) & "/" & cifre(s + 1)
            Dim p As Integer = expressionToEval.IndexOf(operation)
            LogStringBuilder.Append("Eseguo la moltiplicazione: ")
            LogStringBuilder.Append(operation & " => ")

            lstValori(s) = System.Math.Round(lstValori(s) * lstValori(s + 1), 5)
            lstCifre(s) = lstValori(s).ToString
            LogStringBuilder.AppendLine(lstCifre(s))
            expressionToEval = expressionToEval.Remove(p, operation.Length)
            expressionToEval = expressionToEval.Insert(p, lstCifre(s))

            lstCifre.RemoveAt(s + 1) 'cifre(s + 1) = ""
            lstValori.RemoveAt(s + 1)
            lstSegni.RemoveAt(s) 'segni(s) = ""
            LogStringBuilder.AppendLine( _
              "L'espressione da risolvere diventa: " & expressionToEval)
          Loop

Inoltre, l'ultima istruzione del ciclo For prima di questo codice, va corretta così:

            If cifre(indice) <> "" AndAlso cifre(indice) <> "-" Then _
                valori(indice) = Decimal.Parse(cifre(indice))

Alla fine, scandisco gli elementi rimasti, dato che sono sicuramente soltanto somme algebriche:

(Naturalmente, invece dei vettori, come nella vecchia versione, uso le liste)

          For e As Integer = 0 To lstSegni.Count - 1
            Dim operation As String
            If lstSegni(e) = "+" Then
              If lstValori(e + 1) < 0 Then
                LogStringBuilder.Append("Eseguo la sottrazione : ")
                operation = lstCifre(0) & lstCifre(e + 1)
              Else
                LogStringBuilder.Append("Eseguo l'addizione : ")
                operation = lstCifre(0) & "+" & lstCifre(e + 1)
              End If
              LogStringBuilder.Append(operation & " = ")
              lstValori(0) += lstValori(e + 1)
              Dim p As Integer = expressionToEval.IndexOf(operation)
              lstValori(0) = System.Math.Round(lstValori(0), 5)
              lstCifre(0) = lstValori(0).ToString
              expressionToEval = expressionToEval.Remove(p, operation.Length)
              expressionToEval = expressionToEval.Insert(p, lstCifre(0))
              lstCifre(e + 1) = ""
              lstSegni(e) = ""
              LogStringBuilder.AppendLine(lstCifre(0))
              LogStringBuilder.AppendLine( _
                "L'espressione da risolvere diventa: " & expressionToEval)
            End If
          Next

Dopo la conclusione del ciclo, restituisco il risultato:

      LogStringBuilder.AppendLine()
      LogStringBuilder.AppendLine("Risultato finale= " &
      expressionToEval)
      LogStringBuilder.AppendLine( _
                "________________________________________________________________")
      LogStringBuilder.AppendLine()

      Return Decimal.Parse(expressionToEval)

Come si usa la classe
Supponendo che l'utente scriva l'espressione in una TextBox di nome txtEspressione e prema un pulsante, il codice eseguito da questo pulsante può essere:

    Dim oEval As New StringEvaluator
    Dim result As Decimal = oEval.DidacticEval(txtEspressione.Text)
    MessageBox.Show(oEval.ExpressionResolvingLog)

Valutare una stringa senza occuparsi della didattica
Il codice fin qui visto ha lo scopo, lo ricordo, di illustrare la risoluzione di una espressione passaggio dopo passaggio.
Ma se si volesse risolvere una espressione 'tout court', lasciando al sistema l'onere di scandire e validare la stringa passata?

Naturalmente è possibile, solo che le risorse impiegate sono diverse.
In Visual Basic 6 si referenzia al progetto il componente Microsoft Script Control e se ne usa il metodo Eval.
In VBA si potrebbe fare lo stesso, ma Anonimo ha fatto di più: ha implementato un codice che crea ed esegue dell'altro codice, basandosi sull'oggetto Module di Access:

Option Compare Database
Option Explicit

  Private Result As Long

  Public Function Evaluate(ByVal CompareString As String) As Long

    Dim ModuleObject As Module
    ModuleObject = Modules!Classe1
    ModuleObject.ReplaceLine(ModuleObject.CountOfLines - 1, "Result=" & CompareString())
    Call Calc()
    Evaluate = Result
    ModuleObject.ReplaceLine(ModuleObject.CountOfLines - 1, "")

  End Function

  Private Function Calc()

  End Function

Questo codice acquisisce un riferimento al proprio modulo, sostituisce la penultima riga (il contenuto della funzione Calc) con una assegnazione

  variabile di modulo = espressione da calcolare

richiama la funzione e restituisce il risultato, ripristinando quindi la riga vuota.

Il metodo Evaluate
Anonimo ha pensato di seguire lo stesso schema anche in Visual Basic.Net.
Ha dato una bella occhiata alle varie classi offerte dal Framework e ha implementato il codice seguente (in verità ne ha fatto una seconda Tip, che io ho rielaborato come metodo della stessa classe StringEvaluator):

  Public Function Evaluate(ByVal expressionToEval As String) As Object
    ' istanza di un fornitore di accesso al generatore di codice e al compilatore VisualBasic
    Dim codeProvider As New Microsoft.VisualBasic.VBCodeProvider()
    ' istanza dei parametri per il compilatore
    Dim params As New System.CodeDom.Compiler.CompilerParameters()
    ' codice da compilare ed eseguire
    Dim codeToExec As String = _
      "Class Evaluate" & Environment.NewLine & _
        "Public function Eval() As Object" & Environment.NewLine & _
          "Return " & expressionToEval & Environment.NewLine & _
        "End function" & Environment.NewLine & _
      "End Class"

    ' visualizzazione per verifiche in fase di testing
    ' MessageBox.Show(codeToExec)

    ' assembly risultante dalla compilazione del codice
    Dim assembly As System.CodeDom.Compiler.CompilerResults = _
      codeProvider.CompileAssemblyFromSource(params, codeToExec)
    ' istanza della classe Evaluate definita nel codice compilato
    Dim instance As Object = assembly.CompiledAssembly.CreateInstance("Evaluate")
    ' acquisisce le informazioni necessarie all'uso del metodo Eval
    Dim methodCall As System.Reflection.MethodInfo = instance.GetType().GetMethod("Eval")
    ' esegue il metodo e restituisce il risultato
    Return methodCall.Invoke(instance, Nothing)
  End Function

Il codice è abbondantemente commentato. C'è da precisare che, di default, i parametri di compilazione si riferiscono a una libreria, piuttosto che a un eseguibile (ecco perché non ne viene impostato alcuno).
Il codice della libreria è una classe con un unico metodo che restituisce il calcolo della espressione da valutare.

Ovviamente, la stringa da passare al metodo StringEvaluator.Evaluate dovrebbe essere controllata e validata (parentesi, operatori e funzioni riconoscibili dal Framework: quindi niente parentesi quadre o grafe, o caratteri 'v' per le radici quadrate, eccetera).

Conclusioni
In questo articolo è stata trattata la revisione di codice simil-Basic in codice VB2005, nonché l'uso della compilazione al volo per calcolare un'espressione numerica.
Il codice a corredo (corretto) è scaricabile dall'area download.
Per critiche o suggerimenti, scrivetemi.