El código de la clase EvaluarExpresiones

'------------------------------------------------------------------------------
' Evaluar expresiones                                               (01/Oct/07)
' Evaluar expresiones numéricas simples a partir de una cadena
'
' Compilado como una DLL                                            (04/Oct/07)
'
'
' ©Guillermo 'guille' Som, 2007
'
' El espacio de nombre de esta librería es:
' elGuille.Developer
'------------------------------------------------------------------------------
Option Strict On
Option Compare Text

Imports Microsoft.VisualBasic
Imports System

Imports System.Collections.Generic
Imports System.Text
Imports System.Threading

Imports System.Globalization

Imports System.Reflection
Imports System.Diagnostics

''' <summary>
''' Evaluar expresiones aritméticas
''' Se admiten: +, -, *, x, /, ^, %, \u412?
''' </summary>
''' <remarks>
''' El signo \u412? se usa para Mod
''' El signo % se usa para calcular porcentajes
''' </remarks>
Public Class EvaluarExpresiones

    Protected Const letras As String = "abcdefghijklmnopqrstuvwxyz"
    Protected Const cifras As String = "0123456789" 'E"

    ' el % es porcentaje
    ' el \u412? es Mod
    ' la x es para multiplicar * (lo vuelvo a añadir:       v.39    (13/Oct/07)
    Protected Const operadores As String = "^*x/\m\u623?%+-" ' \u623?\u412?

    ' Los números negativos (los marcados como negativos o positivos con - y +)
    ' se tienen en cuenta justo después de la exponenciación ^

    ' Se recorrerá de mayor proridad a menor, 
    Protected Const prioridadOp As String = operadores

    '' Los separadores, etc.                                 v.30    (12/Oct/07)
    '' contar los números como separadores...                v.31    (12/Oct/07)
    '' ¿no sería más fácil que NO sea letra?
    'Protected Const separadores As String = cifras & _
    '                                        operadores & _
    '                                        " ()[]{}<>=,;:_!#$%&'" & _
    '                                        ChrW(34) & vbCrLf & vbTab



    ''' <summary>
    ''' Comprobar si la cadena a evaluar tiene números negativos.
    ''' Si los hay, cambia el signo - (menos) por _ (subrayado)
    ''' </summary>
    ''' <param name="texto"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' 08/Oct/2007
    ''' </remarks>
    Protected Function comprobarNegativos(ByVal texto As String) As String
        ' Buscar un operador seguido de un signo - (puede tener espacios)
        ' (2 + -5) ^ 2 * 23

        ' si no hay -, salir sin hacer nada más
        Dim p As Integer = InStr(texto, "-")
        If p = 0 Then Return texto
        Dim hayOperador As Boolean = False


        For Each op As String In operadores
            p = 1
            Do
                Dim i As Integer = InStr(p, texto, op)
                If i = 0 Then Exit Do
                p = i + 1
                If op = "-" Then
                    hayOperador = True
                    ' Si es el signo -
                    ' buscar hacia atrás
                    ' si es una cifra o un operador, no es negativo
                    For j As Integer = i - 1 To 1 Step -1
                        Dim c As String = Mid(texto, j, 1)
                        Dim k As Integer = (cifras & operadores).IndexOf(c)
                        If k > -1 Then
                            hayOperador = False
                            Exit For
                        End If
                    Next
                    If hayOperador Then
                        Mid(texto, i, 1) = "_"
                    End If
                Else
                    ' Buscar un signo -
                    ' solo admitir espacios
                    ' cualquier otra cosa es que no es un número negativo
                    For j As Integer = p To Len(texto)
                        Dim c As String = Mid(texto, j, 1)
                        Dim k As Integer = cifras.IndexOf(c)
                        If k > -1 Then
                            Exit For
                        End If
                        If c = "-" Then
                            ' Es negativo
                            Mid(texto, j, 1) = "_"
                            Exit For
                        End If
                    Next
                End If
            Loop
        Next

        Return texto
    End Function

    ''' <summary>
    ''' Analiza la prioridad de los operadores
    ''' Si hay diferentes niveles los pone dentro de paréntesis
    ''' </summary>
    ''' <param name="texto"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function analizarPrioridad(ByVal texto As String) As String

        '
        '======================================================================
        ' TODO: (12/Oct/07)
        '======================================================================
        ' Para analizar los parámetros de las funciones,
        ' comprobar si la cadena tiene comas,
        ' en ese caso, agrupar los que estén en cada grupo de comas.
        '
        '======================================================================
        '

        ' Agregar el texto en el stringBuilder
        ' los cambios se harán con esta clase
        Dim sb As New StringBuilder(texto)
        Dim op1, op2 As String
        Dim p As Integer

        ' Se debe evaluar si todos los operadores son del mismo nivel
        ' en ese caso no se encierran las operaciones entre paréntesis
        Dim mismoNivel As Boolean = todosMismoNivelPri(texto)

        For Each op As String In prioridadOp
            ' Repetir mientras haya de este signo
            p = 1
            Do
                Dim i As Integer
                i = InStr(p, texto, op)
                If i = 0 Then Exit Do
                p = i + 1

                ' Buscar cifra anterior
                op1 = buscarCifra(texto, i, anterior:=True)

                ' Si es un paréntesis,
                ' es que el anterior está entre paréntesis
                ' por tanto, no evaluar.
                ' Nota:
                '   Aunque se compare con ) es un indicador
                '   de que se ha encontrado un paréntesis
                '   y por tanto no hay que evaluarlo por separado
                If op1 = ")" Then Continue Do

                op2 = buscarCifra(texto, i, anterior:=False)
                If op2 = ")" Then Continue Do

                ' Tener en cuenta el texto a reemplazar             (07/Oct/07)
                If mismoNivel = False Then
                    sb.Replace(op1 & op & op2, "(" & op1 & op & op2 & ")")
                End If
            Loop
        Next

        Return sb.ToString
    End Function

    ''' <summary>
    ''' Comprueba si todos los operadores de la expresión tienen el mismo nivel
    ''' </summary>
    ''' <param name="texto"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function todosMismoNivelPri(ByVal texto As String) As Boolean
        Dim opAnt As String = ""
        Dim p As Integer = 0
        Do
            Dim i As Integer = texto.IndexOfAny(prioridadOp.ToCharArray, p)
            If i > -1 Then
                p = i + 1
                If Len(opAnt) > 0 Then
                    If mismoNivelPri(opAnt, texto(i)) = False Then
                        Return False
                    End If
                End If
                opAnt = texto(i)
            Else
                Exit Do
            End If
        Loop

        Return True
    End Function

    ''' <summary>
    ''' Función de apoyo para todosMismoNivelPri
    ''' </summary>
    ''' <param name="op1"></param>
    ''' <param name="op2"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function mismoNivelPri(ByVal op1 As String, ByVal op2 As String) As Boolean
        If op1 = op2 Then Return True

        Select Case op1
            Case "^"
                Return False
                'Case "*", "/", "÷", ":" ', "x" ' No usar la x porque se lía con Max
            Case "*", "/", "x"
                ' If "*/÷x:".IndexOf(op2) > -1 Then
                ' If "*/÷:".IndexOf(op2) > -1 Then
                If "*x/".IndexOf(op2) > -1 Then
                    Return True
                Else
                    Return False
                End If
            Case "\"
                Return False
            Case "%", "\u412?", "\u623?", "m"
                Return False
            Case "+", "-"
                If op2 = "+" OrElse op2 = "-" Then
                    Return True
                Else
                    Return False
                End If
        End Select

        Return False
    End Function

    ''' <summary>
    ''' Buscar la siguiente cifra
    ''' </summary>
    ''' <param name="texto"></param>
    ''' <param name="i">
    ''' El valor de i será en base 1
    ''' primer caracter es el 1 no el 0
    ''' </param>
    ''' <param name="anterior"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function buscarCifra(ByVal texto As String, ByVal i As Integer, _
                                   Optional ByVal anterior As Boolean = True) As String
        Dim iPila As Integer = 0
        Dim iB, fB, pB As Integer
        Dim hayCifras As Boolean = False
        Dim ultimo As Integer = -1
        If anterior Then
            iB = i - 1
            fB = 1
            pB = -1
        Else
            iB = i + 1
            fB = texto.Length
            pB = 1
        End If

        For j As Integer = iB To fB Step pB
            Dim c As String = Mid(texto, j, 1)
            Select Case c
                Case " "
                    ' Añadirlo, pero no tenerlo en cuenta
                    ' como cifra
                    ultimo = j

                Case "(", "["
                    iPila = 0
                    If anterior Then
                        ' Solo vale si es )
                        ' salir
                        If ultimo > -1 Then
                            Exit For
                        End If
                        Return ")"
                    End If
                    iPila += 1 ' push
                    ' seguir buscando la pareja
                    For k As Integer = j + pB To fB Step pB
                        If Mid(texto, k, 1) = "(" Then
                            iPila += 1 ' push
                        End If
                        If Mid(texto, k, 1) = ")" Then
                            iPila -= 1 ' pop
                            If iPila = 0 Then
                                ' Lo hemos encontrado
                                ultimo = k
                                j = fB
                                Exit For
                            End If
                        End If
                    Next

                Case ")", "]"
                    iPila = 0
                    If anterior = False Then
                        ' Solo vale si es (
                        ' salir
                        ' pero no si hay alguna cifra
                        If ultimo > -1 Then
                            Exit For
                        End If
                        Return ")"
                    End If
                    iPila += 1 ' push
                    ' seguir buscando la pareja
                    For k As Integer = j + pB To fB Step pB
                        If Mid(texto, k, 1) = ")" Then
                            iPila += 1 ' push
                        End If
                        If Mid(texto, k, 1) = "(" Then
                            iPila -= 1 ' pop
                            If iPila = 0 Then
                                ' Lo hemos encontrado
                                ultimo = k
                                j = fB
                                Exit For
                            End If
                        End If
                    Next
                    ' Quitar el comentario
                    ' para poner un punto de interrupción (debug)
                    'j = j


                    ' debe ser un operador
                    ' No usar la x para multiplicar
                    ' Case "^", "*", "/", ":", "÷", "\", "\u412?", "\u623?", "%", "+", "-" ', "x"
                Case "^", "*", "/", "\", "\u412?", "\u623?", "%", "+", "-", "m", "x"
                    Exit For

                Case Else
                    ' es una cifra, añadirla
                    hayCifras = True
                    ultimo = j

            End Select
        Next
        If ultimo > -1 Then
            If anterior Then
                Return Mid(texto, ultimo, i - ultimo)
            Else
                Return Mid(texto, i + 1, ultimo - i)
            End If
        End If
        Return ")"
    End Function



    ''' <summary>
    ''' Evalúa una expresión y devuelve el resultado
    ''' Las expresiones indicadas solo deben ser cifras y operaciones aritméticas
    ''' </summary>
    ''' <param name="expresion"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' NO debe contener funciones y pueden estar encerradas entre paréntesis,
    ''' pero no contener varios paréntesis anidados.
    ''' Si tiene decimales, estarán en el formato del idioma (la coma para España)
    ''' Solo se usarán operadores del mismo nivel de prioridad.
    ''' (todas estas cosas se tienen ya en cuenta, pero... para que quede claro)
    ''' 12 + 5 + 10
    ''' 
    ''' </remarks>
    Protected Overridable Function evaluaExp(ByVal expresion As String) As String
        If expresion.StartsWith("(") AndAlso expresion.EndsWith(")") Then
            expresion = expresion.Substring(1, expresion.Length - 2)
        End If

        '' Cambiar los puntos por comas
        'If sepDec = "," Then
        '    expresion = expresion.Replace(".", ",")
        'End If

        Dim exp As String = expresion

        Dim op As String
        Dim pIz As Integer
        Dim opIz, opDe As String
        Dim res As String = ""
        Dim pDer As Integer

        ' No sustituir todos los - con _
        ' si empieza con - es negativo, sustituir con _             (08/Oct/07)
        If expresion.TrimStart().StartsWith("-") Then
            expresion = "_" & expresion.TrimStart().Substring(1)
        End If
        If expresion.Contains("[") AndAlso expresion.Contains("]") Then
            exp = expresion
        Else
            exp = analizarPrioridad(expresion)
        End If
        'exp = analizarPrioridad(expresion)
        ' si al analizar solo se añaden paréntesis                  (07/Oct/07)
        ' no añadirlos, que se cuelga...
        If "(" & expresion & ")" = exp Then
            exp = expresion
        End If

        ' Si hay paréntesis anidados
        ' (después de analizar prioridad
        pIz = exp.IndexOf("(")
        While pIz > -1
            pDer = exp.IndexOf(")", pIz)
            If pDer > -1 Then
                ' Analizar lo que está entre paréntesis
                ' Tomar el inicio desde el final hallado            (07/Oct/07)
                pIz = exp.LastIndexOf("(", pDer)
                Dim exp2 As String = exp.Substring(pIz, pDer - pIz + 1)
                exp2 = Evalua(exp2)
                exp = exp.Substring(0, pIz) & exp2 & exp.Substring(pDer + 1)
                ' Seguir analizando                                 (04/Oct/07)
                pIz = exp.IndexOf("(")
            End If
        End While

        pIz = exp.IndexOfAny(operadores.ToCharArray)
        If pIz = -1 Then
            pIz = exp.IndexOf("_")
        End If
        If pIz = -1 Then Return exp


        opIz = exp.Substring(0, pIz).Trim
        If opIz = "" Then
            ' Seguramente el número es negativo
            pIz = exp.IndexOfAny("-_".ToCharArray)
            If pIz > -1 Then
                ' Sumar 1 porque en buscarCifra el primero es el 1
                opIz = buscarCifra(exp, pIz + 1, anterior:=False)
                opIz = "-" & opIz
                ' Si es negativo, no seguir comprobando             (06/Oct/07)
                ' (al menos si no queda más)
                If pIz <= 1 Then
                    exp = exp.Substring(pIz + opIz.Length)
                    If Len(exp) = 0 Then
                        Return opIz '& exp
                    End If
                Else
                    exp = exp.Substring(0, pIz - 1)
                End If
                pIz = exp.IndexOfAny(operadores.ToCharArray)
                If pIz = -1 Then Return opIz
            End If
        End If
        exp = exp.Substring(pIz)
        Do
            pIz = exp.IndexOfAny(operadores.ToCharArray)
            If pIz = -1 Then
                Exit Do
            End If
            op = exp(pIz)
            pDer = exp.IndexOfAny(operadores.ToCharArray, pIz + 1)
            If pDer = -1 Then
                pDer = exp.Length
            End If
            opDe = exp.Substring(pIz + 1, pDer - pIz - 1).Trim
            If opDe = "" Then
                ' Seguramente el número es negativo
                pDer = exp.IndexOf("-")
                If pDer > -1 Then
                    ' Sumar 1 porque en buscarCifra el primero es el 1
                    opDe = buscarCifra(exp, pDer + 1, anterior:=False)
                    opDe = "-" & opDe
                    ' si es negativo, posicionar bien el puntero    (06/Oct/07)
                    pDer = pDer + Len(opDe)
                Else
                    res = opIz
                    Exit Do
                End If
            End If
            ' Por si tiene _ en vez de -                            (07/Oct/07)
            If opIz.Contains("_") Then
                opIz = opIz.Replace("_", "-")
            End If
            If opDe.Contains("_") Then
                opDe = opDe.Replace("_", "-")
            End If
            '
            ' El cambio de los decimales solo debe hacerse aquí     (09/Oct/07)
            '
            ' Ya no se debe cambiar nada                    v.20    (12/Oct/07)
            ' se usa la cultura Invariant
            '' Cambiar los puntos por comas
            'If sepDec = "," Then
            '    opIz = opIz.Replace(".", ",")
            '    opDe = opDe.Replace(".", ",")
            'End If

            ' Si tiene la letra E,                          v.13  (12/Oct/07)
            ' ¡es que es un número a lo bestia!
            ' Las mayúsculas y minúsculas...                v.26    (12/Oct/07)
            If opIz.ToLower.EndsWith("e") Then
                opIz = opIz & op & opDe
                opDe = "0"
            End If
            res = Evalua(op, CDbl(opIz), CDbl(opDe)).ToString

            ' Ya no se debe cambiar nada                    v.20    (12/Oct/07)
            ' se usa la cultura Invariant
            '' Volver a dejar el punto como decimal
            'If sepDec = "," Then
            '    res = res.Replace(",", ".")
            'End If

            opIz = res
            exp = exp.Substring(pDer)
            If Len(exp) = 0 Then
                Exit Do
            End If
        Loop

        Return res
    End Function

    '
    '--------------------------------------------------------------------------
    ' Métodos públicos de instancia
    '--------------------------------------------------------------------------
    '


    ''' <summary>
    ''' Evaluar una expresión.
    ''' Este es el método principal al que debe llamarse para evaluar
    ''' </summary>
    ''' <param name="expresion">
    ''' La expresión a evaluar
    ''' </param>
    ''' <param name="mostrarInfo">
    ''' True si se quiere mostrar detalle de las operaciones realizadas
    ''' False (valor por defecto) para no mostrar detalle
    ''' </param>
    ''' <returns></returns>
    ''' <remarks>
    ''' Los operadores son los operadores simples
    ''' no las palabras, por ejemplo * o x para multiplicar, pero no mult
    ''' </remarks>
    Public Overridable Function Evalua(ByVal expresion As String, _
                                       Optional ByVal mostrarInfo As Boolean = False _
                                       ) As String
        expresion = Trim(expresion)
        If Len(expresion) = 0 Then Return "0"

        Dim negativo As String = ""
        If expresion.StartsWith("-") Then
            negativo = "-"
            expresion = expresion.Substring(1)
        End If

        ' Tener en cuenta la prioridad de los operadores
        ' (se encerrarán entre paréntesis extras)
        Dim expAnt As String = expresion

        ' Comprobar si hay números negativos                        (08/Oct/07)
        ' El caso es que si hay un operador y después un -
        ' es que es negativo
        expAnt = comprobarNegativos(expresion)

        ' Si es una función,                                v.21    (12/Oct/07)
        ' no evaluar la prioridad
        ' Las funciones contienen corchetes
        If Not (expAnt.Contains("[") AndAlso expAnt.Contains("]")) Then
            expresion = analizarPrioridad(expAnt)
            '#If DEBUG Then
            '        Else
            '            Debug.WriteLine(expAnt)
            '#End If
        End If
        ' si al analizar solo se añaden paréntesis                  (07/Oct/07)
        ' no añadirlos, que se cuelga...
        If "(" & expAnt & ")" = expresion Then
            expresion = expAnt
        End If

        If mostrarInfo Then
            Console.WriteLine(expAnt)
            Console.WriteLine(expresion)
        End If

        Dim pGr As AgruparPares

        ' No cambiar los puntos por comas                           (08/Oct/07)
        ' se cambiará en evaluaExp
        ' con idea de tener en cuenta los parámetros

        '' No añadir paréntesis extras (por si ya tiene)             (09/Oct/07)
        'If expresion.Trim().StartsWith("(") = False OrElse expresion.Trim().EndsWith(")") = False Then
        '    expresion = "(" & expresion & ")"
        'End If
        expresion = "(" & expresion & ")"

        pGr = AgruparPares.CrearInstancia(expresion, "(", ")")

        Dim parametros(0 To pGr.Sustituciones.Count - 1) As String

        If mostrarInfo Then
            ' Mostrar las sustituciones
            Console.WriteLine(pGr.Texto)
            For i As Integer = 0 To pGr.Sustituciones.Count - 1
                Dim s As String = pGr.Sustituciones(i)
                Console.WriteLine("  {0}: {1}", i, s)
            Next
            Console.WriteLine()
        End If

        For i As Integer = 0 To pGr.Sustituciones.Count - 1
            Dim s As String = pGr.Sustituciones(i)
            If pGr.ContieneMarcador(i) Then
                Dim j As Integer
                Dim res As String = ""
                Do
                    j = s.IndexOf(pGr.MarcadorInicio)
                    If j > -1 Then
                        Dim marca As String = s.Substring(j + pGr.MarcadorInicio.Length, pGr.LenFormato) '2)
                        Dim index As Integer = CInt(marca)
                        s = s.Replace(s.Substring(j, pGr.LenMarcador), parametros(index))
                    End If
                Loop While j > -1

                'If sepDec = "," Then
                '    res = evaluaExp(s).Replace(",", ".")
                'Else
                '    res = evaluaExp(s)
                'End If

                res = evaluaExp(s)
                parametros(i) = res

                If mostrarInfo Then
                    Console.WriteLine("  {0,2}: {1} --> {2}", i, s, res)
                End If
            Else
                Dim res As String

                ' Si tiene coma, es que son parámetros
                If s.IndexOf(",") = -1 Then
                    res = evaluaExp(s)

                    '' Convertir las comas en puntos después de evaluar
                    '' Comprobar con el separador correcto           (09/Oct/07)
                    'If sepDec = "," Then
                    '    res = evaluaExp(s).Replace(",", ".")
                    'Else
                    '    res = evaluaExp(s)
                    'End If

                ElseIf s.IndexOf("[") > -1 Then
                    res = evaluaExp(s)

                    'If sepDec = "," Then
                    '    res = evaluaExp(s).Replace(",", ".")
                    'Else
                    '    res = evaluaExp(s)
                    'End If
                Else
                    res = s
                End If
                parametros(i) = res

                If mostrarInfo Then
                    Console.WriteLine("  {0,2}: {1} --> {2}", i, s, res)
                End If
            End If
        Next

        '----------------------------------------------------------------------

        ' En realidad ya se cambia en la clase base, pero...
        ' De todas formas, como se ha cambiado el valor en el constructor
        ' esto NUNCA se cumplirá...


        ' Aquí si se debe cambiar,                          v.27    (12/Oct/07)
        ' ya que esto es para devolverlo adecuado a la configuración regional

        ' Antes no estaba el cambio aquí,
        ' pero es preferible hacerlo en Evalua,
        ' por si se usa una instancia en lugar del método compartido

        expAnt = negativo & parametros(parametros.Length - 1)

        If sepDec = "," Then
            expAnt = expAnt.Replace(".", ",")
        End If

        '----------------------------------------------------------------------

        Return expAnt
        'Return negativo & parametros(parametros.Length - 1)
    End Function


    ''' <summary>
    ''' Evalúa una operación con el operador indicado
    ''' </summary>
    ''' <param name="operador">
    ''' Operador o función a utilizar.
    ''' Los operadores y funciones reconocidos son:
    ''' +
    ''' -, (también acepta _ para restar)
    ''' *
    ''' /, \ (no distingue división entera)
    ''' ^
    ''' \u412? o m (mod)
    ''' % calcular el porcentaje
    ''' </param>
    ''' <param name="num1">
    ''' Primer operando (izquierda del operador)
    ''' </param>
    ''' <param name="num2">
    ''' (Opcional) Segundo operando (derecha del operador)
    ''' </param>
    ''' <returns>
    ''' Un valor Double con el resultado
    ''' </returns>
    ''' <remarks></remarks>
    Public Overridable Function Evalua(ByVal operador As String, _
                                       ByVal num1 As Double, _
                                       ByVal num2 As Double) As Double
        Dim numeros(0 To 0) As Double
        If num2 <> Double.NaN Then
            ReDim numeros(0 To 1)
            numeros(1) = num2
        End If
        numeros(0) = num1

        Return Evalua(operador, numeros)
    End Function

    ''' <summary>
    ''' Evalúa una operación con el operador indicado
    ''' </summary>
    ''' <param name="operador">
    ''' Operador o función a utilizar en la expresión
    ''' </param>
    ''' <param name="num1">
    ''' Parámetro a evaluar con la función u operador
    ''' </param>
    ''' <returns>
    ''' Un valor Double con el resultado
    ''' </returns>
    ''' <remarks></remarks>
    Public Overridable Function Evalua(ByVal operador As String, _
                                       ByVal num1 As Double) As Double

        'Return Evalua(operador, num1, Double.NaN)
        Return Evalua(operador, New Double() {num1})
    End Function


    ''' <summary>
    ''' Evalúa una operación usando el mismo operador
    ''' Este método NO evalúa las funciones,
    ''' para evaluar funciones usar la clase EvaluarFunciones.
    ''' </summary>
    ''' <param name="operador">
    ''' Ver la sobrecarga que recibe dos parámetros para los operadores aceptados
    ''' </param>
    ''' <param name="numeros">
    ''' Array de tipo Double con los valores a evaluar
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function Evalua(ByVal operador As String, _
                                       ByVal ParamArray numeros() As Double) As Double

        Select Case operador.ToLower
            Case "+"
                Return Suma(numeros)
            Case "-", "_"
                Return Resta(numeros)
            Case "*", "x"
                Return Multiplica(numeros)
            Case "/" ', "\" ', ":", "÷"
                Return Divide(numeros)
            Case "\"
                Return DivideEnteros(numeros)
            Case "^"
                Return Math.Pow(numeros(0), numeros(1))
            Case "\u623?", "m"
                Return numeros(0) Mod numeros(1)
            Case "%"
                Return Porcentaje(numeros(0), numeros(1))
        End Select
    End Function


    '
    '--------------------------------------------------------------------------
    ' Métodos de instancia protegidos (podían hacerse públicos, pero...)
    '--------------------------------------------------------------------------
    '

    ''' <summary>
    ''' Calcula el porcentaje de los números indicados
    ''' 100 % 10 será el 10% de 100
    ''' </summary>
    ''' <param name="num1">
    ''' Número del que se calculará el porcentaje
    ''' </param>
    ''' <param name="num2">
    ''' Porcentaje a calcular de num1
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Porcentaje(ByVal num1 As Double, ByVal num2 As Double) As Double
        Return num1 * num2 / 100
    End Function

    ''' <summary>
    ''' Suma todos los valores indicados
    ''' </summary>
    ''' <param name="parametros">
    ''' Array de números a sumar
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Suma(ByVal ParamArray parametros() As Double) As Double
        Dim t As Double = parametros(0)
        For i As Integer = 1 To parametros.Length - 1
            t = t + parametros(i)
        Next
        Return t
    End Function

    ''' <summary>
    ''' Resta al primer valor todos los demás
    ''' </summary>
    ''' <param name="parametros">
    ''' Array de números a restar
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Resta(ByVal ParamArray parametros() As Double) As Double
        Dim t As Double = parametros(0)
        For i As Integer = 1 To parametros.Length - 1
            t = t - parametros(i)
        Next
        Return t
    End Function

    ''' <summary>
    ''' Multiplica todos los valores
    ''' </summary>
    ''' <param name="parametros">
    ''' Array de números a multiplicar
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Multiplica(ByVal ParamArray parametros() As Double) As Double
        Dim t As Double = parametros(0)
        For i As Integer = 1 To parametros.Length - 1
            t = t * parametros(i)
        Next
        Return t
    End Function

    ''' <summary>
    ''' Divide todos los valores
    ''' </summary>
    ''' <param name="parametros">
    ''' Array de números a dividir
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Divide(ByVal ParamArray parametros() As Double) As Double
        Dim t As Double = parametros(0)
        For i As Integer = 1 To parametros.Length - 1
            t = t / parametros(i)
        Next
        Return t
    End Function

    ''' <summary>
    ''' División entera
    ''' </summary>
    ''' <param name="parametros"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' v1.0.0.10
    ''' 11/Oct/07
    ''' </remarks>
    Protected Function DivideEnteros(ByVal ParamArray parametros() As Double) As Long
        Dim t As Long = CLng(parametros(0))
        For i As Integer = 1 To parametros.Length - 1
            t = t \ CLng(parametros(i))
        Next
        Return t
    End Function

    ''' <summary>
    ''' Eleva el primer número a la potencia indicada
    ''' </summary>
    ''' <param name="numero"></param>
    ''' <param name="pot"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Potencia(ByVal numero As Double, ByVal pot As Double) As Double
        Return numero ^ pot
        'Return Math.Pow(numero, pot)
    End Function

    '
    '--------------------------------------------------------------------------
    ' Métodos públicos compartidos
    '--------------------------------------------------------------------------
    '

    ''' <summary>
    ''' Este es el método principal al que debe llamarse para evaluar la expresión
    ''' internamente se crea una instancia de la clase y se llama al método Evalua.
    ''' </summary>
    ''' <param name="expresion">
    ''' La expresión a evaluar
    ''' </param>
    ''' <param name="mostrarInfo">
    ''' True si se quiere mostrar detalle de las operaciones realizadas
    ''' False (valor por defecto) para no mostrar detalle
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function Evaluar(ByVal expresion As String, _
                                   ByVal mostrarInfo As Boolean _
                                   ) As String
        Dim ev As New EvaluarExpresiones
        Dim exp As String = ev.Evalua(expresion, mostrarInfo)

        ' Ya no se debe cambiar nada                        v.20    (12/Oct/07)
        ' se usa la cultura Invariant
        'If sepDec = "," Then
        '    exp = exp.Replace(".", ",")
        'End If

        Return exp
    End Function

    Public Shared Function Evaluar(ByVal expresion As String) As String
        Return Evaluar(expresion, False)
    End Function

    ''' <summary>
    ''' Devuelve el número indica en hexadecimal
    ''' </summary>
    ''' <param name="num"></param>
    ''' <param name="cifras">
    ''' Número de cifras a mostrar
    ''' (predetermniado 16)
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function DecToHex(ByVal num As Object, Optional ByVal cifras As Integer = 16) As String
        Dim d As Double = CDbl(num)
        If Double.IsNaN(d) OrElse Double.IsInfinity(d) Then
            Return "NaN"
        End If
        Return CLng(num).ToString("X" & cifras.ToString).TrimStart("0"c)
    End Function

    ''' <summary>
    ''' Devuelve el valor decimal del número hexadecimal indicado
    ''' </summary>
    ''' <param name="num"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' v.22
    ''' 12/Oct/07
    ''' </remarks>
    Public Shared Function HexToDec(ByVal num As Object) As String
        Dim d As Double = CDbl(num)
        If Double.IsNaN(d) OrElse Double.IsInfinity(d) Then
            Return "NaN"
        End If
        Return ("&H" & num.ToString)
    End Function


    ''' <summary>
    ''' Devuelve el número indica en octal
    ''' </summary>
    ''' <param name="num"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function DecToOct(ByVal num As Object) As String
        Dim d As Double = CDbl(num)
        If Double.IsNaN(d) OrElse Double.IsInfinity(d) Then
            Return "NaN"
        End If
        Return Oct(num)
    End Function

    ''' <summary>
    ''' Devuelve el valor decimal del número octal indicado
    ''' </summary>
    ''' <param name="num"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' v.22
    ''' 12/Oct/07
    ''' </remarks>
    Public Shared Function OctToDec(ByVal num As Object) As String
        Dim d As Double = CDbl(num)
        If Double.IsNaN(d) OrElse Double.IsInfinity(d) Then
            Return "NaN"
        End If
        Return ("&O" & num.ToString)
    End Function

    ''' <summary>
    ''' Devuelve el número indicado en binario
    ''' </summary>
    ''' <param name="num">
    ''' Número a convertir en binario
    ''' (primero se convierte a Int64)
    ''' </param>
    ''' <param name="cifras">
    ''' Cantidad de cifras máximas a mostrar
    ''' (predeterminado 32)
    ''' </param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function DecToBin(ByVal num As Object, Optional ByVal cifras As Integer = 32) As String
        Dim d As Double = CDbl(num)
        If Double.IsNaN(d) OrElse Double.IsInfinity(d) Then
            Return "NaN"
        End If

        Dim s As New StringBuilder
        Dim n As Long = CLng(num)
        If n = 0 Then
            Return "0"
        End If

        Dim j As Integer = 0

        For i As Integer = cifras - 1 To 0 Step -1
            If ((n And CInt(2 ^ i)) <> 0) Then
                s.Append("1")
            Else
                s.Append("0")
            End If
            j = j + 1
            If j = 4 Then
                j = 0
                s.Append(" ")
            End If
        Next
        Return s.ToString.TrimStart(" 0".ToCharArray)
    End Function

    ''' <summary>
    ''' Convierte un número binario en decimal
    ''' </summary>
    ''' <param name="sDec"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function BinToDec(ByVal sDec As String) As Long
        Dim i, j As Integer
        Dim n As Long

        For j = sDec.Length - 1 To 0 Step -1
            If (sDec(j) = "1"c) Then
                n += CInt(2 ^ i)
                i += 1
            ElseIf (sDec(j) <> " "c) Then
                i += 1
            End If
        Next
        Return n
    End Function


    ''' <summary>
    ''' La versión actual de la DLL (revisión)
    ''' </summary>
    ''' <param name="conNombre">
    ''' Opcional (False) si se debe devolver el nombre del ensamblado
    ''' </param>
    ''' <returns>
    ''' La versión de la DLL (versión de FileVersion)
    ''' </returns>
    ''' <remarks></remarks>
    Public Shared Function Version(ByVal conNombre As Boolean) As String
        Dim ensamblado As Assembly = Assembly.GetExecutingAssembly
        Dim fvi As FileVersionInfo = FileVersionInfo.GetVersionInfo(ensamblado.Location)
        If conNombre Then
            Return ensamblado.GetName.Name & " v" & fvi.FileVersion
        Else
            Return " v" & fvi.FileVersion
        End If
    End Function

    ''' <summary>
    ''' La versión actual de la DLL (revisión)
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' v.18
    ''' 12/Oct/07
    ''' Lo convierto en función con dos sobrecargas
    ''' El único código que rompe es el de C#...
    ''' </remarks>
    Public Shared Function Version() As String
        Return Version(False)
    End Function


    ' Definir el separador decimal, según la cultura actual         (09/Oct/07)
    ' Pongo que use InvariantCulture                        v.17    (12/Oct/07)
    Public Shared ReadOnly Property SeparadorDecimal() As String
        Get
            Return sepDec
        End Get
    End Property

    Protected Shared sepDec As String

    ' Crear el objeto Random al iniciar la instancia compartida
    ' (primera vez que se use)
    Protected Shared m_Rnd As Random

    ' Usar InvariantCulture para evitar el follón de los decimales  (12/Oct/07)
    Shared Sub New()
        m_Rnd = New Random
        Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture
        sepDec = Thread.CurrentThread.CurrentCulture.NumberFormat.CurrencyDecimalSeparator
    End Sub

d End Class