/*****************************************************************************
* Copyright (c) 2015 CEA LIST.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
*
*****************************************************************************/
package org.eclipse.nebula.widgets.nattable.formula;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.nebula.widgets.nattable.Messages;
import org.eclipse.nebula.widgets.nattable.coordinate.IndexCoordinate;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.formula.function.AbstractFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.AbstractMathSingleValueFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.AbstractSingleValueFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.AverageFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.BigDecimalFunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionException;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.ModFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.MultipleValueFunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.NegateFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.OperatorFunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.PowerFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.ProductFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.QuotientFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.SquareRootFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.StringFunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.SumFunction;
/**
* Parser that is able to parse a formula string and calculate the result.
*
* @since 1.4
*/
public class FormulaParser {
public static final String operatorRegex = "[-+/*\\^]"; //$NON-NLS-1$
public static final String digitRegex = "[\\d]+"; //$NON-NLS-1$
public static final String placeholderRegex = "\\{" + digitRegex + "\\}"; //$NON-NLS-1$ //$NON-NLS-2$
public static final String operatorSplitRegex = "((?<=[-+/*\\^\\s])|(?=[-+/*\\^\\s]))"; //$NON-NLS-1$
public static final String referenceRegex = "[A-Z]+[0-9]+"; //$NON-NLS-1$
public static final String referenceRangeRegex = referenceRegex + ":" + referenceRegex; //$NON-NLS-1$
public static final String columnRangeRegex = "[A-Z]+:[A-Z]+"; //$NON-NLS-1$
public static final String rowRangeRegex = digitRegex + ":" + digitRegex; //$NON-NLS-1$
public static final String rangeRegex = "(" + referenceRangeRegex + "|" + columnRangeRegex + "|" + rowRangeRegex + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
protected DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance();
protected String localizedDigitRegex;
protected String functionRegex;
protected Pattern functionPattern;
protected Pattern referencePattern = Pattern.compile(referenceRegex);
protected Map<String, Class<? extends AbstractFunction>> functionMapping = new HashMap<String, Class<? extends AbstractFunction>>();
protected IDataProvider dataProvider;
/**
* Creates and initializes a new {@link FormulaParser}.
*
* @param dataProvider
* The {@link IDataProvider} that provides the data to perform
* calculations.
*/
public FormulaParser(IDataProvider dataProvider) {
this.dataProvider = dataProvider;
initFunctions();
updateLocalizedDigitRegex();
}
/**
* Register a new function that can be evaluated.
*
* @param functionName
* The name of the function that is used in a formula
* @param value
* The type of {@link AbstractFunction} that should be used when
* evaluation a formula that contains the given function.
*/
public void registerFunction(String functionName, Class<? extends AbstractFunction> value) {
this.functionMapping.put(functionName, value);
updateFunctionRegex();
}
/**
*
* @return The names of the registered functions that can be evaluated by
* this {@link FormulaParser}.
*/
public Collection<String> getRegisteredFunctions() {
return this.functionMapping.keySet();
}
/**
* Initialize the functions that are supported by this {@link FormulaParser}
* .
*/
protected void initFunctions() {
this.functionMapping.put("AVERAGE", AverageFunction.class); //$NON-NLS-1$
this.functionMapping.put("MOD", ModFunction.class); //$NON-NLS-1$
this.functionMapping.put("NEGATE", NegateFunction.class); //$NON-NLS-1$
this.functionMapping.put("POWER", PowerFunction.class); //$NON-NLS-1$
this.functionMapping.put("PRODUCT", ProductFunction.class); //$NON-NLS-1$
this.functionMapping.put("QUOTIENT", QuotientFunction.class); //$NON-NLS-1$
this.functionMapping.put("SQRT", SquareRootFunction.class); //$NON-NLS-1$
this.functionMapping.put("SUM", SumFunction.class); //$NON-NLS-1$
updateFunctionRegex();
}
/**
* Update the regular expression that is used to identify a function in a
* function string.
*/
protected void updateFunctionRegex() {
StringBuilder builder = new StringBuilder("("); //$NON-NLS-1$
for (Iterator<String> it = this.functionMapping.keySet().iterator(); it.hasNext();) {
String functionName = it.next();
builder.append(functionName);
if (it.hasNext()) {
builder.append("|"); //$NON-NLS-1$
}
}
builder.append(")\\("); //$NON-NLS-1$
this.functionRegex = builder.toString();
this.functionPattern = Pattern.compile(this.functionRegex);
}
/**
* Parses the given function string to a {@link FunctionValue} to perform
* calculation.
*
* @param function
* The function string to parse.
* @return The {@link FunctionValue} that represents the calculation result
* of the parsed function string.
*/
public FunctionValue parseFunction(String function) {
return parseFunction(function, new HashMap<Integer, FunctionValue>(), new LinkedHashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
}
/**
* Parses the given function string to a {@link FunctionValue} to perform
* calculation. Creates a new replacement map but keeps the parsed
* references for cycle detection.
*
* @param function
* The function string to parse.
* @param parsedReferences
* The references that where parsed already together with their
* references if any. Needed for cycle detection.
* @param referer
* The coordinate of the cell that refers to the value to add.
* Needed for cycle detection.
* @return The {@link FunctionValue} that represents the calculation result
* of the parsed function string.
*/
protected FunctionValue parseFunction(String function,
Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences, IndexCoordinate referer) {
return parseFunction(function, new HashMap<Integer, FunctionValue>(), parsedReferences, referer);
}
/**
* Parses the given function string to a {@link FunctionValue} to perform
* calculation.
*
* @param function
* The function string to parse.
* @param replacements
* The map of replacements to support iterative parsing of
* sub-functions.
* @param parsedReferences
* The references that where parsed already together with their
* references if any. Needed for cycle detection.
* @param referer
* The coordinate of the cell that refers to the value to add.
* Needed for cycle detection.
* @return The {@link FunctionValue} that represents the calculation result
* of the parsed function string.
*/
protected FunctionValue parseFunction(String function, Map<Integer, FunctionValue> replacements,
Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences, IndexCoordinate referer) {
function = getFunctionOnly(function);
// process functions
String processedFunction = processFunctions(function, replacements, parsedReferences, referer);
// process parenthesis
processedFunction = processParenthesis(processedFunction, replacements, parsedReferences, referer);
String[] operandsAndOperators = processedFunction.split(operatorSplitRegex);
List<FunctionValue> values = new ArrayList<FunctionValue>(operandsAndOperators.length);
for (int i = 0; i < operandsAndOperators.length; i++) {
String part = operandsAndOperators[i].trim();
if (part.matches(operatorRegex)) {
if ("-".equals(part)) { //$NON-NLS-1$
values.add(new NegateFunction());
} else if ("+".equals(part)) { //$NON-NLS-1$
values.add(new SumFunction());
} else if ("*".equals(part)) { //$NON-NLS-1$
values.add(new ProductFunction());
} else if ("/".equals(part)) { //$NON-NLS-1$
values.add(new QuotientFunction());
} else if ("^".equals(part)) { //$NON-NLS-1$
values.add(new PowerFunction());
}
} else if (part.matches(rangeRegex)) {
MultipleValueFunctionValue multi = new MultipleValueFunctionValue();
String[] parts = part.split(":"); //$NON-NLS-1$
if (part.matches(referenceRangeRegex)) {
int[] from = evaluateReference(parts[0]);
int[] to = evaluateReference(parts[1]);
int fromColumn = Math.min(from[0], to[0]);
int toColumn = Math.max(from[0], to[0]);
int fromRow = Math.min(from[1], to[1]);
int toRow = Math.max(from[1], to[1]);
for (int row = fromRow; row <= toRow; row++) {
for (int column = fromColumn; column <= toColumn; column++) {
addDataProviderValue(column, row, multi.getValue(), parsedReferences, referer);
}
}
} else if (part.matches(rowRangeRegex)) {
int from = Integer.valueOf(parts[0]) - 1;
int to = Integer.valueOf(parts[1]) - 1;
if (from > to) {
int tmp = to;
to = from;
from = tmp;
}
for (int row = from; row <= to; row++) {
for (int column = 0; column < getUnderlyingColumnCount(); column++) {
addDataProviderValue(column, row, multi.getValue(), parsedReferences, referer);
}
}
} else if (part.matches(columnRangeRegex)) {
int from = getColumnIndex(parts[0]);
int to = getColumnIndex(parts[1]);
if (from > to) {
int tmp = to;
to = from;
from = tmp;
}
for (int column = from; column <= to; column++) {
for (int row = 0; row < getUnderlyingRowCount(); row++) {
addDataProviderValue(column, row, multi.getValue(), parsedReferences, referer);
}
}
}
values.add(multi);
} else if (part.matches(referenceRegex)) {
int[] coords = evaluateReference(part);
addDataProviderValue(coords[0], coords[1], values, parsedReferences, referer);
} else if (part.matches(placeholderRegex)) {
String number = part.substring(1, part.length() - 1);
try {
values.add(replacements.get(Integer.valueOf(number)));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.replacement"), e); //$NON-NLS-1$
}
} else if (part.matches(this.localizedDigitRegex)) {
// check if last is big decimal and throw exception in that case
if (values.size() > 0 && values.get(values.size() - 1) instanceof BigDecimalFunctionValue) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.missingOperator")); //$NON-NLS-1$
}
values.add(new BigDecimalFunctionValue(convertToBigDecimal(part.trim())));
} else {
String s = part.trim();
if (s.length() > 0) {
values.add(new StringFunctionValue(s));
}
}
}
int parts = 0;
do {
parts = values.size();
values = processMultiplicationAndDivision(values);
} while (parts != values.size());
return combineFunctions(values);
}
/**
* Process parts of a function that represent a function by name. Replaces
* the function result with placeholders whose representations are put in
* the given replacements map.
*
* @param function
* The function string
* @param replacements
* The map of replacements to store the result of the function
* parsing.
* @param parsedReferences
* The references that where parsed already together with their
* references if any. Needed for cycle detection.
* @param referer
* The coordinate of the cell that refers to the value to add.
* Needed for cycle detection.
* @return The modified string that contains placeholders for functions.
*/
protected String processFunctions(String function, Map<Integer, FunctionValue> replacements,
Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences, IndexCoordinate referer) {
StringBuilder result = new StringBuilder();
int startIndex = 0;
Matcher functionMatcher = this.functionPattern.matcher(function);
while (functionMatcher.find(startIndex)) {
String functionName = null;
String parameterString = null;
// find closing parenthesis and update startSearch
int openParanthesisCount = 0;
for (int i = functionMatcher.start(); i < function.length(); i++) {
char c = function.charAt(i);
if (c == '(') {
openParanthesisCount++;
if (i > 0 && openParanthesisCount == 1) {
// remember the left side of the function string
result.append(function.substring(startIndex, functionMatcher.start()));
functionName = function.substring(functionMatcher.start(), i);
startIndex = i;
}
} else if (c == ')') {
openParanthesisCount--;
if (openParanthesisCount < 0) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.functionParameterNotOpened")); //$NON-NLS-1$
}
if (openParanthesisCount == 0) {
parameterString = function.substring(startIndex + 1, i);
startIndex = i + 1;
break;
}
}
}
if (openParanthesisCount != 0) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.functionParameterNotClosed")); //$NON-NLS-1$
}
// determine function and get FunctionValue
Class<? extends AbstractFunction> functionClass = this.functionMapping.get(functionName);
if (functionClass == null) {
throw new IllegalArgumentException("No function '" + functionName + "' registered"); //$NON-NLS-1$ //$NON-NLS-2$
}
AbstractFunction fv = null;
try {
fv = functionClass.newInstance();
} catch (InstantiationException e) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.instantiation", e.getLocalizedMessage()), e); //$NON-NLS-1$
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.instantiation", e.getLocalizedMessage()), e); //$NON-NLS-1$
}
// process parameter
Map<Integer, FunctionValue> nestedReplacements = new HashMap<Integer, FunctionValue>();
parameterString = processFunctions(parameterString, nestedReplacements, parsedReferences, referer);
String[] parameter = parameterString.split(";"); //$NON-NLS-1$
for (String param : parameter) {
fv.addFunctionValue(parseFunction(param, nestedReplacements, parsedReferences, referer));
}
// replace function with placeholder
Integer pos = replacements.size();
replacements.put(pos, fv);
result.append("{").append(pos).append("}"); //$NON-NLS-1$//$NON-NLS-2$
}
// remember the right side of the function string
if (startIndex < function.length()) {
result.append(function.substring(startIndex, function.length()));
}
return result.toString();
}
/**
* Process parts of a function that are combined in parenthesis. Replaces
* the parenthesis with placeholders whose representations are put in the
* given replacements map.
*
* @param function
* The function string.
* @param replacements
* The map of replacements to store the result of the parenthesis
* parsing.
* @param parsedReferences
* The references that where parsed already together with their
* references if any. Needed for cycle detection.
* @param referer
* The coordinate of the cell that refers to the value to add.
* Needed for cycle detection.
* @return The modified string that contains placeholders for parenthesis.
*/
protected String processParenthesis(String function, Map<Integer, FunctionValue> replacements,
Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences, IndexCoordinate referer) {
StringBuilder result = new StringBuilder();
int openParanthesisCount = 0;
int startIndex = 0;
for (int i = 0; i < function.length(); i++) {
char c = function.charAt(i);
if (c == '(') {
openParanthesisCount++;
if (i > 0 && openParanthesisCount == 1) {
result.append(function.substring(startIndex, i));
startIndex = i;
}
} else if (c == ')') {
openParanthesisCount--;
if (openParanthesisCount < 0) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.parenthesisNotOpened")); //$NON-NLS-1$
}
if (openParanthesisCount == 0) {
String paranthesisFunctionString = function.substring(startIndex + 1, i);
FunctionValue paranthesisFunction = parseFunction(paranthesisFunctionString, parsedReferences, referer);
Integer pos = replacements.size();
replacements.put(pos, paranthesisFunction);
result.append("{").append(pos).append("}"); //$NON-NLS-1$//$NON-NLS-2$
startIndex = i + 1;
}
}
}
if (startIndex < function.length()) {
result.append(function.substring(startIndex, function.length()));
}
if (openParanthesisCount != 0) {
throw new IllegalArgumentException(Messages.getString("FormulaParser.error.parenthesisNotClosed")); //$NON-NLS-1$
}
return result.toString();
}
/**
* Process multiplication and division {@link FunctionValue}s first.
*
* @param values
* The list of parsed {@link FunctionValue}s.
* @return The list of {@link FunctionValue}s where multiplication and
* division is already combined.
*/
protected List<FunctionValue> processMultiplicationAndDivision(List<FunctionValue> values) {
List<FunctionValue> result = new ArrayList<FunctionValue>();
// we only process one multiplication/division operation at once
boolean operatorFound = false;
for (Iterator<FunctionValue> it = values.iterator(); it.hasNext();) {
FunctionValue v = it.next();
if (!operatorFound
&& it.hasNext()
&& result.size() > 0
&& (v instanceof ProductFunction || v instanceof QuotientFunction || v instanceof PowerFunction)
&& ((AbstractFunction) v).isEmpty()) {
// remove the last value that was added
FunctionValue previous = result.remove(result.size() - 1);
((OperatorFunctionValue) v).addFunctionValue(previous);
FunctionValue next = it.next();
if (next instanceof NegateFunction) {
((NegateFunction) next).addFunctionValue(it.next());
}
((OperatorFunctionValue) v).addFunctionValue(next);
operatorFound = true;
result.add(v);
} else {
result.add(v);
}
}
return result;
}
/**
* Combines {@link FunctionValue}s for processing the parsed values.
*
* @param values
* The list of {@link FunctionValue}s to combine.
* @return The single {@link FunctionValue} as a result of the value
* combination.
*/
protected FunctionValue combineFunctions(List<FunctionValue> values) {
FunctionValue result = null;
if (values.size() == 1) {
result = values.get(0);
} else {
for (int i = 0; i < values.size(); i++) {
FunctionValue v = values.get(i);
if (v instanceof AbstractSingleValueFunction
|| v instanceof AbstractMathSingleValueFunction) {
i++;
((AbstractFunction) v).addFunctionValue(values.get(i));
if (result != null && v instanceof NegateFunction) {
SumFunction sum = new SumFunction();
sum.addFunctionValue(result);
sum.addFunctionValue(v);
result = sum;
} else {
result = v;
}
} else if (v instanceof OperatorFunctionValue) {
if (i > 0 && result == null) {
((OperatorFunctionValue) v).addFunctionValue(values.get(i - 1));
} else if (result != null) {
((OperatorFunctionValue) v).addFunctionValue(result);
}
if (i > 0) {
i++;
if (i < values.size()) {
((OperatorFunctionValue) v).addFunctionValue(values.get(i));
}
}
result = v;
} else {
result = v;
}
}
}
return result;
}
/**
* Evaluates a reference string to cell coordinates.
*
* @param reference
* The reference string to evaluate.
* @return The cell coordinates for the given reference string.
*/
protected int[] evaluateReference(String reference) {
String columnString = ""; //$NON-NLS-1$
String rowString = ""; //$NON-NLS-1$
for (int i = 0; i < reference.length(); i++) {
char c = reference.charAt(i);
if (Character.isLetter(c)) {
columnString += c;
} else if (Character.isDigit(c)) {
rowString += c;
}
}
int column = getColumnIndex(columnString);
int row = Integer.valueOf(rowString) - 1;
return new int[] { column, row };
}
/**
* Calculate the column index out of a character based string, e.g. A = 0,
* AA = 26
*
* @param columnLiteral
* The string to calculate the column index from.
* @return The column index for the given string.
*/
protected int getColumnIndex(String columnLiteral) {
int column = 0;
int pos = 0;
for (int i = columnLiteral.length() - 1; i >= 0; i--) {
char c = columnLiteral.charAt(i);
column += (((c) - (pos == 0 ? 65 : 64)) * (Math.pow(26, pos)));
pos++;
}
return column;
}
/**
* Converts the given column index to a character based representation for
* reference handling.
*
* @param index
* The column index to convert.
* @return The parsed character representation for a column index.
*/
protected String convertIndexToColumnString(int index) {
int characterAddition = 65;
int quotient = index;
int remainder = 0;
String result = ""; //$NON-NLS-1$
do {
remainder = quotient % 26;
quotient = quotient / 26;
result = Character.toString((char) (remainder + characterAddition)) + result;
characterAddition = 64;
} while (quotient != 0);
return result;
}
/**
* Retrieves a value from the {@link IDataProvider} for the given
* coordinates and adds it to the given list of {@link FunctionValue}s for
* further processing.
*
* @param column
* The column index of the value.
* @param row
* The row index of the value.
* @param parsedReferences
* The references that where parsed already together with their
* references if any. Needed for cycle detection.
* @param referer
* The coordinate of the cell that refers to the value to add.
* Needed for cycle detection.
* @param values
* The list of {@link FunctionValue} to add the data provider
* value to.
*/
protected void addDataProviderValue(int column, int row, List<FunctionValue> values,
Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences, IndexCoordinate referer) {
Object value = getUnderlyingDataValue(column, row);
if (value != null) {
String toParse = value.toString();
if (value instanceof Number) {
toParse = this.decimalFormat.format(value);
}
// avoid circular references
IndexCoordinate ref = new IndexCoordinate(column, row);
if (!parsedReferences.containsKey(ref)) {
parsedReferences.put(ref, new HashSet<IndexCoordinate>());
}
if (referer != null) {
parsedReferences.get(referer).add(ref);
}
if (detectCycle(parsedReferences)) {
throw new FunctionException("#REF!", Messages.getString("FormulaParser.error.circular")); //$NON-NLS-1$//$NON-NLS-2$
}
FunctionValue parseResult = parseFunction(toParse, parsedReferences, ref);
if (parseResult != null) {
values.add(parseResult);
}
}
}
/**
* Updates the localized regular expression for decimal values. Uses the
* current set {@link DecimalFormat} to determine the decimal separator.
*
* @see FormulaParser#setDecimalFormat(DecimalFormat)
*/
protected void updateLocalizedDigitRegex() {
this.localizedDigitRegex = digitRegex + "(\\" + this.decimalFormat.getDecimalFormatSymbols().getDecimalSeparator() + digitRegex + ")?"; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Set the {@link DecimalFormat} that should be used to determine the
* decimal separator. By default the {@link DecimalFormat#getInstance()} is
* set which uses the current default {@link Locale}.
*
* @param format
* The {@link DecimalFormat} to use for determine the decimal
* separator.
*/
public void setDecimalFormat(DecimalFormat format) {
this.decimalFormat = format;
updateLocalizedDigitRegex();
}
/**
* Checks if a given String is a function or not.
*
* @param function
* The function String to check
* @return <code>true</code> if the given String represents a function,
* <code>false</code> if not
*/
public boolean isFunction(String function) {
return function.startsWith("="); //$NON-NLS-1$
}
/**
* Checks if the given string contains a function marker (default:
* leading'=') and returns a string without that marker to have the function
* only.
*
* @param function
* The function string to modify.
* @return The function only string
*/
public String getFunctionOnly(String function) {
if (function.startsWith("=")) { //$NON-NLS-1$
function = function.substring(1);
}
return function;
}
/**
* Checks if the given value is a number value.
*
* @param value
* The value to check.
* @return <code>true</code> if the given value is an integral or decimal
* value, <code>false</code> if not
*/
public boolean isNumber(String value) {
return value.matches(this.localizedDigitRegex);
}
/**
* Checks if the given {@link BigDecimal} is an integer or a decimal value.
*
* @param value
* The value to check.
* @return <code>true</code> if the given value is an integer,
* <code>false</code> if it is a decimal.
*/
public boolean isIntegerValue(BigDecimal value) {
return value.signum() == 0 || value.scale() <= 0 || value.stripTrailingZeros().scale() <= 0;
}
/**
* Converts a given String into a {@link BigDecimal}. Is able to convert
* decimal values with localized decimal separators.
*
* @param value
* The value to convert.
* @return The {@link BigDecimal} for the given value.
*/
public BigDecimal convertToBigDecimal(String value) {
value = value.replaceAll("\\" + this.decimalFormat.getDecimalFormatSymbols().getDecimalSeparator(), "."); //$NON-NLS-1$ //$NON-NLS-2$
return new BigDecimal(value);
}
/**
* @return The column count of the underlying data model. The base
* implementation uses the underlying {@link IDataProvider}.
*/
protected int getUnderlyingColumnCount() {
return this.dataProvider.getColumnCount();
}
/**
* @return The row count of the underlying data model. The base
* implementation uses the underlying {@link IDataProvider}.
*/
protected int getUnderlyingRowCount() {
return this.dataProvider.getRowCount();
}
/**
*
* @param column
* The column index of the cell whose value is requested.
* @param row
* The row index of the cell whose value is requested.
* @return The data value for the given column and row index out of the
* underlying data model. The base implementation uses the
* underlying {@link IDataProvider}.
*/
protected Object getUnderlyingDataValue(int column, int row) {
return this.dataProvider.getDataValue(column, row);
}
/**
* Updates the references in a function string. Needed for copy operations.
*
* @param function
* The function string to update the references.
* @param fromColumn
* The column index from where a formula is transfered.
* @param fromRow
* The row index from where a formula is transfered.
* @param toColumn
* The column index to where a formula is transfered.
* @param toRow
* The row index to where a formula is transfered.
* @return The function string with updated references.
*/
public String updateReferences(String function, int fromColumn, int fromRow, int toColumn, int toRow) {
int columnDiff = toColumn - fromColumn;
int rowDiff = toRow - fromRow;
Matcher referenceMatcher = this.referencePattern.matcher(function);
StringBuilder result = new StringBuilder();
int start = 0;
while (referenceMatcher.find()) {
result.append(function.substring(start, referenceMatcher.start()));
String reference = function.substring(referenceMatcher.start(), referenceMatcher.end());
int[] coords = evaluateReference(reference);
coords[0] += columnDiff;
coords[1] += rowDiff;
if (coords[0] < 0 || (coords[1] + 1) < 0) {
throw new FunctionException("#REF!", Messages.getString("FormulaParser.error.referenceNotExist")); //$NON-NLS-1$ //$NON-NLS-2$
}
String newReference = convertIndexToColumnString(coords[0]) + (coords[1] + 1);
result.append(newReference);
start = referenceMatcher.end();
}
if (start < function.length()) {
result.append(function.substring(start));
}
return result.toString();
}
// cycle detection code
protected boolean detectCycle(Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences) {
Set<IndexCoordinate> initPath = new HashSet<IndexCoordinate>();
for (Map.Entry<IndexCoordinate, Set<IndexCoordinate>> entry : parsedReferences.entrySet()) {
if (isCyclic(new Node(entry.getKey(), entry.getValue()), initPath, parsedReferences)) {
return true;
}
}
return false;
}
private boolean isCyclic(Node currNode, Set<IndexCoordinate> path, Map<IndexCoordinate, Set<IndexCoordinate>> parsedReferences) {
if (currNode != null) {
if (path.contains(currNode.referer)) {
return true;
} else {
if (!currNode.references.isEmpty()) {
path.add(currNode.referer);
for (IndexCoordinate node : currNode.references) {
if (isCyclic(new Node(node, parsedReferences.get(node)), path, parsedReferences)) {
return true;
} else {
path.remove(node);
}
}
}
}
}
return false;
}
/**
* Node class to implement depth-first-search algorithm to search for
* cycles.
*/
class Node {
IndexCoordinate referer;
Set<IndexCoordinate> references;
public Node(IndexCoordinate referer, Set<IndexCoordinate> references) {
this.referer = referer;
this.references = references;
}
}
}