En la materia de ensambladores nos piden armar un ensamblador de juguete [aunque "funcional" en realidad no sirve para nada], pero bueno, el chiste es que se realiza en fases.
La fase 1 contempla simplemente separar los elementos de un archivo de texto y ordenarlos en una lista.
En el moodle dice textualmente:
CONSIDERACIONES:
- Los comentarios que comienzan con punto y coma no se deben considerar, por lo que se deben eliminar.
- Los separadores de elementos son: el espacio, los dos puntos y la coma.
- Los siguientes elementos son compuestos: data segment, code segment, stack segment, dup(xxx), [xxx], "xxxx", 'xxxx'. Estos no se deben separar y deben mostrarse de esa manera en la lista que despliega el programa.
Así pues, la tarea es relativamente sencilla.
Si nos ponemos analizar cada carácter, nos vamos a meter en muchos problemas al darnos cuenta que las restricciones de que abra y cierre corchetes, comillas dobles o simples, que si la cadena tiene un espacio en el medio pero empieza por data, code o stack debemos juntarla con lo que sigue... Que si los punto y coma se deben eliminar... Pues bueno, ya me entienden los que hayan tratado de hacer algo parecido.
Así pues me di a la tarea de buscar cómo hacerlo más sencillo.
Basado en el curso de compiladores y las utilidades de herramientas como flex y bison que usan expresiones regulares, pues seguramente debería haber una forma de usarlas en un lenguaje como java.
Las expresiones regulares no son más que formas de "buscar" o decidir si una cadena cumple con una restricción que nosotros querramos... Asumiré que para cuando necesiten esta información ya sabrán cómo funcionan.estas cosas.
Con esta idea, podemos realizar expresiones regulares que nos agrupen elementos, que nos separen las palabras y que reemplacen los comentarios.
Lo primero es separar los elementos "compuestos"
Lo que necesitamos es que si encontramos "data" o code, o stack, siga un espacio [ya sea tabulador o espacio] y después segment, y si se lo desea, pues evitar que haya algo más que no sean espacios.
Pondré entre paréntesis las cosas para separarlas, esto no es código, sino mi forma de decir "esto y luego aquello" (esto)(aquello). Espero darme a entender.
Y para decir esto o aquello usaré la barra vertical.
esto|aquello
Sería algo como
(data|code|stack)(segment)
Obviamente en lenguaje ensamblador no importa si son mayúsculas o minúsculas, debe poder aceptarse igual.
Bien, pues empecemos con lo bueno.
En java, las expresiones regulares les llaman REGEX, así pueden buscarlo en google por mayor información.
Comencemos por decir qué es un patrón.
Un patrón no es más que una expresión regular que podemos usar con otras instrucciones.
Su declaración es la siguiente:
Pattern patronOro = Pattern.compile("dame oro");
Los patrones siempre van acompañados de un "matcher" que es el encargado de aplicar la expresión regular a algo.
Matcher matcherOro = patronOro.matcher(linea);
El método para crearlo recibe como parámetro un string, en este caso se llama línea, que contiene el texto a evaluar.
Ahora, para probar que está bien, hagamos una cadena y también un pequeño if...
String linea = "dame oro";
if(matcherOro.matches()){
System.out.println("Te doy oro :D");
}
Verán que imprime "Te doy oro" en la consola.
Cambien el contenido de la cadena y verán que no entra.
Ahora, ¿qué pasa si queremos que ignore mayúsculas o minúsculas?
Lo de menos es usar el método toLowerCase() para pasarlo todo a minúsculas y luego cuadrarlo, pero las expresiones regulares tienen modificadores para eso.
El modificador para ignorar mayus, es (?i), se identifican por estar encerrados en paréntesis, empezar con un signo de interrogación y luego una letra, existen otros, pero en esta publicación sólo usaremos este.
Cambiemos el patrón a este...
Pattern patronOro = Pattern.compile("(?i)dame oro");
Cambien el string de prueba y verán que sigue cumpliendo.
Hasta ahora no nos sirve de nada esto, podemos hacerlo con un compareToIgnoreCase() verdad?
Hagámoslo más interesante, qué tal que estamos en una partida de Age of Empires, y no sólo necesitamos oro, sino también alimento porque somos persas...
Los Regex tienen operadores lógicos como el or y el and... etc, igualito que los if, usan la barra |, el & etc...
Cambiemos el patrón a:
Pattern patronOro = Pattern.compile("(?i)(dame oro)|(dame piedra)");
Como verán encerramos las cadenas que queremos comprobar entre paréntesis, esto sirve para dos cosas... Para separar una cadena de otra, y para usar el concepto de "grupos".
Va, ya puedo decir dame oro y dame piedra, en mayus, minus, mezclado etc...
¡Pero mi compañero siempre me da oro! ¿Si le pido piedra, cómo puedo hacer que él me de piedra y no oro? [Aunque con el oro podría comprarla... Pero ese no es el punto]
Pues bien, mencionando los grupos, podemos saber qué cosa cumple con una condición haciendo uso de ellos.
La clase Matcher tiene otro método que se llama group()
Este método nos regresa el último grupo que cumplió con la condición...
Modificando el código anterior un poco, escribamos en el if, la nueva instrucción..
if(matcherOro.matches()){
System.out.println(matcherOro.group());
}
Como vemos, aparece en pantalla el string que cumplió con la condición, cambien en el string línea a su gusto y verán que aparece lo mismo.
Bueno, ya sabemos identificar grupos e ignorar mayúsculas, y podemos actuar en consecuencia.
Esto de los grupos nos sirve para poder "guardar" alguna parte de interés de la cadena, ya sea para reemplazar algo, para copiarlo... No sé...
Bueno, pero hasta ahora, las cadenas de prueba están casi hardcodeadas... ¡Yo quiero que sean flexibles!
Vamos a suponer que en el nuevo Age of Empires, no nos sabemos los recursos existentes... puede ser cualquier nombre... Conozcamos otra forma de decir "or"
En los REGEX de java, podemos agrupar cosas entre corchetes para decir "OR"
Cambiemos el patrón a...
Pattern patronOro = Pattern.compile("(?i)(dame) [abc]");
Nótese que hay un espacio entre el dame y los corchetes... Ese espacio es de hecho el espacio que queremos entre frases... "dame" "a" o "dame" "b"
Los corchetes nos dicen "a" o "b" o "c"...
Cambiemos la cadena de prueba a ...
Luego a...
Y así... noten que cuando ponen:
String linea = "DaMe AB";
No será cierta la expresión regular... Esto es porque dijimos a, b ó c, pero sólo 1 vez alguna de ellas...
Para definir cuántas veces queremos que aparezcan, tenemos varias formas, la primera, son los cuantificadores.
Estos son según la API de Java 1.4.2:
X? | X, una o ninguna. |
X* | X, cero o más veces |
X+ | X, una o más veces |
X{n} | X, exactamente n veces |
X{n,} | X, por lo menos n veces |
X{n,m} | X, al menos n y máxi mo m veces... |
Donde X es una parte de la expresión regular.
Entonces...
Pattern patronOro = Pattern.compile("(?i)(dame) [abc]*");
Nos permitirá cualquier cantidad de a,b y c juntas...
desde la cadena vacía hasta abcbacbacbacbbbbbcccaaa... cualquiera cantidad.
Pattern patronOro = Pattern.compile("(?i)(dame) [abc]{1,2}");
Al menos 1 vez y máximo 2... es decir, puede ser "dAme ab" o "dame bc" o "DaMe ac" pero no "DAME abc" porque ya son 3 letras...
Pattern patronOro = Pattern.compile("(?i)(dame) (abc)*");
Nos aceptará "dame abcabcabcabc" y "dame " [con el espacio...] Pero no "dame abcbca"
Podemos también concatenar cosas...
Pattern patronOro = Pattern.compile("(?i)(dame) [a][b][c]");
Es lo mismo que:
Pattern patronOro = Pattern.compile("(?i)(dame) abc");
Porque dice "a" seguido de "b" seguido de "c"
Pero si ponemos
Pattern patronOro = Pattern.compile("(?i)(dame)[ab]c");
Estamos diciendo "a" o "b" seguido de c
Supongamos que el nuevo recurso del age no se sabe qué letras tiene, por lo que tenemos que poner todo el abecedario...
¡Imagien poner de la a a la z!
Pattern patronOro = Pattern.compile("(?i)(dame) [abcdefghijklmnopqrstuvwxyz]*");
Bueno, ya no lo imagen, ya lo puse...
Eso está muy feo verdad?
Bueno, pues los REGEX nos permiten poner rangos...
La declaración anterior es igual a esta...
Pattern patronOro = Pattern.compile("(?i)(dame) [a-z]");
Y podemos incluir otras cosas, suponiendo que no nos gusta el modificador (?i)
Podríamos escribir...
Pattern patronOro = Pattern.compile("(dame) [a-zA-Z]*");
Esto es... de "a" a "z" minus ó de "A" a "Z" mayus... [Obviamente el dame sí tendría que estar en minúsculas]
Esto también aplica a números, por lo que podemos poner 0-9
Java tiene un pequeño problema a la hora de poner
[a-z0-9] ya que una z seguida de un 0 no sería válido... Cosas del lenguaje.
La solución es poner un guión bajo..
[a-z_0-9]
Aún así, es muy complicado poner todos esos rangos, ¿no?
Java tiene otras formas de escribirlos, que son las siguientes:
[Me da flojera traducirlo, creo que se entiende con mínimos conocimientos de inglés...]
. | Punto, cualquier carácter incluyendo signos, enter, fin de archivo... |
\d | A digit: [0-9] |
\D | A non-digit: [^0-9] |
\s | A whitespace character: [ \t\n\x0B\f\r] |
\S | A non-whitespace character: [^\s] |
\w | A word character: [a-zA-Z_0-9] |
\W | A non-word character: [^\w] |
La línea...
Pattern patronOro = Pattern.compile("(dame) [a-zA-Z_0-9]*");
Es equivalente a:
Pattern patronOro = Pattern.compile("(dame) \\w*");
Y si queremos quitar el espacio hardcodeado...
Pattern patronOro = Pattern.compile("(dame)\\s\\w*");
Eso es la palabra "dame" seguida de un espacio [ya sea tab o espacio normal] y una serie de letras o números.
Nótese que agregamos una barra antes de la barra w, esto es para decirle a java que queremos usar un conjunto predefinido...
También podemos negar cosas y hacer intersecciones, etc...
[abc] a, b, or c (simple class)
[^abc] Any character except a, b, or c (negation)
[a-zA-Z] a through z or A through Z, inclusive (range)
[a-d[m-p]] a through d, or m through p: [a-dm-p] (union)
[a-z&&[def]] d, e, or f (intersection)
[a-z&&[^bc]] a through z, except for b and c: [ad-z](subtraction)
[a-z&&[^m-p]] a through z, and not m through p: [a-lq-z]subtraction)
Y... ¿cómo le hacemos si queremos usar signos?
Pues les ponemos doble barra, como a los conjuntos.
Por ejemplo
String linea = "DaMe (abc)";
Para poder verificar eso...
Pattern patronOro = Pattern.compile("(?i)(dame) \\(abc\\)");
Y para String linea = "DaMe (a" seguido de b o c y cerrar paréntesis...
Por ejemplo algo del tipo: "dame (ab)"
"dame (ac)"
Pattern patronOro = Pattern.compile("(?i)(dame) \\(a[bc]\\)");
Existen también conjuntos para especificar signos y caracteres...
Usando POSIX...
POSIX character classes (US-ASCII only) |
\p{Lower} | A lower-case alphabetic character: [a-z] |
\p{Upper} | An upper-case alphabetic character:[A-Z] |
\p{ASCII} | All ASCII:[\x00-\x7F] |
\p{Alpha} | An alphabetic character:[\p{Lower}\p{Upper}] |
\p{Digit} | A decimal digit: [0-9] |
\p{Alnum} | An alphanumeric character:[\p{Alpha}\p{Digit}] |
\p{Punct} | Punctuation: One of !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ |
\p{Graph} | A visible character: [\p{Alnum}\p{Punct}] |
\p{Print} | A printable character: [\p{Graph}] |
\p{Blank} | A space or a tab: [ \t] |
\p{Cntrl} | A control character: [\x00-\x1F\x7F] |
\p{XDigit} | A hexadecimal digit: [0-9a-fA-F] |
\p{Space} | A whitespace character: [ \t\n\x0B\f\r] |
En fin, pueden jugar con ello las veces que quieran...
Como última nota, hay que recordar que los REGEX de java son de tipo greedy, es decir, aplica la expresión regular a toda la cadena.
Por lo tanto...
Si ponemos
".*d"
Nos dará cualquier cadena que termine con d, aunque d esté incluído en el punto que es "cualquier cosa".
Lo mismo si ponemos
"a\\w*a"
Esto es todas las cadenas que empiecen y terminen con a, sin importar que a está incluído en las letras del conjunto \w...
Ahora ya podemos aplicarlo a nuestro proyecto sabiendo qué es un grupo y cómo identificamos un elemento u otro.
Para el ejemplo de los segmentos...
La expresión regular vendría a ser...
Pattern patronSegmentos = Pattern.compile("(?i)(data segment)|(code segment)|(stack segment)");
Recordemos que no podemos poner [data segment][code segment][stack segment]
Porque esto nos daría a escoger en el primer caso "d" o "a" o "t" o "s" o "g"... etc...
Tampoco podemos hacer
[data segment|code segment]
Así mismo, agrupamos las cadenas con paréntesis, que nos servirá para 2 cosas:
Agrupar, y saber cuál de las palabras se leyó...
String linea = "data segment";
Pattern patronSegmentos = Pattern.compile("(?i)(data segment)|(code segment)|(stack segment)");
Matcher matcherSegmentos = patronSegmentos.matcher(linea);
if(matcherSegmentos.matches()){
System.out.println(matcherSegmentos.group());
}
Si vemos, la palabra segment se repite en todas las declaraciones por lo que podemos reescribirlo así:
Pattern patronSegmentos = Pattern.compile("(?i)((data)|(code)|(stack))\\ssegment");
Hay que poner atención que estamos agrupando data code y stack en un paréntesis externo, esto es porque de poner esto... [sin los paréntesis exteriores]
Pattern patronSegmentos = Pattern.compile("(?i)((data)|(code)|(stack))\\ssegment");
Java tomaría como que (stack)\\ssegment es una declaración completa, por lo que sólo funcionaría en el caso de stack segment.
Bueno, ya tenemos la parte de identificar los elementos "compuestos".
Supongamos que queremos ahora identificar los elementos que estén entre corchetes, porque si hay espacios dentro de ellos no debemos separarlos...
Por ejemplo [ax,bx], debemos mostrarlo así y no
[ax
bx]
La expresión regular para identificar esto es:
Pattern patronCorchetes = Pattern.compile("\\s*\\[([\\w\\s\\p{Punct}]*)\\]");
Lo que quiere decir... Si viene uno o más espacios en blanco, seguido de un corchete que abre, agrupoa lo que está dentro de los corchetes... y lo de dentro de los corchetes debe cumplir que sea, o un caracter, o un espacio, o un signo de puntuación una o más veces...
La parte \\p{Punct} es la expresión para decir "cualquier signo de puntuación" está un poco más arriba donde vimos los conjuntos de caracteres, dígitos etc...
Pues bien, ya sólo queda ponernos a chambear y separar los elementos que nos piden...
Espero que les haya sido de ayuda esto.
Para complicarlo un poco más...
La siguiente expresión valida cosas entre corchetes, o, entre comillas dobles o entre comillas simples...
Pattern.compile("[\\w\\s]*((\\[[\\w\\s\\p{Punct}]*\\])|(\'[\\w\\s\\p{Punct}]*\')|(\"[\\w\\s\\p{Punct}]*\"))[\\w\\s]*");
Esto es:
Cualquier cantidad de espacios y letras hasta encontrar un corchete que abre o comillas simples o comillas dobles con letras, espacios o signos dentro... seguido de letras o espacios...
Como pueden notar, se repiten los patrones del medio de espacios en blanco, puntuación y palabras... ¿Por qué no juntar los | con corchetes?
Pattern.compile("[\\w\\s]*([\\[\'\"][\\w\\s\\p{Punct}]*[\\[\'\"])[\\w\\s]*");
Bueno, esto nos hace más pequeño el código, pero trae consigo otro problema, qué tal que el usuario nos da algo como esto:
"press any key...$\'
Que comienza con dobles y termina con simples...
Pues bien, el código pequeño lo aceptaria, por lo que optamos por dejar el anterior que aunque repetía patrones, nos aseguraba que empezara y terminara con el mismo carácter.
Y para identificar una cadena antes de los elementos " o ' o [
Pattern inicio = Pattern.compile("([\\w\\s]*)((\\[|\"|\\').*)");
matcherInicio.matches();
String nueva = matcherInicio.group(1);
Eso hará el trabajo.
¡Saludos y nos vemos en la siguiente situación problemática!