Added // TODO and updated READMEs

This commit is contained in:
Hallvard Trætteberg 2021-08-13 13:46:07 +00:00
parent 8075ed5f4b
commit cf0d1f6e87
6 changed files with 126 additions and 79 deletions

View File

@ -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)

View File

@ -2,7 +2,6 @@ package app;
import java.util.List; import java.util.List;
import java.util.function.BinaryOperator; import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -39,16 +38,12 @@ public class AppController {
updateOperandsView(); updateOperandsView();
} }
private int minOperandCount = 2;
private void updateOperandsView() { private void updateOperandsView() {
while (calc.getOperandCount() < minOperandCount) {
calc.pushOperand(calc.getOperandCount(), 0.0);
}
List<Double> operands = operandsView.getItems(); List<Double> operands = operandsView.getItems();
operands.clear(); operands.clear();
for (int i = 0; i < minOperandCount; i++) { int elementCount = Math.min(calc.getOperandCount(), 3);
operands.add(calc.peekOperand(minOperandCount - i - 1)); for (int i = 0; i < elementCount; i++) {
operands.add(calc.peekOperand(elementCount - i - 1));
} }
} }
@ -68,14 +63,6 @@ public class AppController {
operandView.setText(operandString); operandView.setText(operandString);
} }
private void setOperand(double d) {
setOperand(Double.toString(d));
}
private void clearOperand() {
setOperand("");
}
@FXML @FXML
void handleEnter() { void handleEnter() {
if (hasOperand()) { if (hasOperand()) {
@ -83,18 +70,18 @@ public class AppController {
} else { } else {
calc.dup(); calc.dup();
} }
clearOperand(); setOperand("");
updateOperandsView(); updateOperandsView();
} }
private void appendToOperand(String s) { private void appendToOperand(String s) {
setOperand(operandView.getText() + s); // TODO
} }
@FXML @FXML
void handleDigit(ActionEvent ae) { void handleDigit(ActionEvent ae) {
if (ae.getSource() instanceof Labeled l) { if (ae.getSource() instanceof Labeled l) {
appendToOperand(l.getText()); // TODO append button label to operand
} }
} }
@ -102,70 +89,40 @@ public class AppController {
void handlePoint() { void handlePoint() {
var operandString = getOperandString(); var operandString = getOperandString();
if (operandString.contains(".")) { if (operandString.contains(".")) {
setOperand(operandString.substring(0, operandString.indexOf(".") + 1)); // TODO remove characters after point
} else { } else {
appendToOperand("."); // TODO append point
} }
} }
@FXML @FXML
void handleClear() { void handleClear() {
clearOperand(); // TODO clear operand
}
private void withOperand(Runnable proc) {
if (hasOperand()) {
calc.pushOperand(getOperand());
clearOperand();
}
proc.run();
updateOperandsView();
} }
private void performOperation(UnaryOperator<Double> op) { private void performOperation(UnaryOperator<Double> op) {
withOperand(() -> calc.performOperation(op)); // TODO
} }
private void performOperation(boolean swap, BinaryOperator<Double> op) { private void performOperation(boolean swap, BinaryOperator<Double> op) {
withOperand(() -> { if (hasOperand()) {
if (swap) { // TODO push operand first
calc.swap(); }
} // TODO perform operation, but swap first if needed
calc.performOperation(op);
});
} }
@FXML @FXML
void handleOpAdd() { void handleOpAdd() {
performOperation(false, (op1, op2) -> op1 + op2); // TODO
} }
@FXML @FXML
void handleOpSub() { void handleOpSub() {
performOperation(true, (op1, op2) -> op1 - op2); // TODO
} }
@FXML @FXML
void handleOpMult() { void handleOpMult() {
performOperation(false, (op1, op2) -> op1 * op2); // TODO
}
@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");
} }
} }

View File

@ -13,41 +13,79 @@ public class Calc {
operandStack.addAll(List.of(operands)); operandStack.addAll(List.of(operands));
} }
/**
* @return the number of operands on the stack
*/
public int getOperandCount() { public int getOperandCount() {
return operandStack.size(); 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) { public void pushOperand(int n, double d) {
operandStack.add(operandStack.size() - n, 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) { public void pushOperand(double d) {
pushOperand(0, 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) { public double peekOperand(int n) {
return operandStack.get(operandStack.size() - n - 1); return operandStack.get(operandStack.size() - n - 1);
} }
/**
* @return the top operand
*/
public double peekOperand() { public double peekOperand() {
return peekOperand(0); 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) { public double popOperand(int n) {
return operandStack.remove(operandStack.size() - n - 1); return operandStack.remove(operandStack.size() - n - 1);
} }
/**
* Removes and returns the top operand.
* @return the top operand
*/
public double popOperand() { public double popOperand() {
return popOperand(0); 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) { public double performOperation(UnaryOperator<Double> op) {
var op1 = popOperand(); // TODO
var result = op.apply(op1); return 0.0;
pushOperand(result);
return result;
} }
/**
* 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) { public double performOperation(BinaryOperator<Double> op) {
var op1 = popOperand(); var op1 = popOperand();
var op2 = popOperand(); var op2 = popOperand();
@ -56,14 +94,17 @@ public class Calc {
return result; return result;
} }
/**
* Swaps the two topmost operands.
*/
public void swap() { public void swap() {
var op1 = popOperand(); // TODO
var op2 = popOperand();
pushOperand(op1);
pushOperand(op2);
} }
/**
* Duplicates the top operand.
*/
public void dup() { public void dup() {
pushOperand(peekOperand()); // TODO
} }
} }

View File

@ -13,6 +13,7 @@
<Label text="" fx:id="operandView" <Label text="" fx:id="operandView"
GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="4"/> GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="4"/>
<!-- multi-line button label with XML entity for newline -->
<Button text="E&#10;n&#10;t&#10;e&#10;r" onAction="#handleEnter" <Button text="E&#10;n&#10;t&#10;e&#10;r" onAction="#handleEnter"
GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="4"/> GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="4"/>
@ -47,10 +48,12 @@
GridPane.rowIndex="6" GridPane.columnIndex="1"/> GridPane.rowIndex="6" GridPane.columnIndex="1"/>
<Button text="*" onAction="#handleOpMult" <Button text="*" onAction="#handleOpMult"
GridPane.rowIndex="6" GridPane.columnIndex="2"/> GridPane.rowIndex="6" GridPane.columnIndex="2"/>
<Button text="/" onAction="#handleOpDiv"
<!-- TODO -->
<Button text="/"
GridPane.rowIndex="6" GridPane.columnIndex="3"/> GridPane.rowIndex="6" GridPane.columnIndex="3"/>
<Button text="√" onAction="#handleOpSquareRoot" <Button text="√"
GridPane.rowIndex="7" GridPane.columnIndex="0"/> GridPane.rowIndex="7" GridPane.columnIndex="0"/>
<Button text="π" onAction="#handlePi" <Button text="π"
GridPane.rowIndex="7" GridPane.columnIndex="1"/> GridPane.rowIndex="7" GridPane.columnIndex="1"/>
</GridPane> </GridPane>

View File

@ -9,7 +9,6 @@ import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions; 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.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.opentest4j.AssertionFailedError;
import org.testfx.framework.junit5.ApplicationTest; import org.testfx.framework.junit5.ApplicationTest;
import org.testfx.matcher.control.LabeledMatchers; import org.testfx.matcher.control.LabeledMatchers;
@ -106,7 +104,7 @@ public class AppTest extends ApplicationTest {
for (var label : labels.split(" ")) { for (var label : labels.split(" ")) {
click(label.equals("\n") ? enterLabel : label); 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() { private static Stream<Arguments> testClicksOperands() {

View File

@ -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)