Operand.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2010  AAVSO (http://www.aavso.org/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package org.aavso.tools.vstar.vela;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;

import org.aavso.tools.vstar.util.prefs.NumericPrecisionPrefs;

/**
 * VeLa: VStar expression Language
 *
 * A class that represents typed operands.
 * 
 * Note: should cache Operand instances
 */
public class Operand {

    private Type type;
    private long intVal;
    private double doubleVal;
    private String stringVal;
    private boolean booleanVal;
    private List<Operand> listVal;
    private FunctionExecutor functionVal;

    public static Operand EMPTY_LIST = new Operand(Type.LIST, Collections.emptyList());

    public static Operand NO_VALUE = new Operand(Type.NONE, false);

    public Operand(Type type, long value) {
        this.type = type;
        intVal = value;
    }

    public Operand(Type type, double value) {
        this.type = type;
        doubleVal = value;
    }

    public Operand(Type type, String value) {
        this.type = type;
        stringVal = value;
    }

    public Operand(Type type, boolean value) {
        this.type = type;
        booleanVal = value;
    }

    public Operand(Type type, List<Operand> value) {
        this.type = type;
        listVal = value;
    }

    public Operand(Type type, FunctionExecutor value) {
        this.type = type;
        functionVal = value;
    }

    // For object copy
    private Operand() {
    }

    /**
     * @return the type
     */
    public Type getType() {
        return type;
    }

    /**
     * @param type the type to set
     */
    public void setType(Type type) {
        this.type = type;
    }

    /**
     * @param intVal the intVal to set
     */
    public void setIntegerVal(long intVal) {
        this.intVal = intVal;
    }

    /**
     * @param doubleVal the doubleVal to set
     */
    public void setDoubleVal(double doubleVal) {
        this.doubleVal = doubleVal;
    }

    /**
     * @return the intVal
     */
    public long intVal() {
        return intVal;
    }

    /**
     * @return the doubleVal
     */
    public double doubleVal() {
        return doubleVal;
    }

    /**
     * @param stringVal the stringVal to set
     */
    public void setStringVal(String stringVal) {
        this.stringVal = stringVal;
    }

    /**
     * @return the stringVal
     */
    public String stringVal() {
        return stringVal;
    }

    /**
     * @param booleanVal the booleanVal to set
     */
    public void setBooleanVal(boolean booleanVal) {
        this.booleanVal = booleanVal;
    }

    /**
     * @return the booleanVal
     */
    public boolean booleanVal() {
        return booleanVal;
    }

    /**
     * @return the listVal
     */
    public List<Operand> listVal() {
        return listVal;
    }

    /**
     * @param listVal the listVal to set
     */
    public void setListVal(List<Operand> listVal) {
        this.listVal = listVal;
    }

    /**
     * @return the functionVal
     */
    public FunctionExecutor functionVal() {
        return functionVal;
    }

    /**
     * @param functionVal the functionVal to set
     */
    public void setFunctionVal(FunctionExecutor functionVal) {
        this.functionVal = functionVal;
    }

    /**
     * Convert this operand to the required type, if possible, first making a copy
     * if the required type is different from the current type.
     * 
     * @param requiredType The required type.
     * @return The unchanged operand or a new operand that conforms to the required
     *         type.
     */
    public Operand convert(Type requiredType) {
        Operand operand = this;

        if (!type.isComposite()) {
            if (type != requiredType) {
                if (type == Type.INTEGER && requiredType == Type.REAL) {
                    operand = new Operand(Type.REAL, (double) intVal);
                } else if (type != Type.STRING && requiredType == Type.STRING) {
                    operand = operand.convertToString();
                }
            }
        }

        return operand;
    }

    /**
     * Convert this operand's type to string.
     */
    public Operand convertToString() {
        assert type == Type.INTEGER || type == Type.REAL || type == Type.BOOLEAN;

        Operand operand = this;

        switch (type) {
        case INTEGER:
            operand = new Operand(Type.STRING, Long.toString(intVal));
            break;
        case REAL:
            operand = new Operand(Type.STRING, NumericPrecisionPrefs.formatOther(doubleVal));
            break;
        case BOOLEAN:
            operand = new Operand(Type.STRING, Boolean.toString(booleanVal));
            break;
        default:
            break;
        }

        return operand;
    }

    public String toHumanReadableString() {
        String str = "";

        switch (type) {
        case INTEGER:
            str = Long.toString(intVal);
            break;
        case REAL:
            str = NumericPrecisionPrefs.formatOther(doubleVal);
            break;
        case BOOLEAN:
            str = booleanVal ? "True" : "False";
            break;
        case STRING:
            str = stringVal;
            break;
        case LIST:
            str = listVal.toString().replace(",", "");
            break;
        case FUNCTION:
            str = functionVal.toString();
            break;
        default:
            break;
        }

        return str;
    }

    @Override
    public String toString() {
        String str = "";

        switch (type) {
        case INTEGER:
            str = Long.toString(intVal);
            break;
        case REAL:
            str = NumericPrecisionPrefs.formatOther(doubleVal);
            break;
        case BOOLEAN:
            str = booleanVal ? "True" : "False";
            break;
        case STRING:
            str = "\"" + stringVal + "\"";
            break;
        case LIST:
            str = listVal.toString().replace(",", "");
            break;
        case FUNCTION:
            str = functionVal.toString();
            break;
        default:
            break;
        }

        return str;
    }

    /**
     * Given a VeLa type and a Java object, return an Operand instance.
     * 
     * @param type The VeLa type.
     * @param obj  The Java object.
     * @return A corresponding Operand instance.
     */
    public static Operand object2Operand(Type type, Object obj) {
        Operand operand = null;

        try {
            switch (type) {
            case INTEGER:
                if (obj instanceof Integer) {
                    operand = new Operand(Type.INTEGER, (int) obj);
                } else {
                    operand = new Operand(Type.INTEGER, (long) obj);
                }
                break;
            case REAL:
                if (obj instanceof Float) {
                    operand = new Operand(Type.REAL, (float) obj);
                } else {
                    operand = new Operand(Type.REAL, (double) obj);
                }
                break;
            case BOOLEAN:
                operand = new Operand(Type.BOOLEAN, (boolean) obj);
                break;
            case STRING:
                operand = new Operand(Type.STRING, (String) obj);
                break;
            case LIST:
                List<Operand> arr = new ArrayList<Operand>();

                try {
                    if (obj.getClass() == Type.FLOAT_ARR.getClass()) {
                        for (float n : (float[]) obj) {
                            arr.add(new Operand(Type.REAL, n));
                        }
                    } else if (obj.getClass() == Type.DBL_ARR.getClass()) {
                        for (double n : (double[]) obj) {
                            arr.add(new Operand(Type.REAL, n));
                        }
                    } else if (obj.getClass() == Type.DBL_CLASS_ARR.getClass()) {
                        for (Double n : (Double[]) obj) {
                            arr.add(new Operand(Type.REAL, n));
                        }
                    } else if (obj.getClass() == Type.INT_ARR.getClass()) {
                        for (int n : (int[]) obj) {
                            arr.add(new Operand(Type.INTEGER, n));
                        }
                    } else if (obj.getClass() == Type.LONG_ARR.getClass()) {
                        for (long n : (long[]) obj) {
                            arr.add(new Operand(Type.INTEGER, n));
                        }
                    } else if (obj.getClass() == Type.BOOL_ARR.getClass()) {
                        for (boolean b : (boolean[]) obj) {
                            arr.add(new Operand(Type.BOOLEAN, b));
                        }
                    } else if (obj.getClass() == Type.STR_ARR.getClass()) {
                        for (String s : (String[]) obj) {
                            arr.add(new Operand(Type.STRING, s));
                        }
                    }
                } finally {
                    if (!arr.isEmpty()) {
                        operand = new Operand(Type.LIST, arr);
                    }
                }
                break;
            case FUNCTION:
                operand = new Operand(Type.FUNCTION, (FunctionExecutor) obj);
                break;
            case OBJECT:
                // TODO
                break;
            case NONE:
                operand = NO_VALUE;
                break;
            }
        } catch (Exception e) {
            java2VeLaTypeError(obj.getClass(), type);
        }

        return operand;
    }

    /**
     * Return a Java object corresponding to this Operand instance.
     * 
     * @param javaType The target Java type, that may be necessary to know.
     * @return A corresponding Java object.
     */
    public Object toObject(Class<?> javaType) {
        Object obj = null;

        switch (type) {
        case INTEGER:
            obj = numericVeLaToJava(javaType, intVal);
            break;
        case REAL:
            obj = numericVeLaToJava(javaType, doubleVal);
            break;
        case BOOLEAN:
            if (javaType == boolean.class) {
                obj = booleanVal;
            } else {
                vela2JavaTypeError(this, javaType);
            }
            break;
        case STRING:
            if (javaType == String.class || javaType == CharSequence.class) {
                obj = stringVal;
            } else {
                vela2JavaTypeError(this, javaType);
            }
            break;
        case LIST:
            try {
                if (javaType == Type.DBL_ARR.getClass() || javaType == Type.DBL_CLASS_ARR.getClass()) {
                    double[] reals = new double[listVal.size()];
                    list2Array(Type.REAL, (op, i) -> {
                        reals[i++] = op.doubleVal;
                    });
                    obj = reals;
                } else if (javaType == Type.INT_ARR.getClass()) {
                    long[] ints = new long[listVal.size()];
                    list2Array(Type.INTEGER, (op, i) -> {
                        ints[i++] = op.intVal;
                    });
                    obj = ints;
                } else if (javaType == Type.BOOL_ARR.getClass()) {
                    boolean[] booleans = new boolean[listVal.size()];
                    list2Array(Type.BOOLEAN, (op, i) -> {
                        booleans[i++] = op.booleanVal;
                    });
                    obj = booleans;
                } else if (javaType == Type.STR_ARR.getClass()) {
                    String[] strings = new String[listVal.size()];
                    list2Array(Type.STRING, (op, i) -> {
                        strings[i++] = op.stringVal;
                    });
                    obj = strings;
                }
            } catch (Exception e) {
                vela2JavaTypeError(this, javaType);
            }
            break;
        case FUNCTION:
            // TODO
            break;
        case OBJECT:
            // TODO
            break;
        case NONE:
            // TODO
            break;
        }

        return obj;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (booleanVal ? 1231 : 1237);
        long temp;
        temp = Double.doubleToLongBits(doubleVal);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        result = prime * result + ((functionVal == null) ? 0 : functionVal.hashCode());
        result = prime * result + (int) (intVal ^ (intVal >>> 32));
        result = prime * result + ((listVal == null) ? 0 : listVal.hashCode());
        result = prime * result + ((stringVal == null) ? 0 : stringVal.hashCode());
        result = prime * result + ((type == null) ? 0 : type.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Operand other = (Operand) obj;
        if (booleanVal != other.booleanVal)
            return false;
        if (Double.doubleToLongBits(doubleVal) != Double.doubleToLongBits(other.doubleVal))
            return false;
        if (functionVal == null) {
            if (other.functionVal != null)
                return false;
        } else if (!functionVal.equals(other.functionVal))
            return false;
        if (intVal != other.intVal)
            return false;
        if (listVal == null) {
            if (other.listVal != null)
                return false;
        } else if (!listVal.equals(other.listVal))
            return false;
        if (stringVal == null) {
            if (other.stringVal != null)
                return false;
        } else if (!stringVal.equals(other.stringVal))
            return false;
        if (type != other.type)
            return false;
        return true;
    }

    public Operand copy() {
        Operand operand = new Operand();

        operand.type = type;

        switch (type) {
        case INTEGER:
            operand.intVal = intVal;
            break;
        case REAL:
            operand.doubleVal = doubleVal;
            break;
        case BOOLEAN:
            operand.booleanVal = booleanVal;
            break;
        case STRING:
            operand.stringVal = stringVal;
            break;
        case LIST:
            List<Operand> list = new ArrayList<Operand>();
            for (Operand op : listVal) {
                list.add(op.copy());
            }
            operand.listVal = list;
            break;
        case FUNCTION:
            operand.functionVal = functionVal;
            break;
        default:
            break;
        }

        return operand;
    }

    // Helpers

    private Number numericVeLaToJava(Class<?> javaType, Object obj) {

        Number num = null;

        if (javaType.equals(int.class)) {
            num = ((Number) obj).intValue();
        } else if (javaType.equals(long.class)) {
            num = ((Number) obj).longValue();
        } else if (javaType.equals(float.class)) {
            num = ((Number) obj).floatValue();
        } else if (javaType.equals(double.class)) {
            num = ((Number) obj).doubleValue();
        } else {
            throw new VeLaEvalError("Cannot convert object to numeric value");
        }

        return num;
    }

    /**
     * Build an array from a VeLa list via an assigner object carrying out type
     * conversions where necessary/possible. Concrete assigner objects exploit the
     * existence of an array in their environment (see toObject()).
     *
     * The purpose of this function is to avoid code duplication in toObject().
     *
     * @param requiredType The array element type required.
     * @param assigner     An object that assigns the appropriately typed value from
     *                     an operand to the ith element of an array.
     */
    private void list2Array(Type requiredType, BiConsumer<Operand, Integer> assigner) {
        for (int i = 0; i < listVal.size(); i++) {
            Operand op = listVal.get(i);
            op = op.convert(requiredType);
            if (op.type == requiredType) {
                assigner.accept(op, i);
            } else {
                throw new VeLaEvalError("Cannot convert from " + op.type + " to " + requiredType);
            }
        }
    }

    private static void vela2JavaTypeError(Operand op, Class<?> requiredType) {
        throw new VeLaEvalError("Cannot convert from " + op.type + " to " + requiredType);

    }

    private static void java2VeLaTypeError(Class<?> jtype, Type vtype) {
        throw new VeLaEvalError("Cannot convert from " + jtype + " to " + vtype);

    }
}