7
\$\begingroup\$

There have been a lot of calculators lately. It seems like this community challenge all over again, I didn't make a proper one then, so I'm joining the calculatrain now.

Building from the lessons learned from a preceding far simpler calculator.

application screenshot

The goal was to encompass corner cases that are otherwise easy to miss. I'd like pinpoint whether I've missed anything and whether I'm doing something inefficiently that should/could be done in a cleaner way.

I'm also concerned about names and readability, I want to make this easy to follow and understand, despite it being a solo venture, the purpose is practice, and if CR has taught me anything it's that that is crucial.

Calculator.java

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Calculator extends Application {
    static Operator currentOperator;
    static boolean operatorSelected;
    static boolean resultDisplayed;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        BorderPane layout = new BorderPane();

        TextField auxiliary = new TextField();
        auxiliary.setStyle("-fx-font-size: 15; -fx-text-fill: gray");
        auxiliary.setMaxWidth(415); // 415 = total width, including margins of buttons
        auxiliary.setEditable(false);

        TextField result = new TextField();
        result.setStyle("-fx-font-size: 40");
        result.setMaxWidth(415); 
        result.setEditable(false);

        VBox resultLayout = new VBox();
        resultLayout.getChildren().addAll(auxiliary, result);
        layout.setTop(resultLayout);


        GridPane buttonLayout = new GridPane();
        buttonLayout.setPadding(new Insets(10, 0, 0, 0));
        buttonLayout.setHgap(5);
        buttonLayout.setVgap(5);
        layout.setCenter(buttonLayout);

        Button backButton = new Button("\u2190");
        backButton.setMinSize(100, 100);
        backButton.setOnAction(e -> {
            String currentText = result.getText();
            if (!currentText.isEmpty() && !resultDisplayed){
                result.setText(currentText.substring(0, currentText.length() - 1));
            }
        });
        buttonLayout.add(backButton, 2, 0);

        Button clearButton = new Button("Clear");
        clearButton.setMinSize(200, 100);
        clearButton.setOnAction(e -> {
            result.clear();
            auxiliary.clear();
            operatorSelected = false;
        });
        GridPane.setColumnSpan(clearButton, 2); 
        buttonLayout.add(clearButton, 0, 0);

        Button[] numberButtons = new Button[10];
        for (int i = 3, target = 1; i >= 1; i--) {
            for (int j = 0; j <= 2; j++) {
                String number = Integer.toString(target);

                numberButtons[target] = new Button(number);
                numberButtons[target].setMinSize(100, 100);
                numberButtons[target].setOnAction(e -> {
                    if (resultDisplayed) {
                        result.setText(number);
                        resultDisplayed = false;
                    } else {
                        result.appendText(number);
                    }

                    operatorSelected = false;
                });
                buttonLayout.add(numberButtons[target++], j, i);
            }   
        }

        numberButtons[0] = new Button("0");
        numberButtons[0].setMinSize(200, 100);
        numberButtons[0].setOnAction(e -> {
            if (!result.getText().isEmpty() && !resultDisplayed) {
                result.appendText("0");
                operatorSelected = false;
            }
        });
        GridPane.setColumnSpan(numberButtons[0], 2);
        buttonLayout.add(numberButtons[0], 0, 4);


        Button decimalButton = new Button(".");
        decimalButton.setMinSize(100, 100);
        decimalButton.setOnAction(e -> {
            if (result.getText().indexOf('.') == -1) {
                result.appendText(".");
            }
        });
        buttonLayout.add(decimalButton, 2, 4);

        for (Operator op : Operator.values()) {
            String symbol = op.toString();

            Button button = new Button(symbol);
            button.setMinSize(100, 100);
            button.setStyle("-fx-color: orange");
            button.setOnAction(e -> {
                if (auxiliary.getText().isEmpty()) {
                    auxiliary.setText(result.getText().isEmpty() ? "0" : acquireValue(result.getText()));
                    auxiliary.appendText(" " + symbol);
                    currentOperator = op;
                    resultDisplayed = true;
                    operatorSelected = true;
                } else if (operatorSelected) {
                    currentOperator = op;
                    int end = auxiliary.getText().length();
                    auxiliary.replaceText(end - 1, end,  symbol);
                } else {
                    auxiliary.setText(calculate(currentOperator, result, auxiliary) + " " + symbol);
                    result.clear();
                    currentOperator = op;
                    resultDisplayed = true;
                    operatorSelected = true;
                }
            });
            buttonLayout.addColumn(3, button);
        }

        Button equalsButton = new Button("=");
        equalsButton.setStyle("-fx-color: orange");
        equalsButton.setMinSize(100, 100);
        equalsButton.setOnAction(e -> {
            if (!auxiliary.getText().isEmpty()) {
                result.setText(
                    calculate(currentOperator, result, auxiliary)
                );
                resultDisplayed = true;
                operatorSelected = false;
                auxiliary.clear();
            }
        });
        buttonLayout.addColumn(3, equalsButton);
        equalsButton.setDefaultButton(true);

        Scene scene = new Scene(layout);
        scene.getStylesheets().add("Calculator.css");

        stage.setScene(scene);
        stage.setResizable(false);
        stage.sizeToScene();
        stage.setTitle("Legato's Calculator");
        stage.show();
    }

    private static String calculate(Operator op, TextField main, TextField auxiliary) {
        double val1 = Double.parseDouble(auxiliary.getText().replaceAll("[^\\.0-9]", ""));
        double val2 = Double.parseDouble(main.getText());

        if (val2 == 0 && op == Operator.DIVIDE) {
            return "Cannot divide by 0";
        }

        double result = op.compute(val1, val2);
        return toCalculatorString(result);
    }

    private static String acquireValue(String val) {
        double result = Double.parseDouble(val);
        return toCalculatorString(result);
    }

    private static String removeDecimalTrailingZeroes(String s) {
        return s.indexOf(".") < 0 ? s : s.replaceAll("0*$", "").replaceAll("\\.$", "");
    }

    private static String toCalculatorString(double input) {
        return input == (int)input ? 
            Integer.toString((int)input) : removeDecimalTrailingZeroes(String.format("%.6f", input));
    }
}

Operator.java

import java.util.function.BinaryOperator;

public enum Operator {
    DIVIDE("\u00F7", (x, y) -> x / y),
    MULTIPLY("x", (x, y) -> x * y),
    SUBTRACT("-", (x, y) -> x - y),
    ADD("+", (x, y) -> x + y);

    private final String symbol;
    private final BinaryOperator<Double> equation;


    Operator(String symbol, BinaryOperator<Double> equation) {
        this.symbol = symbol;
        this.equation = equation;
    }

    public double compute(double alpha, double beta) {
        return equation.apply(alpha, beta);
    }

    @Override
    public String toString() {
        return symbol;
    }
}

Calculator.css

 .text-field {
    -fx-text-box-border: transparent;
    -fx-background-color: -fx-control-inner-background;
    -fx-background-insets: 0;
    -fx-padding: 1 3 1 3;
}


.text-field:focused {
    -fx-focus-color: transparent;
}

Feel free to download and run it. Requires JRE 8.

\$\endgroup\$
2
\$\begingroup\$

There is no way around it, but start() is too lengthy... you should consider breaking it down. For example, you can consolidate your UI elements in the following manner:

  • Auxillary and main text fields
  • The Clear and "backspace" buttons
  • The numeric and dot buttons
  • The operator buttons

Then, start() will only have four such method calls, which can make its readability much better.

As for the Operator enum, you may want to consider using DoubleBinaryOperator instead of BinaryOperator<Double> to avoid the auto-boxing.

\$\endgroup\$

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.