• No se han encontrado resultados

Ejemplo adicional

In document Java soluciones de programación (página 61-70)

Aunque en el ejemplo anterior se muestra el mecanismo básico para la división en fi chas de una cadena, no ilustra la verdadera capacidad y fl exibilidad del método. Por esto, se incluye un ejemplo sustancialmente más complejo que crea un divisor de propósito general. Este divisor puede adaptarse para dividir en fi chas casi cualquier tipo de cadena de entrada basada en casi cualquier tipo de fi cha.

El divisor de propósito general está contenido por completo dentro de una clase llamada DivisorFichas, que defi ne varios patrones integrados. Cuatro son patrones de propósito general, que dividen una cadena en palabras, números, signos de puntuación y espacios en blanco, que son similares a los usados en el ejemplo anterior. El segundo conjunto defi ne fi chas que representan un subconjunto del lenguaje Java. (Puede agregar otros patrones, si lo desea).

Se incluye una enumeración que representa el tipo de cada fi cha. El tipo describe el patrón básico con el que coincide la fi cha, como palabra, puntuación, operador, etc. Tanto la fi cha como su tipo se devuelven cada vez que se obtiene una nueva fi cha. La vinculación de una fi cha con su tipo es común en todas las situaciones de división en fi chas, excepto las más simples.

El constructor predeterminado crea un objeto que puede dividir en fi chas texto normal en español, sin acentos ni caracteres de escape. El constructor parametrizado le permite especifi car una matriz de tipos de fi cha que quiere que use el divisor, en el orden en que habrán de aplicarse. Por tanto, este constructor le permite adecuar de manera precisa un divisor para manejar casi cualquier tipo de cadena de entrada.

Con el siguiente programa se demuestra la división en fi chas de texto normal y fragmentos de programa.

// Una clase divisora de propósito general.

// Usa el comparador de límite \G para permitir que la clase // recorra la cadena de entrada de principio a fin. Puede

// adaptarse para dividir diferentes tipos de secuencias de entrada. import java.util.regex.*;

class DivisorFichas {

// Éste es el Matcher usado por el divisor. private Matcher mat;

// Aquí se definen varios patrones de fichas. Puede // agregar otros propios, si lo desea.

// EOS es the patrón que describe el final // de la cadena de entrada.

private static Pattern EOS = Pattern.compile("\\G\\z"); // El patrón desconocido busca un carácter para

// permitir que la división en fichas avance. Por supuesto, // los usuarios de DivisorFichas tienen la libertad de detenerla // si se encuentra una ficha desconocida.

private static Pattern desconocido = Pattern.compile("\\G."); // Algunos patrones generales de fichas.

private static Pattern palabra = Pattern.compile("\\G\\p{Alpha}+"); private static Pattern punt = Pattern.compile("\\G\\p{Punct}"); private static Pattern espacio = Pattern.compile("\\G\\s+"); private static Pattern numero = Pattern.compile("\\G\\d+\\.?\\d*"); // Algunos patrones relacionados con programas parecidos a Java. // Esto coincide con una palabra clave o un identificador, // que no debe empezar con un dígito.

private static Pattern pcOIden =

Pattern.compile("\\G[\\w&&\\D]\\w*"); // Especifica los varios separadores. private static Pattern separador =

Pattern.compile("\\G[( ){}\\[\\];,.]"); // Esto busca los diversos operadores. private static Pattern opUnico =

Pattern.compile("\\G[=><!~?:+\\–*/&|\\^%@]"); private static Pattern opDoble =

Pattern.compile("\\G((<=)|(>=)|(==)|(!=)|(\\|\\|)|" + "(\\&\\&)|(<<)|(>>)|(––)|(\\+\\+))");

private static Pattern opAsign =

Pattern.compile("\\G((\\+=)|(–=)|(/=)|(\\*=)|(<<=)|" + "(>>=)|(\\|=)|(&=)|(\\^=)|(>>>=))");

private static Pattern opTriple = Pattern.compile("\\G>>>"); // Esto busca una literal de cadena.

private static Pattern literalCadena = Pattern.compile("\\G\".*?\""); // Esto busca un comentario.

private static Pattern comentario =

Pattern.compile("\\G((//.*(?m)$)|(/\\*(?s).*?\\*/))"); // Esta enumeración define los tipos de ficha

// y asocia un patrón con cada tipo. // Si define otro tipo de ficha, entonces // la agrega a la enumeración.

enum TipoFicha {

// Inicializa los valores de enumeración con sus // patrones correspondientes.

PALABRA(palabra), PUNT(punt), ESPACIO(espacio), NUMERO(numero), PC_O_IDENT(pcOIden), SEPARADOR(separador),

OP_UNICO(opUnico), OP_DOBLE(opDoble), OP_TRIPLE(opTriple), OP_ASIGN(opAsign),

LITERAL_CADENA(literalCadena), COMENTARIO(comentario), FINAL(EOS), DESCONOCIDO(desconocido);

// Esto conserva el patrón asociado con cada tipo de ficha. Pattern pat;

TipoFicha(Pattern p) { pat = p;

} }

// Esta matriz contiene la lista de fichas que este // divisor buscará, en el orden en que se realizará // la búsqueda. Por tanto, el orden de los elementos // en esta matriz es importante porque determina // el orden en que se probarán las coincidencias. TipoFicha patrones[ ];

// Cada vez que se obtiene una nueva ficha, ésta // y su tipo se regresan en un objeto de tipo Ficha. class Ficha {

String ficha; // la cadena contiene la ficha TipoFicha tipo; // el tipo de la ficha }

// Esto contiene la ficha actual. Ficha fichaActual;

// Crea un DivisorFichas personalizado. Divide el texto en fichas. DivisorFichas(String cad) {

mat = desconocido.matcher(cad); // Esto describe las fichas para la // división simple de texto en fichas. TipoFicha fichasTexto[ ] = { TipoFicha.NUMERO, TipoFicha.PALABRA, TipoFicha.PUNT, TipoFicha.FINAL, TipoFicha.DESCONOCIDO }; patrones = fichasTexto; fichaActual = new Ficha( ); }

// Crea un DivisorFichas personalizado que coincide // con la lista de fichas dados los tipos pasados a dfp. DivisorFichas(String cad, TipoFicha dfp[ ]) {

mat = desconocido.matcher(cad);

// Siempre agregue FINAL y DESCONOCIDO al final // de la matriz de patrones.

TipoFicha tmp[ ] = new TipoFicha[dfp.length+2]; System.arraycopy(dfp, 0, tmp, 0, dfp.length); tmp[dfp.length] = TipoFicha.FINAL;

tmp[dfp.length+1] = TipoFicha.DESCONOCIDO; patrones = tmp;

fichaActual = new Ficha( ); }

// Este método devuelve la siguiente ficha de la // cadena de entrada. Lo que constituye una ficha // se determina con el contenido de la matriz // de patrones. Por tanto, al cambiar la matriz, // se cambia el tipo de fichas que se obtiene. Ficha obtenerFicha( ) {

// En primer lugar, omite cualquier espacio en blanco inicial. mat.usePattern(espacio).find( );

for(int i=0; i<patrones.length; i++) {

// Selecciona el siguiente patrón de ficha que se usará. mat.usePattern(patrones[i].pat);

// Ahora, pruebe a encontrar una coincidencia. if(mat.find( )) { fichaActual.tipo = patrones[i]; fichaActual.ficha = mat.group( ); break; } }

return fichaActual; // devuelve la ficha y su tipo }

}

// Demuestra DivisorFichas. class DemoDivisorFichas {

public static void main(String args[ ]) { DivisorFichas.Ficha t;

// Demuestra el texto que se dividirá en fichas. DivisorFichas divf =

new DivisorFichas("Este es un texto de ejemplo. Hoy es lunes, " + " 28 de febrero de 2008");

// Lee y despliega las fichas de texto hasta que se lee // la ficha FINAL.

System.out.println("Dividiendo texto en fichas."); do {

// Obtiene la siguiente ficha. t = divf.obtenerFicha( ); // Despliega la ficha y su tipo.

System.out.println("Ficha: " + t.ficha + "\tTipo: " + t.tipo);

} while(t.tipo != DivisorFichas.TipoFicha.FINAL); // Ahora crea un divisor para un subconjunto de Java. // Recuerde que el orden importa. Por ejemplo, un // intento para buscar un doble operador, como <= // debe presentarse antes de que se haga un intento por // buscar un solo operador, como <.

DivisorFichas.TipoFicha fichasProg[ ] = { DivisorFichas.TipoFicha.NUMERO, DivisorFichas.TipoFicha.PC_O_IDENT, DivisorFichas.TipoFicha.LITERAL_CADENA, DivisorFichas.TipoFicha.COMENTARIO, DivisorFichas.TipoFicha.OP_ASIGN, DivisorFichas.TipoFicha.OP_TRIPLE, DivisorFichas.TipoFicha.OP_DOBLE, DivisorFichas.TipoFicha.OP_UNICO, DivisorFichas.TipoFicha.SEPARADOR, };

// Demuestra la división en fichas de un programa.

divf = new DivisorFichas("// comentario\n int count=10; if(a<=b) count––;"+ "a = b >>> c; a = b >> d; resultado = meth(3);" + "w = a<0 ? b*4 : c/2; done = !done;" +

"for(int i=0; i<10; i++) sum += i;" + "String cad = \"una literal de cadena\"" + "class Prueba { /* ... */ }", fichasProg);

// Despliega cada ficha y su tipo.

System.out.println("\nDividiendo fragmentos del programa."); do { t = divf.obtenerFicha( ); System.out.println("Ficha: " + t.ficha + "\tTipo: " + t.tipo); } while(t.tipo != DivisorFichas.TipoFicha.FINAL); } }

A continuación se muestra una parte de la salida: Dividiendo texto en fichas.

Ficha: Este Tipo: PALABRA Ficha: es Tipo: PALABRA Ficha: un Tipo: PALABRA Ficha: texto Tipo: PALABRA Ficha: de Tipo: PALABRA Ficha: ejemplo Tipo: PALABRA Ficha: . Tipo: PUNT Ficha: Hoy Tipo: PALABRA Ficha: es Tipo: PALABRA Ficha: lunes Tipo: PALABRA Ficha: , Tipo: PUNT Ficha: 28 Tipo: NUMERO Ficha: de Tipo: PALABRA Ficha: febrero Tipo: PALABRA Ficha: de Tipo: PALABRA Ficha: 2008 Tipo: NUMERO Ficha: Tipo: FINAL

Dividiendo fragmentos del programa. Ficha: // comentario Tipo: COMENTARIO Ficha: int Tipo: PC_O_IDENT

Ficha: count Tipo: PC_O_IDENT Ficha: = Tipo: OP_UNICO Ficha: 10 Tipo: NUMERO Ficha: ; Tipo: SEPARADOR Ficha: if Tipo: PC_O_IDENT Ficha: ( Tipo: SEPARADOR Ficha: a Tipo: PC_O_IDENT Ficha: <= Tipo: OP_DOBLE Ficha: b Tipo: PC_O_IDENT Ficha: ) Tipo: SEPARADOR Ficha: count Tipo: PC_O_IDENT Ficha: –– Tipo: OP_DOBLE Ficha: ; Tipo: SEPARADOR Ficha: a Tipo: PC_O_IDENT Ficha: = Tipo: OP_UNICO Ficha: b Tipo: PC_O_IDENT Ficha: >>> Tipo: OP_TRIPLE .

. .

Éste es un ejemplo muy complejo y garantiza un examen a profundidad de su operación. La clase DivisorFichas defi ne estos elementos clave:

Elemento Descripción

La variable de instancia mat Contiene una referencia al Matcher que usará la instancia de DivisorFichas.

Varios patrones precompilados de fichas Son los patrones que describen los diversos tipos de fichas. La enumeración TipoFicha Esta enumeración representa el tipo de cada ficha. También

vincula a un tipo de ficha con su patrón.

La matriz patrones Esta matriz contiene un conjunto ordenado de objetos de

TipoFicha que especifican los tipos de fichas que se obtendrán.

El orden de los elementos de la matriz es el orden en que

DivisorFichas prueba los patrones, buscando una coincidencia.

La clase Ficha Esta clase de conveniencia vincula la ficha actual con su tipo. Está anidada dentro de DivisorFichas. Por tanto, para hacer referencia a ella fuera de ésta, debe calificarla con

DivisorFichas, como en DivorFichas.Fichas.

La variable de instancia fichaActual Contiene una referencia a la ficha actual, que es la obtenida por la llamada más reciente a obtenerFicha( ). Es devuelta por

obtenerFicha( ).

Los constructores DivisorFichas Construyen un divisor de fichas, el constructor de un parámetro divide en fichas el texto normal en español, sin acentos. El constructor de dos parámetros permite que el divisor sea configurado para trabajar con otros tipos de entrada. El método obtenerFicha( ) Devuelve la siguiente ficha de la cadena.

Revisemos más de cerca cada parte. En primer lugar, cada instancia de DivisorFichas tiene su propio Matcher, al que se hace referencia mediante mat. Esto signifi ca que pueden usarse dos o más divisores de fi chas dentro del mismo programa, cada uno operando de manera independiente. A continuación, observe los diversos patrones. Todas las expresiones regulares empiezan con

\G, lo que signifi ca que una secuencia coincidente debe empezar al fi nal de la coincidencia anterior. Como ya se explicó, esto permite que el motor de expresiones regulares avance por la cadena. (Si no incluye el comparador de límite \G, entonces el motor de expresiones regulares encontrará una secuencia coincidente en cualquier lugar de la cadena, omitiendo posiblemente varias fi chas en el proceso). Si agrega patrones adicionales, entonces debe estar seguro de empezar con \G.

Observe los patrones que dividen en fi chas un subconjunto de Java. El patrón pcOIdent coincide con palabras clave o identifi cadores. (No suele ser práctico distinguir entre ambos durante el proceso de división. Otras partes de un compilador o un intérprete suelen manejar esta tarea). Otros patrones coinciden con los diversos separadores u operadores. Observe que se requieren cuatro patrones diferentes para manejar los operadores, y que cada uno busca un tipo distinto de operador, incluidos los compuestos por un solo operador, por dos o por tres. Los operadores

de asignación pudieran manejarse como operadores de doble carácter, pero por razones de claridad, se les dio un patrón propio. Se proporcionan también patrones que coinciden con los comentarios y las literales de cadena.

Después que se han compilado los patrones, se crea la enumeración TipoFicha. Defi ne los tipos de fi cha que corresponden a los patrones y vinculan a cada patrón con su tipo. Esta vinculación muestra la capacidad de las enumeraciones de Java, que son tipos de clase más que simples listas de enteros con nombre (como lo son en varios otros lenguajes).

La matriz patrones contiene una lista de objetos TipoFicha que especifi can los tipos de fi cha que obtendrá el divisor. El orden de los tipos en la matriz especifi ca el orden en que DivisorFichas buscará una fi cha. Debido a que el orden en que se realiza la búsqueda puede ser importante (por ejemplo, debe buscar <= antes de <), debe cuidar el orden apropiado de la matriz. En todos los casos, las últimas dos entradas de la matriz deben ser FINAL y DESCONOCIDO.

A continuación, se defi ne la clase Ficha. Esta clase anidada no es técnicamente necesaria, pero permite que una fi cha y su tipo se encapsulen dentro de un solo objeto. La variable fi chaActual, que es una instancia de Ficha, contiene la fi cha obtenida más recientemente. obtenerFicha( ) devuelve una referencia a ella. Desde el punto de vista técnico, fi chaActual no es necesaria porque bastaría con construir un nuevo objeto de Ficha para cada fi cha obtenida, y hacer que obtenerFicha( ) devuelva el nuevo objeto. Sin embargo, cuando se divida en fi chas una cadena muy larga, se crearía un gran número de objetos y luego se descartarían. Esto daría como resultado ciclos adicionales de recolección de basura. Al emplear sólo un objeto de Ficha, se elimina esta posible inefi ciencia.

El constructor DivisorFichas de un parámetro crea un divisor que maneja el texto regular en español, sin acentos, al reducirlo a palabras, signos de puntuación y números. La cadena que habrá de dividirse en fi chas se pasa al constructor. Asigna a patrones una matriz que maneja estos elementos de texto simple.

El constructor de dos parámetros le permite especifi car una matriz de TipoFicha que se

asignará a patrones. Esto le permite confi gurar un divisor que manejará diferentes tipos de entrada. En DemoDivisorFichas, la matriz fi chasProg se construye de tal manera que puede dividir en fi chas un subconjunto de lenguaje Java. Otros tipos de división pueden crearse al especifi car una matriz tipoFicha diferente. Un tema adicional: observe que este constructor siempre agrega FINAL y DESCONOCIDO a la lista de tipos de fi cha. Esto es necesario para asegurar que obtenerFicha( ) encuentra el fi nal de la cadena y que devuelve DESCONOCIDO cuando no puede encontrar ninguno de los patrones.

El método obtenerFicha( ) obtiene la siguiente fi cha de la cadena. Siempre lo hace correctamente porque la fi cha será FINAL cuando se ha alcanzado el fi nal de la cadena y DESCONOCIDO si no se encuentran fi chas coincidentes. Tome en cuenta que obtenerFicha( ) omite cualquier espacio inicial. Por lo general, las fi chas no incluyen espacios en blanco, de modo que obtenerFicha( ) simplemente los descarta.

Para usar el divisor de fi chas, cree primero un DivisorFichas que busque las fi chas que desee. A continuación, confi gure un bucle que llame a obtenerFicha( ) en el divisor hasta que se llegue al fi nal de la cadena. Cuando esto ocurre, el tipo de la fi cha devuelta será EOS. En el programa, este procedimiento se demuestra con la clase DemoDivisorFichas. Una fi cha desconocida puede ignorarse (como en el ejemplo) o tratarse como una condición de error.

Opciones

Como se mencionó, cuando se divide en fi chas con base en delimitadores, puede usar StringTokenizer (ahora obsoleto), StreamTokenizer, Scanner o el método split( ). Si su entrada puede dividirse en fi chas con base en delimitadores, entonces es fácil implementar estas otras soluciones efectivas.

Como se ha mostrado en los ejemplos anteriores, la división en fi chas basada en patrones puede implementarse de manera efi ciente empleando las clases Pattern y Matcher defi nidas por la API de expresiones regulares. Éste es el método que prefi ero. Sin embargo, son posibles otras soluciones.

Una opción está basada en la clase Scanner, que puede usarse para división en fi chas basada en patrones al emplear su método fi ndWithinHorizon( ) para obtener una fi cha. Como se mencionó antes, por lo general Scanner se basa en delimitadores. Sin embargo, el método fi ndWithinHorizon( ) ignora éstos y trata de encontrar una coincidencia con la expresión regular que se pasa como argumento. La coincidencia se prueba dentro de una parte especifi cada de la cadena de entrada, que puede ser toda la cadena, si es necesario. Aunque este método funciona, Matcher ofrece un método más simple y directo. (Por supuesto, si quiere alternar entre división basada en delimitadores y en patrones, entonces el uso de Scanner sería la solución perfecta).

La división en fi chas basada en patrones también puede implementarse a mano, haciendo que la cadena se revise carácter por carácter, y que las fi chas se construyan carácter por carácter. Éste era el método que solía usarse antes de que se tuviera la API de expresiones regulares. Para algunos casos, aún podría ser el método más efi ciente. Sin embargo, las expresiones regulares ofrecen código sustancialmente más compacto y fácil de mantener.

Como opción predeterminada, se buscará una coincidencia en toda la cadena pasada a Matcher. Sin embargo, puede restringir la búsqueda a una región más pequeña al llamar a region( ), como se muestra aquí:

Matcher region(int inicio, int final)

El índice en que se empezará a buscar se pasa mediante inicio. La búsqueda se detiene en fi nal–1. Puede obtener los límites de búsqueda actuales al llamar a regionStart( ) y regionEnd( ).

CAPÍTULO

In document Java soluciones de programación (página 61-70)