/** * Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT * All rights reserved. Use is subject to license terms. See LICENSE.TXT */ package org.diirt.datasource.formula; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.TokenStream; import org.diirt.datasource.expression.DesiredRateExpression; import org.diirt.datasource.expression.DesiredRateExpressionList; import org.diirt.datasource.expression.DesiredRateExpressionListImpl; import org.diirt.util.text.StringUtil; /** * The abstract syntax tree corresponding to a formula expression. * This class provides a logical representation of the expression, * static factory methods to create such expressions from text representation * (i.e. parsing) and the ability to convert to datasource expressions. * * @author carcassi */ public class FormulaAst { /** * The type of a formula AST node. */ public enum Type { /** * An operator/function node */ OP, /** * A String literal node */ STRING, /** * An integer literal node */ INTEGER, /** * A floating point literal node */ FLOATING_POINT, /** * A channel node */ CHANNEL, /** * An id node */ ID}; private final Type type; private final List<FormulaAst> children; private final Object value; private FormulaAst(Type type, List<FormulaAst> children, Object value) { this.type = type; this.children = children; this.value = value; } /** * The type of the AST node. * * @return the node type */ public Type getType() { return type; } /** * The value corresponding to the node. The value depends on the type * as follows: * <ul> * <li>OP: String with the name of the function/operator</li> * <li>STRING: the String constant (unquoted)</li> * <li>INTEGER: the Integer constant</li> * <li>FLOATING_POINT: the Double constant</li> * <li>CHANNEL: String with the channel name (unquoted)</li> * <li>ID: String with the name of the id</li> * </ul> * @return the value of the node */ public Object getValue() { return value; } /** * The children of this node, if IO, null otherwise. * * @return the node children; null if no children */ public List<FormulaAst> getChildren() { return children; } /** * Lists all the channel names used in the AST. * * @return a list of channel names */ public List<String> listChannelNames() { List<String> names = new ArrayList<>(); listChannelNames(names); return Collections.unmodifiableList(names); } private void listChannelNames(List<String> names) { switch(getType()) { case OP: for (FormulaAst child : getChildren()) { child.listChannelNames(names); } break; case CHANNEL: names.add((String) getValue()); default: } } /** * A STRING node from a quoted token. * * @param token the quoted string * @return the new node */ public static FormulaAst stringFromToken(String token) { return string(StringUtil.unquote(token)); } /** * A STRING node representing the given string. * * @param unquotedString the string * @return the new node */ public static FormulaAst string(String unquotedString) { return new FormulaAst(Type.STRING, null, unquotedString); } /** * An INTEGER node from a token. * * @param token a string parsable to an integer * @return the new node */ public static FormulaAst integerFromToken(String token) { return integer(Integer.parseInt(token)); } /** * An INTEGER node from the given value. * * @param integer the integer value * @return the new node */ public static FormulaAst integer(int integer) { return new FormulaAst(Type.INTEGER, null, integer); } /** * A FLOATING_POINT node from a token. * * @param token a string parseable to a double * @return the new node */ public static FormulaAst floatingPointFromToken(String token) { return floatingPoint(Double.parseDouble(token)); } /** * A FLOATING_POINT node from the given value. * * @param floatingPoint the double value * @return the new node */ public static FormulaAst floatingPoint(double floatingPoint) { return new FormulaAst(Type.FLOATING_POINT, null, floatingPoint); } /** * A CHANNEL node from a quoted token. * * @param token the quoted channel name * @return the new node */ public static FormulaAst channelFromToken(String token) { return channel(StringUtil.unquote(token)); } /** * A CHANNEL node representing the given channel name. * * @param channelName the channel name * @return the new node */ public static FormulaAst channel(String channelName) { return new FormulaAst(Type.CHANNEL, null, channelName); } /** * An ID node representing the given id. * * @param id the id * @return the new node */ public static FormulaAst id(String id) { return new FormulaAst(Type.ID, null, id); } /** * An OP node representing the given operator/function with the given * arguments. * * @param opName the name of the operator/function * @param children the node children * @return the new node */ public static FormulaAst op(String opName, FormulaAst... children) { return op(opName, Arrays.asList(children)); } /** * An OP node representing the given operator/function with the given * arguments. * * @param opName the name of the operator/function * @param children the node children * @return the new node */ public static FormulaAst op(String opName, List<FormulaAst> children) { return new FormulaAst(Type.OP, children, opName); } /** * Creates a parser for the given text. * * @param text the string to be parsed * @return the new parser */ static FormulaParser createParser(String text) { CharStream stream = new ANTLRStringStream(text); FormulaLexer lexer = new FormulaLexer(stream); TokenStream tokenStream = new CommonTokenStream(lexer); return new FormulaParser(tokenStream); } /** * The AST corresponding to the parsed formula. * * @param formula the string to be parsed * @return the parsed AST */ public static FormulaAst formula(String formula) { FormulaAst ast = staticChannel(formula); if (ast != null) { return ast; } formula = formula.substring(1); try { ast = createParser(formula).formula(); if (ast == null) { throw new IllegalArgumentException("Parsing failed"); } return ast; } catch (Exception ex) { throw new IllegalArgumentException("Error parsing formula: " + ex.getMessage(), ex); } } private static FormulaAst staticChannel(String formula) { if (formula.startsWith("=")) { return null; } if (formula.trim().matches(StringUtil.SINGLEQUOTED_STRING_REGEX)) { return channel(formula.trim()); } return channel(formula); } /** * The AST corresponding to a single channel, if the formula represents one, * or null, if the formula is not a single channel. * * @param formula the string to be parsed * @return the parsed AST */ public static FormulaAst singleChannel(String formula) { FormulaAst ast = staticChannel(formula); if (ast != null) { return ast; } formula = formula.substring(1); try { ast = createParser(formula).singleChannel(); return ast; } catch (Exception ex) { return null; } } /** * Converts the AST to a datasource expression. * * @return the new expression */ public DesiredRateExpression<?> toExpression() { switch(getType()) { case CHANNEL: return new LastOfChannelExpression<>((String) getValue(), Object.class); case FLOATING_POINT: return org.diirt.datasource.vtype.ExpressionLanguage.vConst((Double) getValue()); case INTEGER: return org.diirt.datasource.vtype.ExpressionLanguage.vConst((Integer) getValue()); case STRING: return org.diirt.datasource.vtype.ExpressionLanguage.vConst((String) getValue()); case ID: return ExpressionLanguage.namedConstant((String) getValue()); case OP: DesiredRateExpressionList<Object> expressions = new DesiredRateExpressionListImpl<>(); for (FormulaAst child : getChildren()) { expressions.and(child.toExpression()); } return ExpressionLanguage.function((String) getValue(), expressions); default: throw new IllegalArgumentException("Unsupported type " + getType() + " for ast"); } } /** * Returns a new AST where the channel nodes that match the keys of the map * are substituted with the values of the map. * * @param substitutions from channel name to new AST * @return a new AST */ public FormulaAst substituteChannels(Map<String, FormulaAst> substitutions) { switch(getType()) { case CHANNEL: FormulaAst sub = substitutions.get((String) getValue()); if (sub == null) { sub = this; } return sub; case OP: FormulaAst[] subs = new FormulaAst[getChildren().size()]; for (int i = 0; i < subs.length; i++) { subs[i] = getChildren().get(i).substituteChannels(substitutions); } return op((String) getValue(), subs); default: return this; } } @Override public String toString() { switch(getType()) { case OP: return FormulaFunctions.format((String) getValue(), new AbstractList<String>() { @Override public String get(int index) { return getChildren().get(index).toString(); } @Override public int size() { return getChildren().size(); } }); default: return getValue().toString(); } } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof FormulaAst) { FormulaAst other = (FormulaAst) obj; return Objects.equals(getType(), other.getType()) && Objects.equals(getValue(), other.getValue()) && Objects.equals(getChildren(), other.getChildren()); } else { return false; } } @Override public int hashCode() { int hash = 3; hash = 89 * hash + Objects.hashCode(this.type); hash = 89 * hash + Objects.hashCode(this.children); hash = 89 * hash + Objects.hashCode(this.value); return hash; } }