diff --git a/README.md b/README.md index 82c6217450870d7fa7960d464fa9bd66b566f2f1..e59d1151b4a5773b282430899de7c981a005a7df 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ -## Javafx template +# Javafx template -Template for single-module javafx projects, with maven setup for latest java and javafx, and jupiter and testfx for testing. +Template for single-module javafx project, with maven setup for Java 16 and JavaFX 16, and JUnit 5 (Jupiter) and TestFX for testing. + +To make the project more interesting, it is the start of an [RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation) calculator (look for `// TODO`) markers). The core logic is almost implemented (in [Calc.java](src/main/java/app/Calc.java)), the fxml file (in [App.fxml](src/main/resources/app/App.fxml) is almost complete, but the controller class (in [AppController.java](src/main/java/app/AppController.java) is pretty limited. And last, but not least, there is a TestFX-based test (in [AppTest.java](src/test/java/app/AppTest.java), see the [README](src/test/java/app/README.md) for details about what it tests. + +## Trying it out + +The project in javafx-template can be tried out in various ways: + +- compile with `mvn compile` (after `cd javafx-template` of course) +- test with `mvn test` (it should fail until you complete the RPN calculator) +- run with `mvn javafx:run` (it should open, but not work properly) diff --git a/javafx-template/src/main/java/app/AppController.java b/javafx-template/src/main/java/app/AppController.java index 9efef1cdd5e3f12e2ea53e51b5f631dc3d3dd966..1cbbd295aac1d9594c7610f135965215c8bbebbb 100644 --- a/javafx-template/src/main/java/app/AppController.java +++ b/javafx-template/src/main/java/app/AppController.java @@ -2,7 +2,6 @@ package app; import java.util.List; import java.util.function.BinaryOperator; -import java.util.function.Consumer; import java.util.function.UnaryOperator; import javafx.event.ActionEvent; @@ -39,16 +38,12 @@ public class AppController { updateOperandsView(); } - private int minOperandCount = 2; - private void updateOperandsView() { - while (calc.getOperandCount() < minOperandCount) { - calc.pushOperand(calc.getOperandCount(), 0.0); - } List<Double> operands = operandsView.getItems(); operands.clear(); - for (int i = 0; i < minOperandCount; i++) { - operands.add(calc.peekOperand(minOperandCount - i - 1)); + int elementCount = Math.min(calc.getOperandCount(), 3); + for (int i = 0; i < elementCount; i++) { + operands.add(calc.peekOperand(elementCount - i - 1)); } } @@ -68,33 +63,25 @@ public class AppController { operandView.setText(operandString); } - private void setOperand(double d) { - setOperand(Double.toString(d)); - } - - private void clearOperand() { - setOperand(""); - } - @FXML void handleEnter() { if (hasOperand()) { calc.pushOperand(getOperand()); } else { calc.dup(); - } - clearOperand(); + } + setOperand(""); updateOperandsView(); - } + } private void appendToOperand(String s) { - setOperand(operandView.getText() + s); + // TODO } @FXML void handleDigit(ActionEvent ae) { if (ae.getSource() instanceof Labeled l) { - appendToOperand(l.getText()); + // TODO append button label to operand } } @@ -102,70 +89,40 @@ public class AppController { void handlePoint() { var operandString = getOperandString(); if (operandString.contains(".")) { - setOperand(operandString.substring(0, operandString.indexOf(".") + 1)); + // TODO remove characters after point } else { - appendToOperand("."); + // TODO append point } } @FXML void handleClear() { - clearOperand(); - } - - private void withOperand(Runnable proc) { - if (hasOperand()) { - calc.pushOperand(getOperand()); - clearOperand(); - } - proc.run(); - updateOperandsView(); + // TODO clear operand } private void performOperation(UnaryOperator<Double> op) { - withOperand(() -> calc.performOperation(op)); + // TODO } private void performOperation(boolean swap, BinaryOperator<Double> op) { - withOperand(() -> { - if (swap) { - calc.swap(); - } - calc.performOperation(op); - }); + if (hasOperand()) { + // TODO push operand first + } + // TODO perform operation, but swap first if needed } @FXML void handleOpAdd() { - performOperation(false, (op1, op2) -> op1 + op2); + // TODO } @FXML void handleOpSub() { - performOperation(true, (op1, op2) -> op1 - op2); + // TODO } @FXML void handleOpMult() { - performOperation(false, (op1, op2) -> op1 * op2); - } - - @FXML - void handleOpDiv() { - performOperation(true, (op1, op2) -> op1 / op2); - } - - @FXML - void handleOpSquareRoot() { - performOperation(op1 -> Math.sqrt(op1)); - } - - @FXML - void handlePi() { - withOperand(() -> calc.pushOperand(Math.PI)); - } - - public static void main(String[] args) { - System.out.println("\u221A"); + // TODO } } diff --git a/javafx-template/src/main/java/app/Calc.java b/javafx-template/src/main/java/app/Calc.java index 720035912fb68239826229c5a8968e279130f905..6102e31383fcdb96fa94197a73a7774bfcbd70a1 100644 --- a/javafx-template/src/main/java/app/Calc.java +++ b/javafx-template/src/main/java/app/Calc.java @@ -13,41 +13,79 @@ public class Calc { operandStack.addAll(List.of(operands)); } + /** + * @return the number of operands on the stack + */ public int getOperandCount() { return operandStack.size(); } + /** + * Pushes a new operand into the n'th place from the top. + * @param n the place to push + * @param d the new operand + */ public void pushOperand(int n, double d) { operandStack.add(operandStack.size() - n, d); } + /** + * Pushes a new operand onto top of the stack. + * @param d the new operand + */ public void pushOperand(double d) { pushOperand(0, d); } + /** + * @param n the place (from the top) to peek + * @return the n'th operand from the top + */ public double peekOperand(int n) { return operandStack.get(operandStack.size() - n - 1); } + /** + * @return the top operand + */ public double peekOperand() { return peekOperand(0); } + /** + * Removes and returns the n'th operand from the top. + * @param n the place from the top to remove + * @return the n'th operand from the top + */ public double popOperand(int n) { return operandStack.remove(operandStack.size() - n - 1); } + /** + * Removes and returns the top operand. + * @return the top operand + */ public double popOperand() { return popOperand(0); } + /** + * Performs the provided operation in the top operand, and + * replaces it with the result. + * @param op the operation to perform + * @return the result of performing the operation + */ public double performOperation(UnaryOperator<Double> op) { - var op1 = popOperand(); - var result = op.apply(op1); - pushOperand(result); - return result; + // TODO + return 0.0; } + /** + * Performs the provided operation in the two topmost operands, and + * replaces them with the result. + * @param op the operation to perform + * @return the result of performing the operation + */ public double performOperation(BinaryOperator<Double> op) { var op1 = popOperand(); var op2 = popOperand(); @@ -56,14 +94,17 @@ public class Calc { return result; } + /** + * Swaps the two topmost operands. + */ public void swap() { - var op1 = popOperand(); - var op2 = popOperand(); - pushOperand(op1); - pushOperand(op2); + // TODO } + /** + * Duplicates the top operand. + */ public void dup() { - pushOperand(peekOperand()); + // TODO } } \ No newline at end of file diff --git a/javafx-template/src/main/resources/app/App.fxml b/javafx-template/src/main/resources/app/App.fxml index 4669ea86207440f682edfc75ab91eb722cce8e97..41f4e4f1816de5e5e8134f3f6c45307279f7e554 100644 --- a/javafx-template/src/main/resources/app/App.fxml +++ b/javafx-template/src/main/resources/app/App.fxml @@ -13,6 +13,7 @@ <Label text="" fx:id="operandView" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="4"/> + <!-- multi-line button label with XML entity for newline --> <Button text="E n t e r" onAction="#handleEnter" GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="4"/> @@ -47,10 +48,12 @@ GridPane.rowIndex="6" GridPane.columnIndex="1"/> <Button text="*" onAction="#handleOpMult" GridPane.rowIndex="6" GridPane.columnIndex="2"/> - <Button text="/" onAction="#handleOpDiv" + + <!-- TODO --> + <Button text="/" GridPane.rowIndex="6" GridPane.columnIndex="3"/> - <Button text="√" onAction="#handleOpSquareRoot" + <Button text="√" GridPane.rowIndex="7" GridPane.columnIndex="0"/> - <Button text="π" onAction="#handlePi" + <Button text="π" GridPane.rowIndex="7" GridPane.columnIndex="1"/> </GridPane> diff --git a/javafx-template/src/test/java/app/AppTest.java b/javafx-template/src/test/java/app/AppTest.java index aa6dc6e0b0673311745876e8a799ad8f534f4265..209ebfc63ace75f7894281ca73ec606f61da4798 100644 --- a/javafx-template/src/test/java/app/AppTest.java +++ b/javafx-template/src/test/java/app/AppTest.java @@ -9,7 +9,6 @@ import javafx.stage.Stage; import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; @@ -17,7 +16,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.opentest4j.AssertionFailedError; import org.testfx.framework.junit5.ApplicationTest; import org.testfx.matcher.control.LabeledMatchers; @@ -106,7 +104,7 @@ public class AppTest extends ApplicationTest { for (var label : labels.split(" ")) { click(label.equals("\n") ? enterLabel : label); } - checkView(Stream.of(operandsString.split(" ")).mapToDouble(Double::valueOf).toArray()); + checkView("", Stream.of(operandsString.split(" ")).mapToDouble(Double::valueOf).toArray()); } private static Stream<Arguments> testClicksOperands() { diff --git a/javafx-template/src/test/java/app/README.md b/javafx-template/src/test/java/app/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0cc59802a04720e2dfddaa5c14c029328b91ef33 --- /dev/null +++ b/javafx-template/src/test/java/app/README.md @@ -0,0 +1,38 @@ +# Tests for the RPN calculator + +This folder/package contains tests based on TestFX for the RPN Calculator (currently only one test class). + +As can be seen when launching, the app contains a list (top) showing the operands +(topmost operand at the bottom), a text field (below list, initially empty) for a new operand and +the buttons for digits, enter, decimal point, operations etc. + +## What is tested + +The tests simulate clicks on the buttons and checks that the underlying Calc object, +the list (a view of the Calc object's operand stack) and the text field are updated as expected. +E.g. if you click buttons `2 3 . 5` the string `23.5` should be shown, +while the list is not affected. If you then click `enter`, the text field should be emptied, the operand stack should have `23.5` at the top and the list should have `23.5` at the bottom +(logically the top of the operand stack). + +Below are the specific cases that are tested. + +buttons to click `=>` text field content: + +- `2 7` => `27` +- `2 7 .` => `27.` +- `2 7 . 5` => `27.5` +- `2 7 . 5 .` => `27.` (cut at decimal point) + +buttons to click `=>` operand stack/list content (from the bottom): + +- `2 7 . 5 enter"` => `27.5` +- `2 7 enter` => `27.0"` +- `2 enter 7 enter 5 enter` => `5.0 7.0 2.0` +- `2 7 . enter` => `27.0` +- `2 7 . 5 enter` => `27.5` +- `2 enter 7 +` => `9.0` +- `2 enter 7 -` => `-5.0` +- `2 enter 7 *` => `14.0` +- `6 enter 3 /` => `2.0` +- `2 5 enter √` => `5.0` +- `π` => `3.1415...` (the value of the `Math.PI` constant)