I'm working on a library which will provide easier writing and calculations in Java using a fluent API. This is the library I have implemented with basic functionalities and am now working to improve the API.
I followed instructions given by this but I still have some doubts in naming some classes and methods.
I have several questions oriented on naming those:
For calculations, there is the basic class
Calculator
. For executing calculations, there is the methodcalculate()
. Which is more preferable name:calculate()
or simplifiedcalc()
?The
Calculator
class has the ability to track each step of a calculation.For now I have these methods:
setTrackSteps(boolean)
, which enable tracking each step of calculation.getTrackedSteps()
- return list of each step in TrackedStep object.hasTrackedStep
- check is tracking calculation steps is enabled/disabled.
Does somebody have some better suggestions for those names?
Calculator.getTrackedSteps()
return list of objectTrackedStep
.TrackedSteps
is little harsh name. Is there a better name for this?I have separated public API with internal implementations into an internal package. In this package, I have a 'Utils' class with static methods and I'm trying to avoid this generic name. Would
HelperUtils
be a good name?I'm trying to avoid prefixes with
Calc()
orCalculator()
, because when the end user uses some IDE and when it start writing Cal, the IDE will provide several classes which start with Cal, including 'Calc..Utils'
If somebody has some other suggestions, the API is available on GitHub.
The class which is related to question 1, 2 and 3:
package org.jdice.calc;
import java.text.ParseException;
import java.util.Iterator;
import java.util.LinkedList;
import org.jdice.calc.internal.BindExtensionProvider;
import org.jdice.calc.internal.Bracket;
import org.jdice.calc.internal.CList;
import org.jdice.calc.internal.CListListener;
import org.jdice.calc.internal.CacheExtension;
import org.jdice.calc.internal.FunctionData;
import org.jdice.calc.internal.InfixParser;
import org.jdice.calc.internal.PostfixCalculator;
import org.jdice.calc.internal.UseExtension;
/**
* Abstract class that concrete calculator extends
*
* @author Davor Sauer <davor.sauer@gmail.com>
*
* @param <CALC>
*/
public abstract class AbstractCalculator<CALC> {
/**
* Detect changes in infix expression
*/
private CList infix = new CList(new CListListener() {
@Override
public void change() {
isInfixChanged = true;
}
});
private boolean isInfixChanged = true;
private InfixParser infixParser;
private final PostfixCalculator postfixCalculator = new PostfixCalculator();
private CList postfix = new CList();
private Num lastCalculatedValue;
private LinkedList<TrackedStep> calculatingSteps;
private Properties properties;
private UseExtension localUseExtensions;
private static boolean isImplExtRegistered = false;
private AbstractCalculator<CALC> parentCalculator;
private AbstractCalculator<CALC> childCalculator;
private boolean isBind = false;
private boolean isUnbind = false;
private boolean trackSteps = false;
/**
* Register implemented operation and functions by subclass
*
*/
private void useImplmentedExtension() {
if (isImplExtRegistered == false) {
Object o = getThis();
Class thisClass = o.getClass();
// superclass interfaces
Class[] declared = thisClass.getSuperclass().getInterfaces();
for (Class declare : declared) {
useImplmentedExtension(declare);
}
// subclass interfaces
declared = thisClass.getInterfaces();
for (Class declare : declared) {
useImplmentedExtension(declare);
}
isImplExtRegistered = true;
}
}
/**
* Register defined operation or function class to global cache
*
* @param declare
*/
private void useImplmentedExtension(Class declare) {
Class c = BindExtensionProvider.getExtension(declare);
if (c == null && declare.isAnnotationPresent(BindExtension.class)) {
BindExtension impl = (BindExtension) declare.getAnnotation(BindExtension.class);
if (impl != null)
c = impl.implementation();
BindExtensionProvider.bind(declare, c);
}
if (c != null) {
if (Operator.class.isAssignableFrom(c))
CacheExtension.registerOperator(c);
if (Function.class.isAssignableFrom(c))
CacheExtension.registerFunction(c);
}
}
/**
* Return reference of subclass
* @return
*/
protected abstract CALC getThis();
/**
* Provide custom {@link Operator} or {@link Function} inside scope of this instance, that can be used during expression parsing.
* With registration of custom operation it's possible to override existing default operation implementation.
* Because during calculation API first scan scoped (registered) operation and after that default operation implementation inside {@link CacheExtension}
*
* @param operationClass
* @return
*/
public CALC use(Class<? extends Extension> operationClass) {
if (localUseExtensions == null)
localUseExtensions = new UseExtension();
if (Operator.class.isAssignableFrom(operationClass))
localUseExtensions.registerOperator(operationClass.asSubclass(Operator.class));
if (Function.class.isAssignableFrom(operationClass))
localUseExtensions.registerFunction(operationClass.asSubclass(Function.class));
return getThis();
}
/**
* List registered local scoped operations.
*
* @return
*/
public UseExtension getUsedExtensions() {
return this.localUseExtensions;
}
/**
* Append value to expression
*
* @param value
* @return
*/
public CALC val(Object value) {
Num val = null;
if (value instanceof Num)
val = (Num)value;
else
val = new Num(value);
infix.add(val);
return getThis();
}
/**
* Append value to expression
*
* @param value custom type value
* @param converter class for convert custom type to {@link Num}
* @return
*/
public CALC val(Object value, Class<? extends NumConverter> converter) {
infix.add(new Num(value, converter));
return getThis();
}
/**
* Append String value to expression that will be parsed to {@link Num} with defined decimal separator
*
*
* @param value String representation of number
* @param decimalSeparator used in string representation of number
* @return
*/
public CALC val(String value, char decimalSeparator) {
infix.add(new Num(value, decimalSeparator));
return getThis();
}
/**
* Copy calculator expression into this expression within brackets
*
* @param expression
* @return
*/
public CALC append(AbstractCalculator expression) {
return append(expression, true);
}
/**
*
* Copy expression from given calculator into this expression within or without brackets
*
* @param expression
* @param withinBrackets
* @return
*/
public CALC append(AbstractCalculator expression, boolean withinBrackets) {
append(expression.infix, withinBrackets);
return getThis();
}
/**
*
* Append copy of given infix expression into this expression within or without brackets
*
* @param infix
* @param withinBrackets
* @return
*/
public CALC append(CList infix, boolean withinBrackets) {
if (withinBrackets)
this.infix.add(Bracket.OPEN);
Iterator<Object> it = infix.iterator();
while (it.hasNext()) {
Object o = it.next();
if (o instanceof Num) {
this.infix.add((Num) o);
}
else if (o instanceof Operator) {
this.infix.add((Operator) o);
}
else if (o instanceof FunctionData) {
this.infix.add((FunctionData) o);
}
else if (o instanceof Function) {
this.infix.add((Function) o);
}
else if (o instanceof Bracket) {
this.infix.add((Bracket) o);
}
}
if (withinBrackets)
this.infix.add(Bracket.CLOSE);
return getThis();
}
/**
* Append operator to expression
* @param operator
* @return
*/
public final CALC append(Class<? extends Operator> operator) {
infix.add(operator);
return getThis();
}
/**
* Append operator and number to expression
* @param operator
* @param value
* @return
*/
protected final CALC append(Class<? extends Operator> operator, Object value) {
Num tmp = null;
if (value instanceof Num)
tmp = (Num)value;
else
tmp = new Num(value);
infix.add(CacheExtension.getOperator(operator));
infix.add(tmp);
return getThis();
}
/**
* Append operator and parsed String value with custom decimal separator used in String representation of value
* @param operator
* @param value
* @param decimalSeparator
* @return
*/
protected final CALC append(Class<? extends Operator> operator, String value, char decimalSeparator) {
return append(operator, new Num(value, decimalSeparator));
}
/**
* Append function with value to expression.
*
* <br/>
* e.g. Abs.class, -5 => abs(-5)
*
* @param function
* @param values can accept any object that {@link Num} can work with
* @return
* @see {@link Function}
*/
public final CALC append(Class<? extends Function> function, Object... values) {
Function fn = CacheExtension.getFunction(function);
FunctionData fd = new FunctionData(fn, values);
this.infix.addFunction(fd);
return getThis();
}
/**
* Parse and append given expression to existing expression
*
* @param expression
* @return
* @throws ParseException
*/
public CALC expression(String expression) throws ParseException {
useImplmentedExtension();
if (infixParser == null)
infixParser = new InfixParser();
CList infix = infixParser.parse(localUseExtensions, getProperties(), expression);
append(infix, false);
return getThis();
}
/**
* Parse and append given expression to existing expression
* String representation of expression that will be parsed with unknown variables.
* It is possible to define name of <tt>Num</tt> with {@link Num#setName(String)} then name will be matched with name of unknown variable.
* Otherwise unknown variable will be matched by declared order.
*
* <br/>
* e.g. X + 5 - (2 * X - Y)
*
* @param expression
* @param values that match unknown variable by name or by order
* @return {@link AbstractCalculator}
* @throws ParseException
*/
public CALC expression(String expression, Object... values) throws ParseException {
useImplmentedExtension();
if (infixParser == null)
infixParser = new InfixParser();
CList infix = infixParser.parse(localUseExtensions, getProperties(), expression, values);
append(infix, false);
return getThis();
}
/**
* Open bracket
* @return
*/
public CALC openBracket() {
infix.add(Bracket.OPEN);
return getThis();
}
/**
* Close bracket
*
* @return
*/
public CALC closeBracket() {
infix.add(Bracket.CLOSE);
return getThis();
}
/**
* Get defined properties
* @return
*/
public Properties getProperties() {
if (properties == null)
properties = new Properties();
return properties;
}
/**
* Set proeprties
* @param properties
* @return
*/
public CALC setProperties(Properties properties) {
this.properties = properties;
return getThis();
}
/**
* Set scale for entire expression
* @param scale
* @return
*/
public CALC setScale(Integer scale) {
getProperties().setScale(scale);
return getThis();
}
/**
* Get scale mode used throughout expression
* @return
*/
public Integer getScale() {
return getProperties().getScale();
}
/**
* Set rounding mode for entire expression
* @param roundingMode
* @return
*/
public CALC setRoundingMode(Rounding roundingMode) {
getProperties().setRoundingMode(roundingMode);
return getThis();
}
/**
* Get rounding mode used throughout expression
* @return
*/
public Rounding getRoundingMode() {
return getProperties().getRoundingMode();
}
/**
* Set decimal separator for entire expression
* @param decimalSeparator
* @return
*/
public CALC setDecimalSeparator(char decimalSeparator) {
getProperties().setInputDecimalSeparator(decimalSeparator);
getProperties().setOutputDecimalSeparator(decimalSeparator);
return getThis();
}
/**
* Get decimal separator used throughout expression
* @return
*/
public char getDecimalSeparator() {
return getProperties().getInputDecimalSeparator();
}
/**
* Set number grouping separator for entire expression
* @param decimalSeparator
* @return
*/
public CALC setGroupingSeparator(char decimalSeparator) {
getProperties().setGroupingSeparator(decimalSeparator);
return getThis();
}
/**
* Get grouping separator used throughout expression
* @return
*/
public char getGroupingSeparator() {
return getProperties().getGroupingSeparator();
}
public boolean hasStripTrailingZeros() {
return getProperties().hasStripTrailingZeros();
}
public CALC setStripTrailingZeros(boolean stripTrailingZeros) {
getProperties().setStripTrailingZeros(stripTrailingZeros);
return getThis();
}
public CALC setTrackSteps(boolean trackSteps) {
this.trackSteps = trackSteps;
return getThis();
}
public boolean hasTrackSteps() {
return trackSteps;
}
/**
* Get calculation steps if {@link #hasTrackSteps()} is TRUE
*
* @return
* @see {@link}
*/
public LinkedList<TrackedStep> getTrackedSteps() {
return calculatingSteps;
}
/**
* Calculate prepared expression.
*
* For tracking calculation
*
* @return
* @see {@link #calculate()}
* @see {@link #getCalculatedValue()}
*/
public Num calculate() {
unbind();
prepareForNewCalculation();
PostfixCalculator pc = convertToPostfix();
Num cv = pc.calculate(this, postfix, trackSteps);
lastCalculatedValue = cv.clone();
return cv;
}
/**
* Bind another Calculator class functionalities to expression.
*
* Way to combine two different implementation of calculators
*
* @param clazz
* @return
*/
public <T extends AbstractCalculator> T bind(Class<T> clazz) {
T childCalc = null;
try {
childCalc = clazz.newInstance();
}
catch (Exception e) {
throw new CalculatorException(e);
}
if (childCalc instanceof AbstractCalculator) {
// find last child from root
AbstractCalculator<CALC> bParent = this;
while (bParent != null) {
if (bParent.childCalculator != null)
bParent = bParent.childCalculator;
else
break;
}
((AbstractCalculator) childCalc).parentCalculator = bParent;
((AbstractCalculator) childCalc).isBind = true;
bParent.childCalculator = childCalc;
}
else {
throw new CalculatorException("Use calculator which is type of AbstractCalculator", new IllegalArgumentException());
}
return childCalc;
}
/**
* Unbind binded calculator
* @return
*/
private CALC unbind() {
if (childCalculator != null)
unbindAll(this);
return (CALC) this;
}
/**
* Unbind all binded calculators
* @param undbindFrom
* @return
*/
private CALC unbindAll(AbstractCalculator<CALC> undbindFrom) {
// find root and first child
AbstractCalculator root = undbindFrom.parentCalculator != null ? undbindFrom.parentCalculator : undbindFrom;
AbstractCalculator child = root.childCalculator;
while (root != null) {
AbstractCalculator tmpParent = root.parentCalculator;
if (tmpParent == null)
break;
else
root = tmpParent;
child = root.childCalculator;
}
// undbind all from root to last child
while (child != null) {
if (child.isUnbind == false)
root.append(child, false);
child.isUnbind = true;
child = child.childCalculator; // new unbind child
}
return (CALC) undbindFrom;
}
/**
* Convert defined expression to postfix <tt>String</tt>
*
* @return
*/
public String getPostfix() {
unbind();
convertToPostfix();
return InfixParser.toString(this.postfix);
}
/**
* Convert infix to postfix
* Conversion is made only first time or after any change in structure of infix expression
* @return
*/
private PostfixCalculator convertToPostfix() {
if (postfix == null || postfix.size() == 0 || isInfixChanged) {
postfixCalculator.toPostfix(infix);
postfix = postfixCalculator.getPostfix();
isInfixChanged = false;
}
return postfixCalculator;
}
/**
* Get infix (common arithmetic and logical expression notation) representation of given expression
*
* @return
* @see {@link getPostfix()}
*/
public String getInfix() {
unbind();
return toString();
}
/**
* Provide infix list for this calculator.
*
* @param infix
* @return
*/
public final CALC setInfix(CList infix) {
this.infix = infix;
return getThis();
}
/**
* Check whether the calculation is made according to a expression
*
* @return
* @see {@link getResult()}
*/
public boolean isCalculated() {
if (lastCalculatedValue != null)
return true;
else
return false;
}
/**
* Return calculated value
*
* @return
* @see {@link hasResult()}
*/
public Num getCalculatedValue() {
if (lastCalculatedValue != null)
return lastCalculatedValue.clone();
else
return null;
}
/**
* Reset result and calculation steps of previous calculation
*/
private void prepareForNewCalculation() {
lastCalculatedValue = null;
this.calculatingSteps = null;
}
public final void setSteps(LinkedList<TrackedStep> calculationSteps) {
this.calculatingSteps = calculationSteps;
}
@Override
public String toString() {
return InfixParser.toString(this.infix);
}
}
Code related to question 4:
package org.jdice.calc.internal;
import org.jdice.calc.Num;
import org.jdice.calc.Properties;
public class Utils {
public static boolean equals(Object objA, Object objB) {
if (objA == objB)
return true;
else if (objA == null && objB == null)
return true;
else if (objA != null && objB == null)
return false;
else if (objA == null && objB != null)
return false;
else if (objA != null && objB != null && objA.getClass().isAssignableFrom(objB.getClass()))
return objA.equals(objB);
return false;
}
public static Num[] toNums(Object... object) {
Num[] values = new Num[object.length];
for (int i = 0; i < object.length; i++) {
values[i] = Num.toNum(object[i]);
}
return values;
}
/**
* Remove from string number representation all character except numbers and
* decimal point. <br>
* And replace given decimalSeparator with '.'
*
* <pre>
* ("44,551.06", '.') => 44551.06
* ("1 255 844,551.06", '.') => 1255844551.06
* ("44,551..06", '.') => 44551.06
* </pre>
*
* @param decimalSeparator
* @param value
*/
public static String extractNumber(String value, char decimalSeparator) {
String regex = "[^0-9-" + decimalSeparator + "]";
if (decimalSeparator == '.')
regex = regex.replace(".", "\\.");
String strip = value.replaceAll(regex, "");
strip = strip.replace(decimalSeparator + "", Properties.DEFAULT_DECIMAL_SEPARATOR + "");
return strip;
}
}