El código de la clase AgruparPares

'------------------------------------------------------------------------------
' Agrupar pares                                                     (22/Sep/07)
' Le cambio el nombre de la clase a ProcesarGrupos                  (23/Sep/07)
'
' Clase para manejar los grupos de textos en las palabras clave de Eliza
' Reducida en funcionalidad para la clase EvaluarExpresiones
'
' Vuelvo a cambiarle el nombre a AgruparPares                       (02/Oct/07)
' La usaré como clase base para ProcesarGrupos en ElizaNET.
'
' Lo incluyo en una DLL con EvaluarExpresiones                      (04/Oct/07)
' Cambio el ámbito a Friend
'
' ©Guillermo 'guille' Som, 2007
'
' El espacio de nombre de esta librería es:
' elGuille.Developer
'------------------------------------------------------------------------------
Option Strict On

Imports Microsoft.VisualBasic

Imports System

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

'--------------------------------------------------------------------------
' Para procesar los grupos de paréntesis
'--------------------------------------------------------------------------

''' <summary>
''' Delegado para el evento de la clase AgruparPares
''' </summary>
''' <param name="mensaje">
''' El mensaje de notificación
''' </param>
''' <remarks></remarks>
Friend Delegate Sub AgruparParesEventHandler(ByVal mensaje As String)

''' <summary>
''' Agrupar las cadenas que estén entre los pares indicados.
''' Por ejemplo, para agrupar lo que está entre paréntesis,
''' de esa forma si hay anidamiento se puede procesar bien.
''' </summary>
''' <remarks>
''' La idea original la usé en el conversor de VB a C#
''' después lo puse como case independiente (ProcesarGrupos)
''' para el programa ElizaNET, finalmente reduje la clase
''' para usarla en EvaluarExpresiones.
''' </remarks>
Friend Class AgruparPares

    Public Event ErrorAgruparPares As AgruparParesEventHandler

    Protected Overridable Sub OnErrorAgruparPares(ByVal mensaje As String)
        RaiseEvent ErrorAgruparPares(mensaje)
    End Sub

    Protected m_Sustituciones As New List(Of String)
    Protected m_Texto As String
    Protected m_TextoSustituido As String
    ' En esta clase solo se usa este símbolo para agrupar pares
    ' (si se quiere hacer más genérica, hay que permitir otros símbolos,
    ' en ese caso hay que tener en cuenta el valor de la cadena sSeparadores)
    Protected Const m_Simbolo As Char = "·"c
    ' Para que permita hasta 9999 niveles                           (03/Oct/07)
    Protected Const formatoNivel As String = "0000"
    Protected Const m_MarcadorInicio As String = "ñ" & m_Simbolo
    Protected Const m_MarcadorFin As String = m_Simbolo & "ñ"


    ''' <summary>
    ''' Devuelve True si la sustitución indicada tiene marcadores
    ''' </summary>
    ''' <param name="index"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' 01/Oct/07
    ''' </remarks>
    Public Function ContieneMarcador(ByVal index As Integer) As Boolean
        Return m_Sustituciones(index).Contains(m_MarcadorInicio)
    End Function

    ''' <summary>
    ''' Los caracteres usados para indicar un marcador
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' 01/Oct/07
    ''' </remarks>
    Public ReadOnly Property MarcadorInicio() As String
        Get
            Return m_MarcadorInicio
        End Get
    End Property

    Public ReadOnly Property LenFormato() As Integer
        Get
            Return formatoNivel.Length
        End Get
    End Property

    ''' <summary>
    ''' Longitud del texto usado como marcador
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property LenMarcador() As Integer
        Get
            Return formatoNivel.Length + 4
        End Get
    End Property

    ''' <summary>
    ''' El marcador correspondiente al índice indicado
    ''' </summary>
    ''' <param name="index">
    ''' Índice el marcador,
    ''' será un valor entre cero y Sustituciones.Count - 1
    ''' (ambos incluidos)
    ''' </param>
    ''' <returns></returns>
    ''' <remarks>
    ''' Se usa de forma interna para hacer las sustituciones
    ''' pero lo dejo público por si se quiere usar desde fuera
    ''' </remarks>
    Public Function Marcador(ByVal index As Integer) As String
        Return m_MarcadorInicio & index.ToString(formatoNivel) & m_MarcadorFin
    End Function

    ''' <summary>
    ''' El texto de cada sustitución
    ''' Este será el texto a poner en cada marcador
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Sustituciones() As List(Of String)
        Get
            Return m_Sustituciones
        End Get
    End Property

    ''' <summary>
    ''' Obtiene las sustituciones
    ''' Si invertido = False es como llamar a la propiedad Sustituciones
    ''' </summary>
    ''' <param name="invertido">
    ''' True si se quiere invertir el orden de las sustituciones.
    ''' False para obtener lo mismo que llamando a la propiedad Sustituciones,
    ''' en realidad solo tiene utilidad si el valor es True
    ''' </param>
    ''' <returns>
    ''' Una lista con los grupos correspondientes a las sustituciones
    ''' </returns>
    ''' <remarks></remarks>
    Public Function GetSustituciones(ByVal invertido As Boolean) As List(Of String)
        If invertido Then
            Dim col As New List(Of String)
            col.AddRange(m_Sustituciones)
            col.Reverse()
            Return col
        End If
        Return m_Sustituciones
    End Function

    ''' <summary>
    ''' Cadena con el texto original
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Texto() As String
        Get
            Return m_Texto
        End Get
    End Property

    ''' <summary>
    ''' El texto con las sustituciones hechas
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property TextoSustituido() As String
        Get
            Return m_TextoSustituido
        End Get
    End Property

    ''' <summary>
    ''' Constructor privado para usar desde GetAgruparPares
    ''' </summary>
    ''' <remarks></remarks>
    Protected Sub New()
    End Sub

    '-------------------------
    ' Los métodos compartidos
    '-------------------------

    ''' <summary>
    ''' El texto que se ha indicado para analizar
    ''' Es el mismo que Texto, pero en este caso, se usa para 
    ''' los mensajes de error que se produzcan al analizar sub grupos
    ''' que estén mal anidados o que no se permitan
    ''' </summary>
    ''' <remarks></remarks>
    Protected Shared TextoInicial As String

    ''' <summary>
    ''' Obtiene un objeto con los grupos del texto indicado
    ''' </summary>
    ''' <param name="texto">
    ''' Texto a analizar en los que se incluyen grupos
    ''' </param>
    ''' <param name="parIni">
    ''' Caracteres para indicar el inicio de un grupo
    ''' (será la llave de apertura seguida de un asterisco {* )
    ''' </param>
    ''' <param name="parFin">
    ''' Caracteres para indicar el final de un grupo
    ''' (será la llave de cierre })
    ''' </param>
    ''' <returns>
    ''' Devuelve una instancia de esta clase
    ''' </returns>
    ''' <remarks>
    ''' La única forma de crear un objeto de esta clase 
    ''' es por medio de este método
    ''' </remarks>
    Public Shared Function CrearInstancia(ByVal texto As String, _
                                          ByVal parIni As String, _
                                          ByVal parFin As String _
                                          ) As AgruparPares
        TextoInicial = texto
        Return getAgruparPares(texto, parIni, parFin)
    End Function

    ''' <summary>
    ''' Obtiene un objeto con los grupos del texto indicado
    ''' Este método es para uso interno, ya que desde esta clase
    ''' se crean nuevas clases, pero no deben cambiar el valor de TextoInicial
    ''' </summary>
    ''' <param name="texto">
    ''' Texto a analizar en los que se incluyen grupos
    ''' </param>
    ''' <param name="parIni">
    ''' Caracteres para indicar el inicio de un grupo
    ''' (será la llave de apertura seguida de un asterisco {* )
    ''' </param>
    ''' <param name="parFin">
    ''' Caracteres para indicar el final de un grupo
    ''' (será la llave de cierre })
    ''' </param>
    ''' <returns>
    ''' Devuelve una instancia de esta clase
    ''' </returns>
    ''' <remarks>
    ''' Este método se usa internamente en la clase
    ''' </remarks>
    Protected Shared Function getAgruparPares(ByVal texto As String, _
                                              ByVal parIni As String, _
                                              ByVal parFin As String _
                                              ) As AgruparPares
        Dim pGr As New AgruparPares()
        pGr.m_Texto = texto

        ' Si no tiene ninguno de los caracteres                 (01/Oct/07)
        If texto.IndexOfAny((parIni & parFin).ToCharArray) = -1 Then
            texto = parIni & texto & parFin
        End If

        Dim i, j As Integer
        Dim s1 As String
        Dim sb As New StringBuilder(texto)

        i = 0
        j = 0

        Do
            ' Buscar el último
            i = sb.ToString.LastIndexOf(parIni)
            If i > -1 Then
                ' buscar el siguiente
                j = sb.ToString.IndexOf(parFin, i + 1)
                ' si no está el de cierre, se supone el final...
                ' aunque se debería producir un error
                If j = -1 Then
                    Throw New ArgumentException( _
                            "Los caracteres de apertura y cierre no están emparejados," & _
                            " falta uno de cierre.")
                    Return Nothing
                End If

                pGr.m_Sustituciones.Add(sb.ToString.Substring(i, j - i + parFin.Length))
                s1 = pGr.Marcador(pGr.Sustituciones.Count - 1)
                ' quitar lo hallado
                sb.Remove(i, j - i + parFin.Length)
                ' insertar la variable
                sb.Insert(i, s1)
            Else
                Exit Do
            End If
        Loop

        pGr.m_TextoSustituido = sb.ToString

        Return pGr
    End Function

End Class