Ir al contenido principal

Analizador de expresiones algebraicas recursivo decendente


Como les mencione en un post previo, estoy leyendo el libro el arte de programar en Java, el primer ejercicio consiste en un analizador de expresiones algebraicas recursivo descendente, el mismo consiste en la posibilidad de tomar una cadena que contenga una expresión matemática, la misma puede contener valores en punto flotante, sumar, restar, dividir, multiplicar, sacar exponente (potencia), uso de paréntesis para priorizar una operación, etc.

A continuación clase a clase, con una pequeña explicación

Lo primero que definiremos es una suite de excepciones para reportar errores, no tiene mucha ciencia, hay una para la division entre cero, cuando no existe una expresión valida, error de sintaxis o cuando los paréntesis no se encuentran balanceados, veamos





package cap2;

/**
 * Exception para reportar que hay al intentar dividir entre cero
 *
 * User: jsanca
 * Date: 4/16/13
 * Time: 1:30 AM
 * @author jsanca
 */
public class DividedByZeroException extends RuntimeException {

    public DividedByZeroException() {
    }

    public DividedByZeroException(String message) {
        super(message);
    }

    public DividedByZeroException(String message, Throwable cause) {
        super(message, cause);
    }

    public DividedByZeroException(Throwable cause) {
        super(cause);
    }
}

//////

package cap2;

/**
 * Exception para reportar que no Hay expresion presente
 *
 * User: jsanca
 * Date: 4/16/13
 * Time: 1:30 AM
 * @author jsanca
 */
public class NonExpressionDefinedException extends RuntimeException {

    public NonExpressionDefinedException() {
    }

    public NonExpressionDefinedException(String message) {
        super(message);
    }

    public NonExpressionDefinedException(String message, Throwable cause) {
        super(message, cause);
    }

    public NonExpressionDefinedException(Throwable cause) {
        super(cause);
    }
}


////////

package cap2;

/**
 * Exception para reportar que hay un error de sintaxis
 *
 * User: jsanca
 * Date: 4/16/13
 * Time: 1:30 AM
 * @author jsanca
 */
public class SintaxErrorException extends RuntimeException {

    public SintaxErrorException() {
    }

    public SintaxErrorException(String message) {
        super(message);
    }

    public SintaxErrorException(String message, Throwable cause) {
        super(message, cause);
    }

    public SintaxErrorException(Throwable cause) {
        super(cause);
    }
}

package cap2;

/**
 * Exception para reportar que hay parentesis desbalanceados
 *
 * User: jsanca
 * Date: 4/16/13
 * Time: 1:30 AM
 * @author jsanca
 */
public class UnbalanceParenthesesException extends RuntimeException {

    public UnbalanceParenthesesException() {
    }

    public UnbalanceParenthesesException(String message) {
        super(message);
    }

    public UnbalanceParenthesesException(String message, Throwable cause) {
        super(message, cause);
    }

    public UnbalanceParenthesesException(Throwable cause) {
        super(cause);
    }
}

Seguidamente veremos el parser; el mismo contiene un constructor donde pasaras la formula a evaluar, seguidamente se normalizara (se eliminan los espacios).

Lo siguiente que podrás ver, es el TokenType. Este enumera los tipos de tokens, delimilador (un operador), variables (no soportadas en este ejemplo), números, fin de ecuación, etc.

Lo tercero son las variables de la clase, la expresión antes mencionada, el índice que se utilizara como veras mas adelante para recorrer la expresión índice a índice, el token (el numero, operador, etc, actual, en análisis) y el tipo de token como antes mencionamos.

Seguimos con la función "getResult" única función publica con la que el usuario puede interactuar, lo primer que hace la función es pedir el primer token, si este es fin de ecuación disparamos un excepción pues no debería ser (en este caso una expresión vacía), seguidamente por recursividad llamamos a evalExp2 (ya veras que lo que hace el algoritmo es encadenas descendentemente del operador de menor prioridad al de mayor, es decir vamos de sumas y restas, hasta multiplicación, exponentes, etc), esta evalExp2 debes conceptualizarla como la evaluacion de una suma si esta existe, pero antes buscara funciones de mayor prioridad; evalExp3.

EvalExp3 presenta un panorama similar, en este vemos como se evalúa las operaciones de multiplicar, division y modulo, sin no antes llamar antes a evalExp4.

La versión 4, evalúa la potencia, el exponente; así mismo este llamara al evalExp5 que evalua los algún signo de un operador como + o - (positivo o negativo).

Por ultimo evalExp6, evaluara paréntesis y valores atómicos, en nuestro caso solo números. En el caso de los paréntesis tomar en cuenta que cuando se encuentra un match, se vuelve a iniciar el proceso a través de un llamado recursivo a evalExp2, que evaluara la expresión dentro de los paréntesis.

No estoy seguro si entendiste la dinámica del algoritmo, pero este se basa en llamar primero a la operación de menor prioridad hasta ir directamente a la de mayor prioridad (paréntesis y números, el eslabón final en la cadena de la ecuación).

Lo ultimo que veras es el nexToken, este analizador resulta muy sencillo básicamente evalúa el carácter actual, en el caso de los delimitadores simplemente lo tome y suma uno mas para ir al siguiente índice en la llamada a nextToken consiguiente, en el caso de los números o variables (no soportadas en el ejemplo); se sacaran todos los digitos hasta encontrar otra cosa (un delimitador en este caso), esta función sera llamada a medida que se necesite analizar el siguiente token.

Pues ahora al código:

package cap2;

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * Representa el parser para las expresiones
 * * User: jsanca
 * Date: 4/16/13
 * Time: 12:29 AM
 *
 * @author jsanca
 */
public class Parser {

    /**
     * Constructor.
     *
     * @param expresion String
     */
    public Parser(String expresion) {

        this.expresion = this.normalizeExpresion(expresion);
    }


    // define los tipos de token
    private enum TokenType {

        NONE, DELIMITER, VARIABLE, NUMBER, EOE
    }

    private String expresion = null;
    private int expresionIndex = 0;
    private String token = null;
    private TokenType tokenType = null;

    private Set delimiters = new HashSet(Arrays.asList(new Character[]{'+', '-', '/', '*', '%', '^', '=', '(', ')'}));

    private String normalizeExpresion(String expr) {

        return expr.replaceAll("\\s*", "");
    }

    /**
     * Gets the results
     *
     * @return double
     */
    public double getResult() {

        double result = 0;

        this.expresionIndex = 0;
        this.nextToken();

        if (this.tokenType == TokenType.EOE) {

            throw new NonExpressionDefinedException();
        }

        // evalua la expresion
        result = this.evalExp2();

        if (this.tokenType != TokenType.EOE) {

            throw new SintaxErrorException();
        }

        return result;
    } // getResult.

    /**
     * Suma o resta terminos
     *
     * @return double
     */
    private double evalExp2() {

        char operator;
        double result;
        double partialResult;

        // llamado decendente a la operacion de multiplicar
        result = this.evalExp3();

        while ( (this.tokenType != TokenType.EOE) &&
                ((operator = this.token.charAt(0)) == '+' || operator == '-')) {

            this.nextToken();
            partialResult = evalExp3();

            switch (operator) {

                case '-':

                    result = result - partialResult;
                    break;

                case '+':

                    result = result + partialResult;
                    break;

            }
        }

        return result;
    } // evalExp2.

    /**
     * Multiplica y divide
     *
     * @return double
     */
    private double evalExp3() {

        char operator;
        double result;
        double partialResult;

        // llamado decendente a la operacion de exponente
        result = this.evalExp4();


        while ( (this.tokenType != TokenType.EOE) &&
                ((operator = this.token.charAt(0)) == '*' || operator == '/' || operator == '%')) {

            this.nextToken();
            partialResult = evalExp4();

            switch (operator) {

                case '*':

                    result = result * partialResult;
                    break;

                case '/':

                    if (partialResult == 0.0) {

                        throw new DividedByZeroException();
                    }

                    result = result / partialResult;
                    break;

                case '%':

                    if (partialResult == 0.0) {

                        throw new DividedByZeroException();
                    }

                    result = result % partialResult;
                    break;

            }
        }

        return result;
    } // evalExp3,

    /**
     * Evalua el exponente
     *
     * @return double
     */
    private double evalExp4() {

        double result;
        double partialResult;

        // llamado decendente a la operacion de signo (unitario)
        result = this.evalExp5();

        if ("^".equals(this.token)) {

            this.nextToken();
            partialResult = this.evalExp4();

            if (partialResult == 0.0) { // x^0.0 = 1.0

                result = 1.0; // caso trivial
            } else {

                result =
                        Math.pow(result, partialResult);
            }
        }

        return result;
    } // evalExp4.

    /**
     * Evalua el signo de un valor + 0  -
     *
     * @return
     */
    private double evalExp5() {

        double result;
        String operator = "";

        if ((this.tokenType == TokenType.DELIMITER) &&
                "+".equals(this.token) || "-".equals(this.token)) {

            operator = this.token;
            this.nextToken();
        }

        // decendente parentesis
        result = this.evalExp6();

        if ("-".equals(operator)) {

            result = -result;
        }

        return result;
    } // evalExp5.

    /**
     * Evalua los parentesis.
     *
     * @return double
     */
    private double evalExp6() {

        double result;

        if ("(".equals(this.token)) {

            this.nextToken();
            result = this.evalExp2();

            if (!")".equals(this.token)) {

                throw new UnbalanceParenthesesException();
            }

            this.nextToken();

        } else {

            result = this.getAtomicValue();
        }

        return result;
    } // evalExp6.

    /**
     * Obtiene un valor numerico atomico
     *
     * @return double
     */
    private double getAtomicValue() {

        double result = 0.0;

        if (this.tokenType == TokenType.NUMBER) {

            result = Double.parseDouble(this.token);

            this.nextToken();
        } else {

            throw new SintaxErrorException();
        }

        return result;
    } // getAtomicValue.


    /**
     * Obtiene el siguiente token
     */
    protected void nextToken() {

        this.tokenType = TokenType.NONE;
        this.token = "";

        if (!isTheEndOfExpresion()) {

            // Si es un operador
            if (this.isDelimiter(this.expresion.charAt(this.expresionIndex))) {

                this.parseDelimiter();

            } else if (this.isVariable(this.expresion.charAt(this.expresionIndex))) { // si es una variable.

                this.parseVariable();

            } else if (this.isNumber(this.expresion.charAt(this.expresionIndex))) { // si es un numero.

                this.parseNumber();

            } else {

                this.tokenType = TokenType.EOE; // caracter desconocido.
            }
        } else {

            this.tokenType = TokenType.EOE;
        }

        System.out.println(MessageFormat.format("token {0} and type {1}", this.token, this.tokenType));

    } // nextToken.

    private void parseFactor() {

        while (!this.isTheEndOfExpresion() && !this.isDelimiter(this.expresion.charAt(this.expresionIndex))) {

            this.token += this.expresion.charAt(this.expresionIndex);
            this.expresionIndex++;
        }
    }

    protected void parseNumber() {

        this.parseFactor();

        this.tokenType = TokenType.NUMBER;
    }


    protected void parseVariable() {

        this.parseFactor();

        this.tokenType = TokenType.VARIABLE;
    }

    protected void parseDelimiter() {

        this.token +=
                this.expresion.charAt(this.expresionIndex);

        this.expresionIndex++;
        this.tokenType = TokenType.DELIMITER;
    }

    private boolean isNumber(final char c) {

        return Character.isDigit(c);
    } // isNumber,

    private boolean isDelimiter(final char c) {

        return this.delimiters.contains(c);
    } // isDelimiter.

    private boolean isVariable(final char c) {

        return Character.isLetter(c);
    } // isVariable.

    private boolean isTheEndOfExpresion() {

        return (this.expresionIndex >= this.expresion.length());
    } // isTheEndOfExpresion.


} // E:O:F:Parser.


Comentarios

Unknown dijo…
Seria interesante ampliar el tema para gramaticas no solo LL(1) , sino con parsers bottom up , para analisis de SLR, LALR, Y LOOKAHEAD
Unknown dijo…
Este comentario ha sido eliminado por el autor.
Unknown dijo…
Ya que un top down parser se queda cortillo para ciertas gramatic as
ArtEze dijo…
Hola, muy bueno, pero estaría bueno pasarlo a C o C++, o javascript... ¿Te animarías?

Entradas más populares de este blog

Pasos para remover Postgresql 8.3 en MAC OS

Tomado de: http://forums.enterprisedb.com/posts/list/1437.page In Mac OSX: (Assuming Default Locations) Via uninstaller: 1) In the installation directory, there will be a uninstall-postgresql.app file will be there, executing (double clicking) that will uninstall the postgresql installation. Manual Uninstallation: 1) Stop the server sudo /sbin/SystemStarter stop postgresql-8.3 2) Remove menu shortcuts: sudo rm -rf /Applications/PostgreSQL 8.3 3) Remove the ini file sudo rm -rf /etc/postgres-reg.ini 4) Removing Startup Items sudo rm -rf /Library/StartupItems/postgresql-8.3 5) Remove the data and installed files sudo rm -rf /Library/PostgreSQL/8.3 6) Delete the user postgres sudo dscl . delete /users/postgres

Validaciones con HTML5 sin necesidad de form.submit

Como parte de HTML5 existe la posibilidad de agregar información a los inputs de un form, para realizar validaciones; podemos indicar si queremos que sea requerido, con el tipo de datos; number, email, etc restringimos los valores que pueden ser agregados, podemos usar alguna mascara para validaciones, colocar mensajes de error custom, etc (en la red existen muchos ejemplos acerca de como customizar formularios). Ahora bien pongamos en contexto, tengo un formulario como este: <form name="managerForm"  id="managerForm">              <p>                  Name:                 <input id="managerNameText" required="required" placeholder="Write here the new manager name" size="40"/>              </p>             <p>                 Email:                 <input id="emailText" required="required" placeholder="myemail@myserver.com" type="email" />

Inventario anual de bebidas

Hola gente, Solo quería compartir mi inventario anual de bebidas (así conocer gustos), excluyendo algunas cervecillas que tengo por ahí guardadas, este es mi inventario: Ron: Flor de Cana 1 botella 5 anos. 2 botellas 7 anos una pacha 7 anos 2 botellas 12 anos 1 botella 18 anos Ron Zacapa 15 anos Centenario pachita 7 anos Centanario pachita 12 anos Bacardi limon Bacardi Razz Ron abuelo 7 anos Bacardi superior 1862 Ron Boltran XL Ron Centenario Garrafon Ron Jamaica Appleton 7 anos Ron Jamaica Appleton 12 anos (muchisimas gracias a Mayra :) Capitan Morgan Rum Jumbie, coconnut splash Ron coconut Malibu Ron Tequila Milagro Silver (muchisimas gracias a Pablito :) Sauza Gold Sauza Reposado Don Julio Reposado Vino Luigi Borer Malbec 2006 Casillero del Diablo, Caberut Sauviguon 2009 Vodka 2 botellas smirnoff y una smirnoff con sabor cranberry Cremas y otro licores Cahuita pacha Amaretto Barinet Licor de menta Licor de agave Rancho Escondido Bayleys 2 botellas (muchisimas gracias a Brian B :) Li