/*
GeoGebra - Dynamic Mathematics for Everyone
http://www.geogebra.org
This file is part of GeoGebra.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation.
*/
package org.geogebra.common.cas;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.geogebra.common.cas.giac.Ggb2giac;
import org.geogebra.common.kernel.CASException;
import org.geogebra.common.kernel.CASGenericInterface;
import org.geogebra.common.kernel.CASParserInterface;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.arithmetic.ExpressionNode;
import org.geogebra.common.kernel.arithmetic.ExpressionNodeConstants;
import org.geogebra.common.kernel.arithmetic.ExpressionValue;
import org.geogebra.common.kernel.arithmetic.Function;
import org.geogebra.common.kernel.arithmetic.FunctionVariable;
import org.geogebra.common.kernel.arithmetic.Traversing;
import org.geogebra.common.kernel.arithmetic.Traversing.NonFunctionCollector;
import org.geogebra.common.kernel.arithmetic.Traversing.NonFunctionReplacer;
import org.geogebra.common.kernel.arithmetic.ValidExpression;
import org.geogebra.common.kernel.commands.EvalInfo;
import org.geogebra.common.kernel.geos.GeoCasCell;
import org.geogebra.common.kernel.geos.GeoDummyVariable;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.parser.ParseException;
import org.geogebra.common.kernel.parser.Parser;
import org.geogebra.common.kernel.parser.cashandlers.ParserFunctions;
import org.geogebra.common.main.BracketsError;
import org.geogebra.common.util.StringUtil;
import org.geogebra.common.util.debug.Log;
import org.geogebra.common.util.lang.Unicode;
/**
* Handles parsing and evaluating of input in the CAS view.
*
* @author Markus Hohenwarter
*/
public class CASparser implements CASParserInterface {
private Parser parser;
private ParserFunctions parserFunctions;
// it's defined only for command Solve with parametric equation
private int nrOfVars = 0;
/**
* Creates new CAS parser
*
* @param parser
* parser
* @param pf
* parser functions
*/
public CASparser(Parser parser, ParserFunctions pf) {
this.parser = parser;
this.parserFunctions = pf;
}
@Override
public ValidExpression parseGeoGebraCASInput(final String exp,
GeoCasCell cell) throws CASException {
CASException c;
try {
return parser.parseGeoGebraCAS(exp, cell);
} catch (ParseException e) {
Log.debug(exp);
c = new CASException(e);
c.setKey("InvalidInput");
throw c;
} catch (BracketsError e) {
c = new CASException(e);
c.setKey("UnbalancedBrackets");
throw c;
}
}
@Override
public ValidExpression parseGeoGebraCASInputAndResolveDummyVars(
final String inValue, Kernel kernel, GeoCasCell cell)
throws CASException {
if (inValue == null || inValue.length() == 0) {
return null;
}
try {
// parse input into valid expression
ValidExpression ve = parseGeoGebraCASInput(inValue, cell);
// resolve Variable objects in ValidExpression as GeoDummy objects
ExpressionValue ev = resolveVariablesForCAS(ve, kernel);
if (ev instanceof ValidExpression) {
((ValidExpression) ev).setLabel(ve.getLabel());
ve = (ValidExpression) ev;
}
// resolve Equations as Functions if lhs is y
if (ve instanceof Function) {
ve.traverse(Traversing.FunctionCreator.getCreator());
}
return ve;
// }catch (MaximaVersionUnsupportedExecption e) {
// throw e; // propagate exception
} catch (CASException ce) {
throw ce;
} catch (Throwable e) {
throw new CASException(e);
}
}
/**
* Resolves all variables in ValidExpression. Unknown variables are kept as
* symbolic variables. TODO check that we need default template here
*/
@Override
public synchronized ExpressionValue resolveVariablesForCAS(
ExpressionValue ev, Kernel kernel) {
// add local variables to kernel,
// e.g. f(a,b) := 3*a+c*b has local variables a, b
boolean isFunction = ev instanceof Function;
FunctionVariable[] funVars = null;
if (isFunction) {
Construction cmdCons = kernel.getConstruction();
funVars = ((Function) ev).getFunctionVariables();
for (FunctionVariable funVar : funVars) {
GeoElement localVarGeo = new GeoDummyVariable(cmdCons,
funVar.toString(StringTemplate.defaultTemplate));
cmdCons.addLocalVariable(
funVar.toString(StringTemplate.defaultTemplate),
localVarGeo);
}
}
// resolve variables of valid expression
kernel.setResolveUnkownVarsAsDummyGeos(true);
ev.resolveVariables(new EvalInfo(false));
kernel.setResolveUnkownVarsAsDummyGeos(false);
Set<String> nonFunctions = new TreeSet<String>();
NonFunctionCollector c = NonFunctionCollector
.getCollector(nonFunctions);
NonFunctionReplacer r = NonFunctionReplacer.getCollector(nonFunctions);
ev.traverse(c);
ExpressionValue ret = ev.traverse(r);
// remove local variables
if (isFunction) {
Construction cmdCons = kernel.getConstruction();
for (FunctionVariable funVar : funVars) {
cmdCons.removeLocalVariable(
funVar.toString(StringTemplate.defaultTemplate));
}
}
return ret;
}
/**
* Tries to convert parsed CAS output to GeoGebra syntax.
*
* @param ev
* parsed CAS output
* @param tpl
* string template
* @return GeoGebra string representation of
* @throws CASException
* in case the conversion failed
*/
public String toGeoGebraString(ExpressionValue ev, StringTemplate tpl)
throws CASException {
try {
return toString(ev, tpl);
} catch (Throwable e) {
e.printStackTrace();
throw new CASException(e);
}
}
/**
* Tries to convert the given CAS string to the given syntax.
*
* @param ev
* parsed CAS output
* @param tpl
* template to be used
* @return string representation of ev in given syntax
*/
public String toString(ExpressionValue ev, StringTemplate tpl) {
String GeoGebraString;
ExpressionNode expr;
if (!ev.isExpressionNode()) {
expr = ev.wrap();
} else {
expr = (ExpressionNode) ev;
}
GeoGebraString = expr.getCASstring(tpl, true);
if (GeoGebraString.startsWith("?")) {
return "?";
}
return GeoGebraString;
}
/**
* Tries to convert the given Giac string to GeoGebra syntax.
*
* @param exp
* MPReduce output
* @return parsed expression
* @throws CASException
* if parsing goes wrong
*/
public ValidExpression parseGiac(String exp) throws CASException {
try {
return parser.parseGiac(exp);
} catch (Throwable t) {
throw new CASException(t);
}
}
/**
* Final automata can be in three states * NORMAL -- no index being read *
* UNDERSCORE -- last character was _ * LONG_INDEX -- it found _{, but not
* yet }
*
*/
private enum FA {
NORMAL, UNDERSCORE, LONG_INDEX
}
/**
* Converts all index characters ('_', '{', '}') in the given String to
* "unicode" + charactercode + DELIMITER Strings. This is needed so that
* labels like a_{12} are preserved
*
* @param str
* input string with _,{,}
* @param replaceUnicode
* whether unicode characters need to be encoded
* @return string where _,{,} are replaced
*/
public synchronized String replaceIndices(String str,
boolean replaceUnicode) {
int len = str.length();
StringBuilder replaceIndices = new StringBuilder();
FA state = FA.NORMAL;
// convert every single character and append it to sb
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
switch (state) {
case NORMAL: // start index
if (c == '_') {
if (i > 0 && str.charAt(i - 1) == '\\') {
// \\_ is translated to _
replaceIndices
.deleteCharAt(replaceIndices.length() - 1);
replaceIndices.append('_');
} else {
state = FA.UNDERSCORE;
appendcode(replaceIndices, '_');
}
} else if (replaceUnicode && c > 127
&& c != Unicode.MEASURED_ANGLE) {
appendcode(replaceIndices, c);
// ' replaced in StringTemplate.addTempVariablePrefix() so
// that x', y' work #3607
// } else if (c == '\'') {
// appendcode(replaceIndices, c);
} else {
replaceIndices.append(c);
}
break;
case UNDERSCORE:
if (c == '{') {
state = FA.LONG_INDEX;
} else {
state = FA.NORMAL;
}
appendcode(replaceIndices, c);
break;
case LONG_INDEX:
if (c == '}') {
state = FA.NORMAL;
}
appendcode(replaceIndices, c);
break;
}
}
// Log.debug(insertSpecialChars(replaceIndices.toString())+"
// "+replaceIndices.toString());
return replaceIndices.toString();
}
private static void appendcode(StringBuilder replaceIndices, int code) {
replaceIndices.append(ExpressionNodeConstants.UNICODE_PREFIX);
replaceIndices.append(code);
replaceIndices.append(ExpressionNodeConstants.UNICODE_DELIMITER);
}
/**
* Reverse operation of removeSpecialChars().
*
* @param str
* input string
* @return input string with 'replaced by !' etc.
*/
// see ExpressionNode#operationToString() for XCOORD, YCOORD
public String insertSpecialChars(String str) {
int prefixLen = ExpressionNodeConstants.UNICODE_PREFIX.length();
if (str.length() < prefixLen) {
return str;
}
int len = str.length();
StringBuilder insertSpecial = new StringBuilder();
// convert every single character and append it to sb
char prefixStart = ExpressionNodeConstants.UNICODE_PREFIX.charAt(0);
boolean prefixFound;
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
prefixFound = false;
// first character of prefix found
if (c == prefixStart && i + prefixLen < str.length()) {
prefixFound = true;
// check prefix
int j = i;
for (int k = 0; k < prefixLen; k++, j++) {
if (ExpressionNodeConstants.UNICODE_PREFIX.charAt(k) != str
.charAt(j)) {
prefixFound = false;
break;
}
}
if (prefixFound) {
// try to get the unicode
int code = 0;
char digit;
while (j < len
&& StringUtil.isDigit(digit = str.charAt(j))) {
code = 10 * code + (digit - 48);
j++;
}
if (code > 0 && code < 65536) { // valid unicode
insertSpecial.append((char) code);
i = j;
} else { // invalid
insertSpecial
.append(ExpressionNodeConstants.UNICODE_PREFIX);
i += prefixLen;
}
} else {
insertSpecial.append(c);
}
} else {
insertSpecial.append(c);
}
}
return insertSpecial.toString();
}
/**
* @return parser functions
*/
public ParserFunctions getParserFunctions() {
return parserFunctions;
}
/**
* @return number of variables in Solve command before completion
*/
public int getNrOfVars() {
return nrOfVars;
}
/**
* @param nrOfVars
* - number of variables in Solve command before completion
*/
public void setNrOfVars(int nrOfVars) {
this.nrOfVars = nrOfVars;
}
/**
* Translates a given expression in the format expected by the cas.
*
* @param ve
* the Expression to be translated
* @param casStringType
* one of StringType.{MAXIMA, MPREDUCE, MATH_PIPER}
* @param cas
* CAS interface
* @return the translated String.
*/
public String translateToCAS(ValidExpression ve,
StringTemplate casStringType, CASGenericInterface cas) {
String body = ve.wrap().getCASstring(casStringType, false);
return body;
}
private Map<String, String> rbCasTranslations; // translates from
// GeogebraCAS
// syntax to the internal CAS
// syntax.
/**
* Returns the CAS command for the currently set CAS using the given key.
* For example, getCASCommand"Expand.0" returns "ExpandBrackets( %0 )" when
* Giac is the currently used CAS.
*
* @param command
* The command to be translated (should end in ".n", where n is
* the number of arguments to this command).
* @return The command in CAS format, where parameter n is written as %n.
*
*/
@Override
public String getTranslatedCASCommand(final String command) {
return getTranslationRessourceBundle().get(command);
}
/**
* Returns whether the CAS command key is available, e.g. "Expand.1"
*
* @param commandKey
* command name suffixed by . and number of arguments, e.g.
* Derivative.2, Sum.N
* @return true if available
*/
final public boolean isCommandAvailable(String commandKey) {
return getTranslatedCASCommand(commandKey) != null;
}
/**
* Returns the RessourceBundle that translates from GeogebraCAS commands to
* their definition in the syntax of the current CAS. Loads this bundle if
* it wasn't loaded yet.
*
* @return The current ResourceBundle used for translations.
*
*/
synchronized Map<String, String> getTranslationRessourceBundle() {
if (rbCasTranslations == null) {
rbCasTranslations = Ggb2giac
.getMap(parser.getKernel().getApplication());
}
return rbCasTranslations;
}
}