6
\$\begingroup\$

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:

  1. For calculations, there is the basic class Calculator. For executing calculations, there is the method calculate(). Which is more preferable name: calculate() or simplified calc()?

  2. 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?

  3. Calculator.getTrackedSteps() return list of object TrackedStep. TrackedSteps is little harsh name. Is there a better name for this?

  4. 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() or Calculator(), 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;
    }
}
\$\endgroup\$
2
\$\begingroup\$

For your specific questions:

For calculation there is basic class Calculator and for execute calculation, there is method calculate().

Which is more preferable name calculate() or simplified calc() ?

I'm cool with either, as long as it's applied somewhat consistently. If you don't use calculate and calc in different contexts and implying different meanings, either one's fine.

For now I have those 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.

If you have setTrackSteps, consider mirroring with isTrackSteps or getTrackSteps, just to follow JavaBeans conventions.

Calculator.getTrackedSteps() return list of object TrackedStep. TrackedSteps is little harsh name. Is there a better name for this?

Maybe 'trace' is the word you're looking for?

boolean isTracingSteps()
void setTracingSteps(boolean)
List<CalcStep> getTracedSteps()

I have separated Public API with internal implementations into internal package. In this package, I have 'Utils' class with static methods and I'm trying to avoid this generic name.

I don't really see an issue with having a generic name for a generic class. If your calculator class would've been called Utils, or your utility class Calculator, that would be an issue, but basically a namespace for utility methods? Utils is fine.

That said, about that Utils:

  • Utils.equals could be left out and traded for java.util.Objects.equals, provided you can require Java 7 or up.

  • Num[] toNums(Object[]) relies on Num.toNum(Object); maybe put it with Num as well?

  • extractNumber -> divert this through NumberFormat? Perhaps a protected method in your Calculator class, if it's not used outside of it?


More general comments

Self-typing through generics only works for subclasses one level in. I've tried serious wizardry with generics, and never found a good, reliable way to assure the compiler it would get an instance of the current type without having to override those methods manually.

val instead of append? These methods have different names, but they appear to do the same thing: append something. Is there a reason you chose different names for them?

extractNumber is too liberal in what it accepts. It feels a bit odd that I could write:

"Alfred, we're at 800mBar; add 25 cups of sugar and get me 2 cans of shark repellant."

...and end up with 800252. I'd expect the method to throw a NumberFormatException, and possibly shake its head at me for misquoting Batman.

Utils.equals can be briefer. You're taking some responsibilities out of the hands of Object.equals, which is not necessarily a good thing. If you can't go to Java 7 or import the excellent Guava library, you can copy their implementation:

return (a == b) || (a != null && a.equals(b)); // JDK, Guava
\$\endgroup\$
  • \$\begingroup\$ Thanks for so detail clarification and suggestions. I already change Utils class to Objects class and use implementation Guava library. And remove toNums(..) because it's newer used. extractNumber, well I newer think of situation of giving this method entire sentence, but it's possible situation and thanks for pointing this out. So I was thinking to rename append(..) to two separated methods, operator(Class<? extends Operator> operator) and function(Class<? extends Function> function, Object... values). So it's will be more clear which one is for what purpose. \$\endgroup\$ – d-sauer May 8 '14 at 20:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.