Skip to content
Snippets Groups Projects
Commit 3c8a4c9f authored by Henning Husmo Kristiansen's avatar Henning Husmo Kristiansen
Browse files

Init

parents
Branches main
No related tags found
No related merge requests found
.DS_Store
.vscode.vscode
.vscode
# Javafx template
[open in Eclipse Che (updated link)](https://che.stud.ntnu.no/#https://gitlab.stud.idi.ntnu.no/henninhk/RPN-calc?new)
A repository with three variants of a javafx projects, with maven setup for Java 17 and JavaFX 17, and JUnit 5 (Jupiter) and TestFX for testing.
## javafx-template
Template for single-module, single-package javafx project.
## Trying it out
All projects can be tried out by cd-ing into the corresponding folder and using `mvn`:
- 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)
schemaVersion: 2.1.0
metadata:
name: henninhk-calc
components:
- name: javafx-template
container:
image: adrianstoica/it1901:latest
memoryLimit: 3Gi
endpoints:
- exposure: public
name: 6080-tcp-desktop-ui
protocol: https
targetPort: 6080
- exposure: public
name: 3000-tcp
protocol: http
targetPort: 3000
- exposure: public
name: 5900-tcp
protocol: http
targetPort: 5900
- exposure: public
name: 5901-tcp
protocol: http
targetPort: 5901
- exposure: public
name: 8080-tcp
protocol: http
targetPort: 8080
- exposure: public
name: 5500-tcp
protocol: http
targetPort: 5500
# ignore maven build folder
target/
# pom-derived eclipse jdt files
.project
.classpath
org.eclipse.*.prefs
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it1901</groupId>
<artifactId>henninhk-calc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- javafx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>17.0.8</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>17.0.8</version>
</dependency>
<!-- junit testing with jupiter -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- test javafx with TextFX -->
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-core</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-junit5</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<argLine>
--add-opens app/app=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<!-- Default configuration for running -->
<!-- Usage: mvn javafx:run -->
<configuration>
<mainClass>app/app.CalcApp</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
package app;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
public class Calc {
private final List<Double> operandStack;
public Calc(double... operands) {
operandStack = new ArrayList<>(operands.length + 2);
for (var d : operands) {
operandStack.add(d);
}
}
/**
* @return the number of operands on the stack
*/
public int getOperandCount() {
return operandStack.size();
}
/**
* Pushes a new operand onto top of the stack.
*
* @param d the new operand
*/
public void pushOperand(double d) {
operandStack.add(d);
}
/**
* @param n the place (from the top) to peek
* @return the n'th operand from the top
* @throws IllegalArgumentException if n is larger than the operand count
*/
public double peekOperand(int n) {
if (n >= getOperandCount()) {
throw new IllegalArgumentException("Cannot peek at position " + n + " when the operand count is " + getOperandCount());
}
return operandStack.get(getOperandCount() - n - 1);
}
/**
* @return the top operand
*/
public double peekOperand() {
return peekOperand(0);
}
/**
* Removes and returns the top operand.
*
* @return the top operand
* @throws IllegalStateException if the stack is empty
*/
public double popOperand() {
if (getOperandCount() == 0) {
throw new IllegalStateException("Cannot pop from an empty stack");
}
return operandStack.remove(operandStack.size() - 1);
}
/**
* 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
* @throws IllegalStateException if the operand stack is empty
*/
public double performOperation(UnaryOperator<Double> op) throws IllegalStateException {
// TODO
if (getOperandCount() < 1) {
throw new IllegalStateException();
}
var var = popOperand();
var result = op.apply(var);
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
* @throws IllegalStateException if the operand count is less than two
*/
public double performOperation(BinaryOperator<Double> op) throws IllegalStateException {
if (getOperandCount() < 2) {
throw new IllegalStateException("Too few operands (" + getOperandCount() + ") on the stack");
}
var op2 = popOperand();
var op1 = popOperand();
var result = op.apply(op1, op2);
pushOperand(result);
return result;
}
/**
* Swaps the two topmost operands.
*
* @throws IllegalStateException if the operand count is less than two
*/
public void swap() {
if (getOperandCount() < 2) {
throw new IllegalStateException("Too few operands on the stack");
}
var last = popOperand();
var last2 = popOperand();
pushOperand(last);
pushOperand(last2);
}
/**
* Duplicates the top operand.
*
* @throws IllegalStateException if the operand stack is empty
*/
public void dup() {
if (getOperandCount() == 0) {
throw new IllegalStateException();
}
var var = peekOperand();
pushOperand(var);
}
}
\ No newline at end of file
package app;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
/**
* JavaFX App
*/
public class CalcApp extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("Calc.fxml"));
Parent parent = fxmlLoader.load();
stage.setScene(new Scene(parent));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
\ No newline at end of file
package app;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.ListView;
public class CalcController {
private Calc calc;
public CalcController() {
calc = new Calc(0.0, 0.0, 0.0);
}
public Calc getCalc() {
return calc;
}
public void setCalc(Calc calc) {
this.calc = calc;
updateOperandsView();
}
@FXML
private ListView<Double> operandsView;
@FXML
private Label operandView;
@FXML
void initialize() {
updateOperandsView();
}
private void updateOperandsView() {
List<Double> operands = operandsView.getItems();
operands.clear();
int elementCount = Math.min(calc.getOperandCount(), 3);
for (int i = 0; i < elementCount; i++) {
operands.add(calc.peekOperand(elementCount - i - 1));
}
}
private String getOperandString() {
return operandView.getText();
}
private boolean hasOperand() {
return ! getOperandString().isBlank();
}
private double getOperand() {
return Double.valueOf(operandView.getText());
}
private void setOperand(String operandString) {
operandView.setText(operandString);
}
@FXML
void handleEnter() {
if (hasOperand()) {
calc.pushOperand(getOperand());
} else {
calc.dup();
}
setOperand("");
updateOperandsView();
}
private void appendToOperand(String s) {
setOperand(getOperandString() + s);
}
@FXML
void handleDigit(ActionEvent ae) {
if (ae.getSource() instanceof Labeled l) {
appendToOperand(l.getText());
}
}
@FXML
void handlePoint() {
var operandString = getOperandString();
if (operandString.contains(".")) {
String s = getOperandString().split("\\.")[0];
setOperand(s + ".");
} else {
appendToOperand(".");
}
}
@FXML
void handleClear() {
if (hasOperand()) {
setOperand("");
}
else {
calc.popOperand();
updateOperandsView();
}
}
@FXML
void handleSwap() {
calc.swap();
updateOperandsView();
}
private void performOperation(UnaryOperator<Double> op) {
if (hasOperand()) {
handleEnter();
}
calc.performOperation(op);
updateOperandsView();
}
private void performOperation(boolean swap, BinaryOperator<Double> op) {
if (hasOperand()) {
handleEnter();
}
calc.performOperation(op);
updateOperandsView();
}
@FXML
void handleOpAdd() {
performOperation(false, ((a, b) -> a + b));
}
@FXML
void handleOpSub() {
performOperation(false, ((a, b) -> a - b));
}
@FXML
void handleOpMult() {
performOperation(false, ((a, b) -> a * b));
}
@FXML
void handleDivide() {
try {
performOperation(false, ((a, b) -> a / b));
} catch (Exception e) {
// Handle division by zero error
// You can display an error message or perform any other necessary action
e.printStackTrace();
}
}
@FXML
void handleRoot() {
performOperation(n -> Math.sqrt(n));
}
@FXML
void handlePI() {
calc.pushOperand(Math.PI);
updateOperandsView();
}
}
module app {
requires javafx.controls;
requires javafx.fxml;
opens app to javafx.graphics, javafx.fxml;
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<GridPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="app.CalcController"
alignment="CENTER" hgap="10.0" vgap="10.0" >
<ListView fx:id="operandsView" prefHeight="80.0"
GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="4"/>
<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&#10;n&#10;t&#10;e&#10;r" onAction="#handleEnter"
GridPane.rowIndex="2" GridPane.columnIndex="3" GridPane.rowSpan="3"/>
<Button text="7" onAction="#handleDigit"
GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<Button text="8" onAction="#handleDigit"
GridPane.rowIndex="2" GridPane.columnIndex="1"/>
<Button text="9" onAction="#handleDigit"
GridPane.rowIndex="2" GridPane.columnIndex="2"/>
<Button text="4" onAction="#handleDigit"
GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<Button text="5" onAction="#handleDigit"
GridPane.rowIndex="3" GridPane.columnIndex="1"/>
<Button text="6" onAction="#handleDigit"
GridPane.rowIndex="3" GridPane.columnIndex="2"/>
<Button text="1" onAction="#handleDigit"
GridPane.rowIndex="4" GridPane.columnIndex="0"/>
<Button text="2" onAction="#handleDigit"
GridPane.rowIndex="4" GridPane.columnIndex="1"/>
<Button text="3" onAction="#handleDigit"
GridPane.rowIndex="4" GridPane.columnIndex="2"/>
<Button text="0" onAction="#handleDigit"
GridPane.rowIndex="5" GridPane.columnIndex="0"/>
<Button text="." onAction="#handlePoint"
GridPane.rowIndex="5" GridPane.columnIndex="1"/>
<Button text="C" onAction="#handleClear"
GridPane.rowIndex="5" GridPane.columnIndex="2"/>
<Button text="~" onAction="#handleSwap"
GridPane.rowIndex="5" GridPane.columnIndex="3"/>
<Button text="+" onAction="#handleOpAdd"
GridPane.rowIndex="6" GridPane.columnIndex="0"/>
<Button text="-" onAction="#handleOpSub"
GridPane.rowIndex="6" GridPane.columnIndex="1"/>
<Button text="*" onAction="#handleOpMult"
GridPane.rowIndex="6" GridPane.columnIndex="2"/>
<!-- TODO -->
<Button text="/" onAction="#handleDivide"
GridPane.rowIndex="6" GridPane.columnIndex="3"/>
<Button text="√" onAction="#handleRoot"
GridPane.rowIndex="7" GridPane.columnIndex="0"/>
<Button text="π" onAction="#handlePI"
GridPane.rowIndex="7" GridPane.columnIndex="1"/>
</GridPane>
package app;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
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.testfx.framework.junit5.ApplicationTest;
import org.testfx.matcher.control.LabeledMatchers;
/**
* TestFX App test
*/
public class CalcAppTest extends ApplicationTest {
private CalcController controller;
private Parent root;
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("Calc.fxml"));
root = fxmlLoader.load();
controller = fxmlLoader.getController();
stage.setScene(new Scene(root));
stage.show();
}
public Parent getRootNode() {
return root;
}
private String enterLabel = """
E
n
t
e
r
""".stripTrailing();
private void click(String... labels) {
for (var label : labels) {
clickOn(LabeledMatchers.hasText(label));
}
}
private String getOperandString() {
return ((Label) getRootNode().lookup("#operandView")).getText();
}
private ListView<Double> getOperandsView() {
return (ListView<Double>) getRootNode().lookup("#operandsView");
}
private void checkView(double... operands) {
for (int i = 0; i < operands.length; i++) {
Assertions.assertEquals(operands[i], controller.getCalc().peekOperand(i), "Wrong value at #" + i + " of operand stack");
}
List<Double> viewItems = getOperandsView().getItems();
for (int i = 0; i < operands.length; i++) {
Assertions.assertEquals(operands[i], viewItems.get(viewItems.size() - i - 1), "Wrong value at #" + i + " of operands view");
}
}
private void checkView(String operandString, double... operands) {
Assertions.assertEquals(operandString, getOperandString());
checkView(operands);
}
// see https://www.baeldung.com/parameterized-tests-junit-5
// about @ParameterizedTest
@ParameterizedTest
@MethodSource
public void testClicksOperand(String labels, String operandString) {
for (var label : labels.split(" ")) {
click(label);
}
checkView(operandString);
}
private static Stream<Arguments> testClicksOperand() {
return Stream.of(
Arguments.of("2 7", "27"),
Arguments.of("2 7 .", "27."),
Arguments.of("2 7 . 5", "27.5"),
Arguments.of("2 7 . 5 .", "27.")
);
}
@ParameterizedTest
@MethodSource
public void testClicksOperands(String labels, String operandsString) {
for (var label : labels.split(" ")) {
click(label.equals("\n") ? enterLabel : label);
}
checkView("", Stream.of(operandsString.split(" ")).mapToDouble(Double::valueOf).toArray());
}
private static Stream<Arguments> testClicksOperands() {
return Stream.of(
Arguments.of("2 7 . 5 \n", "27.5"),
Arguments.of("2 7 \n", "27.0"),
Arguments.of("2 \n 7 \n 5 \n", "5.0", "7.0", "2.0"),
Arguments.of("2 7 . \n", "27.0"),
Arguments.of("2 7 . 5 \n", "27.5"),
Arguments.of("2 \n 7 +", "9.0"),
Arguments.of("2 \n 7 -", "-5.0"),
Arguments.of("2 \n 7 *", "14.0"),
Arguments.of("6 \n 3 /", "2.0"),
Arguments.of("2 5 \n √", "5.0")
);
}
@Test
public void testPi() {
click("π");
checkView("", Math.PI);
}
}
package app;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class CalcTest {
private static void checkCalc(Calc calc, double... operands) {
Assertions.assertEquals(operands.length, calc.getOperandCount(), "Wrong operand count");
for (int i = 0; i < operands.length; i++) {
Assertions.assertEquals(operands[i], calc.peekOperand(i), "Wrong value at #" + i + " of operand stack");
}
}
@Test
public void testCalc() {
checkCalc(new Calc());
checkCalc(new Calc(1.0), 1.0);
checkCalc(new Calc(3.14, 1.0), 1.0, 3.14);
}
@Test
public void testPushOperand() {
Calc calc = new Calc();
calc.pushOperand(1.0);
checkCalc(calc, 1.0);
calc.pushOperand(3.14);
checkCalc(calc, 3.14, 1.0);
}
@Test
public void testPeekOperand() {
Calc calc = new Calc(1.0, 3.14);
Assertions.assertEquals(3.14, calc.peekOperand());
Assertions.assertThrows(IllegalArgumentException.class, () -> new Calc().peekOperand());
}
@Test
public void testPeekOperandN() {
Calc calc = new Calc(1.0, 3.14);
Assertions.assertEquals(3.14, calc.peekOperand(0));
Assertions.assertEquals(1.0, calc.peekOperand(1));
Assertions.assertThrows(IllegalArgumentException.class, () -> calc.peekOperand(2));
}
@Test
public void testPopOperand() {
Calc calc = new Calc(1.0, 3.14);
Assertions.assertEquals(3.14, calc.popOperand());
checkCalc(calc, 1.0);
Assertions.assertEquals(1.0, calc.popOperand());
checkCalc(calc);
}
@Test
public void testPopOperand_emptyStack() {
Assertions.assertThrows(IllegalStateException.class, () -> new Calc().popOperand());
}
@Test
public void testPerformOperation1() {
Calc calc = new Calc(1.0);
Assertions.assertEquals(-1.0, calc.performOperation(n -> -n));
checkCalc(calc, -1.0);
}
@Test
public void testPerformOperation1_emptyOperandStack() {
Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation(n -> -n));
}
@Test
public void testPerformOperation2() {
Calc calc = new Calc(1.0, 3.0);
Assertions.assertEquals(-2.0, calc.performOperation((n1, n2) -> n1 - n2));
checkCalc(calc, -2.0);
}
@Test
public void testPerformOperation2_lessThanTwoOperands() {
Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).performOperation((n1, n2) -> n1 - n2));
Assertions.assertThrows(IllegalStateException.class, () -> new Calc().performOperation((n1, n2) -> n1 - n2));
}
@Test
public void testSwap() {
Calc calc = new Calc(1.0, 3.14);
checkCalc(calc, 3.14, 1.0);
calc.swap();
checkCalc(calc, 1.0, 3.14);
calc.swap();
checkCalc(calc, 3.14, 1.0);
}
@Test
public void testSwap_lessThanTwoOperands() {
Assertions.assertThrows(IllegalStateException.class, () -> new Calc(1.0).swap());
Assertions.assertThrows(IllegalStateException.class, () -> new Calc().swap());
}
@Test
public void testDup() {
Calc calc = new Calc(1.0, 3.14);
Assertions.assertEquals(3.14, calc.popOperand());
checkCalc(calc, 1.0);
Assertions.assertEquals(1.0, calc.popOperand());
checkCalc(calc);
}
@Test
public void testDup_emptyOperandStack() {
Assertions.assertThrows(IllegalStateException.class, () -> new Calc().dup());
}
}
# 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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment