Let me start off by saying that this project is a mess, and I know it. I believe it is a mess because I threw it together in a few days just to get it working. Unfortunately, my undergraduate education hasn't taught me design patterns (yet), so there is really no organized structure.
If I want to develop this into a more organized project, and perhaps a more general math library, with more functionality (integration, statistics, etc), what can I do to design it very well? Where do I put the logic, etc. I have been reading up on design patterns but I still feel that I need direction.
MathApp.java
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javax.swing.*;
public class MathApp extends Application {
private CalculusTool cT;
private String expression;
private String deriv;
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("MathApp");
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
Text scenetitle = new Text("Enter an expression");
scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 15));
grid.add(scenetitle, 0, 0, 2, 1);
TextField expBox = new TextField();
grid.add(expBox, 1, 2);
Text error = new Text();
grid.add(error, 1, 6);
error.setVisible(false);
Button btn = new Button("Derive");
HBox hbBtn = new HBox(10);
hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
hbBtn.getChildren().add(btn);
grid.add(hbBtn, 1, 4);
final Text actiontarget = new Text();
grid.add(actiontarget, 1, 6);
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent e) {
//same expression - derive again when clicked
if(expBox.hasProperties() && expBox.getText().equals(expression)) {
deriv = cT.readable(cT.derive());
actiontarget.setVisible(true);
actiontarget.setFill(Color.FIREBRICK);
actiontarget.setText(deriv);
}
else { // new expression, create new object
expression = expBox.getText();
if (!CalculusTool.isValidExpression(expression)) {
actiontarget.setVisible(false);
error.setText(CalculusTool.errorMessage);
error.setVisible(true);
expBox.clear();
} else {
error.setVisible(false);
cT = new CalculusTool(expression);
deriv = cT.readable(cT.derive());
actiontarget.setVisible(true);
actiontarget.setFill(Color.FIREBRICK);
actiontarget.setText(deriv);
}
}
}
});
Scene scene = new Scene(grid, 300, 275);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
ExpNode.java
abstract class ExpNode {
abstract String getVal();
abstract String getType();
abstract ExpNode getLeftChild();
abstract ExpNode getRightChild();
abstract void setLeftChild(ExpNode l);
abstract void setRightChild(ExpNode r);
abstract ExpNode derivative();
}
class OpNode extends ExpNode
{
String operator;
ExpNode leftChild;
ExpNode rightChild;
OpNode mult1;
OpNode mult2;
OpNode mult3;
OpNode div;
OpNode plus;
OpNode minus;
OpNode exponent;
NegateNode unary;
OpNode(String op)
{
operator = op;
leftChild = null;
rightChild = null;
}
String getVal() { return operator; }
String getType() { return "operator"; }
ExpNode getLeftChild() { return leftChild; }
ExpNode getRightChild() { return rightChild; }
void setLeftChild(ExpNode lc) { leftChild = lc; }
void setRightChild(ExpNode rc) { rightChild = rc; }
ExpNode derivative() {
switch(operator) {
case "+": // h(x) = f(x)+g(x) then h′(x) = f′(x)+g′(x)
plus = new OpNode("+");
plus.setLeftChild(getLeftChild().derivative());
plus.setRightChild(getRightChild().derivative());
return plus;
case "-": // h(x) = f(x)-g(x) then h′(x) = f′(x)-g′(x)
minus = new OpNode("-");
minus.setLeftChild(getLeftChild().derivative());
minus.setRightChild(getRightChild().derivative());
return minus;
case "*": // h(x) = f(x)g(x) then h′(x) = f′(x)g(x) + f(x)g′(x)
mult1 = new OpNode("*");
mult2 = new OpNode("*");
plus = new OpNode("+");
mult1.setLeftChild(getLeftChild());
mult1.setRightChild(getRightChild().derivative());
mult2.setLeftChild(getRightChild());
mult2.setRightChild(getLeftChild().derivative());
plus.setLeftChild(mult1);
plus.setRightChild(mult2);
return plus;
case "/": // h(x) = f(x)/g(x) then h′(x) = (f′(x)g(x) − f(x)g′(x))/( g(x)^2)
mult1 = new OpNode("*");
mult2 = new OpNode("*");
mult3 = new OpNode("*");
div = new OpNode("/");
minus = new OpNode("-");
mult1.setLeftChild(getRightChild());
mult1.setRightChild(getLeftChild().derivative());
mult2.setLeftChild(getLeftChild());
mult2.setRightChild(getRightChild().derivative());
minus.setLeftChild(mult1);
minus.setRightChild(mult2);
mult3.setLeftChild(getRightChild());
mult3.setRightChild(getRightChild());
div.setLeftChild(minus);
div.setRightChild(mult3);
return div;
case "^":
/*y = xsin x
ln y = (sin x) ln x ← taking the natural log of both sides.
y'/y = (cos x) ln x + (sin x)/x ← Using the product rule and implicit differentiation.
y' = y((cos x) ln x + (sin x)/x) ← multiplying by y.
y' = xsin x ((cos x) ln x + (sin x)/x) ← substitution.
*/
if(getRightChild().getVal().equals("0")) {
return new ConstNode("1");
}
if(getLeftChild().getType().equals("function")) {
if(getRightChild().getType().equals("variable")) {
}
if(getRightChild().getType().equals("function")) {
}
if(getRightChild().getType().equals("constant")) {
int temp = Integer.parseInt(getRightChild().getVal());
temp--;
ConstNode decrement = new ConstNode(String.valueOf(temp));
mult1 = new OpNode("*");
mult2 = new OpNode("*");
exponent = new OpNode("^");
//getLeftChild() = func(x)
//getLeftChild().derivative() = dx*dfunc(x)
//getRightChild() = C
//decrement = C - 1
//getLeftChild().getRightChild() = x
// func(x)^C ==>> C*dx*func(x)^(C-1)*dfunc(x)
exponent.setLeftChild(getLeftChild());
exponent.setRightChild(decrement);
mult1.setLeftChild(exponent);
mult1.setRightChild(getLeftChild().derivative());
mult2.setLeftChild(getRightChild());
mult2.setRightChild(mult1);
return mult2;
}
}
else if(getLeftChild().getType().equals("variable")) {
if(getRightChild().getType().equals("unaryMinus")) {
if(getRightChild().getRightChild().getType().equals("constant")) { // Ex: x^-3 ==> -3*(x^-4)
int temp = Integer.parseInt(getRightChild().getRightChild().getVal());
temp++;
ConstNode decrement = new ConstNode(String.valueOf(temp));
mult1 = new OpNode("*");
exponent = new OpNode("^");
unary.setRightChild(getRightChild().getRightChild());
exponent.setLeftChild(getLeftChild());
exponent.setRightChild(decrement);
mult1.setLeftChild(unary);
mult1.setRightChild(exponent);
return mult1;
}
}
// if not unaryMinus
if(getRightChild().getType().equals("variable")) {
}
if(getRightChild().getType().equals("function")) {
}
if(getRightChild().getType().equals("constant")) { // Ex: x^3 ==> 3*(x^2)
int temp = Integer.parseInt(getRightChild().getVal());
temp--;
ConstNode decrement = new ConstNode(String.valueOf(temp));
mult1 = new OpNode("*");
exponent = new OpNode("^");
exponent.setLeftChild(getLeftChild());
exponent.setRightChild(decrement);
mult1.setLeftChild(getRightChild());
mult1.setRightChild(exponent);
return mult1;
}
}
else if(getLeftChild().getType().equals("constant")) {
if(getRightChild().getType().equals("variable")) {
}
if(getRightChild().getType().equals("function")) {
}
if(getRightChild().getType().equals("constant")) {
return new ConstNode("0");
}
}
return new ConstNode("0");
}
return null;
}
}
// only has right child
class FuncNode extends ExpNode
{
String func;
ExpNode leftChild;
ExpNode rightChild;
NegateNode unary;
OpNode exponent;
OpNode mult;
OpNode mult2;
OpNode div;
FuncNode sin;
FuncNode cos;
FuncNode sec;
FuncNode cot;
FuncNode csc;
FuncNode tan;
FuncNode ln;
ConstNode two;
ConstNode ten;
FuncNode(String f)
{
func = f;
leftChild = null;
rightChild = null;
}
String getVal() { return func; }
String getType() { return "function"; }
ExpNode getLeftChild() { return leftChild; }
ExpNode getRightChild() { return rightChild; }
void setLeftChild(ExpNode lc) { leftChild = lc; }
void setRightChild(ExpNode rc) { rightChild = rc; }
ExpNode derivative() {
switch(func) {
case "sin":
mult = new OpNode("*");
cos = new FuncNode("cos");
cos.setRightChild(getRightChild());
mult.setLeftChild(getRightChild().derivative());
mult.setRightChild(cos);
return mult;
case "cos":
unary = new NegateNode();
mult = new OpNode("*");
sin = new FuncNode("sin");
sin.setRightChild(getRightChild());
mult.setLeftChild(getRightChild().derivative());
mult.setRightChild(sin);
unary.setRightChild(mult);
return unary;
case "tan":
sec = new FuncNode("sec");
mult = new OpNode("*");
exponent = new OpNode("^");
two = new ConstNode("2");
sec.setRightChild(getRightChild());
exponent.setLeftChild(sec);
exponent.setRightChild(two);
mult.setLeftChild(getRightChild().derivative());
mult.setRightChild(exponent);
return mult;
case "csc":
unary = new NegateNode();
mult = new OpNode("*");
mult2 = new OpNode("*");
cot = new FuncNode("cot");
csc = new FuncNode("csc");
cot.setRightChild(getRightChild());
csc.setRightChild(getRightChild());
mult.setLeftChild(cot);
mult.setRightChild(csc);
mult2.setLeftChild(getRightChild().derivative());
mult2.setRightChild(mult);
unary.setRightChild(mult2);
return unary;
case "sec":
mult = new OpNode("*");
mult2 = new OpNode("*");
sec = new FuncNode("sec");
tan = new FuncNode("tan");
sec.setRightChild(getRightChild());
tan.setRightChild(getRightChild());
mult.setLeftChild(sec);
mult.setRightChild(tan);
mult2.setLeftChild(getRightChild().derivative());
mult2.setRightChild(mult);
return mult2;
case "cot":
unary = new NegateNode();
exponent = new OpNode("^");
mult = new OpNode("*");
csc = new FuncNode("csc");
two = new ConstNode("2");
csc.setRightChild(getRightChild());
exponent.setLeftChild(csc);
exponent.setRightChild(two);
mult.setLeftChild(getRightChild().derivative());
mult.setRightChild(exponent);
unary.setRightChild(mult);
return unary;
case "log":
div = new OpNode("/");
mult = new OpNode("*");
ln = new FuncNode("ln");
ten = new ConstNode("10");
ln.setRightChild(ten);
mult.setLeftChild(getRightChild());
mult.setRightChild(ln);
div.setLeftChild(getRightChild().derivative());
div.setRightChild(mult);
return div;
case "ln":
div = new OpNode("/");
div.setLeftChild(getRightChild().derivative());
div.setRightChild(getRightChild());
return div;
//case "arcsin":
//case "arccos":
}
return null;
}
}
class ConstNode extends ExpNode
{
int num;
String strNum;
ExpNode leftChild;
ExpNode rightChild;
boolean string = false;
ConstNode(int val)
{
num = val;
strNum = "";
leftChild = null;
rightChild = null;
}
ConstNode(String val)
{
strNum = val;
num = 0;
leftChild = null;
rightChild = null;
string = true;
}
String getVal() { return strNum; }
String getType() { return "constant"; }
ExpNode getLeftChild() { return leftChild; }
ExpNode getRightChild() { return rightChild; }
void setLeftChild(ExpNode lc) { leftChild = lc; }
void setRightChild(ExpNode rc) { rightChild = rc; }
ExpNode derivative() { return new ConstNode("0"); }
}
class VarNode extends ExpNode
{
String variable;
ExpNode leftChild;
ExpNode rightChild;
VarNode(String v)
{
variable = v;
leftChild = null;
rightChild = null;
}
String getVal() { return variable; }
String getType() { return "variable"; }
ExpNode getLeftChild() { return leftChild; }
ExpNode getRightChild() { return rightChild; }
void setLeftChild(ExpNode lc) { leftChild = lc; }
void setRightChild(ExpNode rc) { rightChild = rc; }
ExpNode derivative() {
return new ConstNode("1");
}
}
// $ only has a right child
class NegateNode extends ExpNode
{
String unaryMinus;
ExpNode leftChild;
ExpNode rightChild;
NegateNode unary;
NegateNode()
{
unaryMinus = "$";
leftChild = null;
rightChild = null;
}
NegateNode(String un)
{
unaryMinus = un;
leftChild = null;
rightChild = null;
}
String getVal() { return unaryMinus; }
String getType() { return "unaryMinus"; }
ExpNode getLeftChild() { return leftChild; }
ExpNode getRightChild() { return rightChild; }
void setLeftChild(ExpNode lc) { leftChild = lc; }
void setRightChild(ExpNode rc) { rightChild = rc; }
ExpNode derivative() {
if(getRightChild().getType().equals("constant")) {
return new ConstNode("0");
}
unary = new NegateNode("$");
unary.setRightChild(getRightChild().derivative());
return unary;
}
}
CalculusTool.java
import java.util.ArrayList;
import java.util.Stack;
public class CalculusTool {
public static String var = "";
private String expression;
private String derivative;
private ArrayList<String> postFixTokens;
private ExpNode root;
private ExpNode derivativeRoot;
private int COUNT = 10;
static String errorMessage;
public CalculusTool(String exp)
{
expression = formatString(exp);
postFixTokens = tokenize(expression);
root = constructTree(postFixTokens);
derivativeRoot = root;
derivative = expression;
}
public String getExpression()
{
return expression;
}
public void resetDerivative()
{
derivativeRoot = root;
derivative = expression;
}
public static boolean isValidExpression(String exp)
{
exp = exp.replaceAll("\\s","");
exp = exp.toLowerCase();
if(exp.length() == 0) {
errorMessage = "Nothing Entered";
return false;
}
if(!exp.matches("[a-zA-Z0-9+*/^()-]+")) { // contains only operators, numbers, or letters
errorMessage = "Syntax Error";
return false;
}
if(exp.matches("[+*/^()-]+")) { // doesn't contain any operands
errorMessage = "Syntax Error";
return false;
}
String firstChar = exp.substring(0, 1);
String lastChar = exp.substring(exp.length() - 1, exp.length());
if(!firstChar.equals("-") && isOperator(firstChar) || firstChar.equals(")") || isOperator(lastChar) || lastChar.equals("(")) {
errorMessage = "Syntax Error"; //starts with operator or close parenthesis, or ends with operator or open parenthesis
return false;
}
for(int i = 0; i < exp.length(); i++) {
String temp = "";
while(i < exp.length() && exp.substring(i, i + 1).matches("[a-zA-Z]")) {
temp += exp.substring(i, i + 1);
i++;
}
if(temp.length() == 1) {
//i--; // ?? i must be decremented from the above while loop in this if block so the program can check the last character in the string
if(var.length() == 0)
var = temp;
if(!temp.equals(var)) {
errorMessage = "Your expression cannot contain two variables";
return false;
}
else if(i < exp.length() && exp.substring(i, i + 1).matches("[0-9]+")) {
errorMessage = "Can't do this: " + temp + exp.substring(i, i + 1);
return false;
}
}
else if(isFunction(temp)) {
if(i < exp.length()) {
if(!exp.substring(i, i + 1).equals("(")) {
System.out.println("Syntax Error: " + temp + " needs a parenthesis after it");// no parenthesis after function (EX: sin5)
return false;
}
}
else {
System.out.println("Syntax Error: " + temp + " needs an input"); // nothing after function (EX: 5 + sin)
return false;
}
}
else if(temp.length() != 0){
System.out.println(temp + ": function not found");
return false;
}
//i--; // ?? i must be decremented since it was left incremented in the above while loop
}
int cntOpenParen = 0;
int cntCloseParen = 0;
for(int i = 0; i < exp.length() - 1; i++) {
String tmp1 = exp.substring(i, i + 1);
String tmp2 = exp.substring(i + 1, i + 2);
if(tmp1.equals("-")) {
if(isOperator(tmp2) || tmp2.equals(")")) {
System.out.println("Syntax Error: " + tmp1 + tmp2);
return false;
}
}
else if(tmp2.equals("-")) { // Also prevents next else if from rejecting an operator followed by a unary minus
if(tmp1.equals("(")) {
System.out.println("Syntax Error: " + tmp1 + tmp2);
return false;
}
}
else if((isOperator(tmp1) || tmp1.equals("(")) && (isOperator(tmp2) || tmp2.equals(")"))) {
System.out.println("Syntax Error: " + tmp1 + tmp2); // two operands in a row (examples: ++, (+, ())
return false;
}
else if(exp.substring(i, i + 1).equals("("))
cntOpenParen++;
else if(exp.substring(i, i + 1).equals(")"))
cntCloseParen++;
}
if(cntOpenParen < cntCloseParen) { // found a ")" when the end of the expression was expected
System.out.println("Syntax Error: found ')' but expected end of expression");
return false;
}
return true;
}
public static boolean isOperator(String str)
{
if(str.matches("[+*/^-]"))
return true;
return false;
}
public boolean isOperand(String str)
{
if(str.matches("[0-9]+") || str.equals(var))
return true;
return false;
}
public static boolean isFunction(String str)
{
String[] funcs = {"sin", "cos", "tan", "csc", "sec", "cot", "log", "ln"};
for(String temp: funcs)
if(str.equals(temp))
return true;
return false;
}
// "simplifies" the tree by deleting, replacing and merging nodes
public ExpNode simplify(ExpNode root)
{
if(root == null)
{
return root;
}
else if(root.getType().equals("unaryMinus") && root.getRightChild().getType().equals("unaryMinus")) {
return root.getRightChild().getRightChild();
}
else if(root.getVal().equals("*")) {
if(root.getLeftChild().getVal().equals("1")) {
return root.getRightChild();
}
else if(root.getRightChild().getVal().equals("1")) {
return root.getLeftChild();
}
else if(root.getLeftChild().getVal().equals("0")) {
return root.getLeftChild();
}
else if(root.getRightChild().getVal().equals("0")) {
return root.getRightChild();
}
else if(root.getLeftChild().getVal().equals("*")) {
if(root.getRightChild().getType().equals("constant")) {
if(root.getLeftChild().getLeftChild().getType().equals("constant")) { // Ex: (5*x)*6 ==> 30*x
int num1 = Integer.parseInt(root.getRightChild().getVal());
int num2 = Integer.parseInt(root.getLeftChild().getLeftChild().getVal());
OpNode mult = new OpNode("*");
mult.setLeftChild(new ConstNode(String.valueOf(num1 * num2)));
mult.setRightChild(root.getLeftChild().getRightChild());
return mult;
}
if(root.getLeftChild().getRightChild().getType().equals("constant")) { // Ex: (x*5)*6 ==> 30*x
int num1 = Integer.parseInt(root.getRightChild().getVal());
int num2 = Integer.parseInt(root.getLeftChild().getRightChild().getVal());
OpNode mult = new OpNode("*");
mult.setLeftChild(new ConstNode(String.valueOf(num1 * num2)));
mult.setRightChild(root.getLeftChild().getLeftChild());
return mult;
}
}
if(root.getRightChild().getType().equals("variable")) {
if(root.getLeftChild().getLeftChild().getType().equals("variable")) {
OpNode exponent = new OpNode("^");
exponent.setLeftChild(root.getRightChild());
exponent.setRightChild(new ConstNode("2"));
if(root.getLeftChild().getRightChild().getType().equals("function")) { // Ex: (x*sin(x))*x ==> (x^2)*sin(x)
root.setRightChild(root.getLeftChild().getRightChild());
root.setLeftChild(exponent);
}
else { // Ex: (x*5)*x ==> 5*x^2
root.setRightChild(exponent);
root.setLeftChild(root.getLeftChild().getRightChild());
}
return root;
}
if(root.getLeftChild().getRightChild().getType().equals("variable")) {
OpNode exponent = new OpNode("^");
exponent.setLeftChild(root.getRightChild());
exponent.setRightChild(new ConstNode("2"));
if(root.getLeftChild().getLeftChild().getType().equals("function")) { // Ex: (sin(x)*x)*x ==> (x^2)*sin(x)
root.setRightChild(root.getLeftChild().getLeftChild());
root.setLeftChild(exponent);
}
else { // Ex: (5*x)*x ==> 5*x^2
root.setRightChild(exponent);
root.setLeftChild(root.getLeftChild().getLeftChild());
}
return root;
}
}
}
else if(root.getRightChild().getVal().equals("*")) {
if(root.getLeftChild().getType().equals("constant")) {
if(root.getRightChild().getLeftChild().getType().equals("constant")) { // Ex: 5*(6*x) ==> 30*x
int num1 = Integer.parseInt(root.getLeftChild().getVal());
int num2 = Integer.parseInt(root.getRightChild().getLeftChild().getVal());
OpNode mult = new OpNode("*");
mult.setLeftChild(new ConstNode(String.valueOf(num1 * num2)));
mult.setRightChild(root.getRightChild().getRightChild());
return mult;
}
if(root.getRightChild().getRightChild().getType().equals("constant")) { // Ex: 5*(x*6) ==> 30*x
int num1 = Integer.parseInt(root.getLeftChild().getVal());
int num2 = Integer.parseInt(root.getRightChild().getRightChild().getVal());
OpNode mult = new OpNode("*");
mult.setLeftChild(new ConstNode(String.valueOf(num1 * num2)));
mult.setRightChild(root.getRightChild().getLeftChild());
return mult;
}
}
if(root.getLeftChild().getType().equals("variable")) {
if(root.getRightChild().getLeftChild().getType().equals("variable")) {
OpNode exponent = new OpNode("^");
exponent.setLeftChild(root.getLeftChild());
exponent.setRightChild(new ConstNode("2"));
if(root.getRightChild().getRightChild().getType().equals("function")) { // Ex: x*(x*sin(x)) ==> (x^2)*sin(x)
root.setRightChild(root.getRightChild().getRightChild());
root.setLeftChild(exponent);
}
else { // Ex: x*(x*5) ==> 5*x^2
root.setRightChild(exponent);
root.setLeftChild(root.getRightChild().getRightChild());
}
return root;
}
if(root.getRightChild().getRightChild().getType().equals("variable")) {
OpNode exponent = new OpNode("^");
exponent.setLeftChild(root.getLeftChild());
exponent.setRightChild(new ConstNode("2"));
if(root.getRightChild().getLeftChild().getType().equals("function")) { // Ex: x*(sin(x)*x) ==> (x^2)*sin(x)
root.setRightChild(root.getRightChild().getLeftChild());
root.setLeftChild(exponent);
}
else {
root.setRightChild(exponent);
root.setLeftChild(root.getRightChild().getLeftChild()); // Ex: x*(5*x) ==> 5*x^2
}
return root;
}
}
}
else if(root.getLeftChild().getVal().equals("^") && root.getRightChild().getVal().equals("^")) {
}
else if(root.getLeftChild().getVal().equals("^")) { // Ex: (x^2)*x
}
else if(root.getRightChild().getVal().equals("^")) { // Ex: x*(x^2)
if(root.getLeftChild().getType().equals("variable") && root.getRightChild().getLeftChild().getType().equals("variable")) {
}
}
else if(root.getLeftChild().getType().equals("unaryMinus")) {
if(root.getRightChild().getType().equals("constant") && root.getLeftChild().getRightChild().getType().equals("constant")) {
int num1 = Integer.parseInt(root.getRightChild().getVal());
int num2 = Integer.parseInt(root.getLeftChild().getRightChild().getVal());
NegateNode unary = new NegateNode("$");
unary.setRightChild(new ConstNode(String.valueOf(num1 * num2)));
return unary;
}
}
else if(root.getRightChild().getType().equals("unaryMinus")) {
if(root.getLeftChild().getType().equals("constant") && root.getRightChild().getRightChild().getType().equals("constant")) {
int num1 = Integer.parseInt(root.getLeftChild().getVal());
int num2 = Integer.parseInt(root.getRightChild().getRightChild().getVal());
NegateNode unary = new NegateNode("$");
unary.setRightChild(new ConstNode(String.valueOf(num1 * num2)));
return unary;
}
}
// Ex: x*x ==> x^2
else if(root.getLeftChild().getType().equals("variable") && root.getRightChild().getType().equals("variable")) {
OpNode exponent = new OpNode("^");
exponent.setLeftChild(root.getLeftChild());
exponent.setRightChild(new ConstNode("2"));
return exponent;
}
else if(root.getLeftChild().getType().equals("constant") && root.getRightChild().getType().equals("constant")) {
int num1 = Integer.parseInt(root.getLeftChild().getVal());
int num2 = Integer.parseInt(root.getRightChild().getVal());
return new ConstNode(String.valueOf(num1 * num2));
}
}
else if(root.getVal().equals("+")) {
if(root.getLeftChild().getVal().equals("0")) {
return root.getRightChild();
}
else if(root.getRightChild().getVal().equals("0")) {
return root.getLeftChild();
}
}
else if(root.getVal().equals("^")) {
if(root.getRightChild().getVal().equals("1")) {
return root.getLeftChild();
}
}
root.setLeftChild(simplify(root.getLeftChild()));
root.setRightChild(simplify(root.getRightChild()));
return root;
}
public String derive()
{
derivativeRoot = derivativeRoot.derivative();
// need to change simplfy to do depth-first traversal so that the
// tree will be fully "simplified" after one call
derivativeRoot = simplify(derivativeRoot);
derivativeRoot = simplify(derivativeRoot);
derivativeRoot = simplify(derivativeRoot);
return createInfix(derivativeRoot);
}
public int getPrecedence(String str)
{
int val = 0;
if(str.equals("+"))
val = 2;
else if(str.equals("-"))
val = 2;
else if(str.equals("*") || str.equals(("/")))
val = 3;
else if(str.equals("^") || str.equals("$"))
val = 4;
return val;
}
public boolean isLeftAssociative(String s)
{
if(s.equals("^") || s.equals("$") || s.equals("+") || s.equals("*"))
return false;
return true;
}
// modifies the string to look more like it would if someone wrote the expression
// out on paper
public String readable(String s)
{
for(int i = 0; i < s.length(); i++) {
if(s.substring(i, i + 1).equals("*") && i > 0)
if(isOperand(s.substring(i - 1, i)) && i < s.length() - 1 && s.substring(i + 1, i + 2).equals(var))
s = s.substring(0, i) + s.substring(i + 1);
}
return s;
}
// adds and deletes characters to aid in the creation of the binary expression tree
public String formatString(String exp)
{
exp = exp.replaceAll("\\s",""); // why
exp = exp.toLowerCase();
int count = 0;
if(exp.substring(0, 1).equals("-")) { // if expression starts with a minus sign, it is a unary one
exp = "$" + exp.substring(1); // replace
}
for(int i = 0; i < exp.length(); i++) {
if(exp.substring(i, i + 1).equals("("))
count++;
else if(exp.substring(i, i + 1).equals(")"))
count--;
}
while(count > 0) {
exp += ")";
count--;
}
// At the operators, when the operator is "-" and it is preceded by another operator,
// or preceded by a left parenthesis, or when it is the first character of the input
// it is a unary minus rather than binary. In this case, I change it to another
// character, '$', and make its precedence the same as that of '^'.
for(int i = 0; i < exp.length() - 1; i++) {
String tmp1 = exp.substring(i, i + 1);
String tmp2 = exp.substring(i + 1, i + 2);
if(tmp2.equals("-") && (isOperator(tmp1) || tmp1.equals("(")))
exp = exp.substring(0, i + 1) + "$" + exp.substring(i + 2);
else if((tmp1.matches("[0-9]+") || tmp1.equals(var)) && (tmp2.equals("(") || tmp2.equals(var)))
exp = exp.substring(0, i + 1) + "*" + exp.substring(i + 1);
}
return exp;
}
//creates string representing infix expression
public String createInfix(ExpNode root)
{
String str = "";
if(root == null) {
return str;
}
if (root != null) {
if (root.getRightChild() == null) {
str += root.getVal();
}
else if(root.getType().equals("function")) {
str += root.getVal();
//str += "(";
}
else if(root.getType().equals("unaryMinus")) {
str += "-";
}
else {
int parentPrecedence = getPrecedence(root.getVal());
str += root.getVal();
if(root.getLeftChild() != null && (getPrecedence(root.getLeftChild().getVal()) < parentPrecedence || isLeftAssociative(root.getLeftChild().getVal()))) {
//str += "(";
}
/*if(getPrecedence(root.getRightChild().getVal()) < parentPrecedence) {
str += ")";
}*/
}
}
return createInfix(root.getLeftChild()) + str + createInfix(root.getRightChild());
}
// separates the expression string into "tokens" and sorts them in
// postfix order
public ArrayList<String> tokenize(String exp)
{
ArrayList<String> tokens = new ArrayList<>();
Stack<String> stack = new Stack<>();
for(int i = 0; i < exp.length(); i++)
{
String token = "";
if(isOperator(exp.substring(i, i + 1)) || exp.substring(i, i + 1).equals("$")) {
token = exp.substring(i, i + 1);
while ((!stack.isEmpty() && (isOperator(stack.peek()) || stack.peek().equals("$")))
&& ((isLeftAssociative(token) && getPrecedence(token) <= getPrecedence(stack.peek()))
|| (!isLeftAssociative(token) && getPrecedence(token) < getPrecedence(stack.peek())))) {
tokens.add(stack.pop());
}
stack.push(token);
}
else if(exp.substring(i, i + 1).equals(var)) {
token = var;
tokens.add(token);
}
else if(exp.substring(i, i + 1).equals("(")) {
token = exp.substring(i, i + 1);
stack.push(token);
}
else if(exp.substring(i, i + 1).equals(")")) {
while(!stack.isEmpty() && !stack.peek().equals("(")) {
tokens.add(stack.pop());
}
if(!stack.isEmpty())
stack.pop();
if(!stack.isEmpty() && isFunction(stack.peek())) {
tokens.add(stack.pop());
}
}
else if(exp.substring(i, i + 1).matches("[0-9]+")) {
while(i < exp.length() && exp.substring(i, i + 1).matches("[0-9]+")) {
token += exp.substring(i, i + 1);
i++;
}
tokens.add(token);
i--; // i was left incremented after the while loop
}
else if(exp.substring(i, i + 1).equals(var)) {
tokens.add(token);
}
else {
while(i < exp.length() && exp.substring(i, i + 1).matches("[a-zA-Z]+")) {
token += exp.substring(i, i + 1);
i++;
}
if(token.length() != 0) {
stack.push(token);
}
}
}
while(!stack.isEmpty()) {
tokens.add(stack.pop());
}
return tokens;
}
// reads the "tokens" in order from the list and builds a tree
public ExpNode constructTree(ArrayList<String> postTokens)
{
ExpNode root = null;
Stack<ExpNode> nodes = new Stack<>();
for(String str: postTokens)
{
if(str.matches("[0-9]+")) {
nodes.push(new ConstNode(str));
}
else if(str.equals(var))
nodes.push(new VarNode(var));
else if(!nodes.isEmpty() && isFunction(str)) {
FuncNode function = new FuncNode(str);
function.setRightChild(nodes.pop());
nodes.push(function);
}
else if(!nodes.isEmpty() && str.equals("$")) {
NegateNode unary = new NegateNode("$");
unary.setRightChild(nodes.pop());
nodes.push(unary);
}
else if(!nodes.isEmpty()){
OpNode operator = new OpNode(str);
operator.setRightChild(nodes.pop());
operator.setLeftChild(nodes.pop());
nodes.push(operator);
}
}
if(!nodes.isEmpty())
root = nodes.pop();
return root;
}
}
2 Answers 2
Restricted domain
String operator;
Consider
BinaryOperator operator;
Where
Enum BinaryOperator {
PLUS("+"),
MINUS("-"),
MULTIPLY("*"),
DIVIDE("/"),
POWER("^");
final private String display;
BinaryOperator(String display) {
this.display = display;
}
public String toString() {
return display;
}
}
This way, rather than having to do string evaluations to switch, you can just track the operations directly.
Naming
I would prefer a name like Expression
to ExpNode
. ExpNode
tells the user how the structure is implemented. But a user shouldn't have to care about that. Logically, what you have is a representation of an expression.
A name like derivative
is consistent with method names like iterator
, but it is more typical to give methods verb names. E.g. calculateDerivative
or takeDerivative
.
Redundant fields
OpNode mult1; OpNode mult2; OpNode mult3; OpNode div; OpNode plus; OpNode minus; OpNode exponent; NegateNode unary;
I didn't get why you had these at first. But reading through your derivative
method, these are where you store the derivative. Consider instead
private Expression derivative = null;
Then change from
case "/": // h(x) = f(x)/g(x) then h′(x) = (f′(x)g(x) − f(x)g′(x))/( g(x)^2) mult1 = new OpNode("*"); mult2 = new OpNode("*"); mult3 = new OpNode("*"); div = new OpNode("/"); minus = new OpNode("-"); mult1.setLeftChild(getRightChild()); mult1.setRightChild(getLeftChild().derivative()); mult2.setLeftChild(getLeftChild()); mult2.setRightChild(getRightChild().derivative()); minus.setLeftChild(mult1); minus.setRightChild(mult2); mult3.setLeftChild(getRightChild()); mult3.setRightChild(getRightChild()); div.setLeftChild(minus); div.setRightChild(mult3); return div;
to something like
case "/":
// h(x) = f(x)/g(x) then h′(x) = (f′(x)g(x) − f(x)g′(x))/( g(x)^2)
Expression numerator = new BinaryExpression(DIFFERENCE);
Expression denominator = new BinaryExpression(PRODUCT);
Expression minuend = new BinaryExpression(PRODUCT);
Expression subtrahend = new BinaryExpression(PRODUCT);
minuend.setLeftChild(getLeftChild().takeDerivative());
minuend.setRightChild(getRightChild());
subtrahend.setLeftChild(getLeftChid());
subtrahend.setRightChild(getRightChild().takeDerivative());
numerator.setLeftChild(minuend);
numerator.setRightChild(subtrahend);
denominator.setLeftChild(getRightChild());
denominator.setRightChild(getRightChild());
derivative = new BinaryExpression(DIVISION);
derivative.setLeftChild(numerator);
derivative.setRightChild(denominator);
return derivative;
Now you don't have to include every possible permutation as object fields. Only derivative
persists past the length of the method. Everything else is a local variable specific to this operation.
I used operation specific names rather than generic names like left and right.
If you add
if (derivative != null) {
return derivative;
}
Then you don't have to reprocess everything each time.
Or if you want to reprocess everything, then you can make derivative
local as well.
More classes
You have a separate class for unary minus (NegateNode
). Why not do that for other operations? Consider
class Quotient implements Expression {
private final Expression numerator;
private final Expression denominator;
public Quotient(Expression numerator, Expression denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
public Expression getNumerator() {
return numerator;
}
public Expression getDenominator() {
return denominator;
}
@Override
public Expression takeDerivative() {
return new Quotient(
new Difference(
new Product(numerator.takeDerivative(), denominator),
new Product(numerator, denominator.takeDerivative()),
),
new Product(denominator, denominator)
);
}
@Override
public Expression simplify() {
Expression n = numerator.simplify();
Expression d = denominator.simplify();
if (n.equals(Constant.ZERO) || d.equals(Constant.ONE)) {
return n;
}
if (n.equals(d)) {
return Constant.ONE;
}
if (d instanceof Quotient) {
if (n instanceof Quotient) {
return new Quotient(
new Product(n.getNumerator(), d.getDenominator()),
new Product(d.getNumerator(), n.getDenominator())
).simplify();
} else {
return new Quotient(
new Product(n, d.getDenominator()),
d.getNumerator()
).simplify();
}
} else if (n instanceof Quotient) {
return new Quotient(
n.getNumerator(),
new Product(d, n.getDenominator())
).simplify();
}
return new Quotient(n, d);
}
@Override
public Operator getOperator() {
return DIVISION;
}
@Override
public boolean equals(Object object) {
if (! object instanceof Expression) {
return false;
}
if (object == this) {
return true;
}
Expression t = simplify();
Expression o = ((Expression) object).simplify();
if (! t instanceof Quotient) {
return t.equals(o);
}
if (! o instanceof Quotient) {
return false;
}
Quotient qt = (Quotient) t;
Quotient qo = (Quotient) o;
return qt.getNumerator().equals(qo.getNumerator())
&& qt.getDenominator().equals(qo.getDenominator());
}
}
This may be missing a few methods, but it should cover the basics of the idea.
If you make separate classes for each operation, you don't need the switch
anymore. Each class provides its own method instead. This can work for both takeDerivative
and for simplify
.
A side issue is that this eliminates all those object fields entirely through simple construction.
I made this immutable. You may not want to do so. Easy enough to remove, as it's just a few final
keywords.
I switched the object fields to be private
rather than the default package private. You don't seem to be making use of the package private privileges, and this is usually better practice. Similarly, I explicitly made methods public
rather than relying on them being called from the same package.
I did not implement hashCode
, but it might be advisable to do so. Otherwise, funky things could happen if you used this as a key in a HashMap
, put it in a HashSet
, or otherwise did something that relied on hashCode
.
I may be missing simplification cases. For example, \$\frac{x^2 + x}{x}\$ would not simplify to \$x+1 \forall x \ne 0\$ with this method. These are the ones that came to mind with easy implementations.
-
\$\begingroup\$ This is some great insight, I appreciate it. Any recommendations on how to handle my
simplify
(remove redundant terms) method? To make it look cleaner, maybe I put it in theExpression
class and make it a static method(so it could be used anywhere) \$\endgroup\$defoification– defoification2017年05月27日 14:16:15 +00:00Commented May 27, 2017 at 14:16 -
\$\begingroup\$ @defoification I edited the answer in reply. I didn't even see
simplify
originally, as I was thinking more aboutExpNode
. \$\endgroup\$mdfst13– mdfst132017年05月28日 01:11:17 +00:00Commented May 28, 2017 at 1:11 -
\$\begingroup\$ awesome answer. Did you mean
BinaryOperator
instead ofBinaryExpression
in this lineExpression minuend = new BinaryExpression(PRODUCT);
? And since I want to implementExpression
, where should I put myEnum
? \$\endgroup\$defoification– defoification2017年05月29日 04:51:49 +00:00Commented May 29, 2017 at 4:51 -
\$\begingroup\$ Or should I just have two interfaces, a
BinaryExpression
and aUnaryExpression
. Maybe I just havepublic class Expression
and then those two interfaces that extendExpression
, and inExpression
I have the enum. Sorry, I am very confused. I have never made anything with so many parts. \$\endgroup\$defoification– defoification2017年05月29日 05:03:48 +00:00Commented May 29, 2017 at 5:03
Some general things you should always consider:
Keep class and method size small
The average size of your classes and methods is way to small. Try limiting the length drastically.Make a class do one thing and only one thing
This is more or less the single responsibility principle (https://en.wikipedia.org/wiki/Single_responsibility_principle). In particular, avoid having switches based on some internal variable state. Another thing this can be seen is when some variables aren't used depending on the case. Also use all variables when using polymorphism.Don't expose internals of your classes
The user of a ExpNode shouldn't have to worry about setting up the internal state with setLeftNode (what happens when someone forgets to do that? Make your design failsafe)Use appropriate variable types
Don't encode e.g. a number information into a stringFormat your code consistently
You changed the position of your parenthesis. The usual convention of Java is to have the opening parenthesis { on the same line.
ExpNode.java
According to the single responsibility principle I would propose the following class structure: The base class is a Expression. Subclasses involve BinaryOperation and UnaryOperation(i.e. Functions). All those classes are still abstract and you implement your concrete Classes as subclasses of those.
abstract class Expression {
abstract Expression derive();
abstract double getValue(double x);
}
I've kept it simple and required only two methods. The getValue method is used to calculate the value of the expression and derive calculates the deriviative.
One could also add some methods like abstract String getDescription()
for a textual representation or abstract Expression simplify()
for simple simplifications.
abstract class BinaryOperation extends Expression
{
protected Expression left;
protected Expression right;
BinaryOperation(Expression left, Expression right)
{
this.left = left;
this.right = right;
}
}
Note that the variables left and right are directly set in the constructor and a BinaryOperation cannot be in a "wrong" state.
class AdditionOperator extends BinaryOperation {
AdditionOperator(Expression left, Expression right) {
super(left, right);
}
@Override
public Expression derive() {
return new AdditionOperator(left.derive(), right.derive());
}
@Override
double getValue(double x) {
return left.getValue(x) + right.getValue(x);
}
}
Note how simple the methods are now compared to the complicated design of having to check the operator and do something accordingly.
abstract class UnaryOperation extends Expression
{
protected Expression exp;
UnaryOperation(Expression exp)
{
this.exp = exp;
}
}
Again, it isn't possible for the expression to be null. Setting up the class is done in the constructor.
class NaturalLogarithm extends UnaryOperation {
@Override
public Expression derive() {
return new DivisionOperator(exp.derive(), exp); // I haven't implemented Division for you
}
@Override
double getValue(double x) {
return Math.log(x);
}
}
This is an example of a function realized as a unary operation.
class Constant extends Expression
{
double value;
Constant(double value)
{
this.value = value;
}
Expression derive() {
return new Constant(0);
}
@Override
double getValue(double x) {
return value;
}
}
class Variable extends Expression
{
Variable() {
}
Expression derive() {
return new Constant(1);
}
@Override
double getValue(double x) {
return x;
}
}
Variables and Constants are realized as direct subclasses of Expression
CalculusTool.java
After quickly skimming this class I'm not sure what the main purpose of this class is. It looks a bit like you just threw functionality into the class without thinking about how to group it logically.
For example, without closely reading the code I'm completely baffled why there is public static String var = "";
. Why do you use global state? What is the purpose of this variable?
Then there are a bunch of static methods mixed with some non-static methods. Maybe you rework this class(i.e. extract it into different classes!) and post it in another code review.
Just one quick note I noticed in the method isValidExpression(String exp)
:
I noticed you use some global state variable for the errorMessage. How about considering to rename the method to Evaluation evaluateExpression(String exp)
and build a class Evaluation as follows:
class Evaluation {
private boolean isValid;
private List<String> errorMessages;
// constructor and getters
}
MathApp.java
I kept my refactoring here also quite basic. I only extracted some methods in order to keep them shorter. A good refactoring here can only be done after CalculusTool is refactored. Just a pointer though: Again you use some String expression for handling the state. Consider using another event in order to track if there have been changes to the textfield.
public class MathApp extends Application {
private CalculusTool cT;
private String expression;
private String deriv;
private TextField expBox;
private Text errorText;
private Text actionTarget;
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("MathApp");
GridPane grid = constructGrid();
Scene scene = new Scene(grid, 300, 275);
primaryStage.setScene(scene);
primaryStage.show();
}
private GridPane constructGrid() {
GridPane grid = constructGridPane();
addExpressionText(grid);
expBox = constructAndAddExpBox(grid);
errorText = constructAndAddErrorText(grid);
Button btn = addDeriveButton(grid);
actionTarget = addActionTarget(grid);
btn.setOnAction(this::handleButtonClick);
return grid;
}
public void handleButtonClick(ActionEvent e) {
//same expression - derive again when clicked
if(expBox.hasProperties() && expBox.getText().equals(expression)) {
deriveAndUpdateUI();
}
else { // new expression, create new object
expression = expBox.getText();
if (!CalculusTool.isValidExpression(expression)) {
printErrorMessage();
} else {
errorText.setVisible(false);
cT = new CalculusTool(expression);
deriveAndUpdateUI();
}
}
}
private void printErrorMessage() {
errorText.setText(CalculusTool.errorMessage);
actionTarget.setVisible(false);
errorText.setVisible(true);
expBox.clear();
}
private void deriveAndUpdateUI() {
deriv = cT.readable(cT.derive());
actionTarget.setVisible(true);
actionTarget.setFill(Color.FIREBRICK);
actionTarget.setText(deriv);
}
private Text addActionTarget(GridPane grid) {
final Text actiontarget = new Text();
grid.add(actiontarget, 1, 6);
return actiontarget;
}
private Button addDeriveButton(GridPane grid) {
Button btn = new Button("Derive");
HBox hbBtn = new HBox(10);
hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
hbBtn.getChildren().add(btn);
grid.add(hbBtn, 1, 4);
return btn;
}
private Text constructAndAddErrorText(GridPane grid) {
Text error = new Text();
grid.add(error, 1, 6);
error.setVisible(false);
return error;
}
private TextField constructAndAddExpBox(GridPane grid) {
TextField expBox = new TextField();
grid.add(expBox, 1, 2);
return expBox;
}
private void addExpressionText(GridPane grid) {
Text scenetitle = new Text("Enter an expression");
scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 15));
grid.add(scenetitle, 0, 0, 2, 1);
}
private GridPane constructGridPane() {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(25, 25, 25, 25));
return grid;
}
public static void main(String[] args) {
launch(args);
}
}
Explore related questions
See similar questions with these tags.
if
conditions together with Boolean operators. Unfortunately, your code is a bit too long for me to review at this time. Overall, this is a great effort! The comments are especially helpful! \$\endgroup\$getType()
(and the uglyString
type). If I have some time I'll write an answer \$\endgroup\$