Added // TODO and updated READMEs
This commit is contained in:
parent
8075ed5f4b
commit
cf0d1f6e87
14
README.md
14
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)
|
||||||
|
|
|
@ -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,33 +63,25 @@ 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()) {
|
||||||
calc.pushOperand(getOperand());
|
calc.pushOperand(getOperand());
|
||||||
} 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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 n t e r" onAction="#handleEnter"
|
<Button text="E n t e 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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue