From d9295750aab492c188a74a74052c421dadf6026c Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 19:38:12 +0200
Subject: [PATCH 001/142] Updated Exchange
Added private method getTopStocks to avoid duplicate code, and change getGainers and getLosers to use getTopStocks.
---
.../main/java/no/ntnu/gruppe53/Exchange.java | 37 ++++++++++---------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/Exchange.java
index 0f729a5..31fc20f 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/Exchange.java
@@ -147,24 +147,34 @@ public void advance() {
}
/**
- * Lists the given amount of stocks sorted by highest increase in sales price since last week.
- * @param entries amount of stocks to be shown in list
- * @return a descending list of stocks with the highest price increase since last week
- * @throws IllegalArgumentException if number of entries in list is negative
+ * Sorts the stocks in an ascending order and returns a list of stocks.
+ *
+ * @param entries amount of entries in the list to display
+ * @param comparator the comparator used to compare the stocks
+ * @return a list of stocks
+ * @throws IllegalArgumentException if the amount of entries is negative
*/
- public List getGainers(int entries) {
+ private List getTopStocks(int entries, Comparator comparator) {
if (entries < 0) {
throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
}
return stockMap.values().stream()
- .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed())
- .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) > 0)
+ .sorted(comparator)
.limit(entries)
-
.collect(Collectors.toList());
}
+ /**
+ * Lists the given amount of stocks sorted by highest increase in sales price since last week.
+ * @param entries amount of stocks to be shown in list
+ * @return a descending list of stocks with the highest price increase since last week
+ * @throws IllegalArgumentException if number of entries in list is negative
+ */
+ public List getGainers(int entries) {
+ return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange).reversed());
+ }
+
/**
* Lists the given amount of stocks sorted by the stocks with the lowest price increase (or
* highest price decrease)
@@ -174,16 +184,7 @@ public List getGainers(int entries) {
* @throws IllegalArgumentException if number of entries in list is negative
*/
public List getLosers(int entries) {
- if (entries < 0) {
- throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
- }
-
- return stockMap.values().stream()
- .sorted(Comparator.comparing(Stock::getLatestPriceChange))
- .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) < 0)
- .limit(entries)
-
- .collect(Collectors.toList());
+ return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange));
}
From 082a39e4c25f5efe48109f1fd0afa45873ec9f9b Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 19:39:20 +0200
Subject: [PATCH 002/142] Updated ExchangeTest
Added test methods for getGainers and getLosers that also target getTopStocks.
---
.../java/no/ntnu/gruppe53/ExchangeTest.java | 43 +++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java
index d01ca79..7167bd1 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java
@@ -162,4 +162,47 @@ void advanceWeekTest() {
}
+ @Test
+ void getGainersShouldSortCorrectly() {
+ appleStock.addNewSalesPrice(new BigDecimal("3000"));
+ microStock.addNewSalesPrice(new BigDecimal("100"));
+ ntnuStock.addNewSalesPrice(new BigDecimal("1500"));
+
+ var gainList = exchange.getGainers(3);
+
+ assertEquals(appleStock.getSymbol(), gainList.getFirst().getSymbol(),
+ "Biggest gainer should be 'AAPL'.");
+ assertEquals(ntnuStock.getSymbol(), gainList.get(1).getSymbol(),
+ "Second biggest gainer should be 'NTNU'.");
+ assertEquals(microStock.getSymbol(), gainList.getLast().getSymbol(),
+ "Third biggest gainer should be 'MSFT'.");
+ }
+
+ @Test
+ void getLosersShouldSortCorrectly() {
+ appleStock.addNewSalesPrice(new BigDecimal("3000"));
+ microStock.addNewSalesPrice(new BigDecimal("100"));
+ ntnuStock.addNewSalesPrice(new BigDecimal("1500"));
+
+ var gainList = exchange.getLosers(3);
+
+ System.out.println(gainList.size());
+
+ assertEquals(microStock.getSymbol(), gainList.get(0).getSymbol(),
+ "Biggest loser should me 'MSFT'");
+ assertEquals(ntnuStock.getSymbol(), gainList.get(1).getSymbol(),
+ "Second biggest loser should be 'NTNU'");
+ assertEquals(appleStock.getSymbol(), gainList.get(2).getSymbol(),
+ "Least biggest loser should be 'AAPL'");
+ }
+
+ @Test
+ void getTopStocksShouldThrowIllegalArgumentExceptionsIfEntriesIsNegative() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1),
+ "It should not be possible to list a negative amount of entries.");
+ assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-2),
+ "It should not be possible to list a negative amount of entries.");
+
+ }
+
}
From 6f7e3d4cb1c32948a8c042892c0fdf2aa594d501 Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 20:22:58 +0200
Subject: [PATCH 003/142] Updated FileHandler
Changed logic to hopefully be OS independent.
---
.../java/no/ntnu/gruppe53/FileHandler.java | 143 +++++++++---------
1 file changed, 74 insertions(+), 69 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
index 8133e72..3fd4727 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
@@ -4,7 +4,7 @@
import java.io.BufferedWriter;
import java.io.IOException;
import java.math.BigDecimal;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -12,117 +12,122 @@
import java.util.List;
/**
- * Handles file operations for the application.
+ * Handles reading and writing of .csv files for stocks.
*/
-
public class FileHandler {
/**
- * Writes a list of {@link Stock}s to a text-file. Class is structed for {@code .csv} files.
+ * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding.
*
- * @param stockList the list of stocks to be converted to text
- * @param path output path for the file
- * @param filename name of the file (not including extension)
+ * @param stockList the list of stocks to be written
+ * @param path directory path for the file
+ * @param filename name of the file (without or with .csv extension)
+ * @throws IllegalArgumentException if stockList, path, or filename is null, or if stockList is empty
*/
public static void writeStocksToFile(List stockList, String path, String filename) {
- String pathString = path + filename;
- Path filePath = Paths.get(pathString);
-
- try (BufferedWriter writer = Files.newBufferedWriter(filePath, Charset.defaultCharset())) {
- if (!Files.exists(filePath)) {
- Files.createDirectories(filePath);
+ if (stockList == null) throw new IllegalArgumentException("StockList cannot be null");
+ if (path == null || path.isBlank()) throw new IllegalArgumentException("Path cannot be null or blank");
+ if (filename == null || filename.isBlank()) throw new IllegalArgumentException(
+ "Filename cannot be null or blank");
+
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
+
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ Path dirPath = filePath.getParent();
+ if (dirPath != null && !Files.exists(dirPath)) {
+ try {
+ Files.createDirectories(dirPath);
+ } catch (IOException e) {
+ System.out.println("ERROR: Could not create directories.");
+ e.printStackTrace();
+ return;
}
+ }
+ try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
writer.write("#Ticker,Name,Price");
writer.newLine();
- writer.newLine();
for (Stock stock : stockList) {
- String line = String.join(",", stock.getSymbol(), stock.getCompany(),
+ if (stock == null) continue;
+ String line = String.join(",",
+ stock.getSymbol(),
+ stock.getCompany(),
stock.getSalesPrice().toString());
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
- System.out.println("ERROR: Something went wrong while writing the csv file.");
- System.out.println("Message: " + e.getMessage());
+ System.out.println("ERROR: Something went wrong while writing the CSV file.");
+ e.printStackTrace();
}
}
/**
- * Reads a .csv file and converts it to a list of {@code Stock} objects.
+ * Reads a CSV file and converts it to a list of {@link Stock} objects.
*
- * @param path path to the file to be read
- * @param filename name of the file to be read (including extension)
- * @return a list of {@code Stock} objects converted from text
+ * @param path directory path of the CSV file
+ * @param filename CSV file name (with or without .csv extension)
+ * @return list of Stock objects
*/
public static List readStocksFromFile(String path, String filename) {
List stocks = new ArrayList<>();
- String pathString = path + filename;
- Path filePath = Paths.get(pathString);
+ if (path == null || filename == null) return stocks;
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
- try (BufferedReader reader = Files.newBufferedReader(filePath, Charset.defaultCharset())) {
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ if (!Files.exists(filePath)) {
+ System.out.println("ERROR: File not found: " + filePath.toAbsolutePath());
+ return stocks;
+ }
+
+ try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
String line;
- String symbol;
- String name;
- BigDecimal price;
while ((line = reader.readLine()) != null) {
- symbol = null;
- name = null;
- price = null;
- int counter = 0;
- String trimValue = line.trim();
+ line = line.trim();
+
+ if (line.isBlank() || line.startsWith("#")) continue;
- if (trimValue.isBlank() || (line.charAt(0) == '#')) {
+ String[] values = line.split(",", -1);
+ if (values.length != 3) {
+ System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line);
continue;
}
- String[] values = line.split(",");
+ String symbol = values[0].trim();
+ String name = values[1].trim();
+ BigDecimal price;
- if (values.length != 3) {
- System.out.println("ERROR: Bad line in csv: ");
- System.out.print(line);
- System.out.println("There should be 3 values total: " +
- "symbol, name, price.");
+ try {
+ price = new BigDecimal(values[2].trim());
+ } catch (NumberFormatException e) {
+ System.out.println("ERROR: Price is not a valid number: " + values[2]);
continue;
}
- for (String value : values) {
- trimValue = value.trim();
-
- switch (counter) {
- case 0:
- symbol = trimValue;
- break;
- case 1:
- name = trimValue;
- break;
- case 2:
- try {
- price = new BigDecimal(trimValue);
- }
- catch (NumberFormatException e) {
- System.out.println("ERROR: Not a number. Last column in .csv file " +
- "needs to be the price of the stock (number). Skipping...");
- price = null;
- break;
- }
- }
- counter++;
- }
- if (symbol == null || symbol.isBlank() || name == null || name.isBlank() ||
- price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
+ if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) {
+ System.out.println("ERROR: Invalid stock data, skipping: " + line);
continue;
}
+
stocks.add(new Stock(symbol, name, price));
}
+
+ } catch (IOException e) {
+ System.out.println("ERROR: Something went wrong while reading the CSV file.");
+ e.printStackTrace();
}
- catch (IOException e) {
- System.out.println("ERROR: Something went wrong while reading the csv file.");
- System.out.println("Message: " + e.getMessage());
- }
+
return stocks;
}
-}
+}
\ No newline at end of file
From 127dd2f82acb1bb972fe7f65196f9354f331340a Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 20:23:46 +0200
Subject: [PATCH 004/142] Updated FileHandlerTest
Added tests for the new FileHandler logic.
---
.../no/ntnu/gruppe53/FileHandlerTest.java | 148 ++++++++++--------
1 file changed, 83 insertions(+), 65 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
index 76c4914..a5611b6 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
@@ -1,90 +1,108 @@
package no.ntnu.gruppe53;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import java.io.IOException;
+import java.io.File;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
+import java.nio.file.Path;
+import java.util.Arrays;
import java.util.List;
-import static no.ntnu.gruppe53.FileHandler.*;
import static org.junit.jupiter.api.Assertions.*;
class FileHandlerTest {
+
+ private Path tempDir;
+
+ @BeforeEach
+ void setup() throws Exception {
+ tempDir = Files.createTempDirectory("stock_test_");
+ }
+
+ @AfterEach
+ void cleanup() throws Exception {
+ if (tempDir != null && Files.exists(tempDir)) {
+ Files.walk(tempDir)
+ .map(Path::toFile)
+ .forEach(File::delete);
+ }
+ }
+
@Test
- void writeTest() throws IOException {
- String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\";
- String fileName = "stockTest.csv";
+ void testWriteAndReadStocks() {
+ Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
+ Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
+ List stocks = Arrays.asList(stock1, stock2);
+
+ String filename = "testStocks.csv";
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename);
- String symbol1 = "AAPL";
- String company1 = "Apple Inc.";
- BigDecimal price1 = new BigDecimal("100");
+ List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename);
+
+ assertEquals(stocks.size(), readStocks.size(), "Stock list size should match");
+ for (int i = 0; i < stocks.size(); i++) {
+ Stock expected = stocks.get(i);
+ Stock actual = readStocks.get(i);
+ assertEquals(expected.getSymbol(), actual.getSymbol());
+ assertEquals(expected.getCompany(), actual.getCompany());
+ assertEquals(0, expected.getSalesPrice().compareTo(actual.getSalesPrice()));
+ }
+ }
- String symbol2 = "NVDA";
- String company2 = "Nvidia";
- BigDecimal price2 = new BigDecimal("191.27");
+ @Test
+ void testWriteWithEmptyList() {
+ List emptyList = List.of();
+ String filename = "emptyStocks.csv";
- Stock testStock1 = new Stock(symbol1,company1, price1);
- Stock testStock2 = new Stock(symbol2, company2, price2);
+ FileHandler.writeStocksToFile(emptyList, tempDir.toString(), filename);
- List stockList = new ArrayList<>();
- stockList.add(testStock1);
- stockList.add(testStock2);
+ List result = FileHandler.readStocksFromFile(tempDir.toString(), filename);
+ assertTrue(result.isEmpty(), "Reading empty stock list should return empty list");
+ }
- writeStocksToFile(stockList, filePathString, fileName);
+ @Test
+ void testInvalidArguments() {
+ assertThrows(IllegalArgumentException.class, () ->
+ FileHandler.writeStocksToFile(null, tempDir.toString(), "file.csv"));
- List lines = Files.readAllLines(Paths.get(filePathString + fileName));
+ assertThrows(IllegalArgumentException.class, () ->
+ FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), " ", "file.csv"));
- assertTrue(Files.exists(Paths.get(filePathString + fileName)),
- "File should have been created successfully.");
- assertEquals("#Ticker,Name,Price",lines.getFirst(), "First line " +
- "should contain correct headers.");
- assertEquals("", lines.get(1), "Second line should be empty.");
- assertEquals(symbol1 + "," + company1 + "," + price1, lines.get(2),
- "Third line should contain correct values for first stock.");
- assertEquals(symbol2 + "," + company2 + "," + price2, lines.getLast(),
- "Last line should contain correct values for second stock.");
+ assertThrows(IllegalArgumentException.class, () ->
+ FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), " "));
}
@Test
- void readTest() {
- String filePathString = System.getProperty("user.dir") + "\\target\\test-output\\";
- String fileName = "stockTest.csv";
-
-
- String symbol1 = "AAPL";
- String company1 = "Apple Inc.";
- BigDecimal price1 = new BigDecimal("100");
-
- String symbol2 = "NVDA";
- String company2 = "Nvidia";
- BigDecimal price2 = new BigDecimal("191.27");
-
- Stock testStock1 = new Stock(symbol1,company1, price1);
- Stock testStock2 = new Stock(symbol2, company2, price2);
-
- List stockList = new ArrayList<>();
- stockList.add(testStock1);
- stockList.add(testStock2);
-
- writeStocksToFile(stockList, filePathString, fileName);
-
- List readStocks = readStocksFromFile(filePathString, fileName);
-
- assertEquals(symbol1, readStocks.getFirst().getSymbol(),
- "First stock's symbol should be AAPL.");
- assertEquals(company1, readStocks.getFirst().getCompany(),
- "First stock's company name should be Apple Inc.");
- assertEquals(0, price1.compareTo(readStocks.getFirst().getSalesPrice()),
- "First stock's price should be 100.");
- assertEquals(symbol2, readStocks.getLast().getSymbol(),
- "Second stock's symbol should be NVDA.");
- assertEquals(company2, readStocks.getLast().getCompany(),
- "Second stock's company name should be Nvidia.");
- assertEquals(0, price2.compareTo(readStocks.getLast().getSalesPrice()),
- "Second stock's price should be 191.27");
+ void testReadStocksWithMalformedLines() throws Exception {
+ String filename = "malformedStocks.csv";
+ Path filePath = tempDir.resolve(filename);
+
+ List lines = List.of(
+ "# This is a comment",
+ "AAPL,Apple Inc.,150.25",
+ "INVALIDLINEWITHOUTCOMMAS",
+ "MSFT,Microsoft Corp.,300.50",
+ "GOOG,Google,,",
+ "TSLA,Tesla Inc.,-100"
+ );
+
+ Files.write(filePath, lines, StandardCharsets.UTF_8);
+
+ List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename);
+
+ assertEquals(2, stocks.size(), "Only 2 valid stocks should be read");
+
+ assertEquals("AAPL", stocks.get(0).getSymbol());
+ assertEquals("Apple Inc.", stocks.get(0).getCompany());
+ assertEquals(0, stocks.get(0).getSalesPrice().compareTo(new BigDecimal("150.25")));
+
+ assertEquals("MSFT", stocks.get(1).getSymbol());
+ assertEquals("Microsoft Corp.", stocks.get(1).getCompany());
+ assertEquals(0, stocks.get(1).getSalesPrice().compareTo(new BigDecimal("300.50")));
}
}
\ No newline at end of file
From 01aa629fa4d120d07e6f6a6fb4fffee445040b9f Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 22:56:11 +0200
Subject: [PATCH 005/142] Updated FileHandler
Fixed logic for handling exceptions thrown by Stock object for illegal parameters.
---
millions/src/main/java/no/ntnu/gruppe53/FileHandler.java | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
index 3fd4727..b1c616b 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
@@ -38,7 +38,7 @@ public static void writeStocksToFile(List stockList, String path, String
Path filePath = Paths.get(path, filenameWithExtension);
Path dirPath = filePath.getParent();
- if (dirPath != null && !Files.exists(dirPath)) {
+ if (!Files.exists(dirPath)) {
try {
Files.createDirectories(dirPath);
} catch (IOException e) {
@@ -77,7 +77,12 @@ public static void writeStocksToFile(List stockList, String path, String
*/
public static List readStocksFromFile(String path, String filename) {
List stocks = new ArrayList<>();
- if (path == null || filename == null) return stocks;
+ if (path == null || path.isBlank()) {
+ throw new IllegalArgumentException("Path cannot be null or blank.");
+ }
+ if (filename == null || filename.isBlank()) {
+ throw new IllegalArgumentException("Filename cannot be null or blank.");
+ }
path = path.trim();
filename = filename.trim();
From b572be694d22d34ae66c9a327776f806849c5355 Mon Sep 17 00:00:00 2001
From: Roar
Date: Wed, 15 Apr 2026 22:58:54 +0200
Subject: [PATCH 006/142] Updated FileHandlerTest
Added tests for everything but IOException.
---
.../no/ntnu/gruppe53/FileHandlerTest.java | 149 ++++++++++++++++--
1 file changed, 139 insertions(+), 10 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
index a5611b6..8b860b9 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
@@ -5,6 +5,7 @@
import org.junit.jupiter.api.Test;
import java.io.File;
+import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -33,7 +34,7 @@ void cleanup() throws Exception {
}
@Test
- void testWriteAndReadStocks() {
+ void writingAndReadingStocksFromListShouldYieldIdenticalStockList() {
Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
List stocks = Arrays.asList(stock1, stock2);
@@ -55,7 +56,54 @@ void testWriteAndReadStocks() {
}
@Test
- void testWriteWithEmptyList() {
+ void writerShouldIgnoreNullStockObjects() {
+ Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
+ Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
+ List stocks = Arrays.asList(stock1, null, stock2);
+
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), "stocks.csv");
+ List writtenStocks = FileHandler.readStocksFromFile(tempDir.toString(),"stocks.csv");
+
+ assertEquals(3, stocks.size(), "Stock list should include null stock.");
+ assertEquals(stocks.size()-1, writtenStocks.size(),
+ "Null stock should be ignored by the writer.");
+
+ }
+
+ @Test
+ void writeFileNameWithoutExtensionShouldAddExtension() {
+
+ List stocks = List.of(
+ new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00"))
+ );
+
+ String filenameWithoutExtension = "stocks";
+
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithoutExtension);
+
+ Path expectedFile = tempDir.resolve("stocks.csv");
+
+ assertTrue(Files.exists(expectedFile), "File should end with .csv extension.");
+ }
+
+ @Test
+ void writeToNonExistingDirectoriesShouldCreateTheDirectories() {
+ Path nonExistingDir = tempDir.resolve("subdir1/subdir2");
+
+ List stocks = List.of(
+ new Stock("AAPL", "Apple Inc.", new BigDecimal("150.00"))
+ );
+
+ FileHandler.writeStocksToFile(stocks, nonExistingDir.toString(), "stocks");
+
+ Path expectedFile = nonExistingDir.resolve("stocks.csv");
+
+ assertTrue(Files.exists(expectedFile), "File should be created in newly created directories");
+ assertTrue(Files.exists(nonExistingDir), "Directories should have been created");
+ }
+
+ @Test
+ void writeEmptyListShouldReturnEmptyList() {
List emptyList = List.of();
String filename = "emptyStocks.csv";
@@ -66,19 +114,29 @@ void testWriteWithEmptyList() {
}
@Test
- void testInvalidArguments() {
+ void writeInvalidStockListShouldThrowIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
FileHandler.writeStocksToFile(null, tempDir.toString(), "file.csv"));
+ }
+ @Test
+ void writeInvalidFolderPathShouldThrowIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), " ", "file.csv"));
+ assertThrows(IllegalArgumentException.class, () ->
+ FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)),null, "file.csv"));
+ }
+ @Test
+ void writeInvalidFileNameShouldThrowIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), " "));
+ assertThrows(IllegalArgumentException.class, () ->
+ FileHandler.writeStocksToFile(List.of(new Stock("AAPL", "Apple", BigDecimal.ONE)), tempDir.toString(), null));
}
@Test
- void testReadStocksWithMalformedLines() throws Exception {
+ void readStocksShouldIgnoreMalformedLines() throws Exception {
String filename = "malformedStocks.csv";
Path filePath = tempDir.resolve(filename);
@@ -86,9 +144,7 @@ void testReadStocksWithMalformedLines() throws Exception {
"# This is a comment",
"AAPL,Apple Inc.,150.25",
"INVALIDLINEWITHOUTCOMMAS",
- "MSFT,Microsoft Corp.,300.50",
- "GOOG,Google,,",
- "TSLA,Tesla Inc.,-100"
+ "MSFT,Microsoft Corp.,300.50"
);
Files.write(filePath, lines, StandardCharsets.UTF_8);
@@ -97,12 +153,85 @@ void testReadStocksWithMalformedLines() throws Exception {
assertEquals(2, stocks.size(), "Only 2 valid stocks should be read");
- assertEquals("AAPL", stocks.get(0).getSymbol());
- assertEquals("Apple Inc.", stocks.get(0).getCompany());
- assertEquals(0, stocks.get(0).getSalesPrice().compareTo(new BigDecimal("150.25")));
+ assertEquals("AAPL", stocks.getFirst().getSymbol());
+ assertEquals("Apple Inc.", stocks.getFirst().getCompany());
+ assertEquals(0, stocks.getFirst().getSalesPrice().compareTo(new BigDecimal("150.25")));
assertEquals("MSFT", stocks.get(1).getSymbol());
assertEquals("Microsoft Corp.", stocks.get(1).getCompany());
assertEquals(0, stocks.get(1).getSalesPrice().compareTo(new BigDecimal("300.50")));
}
+
+ @Test
+ void readStocksShouldCatchExceptionsFromIllegalStockParameters() throws IOException {
+ String filename = "malformedStocks.csv";
+ Path filePath = tempDir.resolve(filename);
+
+ List lines = List.of(
+ "GOOG,Google,,",
+ "GGL, , 200",
+ " , Google, 100",
+ "GGL, Google, A",
+ " ",
+ "TSLA,Tesla Inc.,-100"
+ );
+ Files.write(filePath, lines, StandardCharsets.UTF_8);
+
+ List stocks = FileHandler.readStocksFromFile(tempDir.toString(), filename);
+
+ assertEquals(0, stocks.size(), "No stocks with illegal parameters should be added.");
+ }
+
+ @Test
+ void readStocksShouldThrowIllegalArgumentExceptionIfPathIsNullOrBlank() {
+ Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
+ Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
+ List stocks = Arrays.asList(stock1, stock2);
+ String filename = "stocks.csv";
+
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename);
+
+ assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(null, filename),
+ "Null path should not be allowed.");
+ assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(" ", filename),
+ "Blank path should not be allowed.");
+ }
+
+ @Test
+ void readStocksShouldThrowIllegalArgumentExceptionIfFileNameIsNullOrBlank() {
+ Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
+ Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
+ List stocks = Arrays.asList(stock1, stock2);
+ String filename = "stocks.csv";
+
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), filename);
+
+ assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(
+ tempDir.toString(), null),
+ "Null filename should not be allowed.");
+ assertThrows(IllegalArgumentException.class, () -> FileHandler.readStocksFromFile(
+ tempDir.toString(), " "),
+ "Blank filename should not be allowed.");
+ }
+
+ @Test
+ void readShouldAddExtensionIfMissingFromFileName() {
+ Stock stock1 = new Stock("AAPL", "Apple Inc.", new BigDecimal("150.25"));
+ Stock stock2 = new Stock("MSFT", "Microsoft Corp.", new BigDecimal("300.50"));
+ List stocks = Arrays.asList(stock1, stock2);
+ String filename = "stocks";
+ String filenameWithExtension = filename + ".csv";
+
+ FileHandler.writeStocksToFile(stocks, tempDir.toString(), filenameWithExtension);
+ List readStocks = FileHandler.readStocksFromFile(tempDir.toString(), filename);
+
+ assertEquals(stocks.size(), readStocks.size(), "All stocks should be read successfully.");
+ }
+
+ @Test
+ void readerShouldReturnEmptyListIfFileIsMissing() {
+ List stocks = FileHandler.readStocksFromFile(tempDir.toString(), "stocklist.csv");
+
+ assertEquals(0, stocks.size(), "Stock list should be empty.");
+ }
}
\ No newline at end of file
From 4f181a3bde8b14c2c1a5aabce3b8893a93e82cd5 Mon Sep 17 00:00:00 2001
From: Roar
Date: Thu, 16 Apr 2026 00:08:44 +0200
Subject: [PATCH 007/142] Updated pom.xml
Added App as the main class.
---
millions/pom.xml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/millions/pom.xml b/millions/pom.xml
index bd65466..9981974 100644
--- a/millions/pom.xml
+++ b/millions/pom.xml
@@ -46,6 +46,9 @@
org.openjfxjavafx-maven-plugin0.0.8
+
+ no.ntnu.gruppe53.App
+ org.apache.maven.plugins
From 9a940644df59ff642f92a7f62570f22ba9d926c7 Mon Sep 17 00:00:00 2001
From: Roar
Date: Thu, 16 Apr 2026 00:11:39 +0200
Subject: [PATCH 008/142] Added ViewController
Class for switching views.
---
.../gruppe53/controller/ViewController.java | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
new file mode 100644
index 0000000..c71dd4c
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
@@ -0,0 +1,60 @@
+package no.ntnu.gruppe53.controller;
+
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages application views and handles switching between them.
+ *
+ * Stores a mapping of view names to JavaFX {@link Parent} nodes and updates
+ * the current Scene by replacing its root node when switching views.
+ */
+public class ViewController {
+ /** Primary application window used to display the Scene. */
+ private Stage stage;
+ /** Scene containing the currently active view. */
+ private Scene scene;
+ /** Map of view identifiers to their corresponding UI roots. */
+ private Map views = new HashMap<>();
+
+ /**
+ * Constructs a ViewController and initializes a new Scene.
+ *
+ * @param stage the primary application stage where the scene will be set
+ */
+ public ViewController(Stage stage) {
+ this.stage = stage;
+ this.scene = new Scene(new javafx.scene.layout.StackPane(), 800, 600);
+ stage.setScene(scene);
+ }
+
+ /**
+ * Registers a view in the controller.
+ *
+ * @param name unique identifier used to reference the view later
+ * @param view the JavaFX root node representing the view
+ */
+ public void addView(String name, Parent view) {
+ views.put(name, view);
+ }
+
+ /**
+ * Switches the currently displayed view by replacing the Scene root.
+ *
+ * @param name the identifier of the view to display
+ * @throws IllegalArgumentException if no view is registered under the given name
+ */
+ public void switchView(String name) {
+ Parent view = views.get(name);
+
+ if (view == null) {
+ throw new IllegalArgumentException("No views found matching: " + name);
+ }
+
+ scene.setRoot(view);
+ }
+}
From 7169f15b41c3dde5c82ae6c727d035f5a295ed35 Mon Sep 17 00:00:00 2001
From: Roar
Date: Thu, 16 Apr 2026 00:12:39 +0200
Subject: [PATCH 009/142] Updated ViewController
Fixed javadoc typos.
---
.../main/java/no/ntnu/gruppe53/controller/ViewController.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
index c71dd4c..f33de22 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
@@ -12,6 +12,7 @@
*
* Stores a mapping of view names to JavaFX {@link Parent} nodes and updates
* the current Scene by replacing its root node when switching views.
+ *
Contains method to advance the week of the market, finding stocks, gainers, and losers on the exchange.
+ */
public class Exchange {
private final String name;
private int week;
private final Map stockMap;
private final Random random;
+ private final ObservableList stocks = FXCollections.observableArrayList();
+ /**
+ * Sets the name of the exchange and the list of stocks to populate it.
+ *
Validates that the values are proper before creating the exchange.
+ *
+ * @param name the name of the exchange
+ * @param stocks the list of stocks to be tradeable on the exchange
+ * @throws IllegalArgumentException if exchange is null or blank, if list of stocks is null, or if
+ * the list of stocks contains a null or duplicate symbol stock
+ */
public Exchange(String name, List stocks) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Exchange name cannot be null or blank");
@@ -35,26 +53,45 @@ public Exchange(String name, List stocks) {
}
stockMap.put(stock.getSymbol(), stock);
+ this.stocks.add(stock);
}
-
}
+ /**
+ * Returns the name of the exchange
+ * @return the name of the exchange
+ */
public String getName() {
- return this.name;
+ return name;
}
+ /**
+ * Returns the current week of the exchange
+ *
+ * @return the current week of the exchange
+ */
public int getWeek() {
- return this.week;
+ return week;
}
-
- // Returns Boolean value of if the exchange has the stock.
+ /**
+ * Checks whether a stock with the given symbol/ticker exists in the exchange.
+ *
Returns {@code true} if it does,v{@code false} otherwise.
+ *
+ * @param symbol the symbol/ticker of the stock
+ * @return {@code true} if the exchange contains the stock, otherwise {@code false}
+ */
public boolean hasStock(String symbol) {
-
return stockMap.containsKey(symbol);
}
- // Returns stock that corresponds to given symbol
+ /**
+ * Returns the stock object matching the given symbol/ticker.
+ *
+ * @param symbol the stock symbol/ticker
+ * @return the stock object matching the symbol/ticker
+ * @throws IllegalArgumentException if search symbol is null or symbol is not in stockMap
+ */
public Stock getStock(String symbol) {
if (symbol == null || !stockMap.containsKey(symbol)) {
throw new IllegalArgumentException("Stock not found: " + symbol);
@@ -62,63 +99,78 @@ public Stock getStock(String symbol) {
return stockMap.get(symbol);
}
+ /**
+ * Returns an {@link ObservableList} of the list of stocks for JavaFX to observe.
+ *
+ * @return an observable list of the stocks in the exchange
+ */
+ public ObservableList getObservableStocks() {
+ return stocks;
+ }
+
+ /**
+ * Returns a list of stocks matching the search term in either the symbol/ticker or company name.
+ *
+ * @param searchTerm the string to search for
+ * @return a list of stocks matching the search term
+ */
public List findStocks(String searchTerm) {
- //Wait for merging of other branches before implementation
- if (searchTerm == null) {
- return Collections.emptyList();
- }
+ if (searchTerm == null || searchTerm.isBlank()) return Collections.emptyList();
String search = searchTerm.toLowerCase();
-
- return stockMap.values().stream().filter(stock ->
- stock.getSymbol().toLowerCase().contains(search) ||
- stock.getCompany().toLowerCase().contains(search)).collect(Collectors.toList());
-
+ return stockMap.values().stream()
+ .filter(stock -> stock.getSymbol().toLowerCase().contains(search) ||
+ stock.getCompany().toLowerCase().contains(search))
+ .collect(Collectors.toList());
}
+ /**
+ * Represents a player buying a share on the exchange.
+ *
Creates a {@link Purchase} {@link Transaction} for the
+ * purchase.
+ *
+ * @param symbol the symbol/ticker of the stock
+ * @param quantity the quantity of shares to buy for said stock
+ * @param player the player making the purchase
+ * @return a purchase transaction containing all details of the purchase
+ * @throws IllegalArgumentException if player or quantity is null
+ */
public Transaction buy(String symbol, BigDecimal quantity, Player player) {
- //Wait for merging of other branches before implementation
-
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ if (player == null) throw new IllegalArgumentException("Player cannot be null");
+ if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0)
throw new IllegalArgumentException("Quantity must be positive");
- }
Stock stock = getStock(symbol);
-
Share share = new Share(stock, quantity, stock.getSalesPrice());
-
Transaction transaction = new Purchase(share, week);
-
transaction.commit(player);
return transaction;
-
}
+ /**
+ * Represents a player selling a share on the exchange.
+ *
Creates a {@link Sale} transaction for the sale.
+ *
+ * @param share the share to be sold
+ * @param player the player selling the share
+ * @return a sale transaction containing all details of the sale
+ * @throws IllegalArgumentException if player or share is null
+ */
public Transaction sell(Share share, Player player) {
- //Wait for merging of other branches before implementation
-
-
-
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
+ if (player == null) throw new IllegalArgumentException("Player cannot be null");
+ if (share == null) throw new IllegalArgumentException("Share cannot be null");
Transaction transaction = new Sale(share, week);
-
transaction.commit(player);
-
return transaction;
}
+ /**
+ * Advances the timeline of the exchange by 1 week.
+ *
Uses a randomized value for the increase and decrease of a stocks sales price, simulating market
+ * fluctuations on a weekly basis.
+ */
public void advance() {
this.week++;
@@ -126,13 +178,8 @@ public void advance() {
double positivePriceChange = 0.1;
for (Stock stock : stockMap.values()) {
-
BigDecimal currentPrice = stock.getSalesPrice();
-
- // Bruh java gjør random shit rart as
- double randomPriceMultiplier = negativePriceChange +
- (positivePriceChange * random.nextDouble());
-
+ double randomPriceMultiplier = negativePriceChange + (positivePriceChange * random.nextDouble());
BigDecimal bdRandomPriceMultiplier = BigDecimal.valueOf(1 + randomPriceMultiplier);
BigDecimal changedPrice = currentPrice.multiply(bdRandomPriceMultiplier)
@@ -147,7 +194,7 @@ public void advance() {
}
/**
- * Sorts the stocks in an ascending order and returns a list of stocks.
+ * Sorts the stocks in descending order and returns a list of stocks.
*
* @param entries amount of entries in the list to display
* @param comparator the comparator used to compare the stocks
@@ -155,10 +202,7 @@ public void advance() {
* @throws IllegalArgumentException if the amount of entries is negative
*/
private List getTopStocks(int entries, Comparator comparator) {
- if (entries < 0) {
- throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
- }
-
+ if (entries < 0) throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
return stockMap.values().stream()
.sorted(comparator)
.limit(entries)
@@ -186,6 +230,4 @@ public List getGainers(int entries) {
public List getLosers(int entries) {
return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange));
}
-
-
-}
+}
\ No newline at end of file
From 03b867d36b379310ddc88a19fd6ace2de22f6e0b Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:34:49 +0200
Subject: [PATCH 043/142] Updated Stock
Added Observables for salesPrice and a getter
to fetch it for JavaFX.
Changed the logic for getLatestPriceChange.
---
.../java/no/ntnu/gruppe53/model/Stock.java | 42 ++++++++++++-------
1 file changed, 28 insertions(+), 14 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java
index 5d06d45..6e3ac66 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java
@@ -1,5 +1,8 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
@@ -21,12 +24,13 @@
* instantiated at construction.
*
*/
-
public class Stock {
private final String symbol;
private final String company;
private final List prices;
+ private final ObjectProperty salesPrice;
+
/**
* Creates a stock with an already defined sales price.
*
@@ -41,11 +45,9 @@ public Stock(String symbol, String company, BigDecimal salesPrice) {
if (symbol == null || symbol.isBlank()) {
throw new IllegalArgumentException("Symbol cannot be null or empty.");
}
-
if (company == null || company.isBlank()) {
throw new IllegalArgumentException("Company cannot be null or empty.");
}
-
if (salesPrice == null || salesPrice.signum() <= 0) {
throw new IllegalArgumentException("SalesPrice cannot be null, negative, or zero.");
}
@@ -54,6 +56,8 @@ public Stock(String symbol, String company, BigDecimal salesPrice) {
this.company = company;
this.prices = new ArrayList<>();
this.prices.add(salesPrice);
+
+ this.salesPrice = new SimpleObjectProperty<>(salesPrice);
}
/**
@@ -62,7 +66,7 @@ public Stock(String symbol, String company, BigDecimal salesPrice) {
* @return the stock's ticker symbol
*/
public String getSymbol() {
- return this.symbol;
+ return symbol;
}
/**
@@ -71,7 +75,7 @@ public String getSymbol() {
* @return the full name of the stock's company
*/
public String getCompany() {
- return this.company;
+ return company;
}
/**
@@ -82,7 +86,7 @@ public String getCompany() {
* @return the current sale price of the stock
*/
public BigDecimal getSalesPrice() {
- return this.prices.get(prices.size() - 1);
+ return salesPrice.get();
}
/**
@@ -102,6 +106,17 @@ public void addNewSalesPrice(BigDecimal price) {
throw new IllegalArgumentException("Price cannot be null, negative, or zero.");
}
this.prices.add(price);
+ this.salesPrice.set(price);
+ }
+
+
+ /**
+ * Returns an {@link ObjectProperty} of the sales price.
+ *
+ * @return an observable of the sales price
+ */
+ public ObjectProperty salesPriceProperty() {
+ return salesPrice;
}
/**
@@ -110,7 +125,7 @@ public void addNewSalesPrice(BigDecimal price) {
* @return a list of all the stock's sales prices
*/
public List getHistoricalPrices() {
- return this.prices;
+ return prices;
}
/**
@@ -119,7 +134,7 @@ public List getHistoricalPrices() {
* @return the stock's highest sales price
*/
public BigDecimal getHighestPrice() {
- return Collections.max(this.prices);
+ return Collections.max(prices);
}
/**
@@ -128,7 +143,7 @@ public BigDecimal getHighestPrice() {
* @return the stock's lowest sales price
*/
public BigDecimal getLowestPrice() {
- return Collections.min(this.prices);
+ return Collections.min(prices);
}
/**
@@ -138,11 +153,10 @@ public BigDecimal getLowestPrice() {
* @return the net difference in sales price between the last and second last week
*/
public BigDecimal getLatestPriceChange() {
- if (!(this.prices.size() < 2)) {
- return this.prices.getLast().subtract(this.prices.get(this.prices.size() - 2));
- }
- else {
+ if (prices.size() >= 2) {
+ return prices.getLast().subtract(prices.get(prices.size() - 2));
+ } else {
return BigDecimal.ZERO;
}
}
-}
+}
\ No newline at end of file
From 112a19ddb3a7e925991adada6295bce3c8a6f5ec Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:37:49 +0200
Subject: [PATCH 044/142] Updated Portfolio
Added Observables for shares and net worth and methods to update and fetch them.
Minor edits to removeShare.
---
.../no/ntnu/gruppe53/model/Portfolio.java | 76 ++++++++++++-------
1 file changed, 48 insertions(+), 28 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java
index 7744c57..1164f6b 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java
@@ -3,6 +3,10 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
/**
* Represents the portfolio of the Player, containing information
@@ -13,20 +17,20 @@
*
*
*
- * Shares are associated with unique Transaction, however, multiple
+ * Shares are associated with a unique Transaction, however, multiple
* shares from the same stock can exist in the portfolio.
*
*/
-
public class Portfolio {
- private final List shares;
+ private final ObservableList shares;
+ private final ObjectProperty netWorthProperty =
+ new SimpleObjectProperty<>(BigDecimal.ZERO);
/**
* Creates an empty portfolio for the player.
*/
-
public Portfolio() {
- this.shares = new ArrayList<>();
+ this.shares = FXCollections.observableArrayList();
}
/**
@@ -41,7 +45,6 @@ public Portfolio() {
* {@code false} otherwise
* @throws IllegalArgumentException if {@code share} is {@code null}
*/
-
public boolean addShare(Share share) {
if (share == null) {
throw new IllegalArgumentException("Share cannot be null");
@@ -49,7 +52,11 @@ public boolean addShare(Share share) {
this.shares.add(share);
- return this.getShares().getLast().equals(share);
+ share.getStock().salesPriceProperty()
+ .subscribe(price -> updateNetWorth());
+
+ updateNetWorth();
+ return true;
}
/**
@@ -65,17 +72,17 @@ public boolean addShare(Share share) {
* @throws IllegalArgumentException if {@code share} is {@code null}
*/
public boolean removeShare(Share share) {
- if (share == null) {
+ if (share == null) {
throw new IllegalArgumentException("Share cannot be null");
}
- for (Share s : this.shares) {
- if (s.equals(share)) {
- this.shares.remove(s);
- return true;
- }
+ boolean removed = this.shares.removeIf(s -> s.equals(share));
+
+ if (removed) {
+ updateNetWorth();
}
- return false;
+
+ return removed;
}
/**
@@ -83,7 +90,7 @@ public boolean removeShare(Share share) {
*
* @return a list of the shares in this portfolio
*/
- public List getShares() {
+ public ObservableList getShares() {
return this.shares;
}
@@ -98,7 +105,7 @@ public List getShares() {
* @throws IllegalArgumentException if {@code symbol} is {@code null} or blank
*/
public List getShares(String symbol) {
- if (symbol == null || symbol.isBlank()) {
+ if (symbol == null || symbol.isBlank()) {
throw new IllegalArgumentException("Symbol cannot be null or blank");
}
@@ -119,27 +126,40 @@ public List getShares(String symbol) {
* @return {@code true} if the player's portfolio contains the share
* {@code false} otherwise
*/
-
public boolean contains(Share share) {
- for (Share s : this.shares) {
- if (s.equals(share)) {
- return true;
- }
- }
- return false;
+ return shares.stream().anyMatch(s -> s.equals(share));
}
/**
* Calculates the net total value of the shares in the portfolio.
- *
Calculations are done using a {@link SaleCalculator}.
+ *
+ * Portfolio value is based on current market price, not transaction rules.
+ *
*
- * @return the total sale value of the portfolio
+ * @return the total market value of the portfolio
*/
public BigDecimal getNetWorth() {
-
return this.shares.stream()
- .map(share -> new SaleCalculator(share).calculateTotal())
+ .map(share ->
+ share.getStock().getSalesPrice()
+ .multiply(share.getQuantity())
+ )
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
-}
+ /**
+ * Returns an {@link ObjectProperty} of the net worth of the portfolio
+ *
+ * @return an observable of the portfolio's net worth
+ */
+ public ObjectProperty netWorthProperty() {
+ return netWorthProperty;
+ }
+
+ /**
+ * Mirrors the portfolio's net worth to the observable.
+ */
+ public void updateNetWorth() {
+ netWorthProperty.set(getNetWorth());
+ }
+}
\ No newline at end of file
From ae9a6b0ceccca24fe0fc94d0e814cac4cab86402 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:39:39 +0200
Subject: [PATCH 045/142] Updated Player
Added Observables for money and net worth with getters.
---
.../java/no/ntnu/gruppe53/model/Player.java | 111 +++++++++++-------
1 file changed, 67 insertions(+), 44 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Player.java b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java
index 84840dd..7c2610d 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Player.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java
@@ -2,6 +2,8 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
/**
* Represents a participant in the trading game.
@@ -16,7 +18,6 @@
* lose funds through transactions.
*
*/
-
public class Player {
private final String name;
private final BigDecimal startingMoney;
@@ -24,8 +25,12 @@ public class Player {
private final Portfolio portfolio;
private final TransactionArchive transactionArchive;
+ private final ObjectProperty moneyProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty netWorthProperty = new SimpleObjectProperty<>();
+
/**
* Creates a player with a given starting balance.
+ *
Adds a listener to the netWorth property to notify of changes in the player's net worth.
*
* @param name the player's name; must not be {@code null} or blank
* @param startingMoney the initial monetary balance of the
@@ -34,30 +39,49 @@ public class Player {
* blank, or if {@code startingMoney}
* is {@code null}, negative, or 0
*/
-
public Player(String name, BigDecimal startingMoney) {
if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Player name must not be " +
- "null or empty.");
+ throw new IllegalArgumentException("Player name must not be null or empty.");
}
if (startingMoney == null || startingMoney.signum() <= 0 ) {
- throw new IllegalArgumentException("Starting money must not be " +
- "null, negative, nor 0.");
+ throw new IllegalArgumentException("Starting money must not be null, negative, nor 0.");
}
+
this.name = name;
this.startingMoney = startingMoney;
this.money = startingMoney;
+ this.moneyProperty.set(startingMoney);
this.portfolio = new Portfolio();
this.transactionArchive = new TransactionArchive();
+
+ portfolio.netWorthProperty().addListener(
+ (obs, oldVal, newVal) -> updateNetWorth());
+ updateNetWorth();
+ }
+
+ /**
+ * Returns an {@link ObjectProperty} of the player's net worth.
+ *
+ * @return an observable of the player's net worth
+ */
+ public ObjectProperty getNetWorthProperty() {
+ return netWorthProperty;
}
+ /**
+ * Mirrors the change in the player's net worth to the observable property.
+ */
+ public void updateNetWorth() {
+ netWorthProperty.set(getNetWorth());
+ }
+
+
/**
* Returns the full name of the player.
*
* @return the player's name
*/
-
public String getName() {
return this.name;
}
@@ -67,7 +91,6 @@ public String getName() {
*
* @return the player's current balance.
*/
-
public BigDecimal getMoney() {
return this.money;
}
@@ -82,17 +105,12 @@ public BigDecimal getMoney() {
* @throws IllegalArgumentException if {@code amount] is {@code null}
* or not positive
*/
-
public void addMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount must not be null.");
- }
-
- if (amount.signum() <= 0) {
- throw new IllegalArgumentException("Amount must be positive.");
- }
-
+ if (amount == null) throw new IllegalArgumentException("Amount must not be null.");
+ if (amount.signum() <= 0) throw new IllegalArgumentException("Amount must be positive.");
this.money = this.money.add(amount);
+ this.moneyProperty.set(this.money);
+ updateNetWorth();
}
/**
@@ -106,29 +124,30 @@ public void addMoney(BigDecimal amount) {
* @throws IllegalArgumentException if the amount is not positive, {@code null}
* , or insufficient funds are available
*/
-
public void withdrawMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount must not be null.");
- }
-
- if (amount.signum() <= 0) {
- throw new IllegalArgumentException("Amount must be positive.");
- }
+ if (amount == null) throw new IllegalArgumentException("Amount must not be null.");
+ if (amount.signum() <= 0) throw new IllegalArgumentException("Amount must be positive.");
+ if (this.money.subtract(amount).signum() < 0) throw new IllegalArgumentException("Insufficient funds.");
+ this.money = this.money.subtract(amount);
+ this.moneyProperty.set(this.money);
+ updateNetWorth();
+ }
- if (this.money.subtract(amount).signum() < 0) {
- throw new IllegalArgumentException("Insufficient funds.");
- }
- this.money = this.money.subtract(amount);
+ /**
+ * Returns an observable of the player's current monetary funds
+ * @return an observable of the player's current money
+ */
+ public ObjectProperty getMoneyProperty() {
+ return moneyProperty;
}
+
/**
* Return's the player's portfolio of shares.
*
* @return the player's portfolio
*/
-
public Portfolio getPortfolio() {
return this.portfolio;
}
@@ -138,28 +157,32 @@ public Portfolio getPortfolio() {
*
* @return the player's transaction archive
*/
-
public TransactionArchive getTransactionArchive() {
return this.transactionArchive;
}
+ /**
+ * Returns the player's current net worth.
+ *
The net worth is the sum of the player's current money and all the value of their shares in their
+ * portfolio.
+ *
+ * @return the player's current net worth
+ */
public BigDecimal getNetWorth() {
- return this.money.add(this.portfolio.getNetWorth());
+ return money.add(portfolio.getNetWorth());
}
+ /**
+ * Returns the current status/title of the player, representing their achievements.
+ *
+ * @param exchange the exchange the player is trading at
+ * @return a string representing the players current status
+ */
public String getStatus(Exchange exchange) {
String status = "Novice";
-
- if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("1.20"))
- >= 0) && exchange.getWeek() >= 10) {
- status = "Investor";
- }
-
- if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("2"))
- >= 0) && exchange.getWeek() >= 20) {
- status = "Speculator";
- }
-
+ BigDecimal ratio = getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN);
+ if (ratio.compareTo(new BigDecimal("1.20")) >= 0 && exchange.getWeek() >= 10) status = "Investor";
+ if (ratio.compareTo(new BigDecimal("2")) >= 0 && exchange.getWeek() >= 20) status = "Speculator";
return status;
}
-}
+}
\ No newline at end of file
From b4304f252e8aac24cd19f6df7208a434cf7f3ed4 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:42:01 +0200
Subject: [PATCH 046/142] Updated PurchaseCalculator
Added JavaDoc.
---
.../gruppe53/model/PurchaseCalculator.java | 62 ++++++++++---------
1 file changed, 33 insertions(+), 29 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
index 0f983ad..37fe66a 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
@@ -2,28 +2,19 @@
import java.math.BigDecimal;
-/*
-PurchaseCalculator class:
-
-Implements from TransactionCalculator
-
-Which contains:
-1. Constructor which passes the values of a Share object
- to its local variables.
-2. calculateGross() function to calculate the gross value
-3. calculateCommission() function to calculate the commission value
-4. calculateTax() function which does nothing as there is no tax
-5. calculateTotal() function which calculates the total purchase price
- by adding all the calculations together.
-
+/**
+ * Calculates the costs associated with a {@link Purchase} of a given amount of {@link Share}s on the
+ * {@link Exchange}.
*/
public class PurchaseCalculator implements TransactionCalculator{
- private final BigDecimal purchasePrice;
- private final BigDecimal quantity;
-
-
- //Takes a Share object to pass its Purchase and Quantity Values to
- //Its local private variables
+ private final BigDecimal purchasePrice;
+ private final BigDecimal quantity;
+
+ /**
+ * Sets the purchase price and quantity based on data from the given share
+ *
+ * @param share the share to calculate purchase costs for
+ */
public PurchaseCalculator(Share share) {
this.purchasePrice = share.getPurchasePrice();
@@ -31,32 +22,45 @@ public PurchaseCalculator(Share share) {
}
- // Multiplies quantity with the purchase price to get the gross value
+ /**
+ * Calculates the gross purchase price of the given share.
+ *
+ * @return the gross associated cost of purchasing the share
+ */
@Override
public BigDecimal calculateGross(){
return this.quantity.multiply(this.purchasePrice);
}
- //Takes the gross value calculated in calculateGross()
- //And multiplies it with the 0.05% commission on purchase to
- //get the total value of the commission
+ /**
+ * Calculates the commission of the exchange for purchasing a share.
+ *
Commission is set to a fixed 0.5% of gross purchase price.
+ *
+ * @return the commission taken by the exchange
+ */
@Override
public BigDecimal calculateCommission() {
- //0.5% commission on purchase
BigDecimal commission = new BigDecimal("0.005");
return calculateGross().multiply(commission);
}
- //No tax on purchase
+ /**
+ * Calculates the tax for a purchase.
+ *
For a purchase, there is no tax.
+ *
+ * @return the tax for a purchase, in this case zero
+ */
@Override
public BigDecimal calculateTax() {
return new BigDecimal(0);
}
-
- //Calculates the total purchase price by adding
- // Gross + Commission + Tax
+ /**
+ * Calculates the total costs associated with the purchase of a share
+ *
+ * @return the net total cost of purchasing a share
+ */
@Override
public BigDecimal calculateTotal() {
From fd03be4f27a2de3c93804a5da70ac677ec27bac5 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:44:44 +0200
Subject: [PATCH 047/142] Updated Sale
Changed commit to throw IllegalStateException if player does not own the share.
---
.../main/java/no/ntnu/gruppe53/model/Sale.java | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
index d8a6c36..2c72306 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
@@ -38,6 +38,7 @@ public Sale(Share share, int week) {
*
* @param player the player performing the transaction; must not be {@code null}
* @throws IllegalArgumentException if {@code player} is {@code null}
+ * @throws IllegalStateException if trying to sell a share not in the player's portfolio
*/
@Override
@@ -46,12 +47,16 @@ public void commit(Player player) {
throw new IllegalArgumentException("Player cannot be null");
}
- if (!this.isCommitted() && (player.getPortfolio().contains(this.getShare()))
- ) {
- player.addMoney(this.getCalculator().calculateTotal());
- player.getPortfolio().removeShare(this.getShare());
- player.getTransactionArchive().add(this);
- super.commit(player);
+ if (this.isCommitted()) {
+ throw new IllegalStateException("Transaction has already been committed.");
}
+ if (!player.getPortfolio().contains(this.getShare())) {
+ throw new IllegalStateException("You do not own this share in your portfolio.");
+ }
+ player.addMoney(this.getCalculator().calculateTotal());
+ player.getPortfolio().removeShare(this.getShare());
+ player.getTransactionArchive().add(this);
+ super.commit(player);
+
}
}
From 8bc07e839efc43c99ba11194d12c77d4f4586863 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:45:16 +0200
Subject: [PATCH 048/142] Updated Sale
Changed commit to throw IllegalStateException if player does not own the share. A
---
millions/src/main/java/no/ntnu/gruppe53/model/Sale.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
index 2c72306..732ec1f 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
@@ -38,7 +38,8 @@ public Sale(Share share, int week) {
*
* @param player the player performing the transaction; must not be {@code null}
* @throws IllegalArgumentException if {@code player} is {@code null}
- * @throws IllegalStateException if trying to sell a share not in the player's portfolio
+ * @throws IllegalStateException if trying to sell a share not in the player's portfolio or if transactions
+ * has already been committed.
*/
@Override
From 0180fdf0129be11cd9c54fe59ff002e08e83acaf Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:54:26 +0200
Subject: [PATCH 049/142] Updates SaleCalculator
Edited tax calculation logic and added JavaDoc.
---
.../ntnu/gruppe53/model/SaleCalculator.java | 96 +++++++++++--------
1 file changed, 54 insertions(+), 42 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
index cdc8371..872aa07 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
@@ -1,80 +1,92 @@
package no.ntnu.gruppe53.model;
import java.math.BigDecimal;
+import java.math.RoundingMode;
-/*
-SaleCalculator class:
-
-Implements from Interface TransactionCalculator
-
-Which contains:
-1. Constructor which passes the values of a Share object
- to its local variables.
-2. calculateGross() function to calculate the gross value
-3. calculateCommision() function to calculate the commission value
-4. calculateTax() function which calculates the 30% tax on winnings
-5. calculateTotal() function which calculates the total sales price
- by subtracting commission and tax from the gross value
-
+/**
+ * Calculates the costs associated with the {@link Sale} of a {@link Share} on the {@link Exchange},
+ * and the net total gain of selling said share.
*/
+public class SaleCalculator implements TransactionCalculator {
-
-
-public class SaleCalculator implements TransactionCalculator{
private final BigDecimal purchasePrice;
private final BigDecimal salesPrice;
private final BigDecimal quantity;
- //Takes a Share object to pass its Quantity, Purchase- and SalesPrice Values to
- //Its local private variables
+ /**
+ * Sets the quantity, purchase and sales price based on the given share.
+ *
+ * @param share the share to calculate the costs and net total of a sale
+ */
public SaleCalculator(Share share) {
this.purchasePrice = share.getPurchasePrice();
this.quantity = share.getQuantity();
this.salesPrice = share.getStock().getSalesPrice();
}
-
- //Multiplies quntity with the sales price to get the gross value
+ /**
+ * Calculates the gross sales price of the given share.
+ *
+ * @return the gross associated gain from selling the share
+ */
@Override
public BigDecimal calculateGross() {
-
- return this.quantity.multiply(this.salesPrice);
+ return quantity.multiply(salesPrice)
+ .setScale(2, RoundingMode.HALF_UP);
}
- //Takes the gross value calculated in calculateGross()
- //And multiplies it with the 0.1% commission on sale to
- //get the total value of the commission
+ /**
+ * Calculates the exchange's commission for selling the share.
+ *
The commission is set to a fixed 1% of the gross total sale value.
+ *
+ * @return the commission taken by the exchange
+ */
@Override
public BigDecimal calculateCommission() {
- //0.1% commission
- BigDecimal commision = new BigDecimal("0.01");
+ BigDecimal commissionRate = new BigDecimal("0.01");
- return calculateGross().multiply(commision);
+ return calculateGross()
+ .multiply(commissionRate)
+ .setScale(2, RoundingMode.HALF_UP);
}
-
- //Calculates the tax on winnings, winnings are defined as
- //Gross - commission - (purchaseprice * quantity)
- //Tax is therefore calculated as winnings * tax(30%)
+ /**
+ * Calculates the taxable profit from the sale.
+ *
Only calculates tax for profitable sales.
>
+ *
+ * @return the tax amount
+ */
@Override
public BigDecimal calculateTax() {
+ BigDecimal taxRate = new BigDecimal("0.30");
- BigDecimal tax = new BigDecimal("0.30");
+ BigDecimal originalCost = purchasePrice.multiply(quantity);
- // (Gross - commission - (Purchase price - quantity)) * 30% tax
- return calculateGross().subtract(calculateCommission())
- .subtract(this.purchasePrice.multiply(this.quantity)).multiply(tax);
+ BigDecimal profit = calculateGross()
+ .subtract(calculateCommission())
+ .subtract(originalCost);
+ if (profit.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ return profit.multiply(taxRate)
+ .setScale(2, RoundingMode.HALF_UP);
}
- //Calculates the total sales price by subtracting
- //Gross - Commission - Tax
+ /**
+ * Returns the net total gain for selling the share.
+ *
+ * @return the net total winnings of selling the share
+ */
@Override
public BigDecimal calculateTotal() {
- return calculateGross().subtract(calculateCommission()).subtract(calculateTax());
-
+ return calculateGross()
+ .subtract(calculateCommission())
+ .subtract(calculateTax())
+ .setScale(2, RoundingMode.HALF_UP);
}
-}
+}
\ No newline at end of file
From d772585008f7fa94fa228d15661d8c5a822c0499 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:57:37 +0200
Subject: [PATCH 050/142] Updated TransactionArchive
Made archive Observable.
---
.../no/ntnu/gruppe53/model/TransactionArchive.java | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java
index 1dc25ba..70bba67 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java
@@ -1,5 +1,8 @@
package no.ntnu.gruppe53.model;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -14,14 +17,14 @@
*/
public class TransactionArchive {
- private final List transactions;
+ private final ObservableList transactions;
/**
* Creates an empty transaction archive.
*/
public TransactionArchive() {
- this.transactions = new ArrayList<>();
+ this.transactions = FXCollections.observableArrayList();
}
/**
@@ -60,6 +63,10 @@ public boolean isEmpty() {
* @return the list of transactions
*/
+ public ObservableList getObservableTransactions() {
+ return this.transactions;
+ }
+
public List getTransactions() {
return this.transactions;
}
From 13a105e04bb2c3ddecd5e4229cb882fe65c7f86a Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 09:58:33 +0200
Subject: [PATCH 051/142] Updated TransactionCalculator
Added JavaDoc.
---
.../ntnu/gruppe53/model/TransactionCalculator.java | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java
index 96ef413..511cdfc 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java
@@ -2,10 +2,14 @@
import java.math.BigDecimal;
+/**
+ * Represents the necessary calculations to perform a transaction.
+ *
{@link Purchase} and {@link Sale} are examples of implementations of a transaction.
+ */
public interface TransactionCalculator {
- BigDecimal calculateGross();
- BigDecimal calculateCommission();
- BigDecimal calculateTax();
- BigDecimal calculateTotal();
+ BigDecimal calculateGross();
+ BigDecimal calculateCommission();
+ BigDecimal calculateTax();
+ BigDecimal calculateTotal();
}
From 3e8be93965e52ecec50f9984e2ac136eb3883376 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:00:40 +0200
Subject: [PATCH 052/142] Refactor of test classes
Refactored into package structure that reflects structure of main code.
---
.../java/no/ntnu/gruppe53/{ => model}/ExchangeTest.java | 6 +-----
.../test/java/no/ntnu/gruppe53/{ => model}/PlayerTest.java | 3 +--
.../java/no/ntnu/gruppe53/{ => model}/PortfolioTest.java | 6 +-----
.../ntnu/gruppe53/{ => model}/PurchaseCalculatorTest.java | 5 +----
.../java/no/ntnu/gruppe53/{ => model}/PurchaseTest.java | 3 +--
.../ntnu/gruppe53/{ => model}/SaleCalculatorUnitTest.java | 5 +----
.../test/java/no/ntnu/gruppe53/{ => model}/SaleTest.java | 3 +--
.../test/java/no/ntnu/gruppe53/{ => model}/ShareTest.java | 4 +---
.../test/java/no/ntnu/gruppe53/{ => model}/StockTest.java | 3 +--
.../ntnu/gruppe53/{ => model}/TransactionArchiveTest.java | 3 +--
.../no/ntnu/gruppe53/{ => service}/FileHandlerTest.java | 2 +-
11 files changed, 11 insertions(+), 32 deletions(-)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/ExchangeTest.java (97%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/PlayerTest.java (99%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/PortfolioTest.java (98%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/PurchaseCalculatorTest.java (90%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/PurchaseTest.java (98%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/SaleCalculatorUnitTest.java (92%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/SaleTest.java (99%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/ShareTest.java (97%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/StockTest.java (99%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => model}/TransactionArchiveTest.java (99%)
rename millions/src/test/java/no/ntnu/gruppe53/{ => service}/FileHandlerTest.java (99%)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
similarity index 97%
rename from millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
index f4b43ef..d1adf0f 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/ExchangeTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
@@ -1,10 +1,6 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.Exchange;
-import no.ntnu.gruppe53.model.Player;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
similarity index 99%
rename from millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
index 845fb75..60b8ac2 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/PlayerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
@@ -1,6 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
similarity index 98%
rename from millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
index badccdf..81cc15e 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/PortfolioTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
@@ -1,9 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.Portfolio;
-import no.ntnu.gruppe53.model.SaleCalculator;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
similarity index 90%
rename from millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
index 0dba3f2..ab6da19 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/PurchaseCalculatorTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
@@ -1,8 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.PurchaseCalculator;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java
similarity index 98%
rename from millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java
index 72816d8..f235b53 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/PurchaseTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseTest.java
@@ -1,6 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java
similarity index 92%
rename from millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java
index 7b4ea9d..7c67885 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/SaleCalculatorUnitTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java
@@ -1,8 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.SaleCalculator;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/SaleTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
similarity index 99%
rename from millions/src/test/java/no/ntnu/gruppe53/SaleTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
index d7820fe..86a38c5 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/SaleTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
@@ -1,6 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/ShareTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java
similarity index 97%
rename from millions/src/test/java/no/ntnu/gruppe53/ShareTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java
index ffb5e0f..125762a 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/ShareTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/ShareTest.java
@@ -1,7 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/StockTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
similarity index 99%
rename from millions/src/test/java/no/ntnu/gruppe53/StockTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
index 69b4a15..38c96cb 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/StockTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
@@ -1,6 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.Stock;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
similarity index 99%
rename from millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
index 39234d3..cb39777 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/TransactionArchiveTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
@@ -1,6 +1,5 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.model;
-import no.ntnu.gruppe53.model.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
diff --git a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
similarity index 99%
rename from millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
index c09fdc9..55e6133 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/FileHandlerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
@@ -1,4 +1,4 @@
-package no.ntnu.gruppe53;
+package no.ntnu.gruppe53.service;
import no.ntnu.gruppe53.model.Stock;
import no.ntnu.gruppe53.view.FileHandler;
From 12d837b7f154d33d86da383f681f05e32ec249cb Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:02:17 +0200
Subject: [PATCH 053/142] Updated ExchangeTest
Added tests for coverage.
---
.../no/ntnu/gruppe53/model/ExchangeTest.java | 141 +++++++++++++++++-
1 file changed, 140 insertions(+), 1 deletion(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
index d1adf0f..128badd 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
@@ -1,16 +1,18 @@
package no.ntnu.gruppe53.model;
+import javafx.collections.ObservableList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
-@DisplayName("Exhange Test")
+@DisplayName("Exchange Test")
public class ExchangeTest {
private Stock appleStock;
@@ -204,4 +206,141 @@ void getTopStocksShouldThrowIllegalArgumentExceptionsIfEntriesIsNegative() {
}
+ @Test
+ void nullExchangeNameShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new Exchange(null, stocks),
+ "Exchange name should not be allowed to be null.");
+ }
+
+ @Test
+ void blankExchangeNameShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new Exchange(" ", stocks),
+ "Exchange name should not be allowed to be blank.");
+ }
+
+ @Test
+ void nullStockListShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", null),
+ "Stocklist should not be allowed to be null.");
+ }
+
+ @Test
+ void nullStockInListShouldThrowIllegalArgumentException() {
+ stocks.add(null);
+
+ assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", stocks),
+ "Exchange should not be allowed to contain a null value for a stock.");
+ }
+
+ @Test
+ void duplicateStockSymbolInExchangeShouldThrowIllegalArgumentException() {
+ stocks.add(new Stock("AAPL", "Eple", new BigDecimal("50")));
+
+ assertThrows(IllegalArgumentException.class, () -> new Exchange("Exchange", stocks),
+ "Duplicate stock symbol should not be allowed");
+ }
+
+ @Test
+ void getNameShouldReturnExchangeName() {
+ assertEquals("Nasdaq", exchange.getName(),
+ "Exchange name should be correct.");
+ }
+
+ @Test
+ void getStockShouldReturnStock() {
+ assertEquals(microStock, exchange.getStock("MSFT"),
+ "Should return the correct stock based on the ticker symbol.");
+ }
+
+ @Test
+ void getStockForNullStockShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.getStock(null),
+ "Searching using null stock should throw an exception.");
+ }
+
+ @Test
+ void getStockForStockNotInExchangeShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.getStock("AAAA"),
+ "Searching for stock not in exchange should throw an exception.");
+ }
+
+ @Test
+ void getObservableListShouldReturnAnObservableList() {
+ assertInstanceOf(ObservableList.class, exchange.getObservableStocks(),
+ "Returned object should be an observableList.");
+ }
+
+ @Test
+ void findStocksForNullStringShouldGiveEmptyList() {
+ List listStocks = exchange.findStocks(null);
+
+ assertTrue(listStocks.isEmpty(), "List should be empty.");
+ }
+
+ @Test
+ void findStocksForBlankStringShouldGiveEmptyList() {
+ List listStocks = exchange.findStocks(" ");
+
+ assertTrue(listStocks.isEmpty(), "List should be empty.");
+ }
+
+ @Test
+ void findStocksShouldReturnRelevantStock() {
+ List listStocks = exchange.findStocks("MSFT");
+
+ assertEquals(listStocks.getFirst(), microStock, "Should return the correct stock.");
+ }
+
+ @Test
+ void findStocksShouldReturnStocksThatContainString() {
+ List listStocks = exchange.findStocks("T");
+
+ assertEquals(2, listStocks.size(), "Should return correct amount of stocks.");
+ assertEquals(listStocks.getFirst(), microStock, "Should return the correct first stock.");
+ assertEquals(listStocks.get(1), ntnuStock, "Should return the correct second stock.");
+ }
+
+ @Test
+ void buyWithNullPlayerShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.buy(
+ "AAPL", new BigDecimal("1"), null),
+ "Should throw if player is null.");
+ }
+
+ @Test
+ void buyWithNullQuantityShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.buy(
+ "AAPL", null, testPlayer),
+ "Should throw if quantity is null.");
+ }
+
+ @Test
+ void buyWithZeroQuantityShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.buy(
+ "AAPL", BigDecimal.ZERO, testPlayer),
+ "Should throw if quantity is zero.");
+ }
+
+ @Test
+ void buyWithNegativeQuantityShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.buy(
+ "AAPL", new BigDecimal("-1"), testPlayer),
+ "Should throw if quantity is negative.");
+ }
+
+ @Test
+ void SellWithNullPlayerShouldThrowIllegalArgumentException() {
+ testPlayer.getPortfolio().addShare(appleShare);
+
+ assertThrows(IllegalArgumentException.class, () -> exchange.sell(appleShare, null),
+ "Should throw if player is null.");
+ }
+
+
+ @Test
+ void sellWithNullShareShouldThrowIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> exchange.sell(null,testPlayer),
+ "Should throw if share is null.");
+ }
+
}
From 2b4146e3ba6e9c4f2e266822328657d1b5b77397 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:03:24 +0200
Subject: [PATCH 054/142] Updated PlayerTest
Added tests for coverage.
---
.../no/ntnu/gruppe53/model/PlayerTest.java | 165 +++++++++++++++++-
1 file changed, 160 insertions(+), 5 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
index 60b8ac2..49fa6fa 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PlayerTest.java
@@ -4,6 +4,8 @@
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.List;
+import javafx.beans.property.ObjectProperty;
import static org.junit.jupiter.api.Assertions.*;
@@ -335,13 +337,13 @@ void week1PlayerShouldHaveStatusNovice() {
@Test
void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() {
String name = "Bob";
- var startingMoney = new BigDecimal("2000");
+ var startingMoney = new BigDecimal("100");
var testPlayer = new Player(name, startingMoney);
String symbol1 = "AAPL";
String company1 = "Apple Inc.";
- BigDecimal price1 = new BigDecimal("100");
+ BigDecimal price1 = new BigDecimal("40");
Stock testStock1 = new Stock(symbol1,company1, price1);
@@ -354,7 +356,6 @@ void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() {
Exchange exchangeTest = new Exchange("test", stockList);
- purchasePrice1 = new BigDecimal("3000");
Share testShare1 = new Share(testStock1, quantity1, purchasePrice1);
testPlayer.getPortfolio().addShare(testShare1);
@@ -365,8 +366,6 @@ void week10OrMorePlayerWithOver20PercentGainShouldHaveStatusInvestor() {
assertEquals("Investor", testPlayer.getStatus(exchangeTest), "Since gain is over 20% and " +
"week is 10, player should get status 'investor'.");
- assertEquals("Investor", testPlayer.getStatus(exchangeTest), "Since gain is under 100% and " +
- "week is 20, player should get status 'investor' and not 'speculator'.");
}
@Test
@@ -443,4 +442,160 @@ void week20OrMorePlayerWithTwiceNetGainShouldHaveStatusSpeculator() {
assertEquals("Speculator", testPlayer.getStatus(exchangeTest), "Should be Speculator as" +
" the player has earned more than twice the starting money and the week is over 20.");
}
+
+ @Test
+ void Week20OrMorePlayerWithNoGainShouldStillHaveNoviceStatus() {
+ String symbol1 = "AAPL";
+ String company1 = "Apple Inc.";
+ BigDecimal price1 = new BigDecimal("100");
+ String name = "Bob";
+ var startingMoney = new BigDecimal("2000");
+
+ Stock testStock1 = new Stock(symbol1,company1, price1);
+ List stockList = List.of(testStock1);
+ Exchange exchangeTest = new Exchange("test", stockList);
+ System.out.println(exchangeTest.getWeek());
+
+ for (int i = 0; i < 11; i++) {
+ exchangeTest.advance();
+ }
+
+ var testPlayer = new Player(name, startingMoney);
+ System.out.println(exchangeTest.getWeek());
+
+ assertEquals("Novice", testPlayer.getStatus(exchangeTest), "Should be Novice");
+ }
+
+ @Test
+ void lessThanWeek10WeekPlayerWithTwiceGainShouldHaveStatusNovice() {
+ String name = "Bob";
+ var startingMoney = new BigDecimal("2000");
+
+ var testPlayer = new Player(name, startingMoney);
+
+ String symbol1 = "AAPL";
+ String company1 = "Apple Inc.";
+ BigDecimal price1 = new BigDecimal("100");
+
+ Stock testStock1 = new Stock(symbol1,company1, price1);
+
+ BigDecimal quantity1 = new BigDecimal("10");
+ var purchasePrice1 = testStock1.getSalesPrice();
+
+ Share testShare1 = new Share(testStock1, quantity1, purchasePrice1);
+
+ String symbol2 = "MSFT";
+ String company2 = "Microsoft Corporation";
+ BigDecimal price2 = new BigDecimal("87");
+
+ Stock testStock2 = new Stock(symbol2, company2, price2);
+
+ BigDecimal quantity2 = new BigDecimal("3");
+ var purchasePrice2 = testStock1.getSalesPrice();
+
+ Share testShare2 = new Share(testStock2, quantity2, purchasePrice2);
+
+ String symbol3 = "GOOG";
+ String company3 = "Alphabet Inc.";
+ BigDecimal price3 = new BigDecimal("75");
+
+ Stock testStock3 = new Stock(symbol3, company3, price3);
+
+ BigDecimal quantity3 = new BigDecimal("7");
+ var purchasePrice3 = testStock3.getSalesPrice();
+
+ Share testShare3 = new Share(testStock3, quantity3, purchasePrice3);
+
+ Portfolio testPortfolio = new Portfolio();
+ testPortfolio.addShare(testShare1);
+ testPortfolio.addShare(testShare2);
+ testPortfolio.addShare(testShare3);
+
+ testPlayer.getPortfolio().addShare(testShare1);
+ testPlayer.getPortfolio().addShare(testShare2);
+ testPlayer.getPortfolio().addShare(testShare3);
+
+ for (int i = 0; i < 200; i++) {
+ testPlayer.getPortfolio().addShare(testShare3);
+ }
+
+ var stockList = new ArrayList();
+
+ stockList.add(testStock1);
+ stockList.add(testStock2);
+ stockList.add(testStock3);
+
+ Exchange exchangeTest = new Exchange("test", stockList);
+
+ for (int i = 0; i < 5; i++) {
+ exchangeTest.advance();
+ }
+
+ assertEquals("Novice", testPlayer.getStatus(exchangeTest),
+ "Should be novice.");
+ }
+
+ @Test
+ void week10OrMorePlayerWithLessThan20PercentGainShouldHaveStatusNovice() {
+ String name = "Bob";
+ BigDecimal startingMoney = new BigDecimal("2000");
+
+ Player testPlayer = new Player(name, startingMoney);
+
+ String symbol = "AAPL";
+ String company = "Apple Inc.";
+ BigDecimal price = new BigDecimal("100");
+
+ Stock stock = new Stock(symbol, company, price);
+
+ BigDecimal quantity = new BigDecimal("2");
+
+ Share share = new Share(stock, quantity, stock.getSalesPrice());
+
+ testPlayer.getPortfolio().addShare(share);
+
+ Exchange exchange = new Exchange("test", List.of(stock));
+
+ for (int i = 0; i < 10; i++) {
+ exchange.advance();
+ }
+
+ assertEquals("Novice", testPlayer.getStatus(exchange));
+ }
+
+ @Test
+ void getNetWorthPropertyShouldReturnObservable() {
+ String name = "Bob";
+ var startingMoney = new BigDecimal("2000");
+
+ String symbol = "AAPL";
+ String company = "Apple Inc.";
+ BigDecimal price = new BigDecimal("100");
+
+ Stock stock = new Stock(symbol, company, price);
+
+ BigDecimal quantity = new BigDecimal("2");
+ Share share = new Share(stock, quantity, stock.getSalesPrice());
+ Player testPlayer = new Player(name, startingMoney);
+
+ testPlayer.getPortfolio().addShare(share);
+
+ assertInstanceOf(ObjectProperty.class, testPlayer.getNetWorthProperty(),
+ "Should be Observable");
+ assertEquals(testPlayer.getNetWorth(), testPlayer.getNetWorthProperty().getValue(),
+ "Should have correct net worth value.");
+ }
+
+ @Test
+ void getMoneyPropertyShouldReturnObservable() {
+ String name = "Bob";
+ var startingMoney = new BigDecimal("2000");
+
+ Player testPlayer = new Player(name, startingMoney);
+
+ assertInstanceOf(ObjectProperty.class, testPlayer.getMoneyProperty(),
+ "Should be Observable.");
+ assertEquals(testPlayer.getMoney(), testPlayer.getMoneyProperty().getValue(),
+ "Should have correct money value.");
+ }
}
\ No newline at end of file
From a34ddf70a5e11a65f131cf5abdad232fb39330ff Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:04:54 +0200
Subject: [PATCH 055/142] Updated PortfolioTest
Fixed test for net worth.
---
.../no/ntnu/gruppe53/model/PortfolioTest.java | 53 ++++---------------
1 file changed, 10 insertions(+), 43 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
index 81cc15e..9549487 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PortfolioTest.java
@@ -295,54 +295,21 @@ void containsShouldReturnFalseIfPortfolioDoesNotContainShare() {
}
@Test
- void getNetWorthShouldReturnCorrectSaleValue() {
- String symbol1 = "AAPL";
- String company1 = "Apple Inc.";
- BigDecimal price1 = new BigDecimal("100");
-
- Stock testStock1 = new Stock(symbol1,company1, price1);
-
- BigDecimal quantity1 = new BigDecimal("10");
- var purchasePrice1 = testStock1.getSalesPrice();
-
- Share testShare1 = new Share(testStock1, quantity1, purchasePrice1);
+ void getNetWorthShouldReturnMarketValue() {
- String symbol2 = "MSFT";
- String company2 = "Microsoft Corporation";
- BigDecimal price2 = new BigDecimal("87");
+ Stock stock = new Stock("AAPL", "Apple Inc.", new BigDecimal("100"));
- Stock testStock2 = new Stock(symbol2, company2, price2);
+ Share share = new Share(stock, new BigDecimal("10"), stock.getSalesPrice());
- BigDecimal quantity2 = new BigDecimal("3");
- var purchasePrice2 = testStock1.getSalesPrice();
-
- Share testShare2 = new Share(testStock2, quantity2, purchasePrice2);
-
- String symbol3 = "GOOG";
- String company3 = "Alphabet Inc.";
- BigDecimal price3 = new BigDecimal("75");
-
- Stock testStock3 = new Stock(symbol3, company3, price3);
-
- BigDecimal quantity3 = new BigDecimal("7");
- var purchasePrice3 = testStock3.getSalesPrice();
-
- Share testShare3 = new Share(testStock3, quantity3, purchasePrice3);
-
- Portfolio testPortfolio = new Portfolio();
-
- testPortfolio.addShare(testShare1);
- testPortfolio.addShare(testShare2);
- testPortfolio.addShare(testShare3);
+ Portfolio portfolio = new Portfolio();
+ portfolio.addShare(share);
- var calcWorth = BigDecimal.ZERO;
- calcWorth = calcWorth.add(new SaleCalculator(testShare1).calculateTotal());
- calcWorth = calcWorth.add(new SaleCalculator(testShare2).calculateTotal());
- calcWorth = calcWorth.add(new SaleCalculator(testShare3).calculateTotal());
+ BigDecimal expected = stock.getSalesPrice()
+ .multiply(share.getQuantity());
- assertEquals(0, testPortfolio.getNetWorth().compareTo(calcWorth),
- "Calculated net worth of the portfolio should equal the " +
- "som of all sales total values.");
+ assertEquals(0,
+ portfolio.getNetWorth().compareTo(expected),
+ "Net worth should equal market value of shares.");
}
@Test
From a7a3dc21cef904b2932aa7b4144c85d2dd1d77f3 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:14:47 +0200
Subject: [PATCH 056/142] Updated SaleCalculatorUnitTest.java
Renamed to SaleCalculatorTest to match name format.
---
.../{SaleCalculatorUnitTest.java => SaleCalculatorTest.java} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename millions/src/test/java/no/ntnu/gruppe53/model/{SaleCalculatorUnitTest.java => SaleCalculatorTest.java} (97%)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
similarity index 97%
rename from millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java
rename to millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
index 7c67885..d5cc6b1 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorUnitTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
@@ -11,7 +11,7 @@
//The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works
@DisplayName("Sale Calculator Test")
-public class SaleCalculatorUnitTest {
+public class SaleCalculatorTest {
private Stock testStock = new Stock("AAPL", "Apple", new BigDecimal(100));
From ff8774c3616f25057320277a4052786650bed3be Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:15:57 +0200
Subject: [PATCH 057/142] Updated SaleTest
AssertThrows for IllegalStateException.
---
.../test/java/no/ntnu/gruppe53/model/SaleTest.java | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
index 86a38c5..3343078 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleTest.java
@@ -140,7 +140,9 @@ void saleShouldNotBeCommittedIfShareDoesNotExistInPortfolio() {
var testSale = new Sale(testShare1, 1);
- testSale.commit(testPlayer);
+ assertThrows(IllegalStateException.class, () -> {
+ testSale.commit(testPlayer);
+ }, "Sale should not be possible to commit as the player does not own the share.");
assertEquals(0, startCash.compareTo(testPlayer.getMoney()),
"Money should remain unchanged.");
@@ -170,11 +172,10 @@ void saleShouldNotBeCommittedIfAlreadyCommited() {
testSale.commit(testPlayer1);
assertTrue(testSale.isCommitted(), "Sale should be committed.");
- testSale.commit(testPlayer2);
-
- System.out.println(testPlayer1.getMoney());
- assertEquals(0, new BigDecimal("1093").compareTo(testPlayer1.getMoney()),
+ assertThrows(IllegalStateException.class, () -> testSale.commit(testPlayer2),
+ "Sale should not be committed a second time.");
+ assertEquals(0, new BigDecimal("1090").compareTo(testPlayer1.getMoney()),
"Player 1 should have correct balance after sale.");
assertEquals(0,startCash.compareTo(testPlayer2.getMoney()),
"Money should not change for player 2.");
@@ -204,8 +205,9 @@ void saleShouldNotBePossibleToCommitTwice() {
testSale.commit(testPlayer);
assertTrue(testSale.isCommitted(), "Sale should be committed.");
BigDecimal cashAfterSale = testPlayer.getMoney();
- testSale.commit(testPlayer);
+ assertThrows(IllegalStateException.class, () -> testSale.commit(testPlayer),
+ "Sale should not be committed a second time.");
assertEquals(0, cashAfterSale.compareTo(testPlayer.getMoney()),
"Money should remain unchanged after second commit.");
assertTrue(testSale.isCommitted(), "Sale should still be committed.");
From 4687be87fa5811df62772f434e52afef4aee68af Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:17:42 +0200
Subject: [PATCH 058/142] Updated StockTest
Fixed test for priceChange
---
.../java/no/ntnu/gruppe53/model/StockTest.java | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
index 38c96cb..b2ae160 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/StockTest.java
@@ -225,7 +225,7 @@ void getLowestPriceShouldReturnTheLowestPrice() {
}
@Test
- void getLatestPriceChange() {
+ void getLatestPriceChangeShouldBeCorrect() {
String symbol = "AAPL";
String company = "Apple Inc.";
BigDecimal price1 = new BigDecimal("100");
@@ -239,8 +239,20 @@ void getLatestPriceChange() {
testStock.addNewSalesPrice(price4);
assertEquals(0, testStock.getLatestPriceChange().compareTo(
- price4.subtract(price3)),
+ price4.subtract(price3)),
"The price difference should be equal to the last value" +
" subtracted from the second last value (i.e. around '-25'.");
}
+
+ @Test
+ void getLatestPriceChangeForPriceHistoryLessThan2ShouldGiveZero() {
+ String symbol = "AAPL";
+ String company = "Apple Inc.";
+ BigDecimal price = new BigDecimal("100");
+
+ Stock testStock = new Stock(symbol,company, price);
+
+ assertEquals(BigDecimal.ZERO, testStock.getLatestPriceChange(),
+ "Price change is zero for only 1 registered price.");
+ }
}
\ No newline at end of file
From 73a7c6e3b366d7d86421e8958046f7edbcc69233 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:18:39 +0200
Subject: [PATCH 059/142] Updated TransactionArchiveTest
Added test for Observable.
---
.../no/ntnu/gruppe53/model/TransactionArchiveTest.java | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
index cb39777..663babe 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/TransactionArchiveTest.java
@@ -1,5 +1,6 @@
package no.ntnu.gruppe53.model;
+import javafx.collections.ObservableList;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
@@ -468,4 +469,12 @@ void countDistinctWeeksShouldNotCountDuplicateWeekNumbers() {
assertEquals(2, transactionArchive.countDistinctWeeks(),
"Returned size of HashSet should be 2.");
}
+
+ @Test
+ void getObservableTransactionsShouldBeObservable() {
+ var transactionArchive = new TransactionArchive();
+
+ assertInstanceOf(ObservableList.class, transactionArchive.getObservableTransactions(),
+ "Should be Observable.");
+ }
}
\ No newline at end of file
From 0158fc395409c860e8cb95c9447db15ad2931d26 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:21:08 +0200
Subject: [PATCH 060/142] Added FormatBigDecimal
Added helper class to format string of big decimals for JavaFX.
---
.../gruppe53/service/FormatBigDecimal.java | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
new file mode 100644
index 0000000..5db1ea5
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
@@ -0,0 +1,20 @@
+package no.ntnu.gruppe53.service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * Service/helper class that formats a {@link BigDecimal} to a string with 2 decimal accuracy.
+ */
+public class FormatBigDecimal {
+ /**
+ * Static method used to format a Big Decimal to a string representation with 2 decimals accuracy.
+ *
Returns "0.00" if the BigDecimal is null.
+ *
+ * @param number the BigDecimal to be formated
+ * @return the string representation of the BigDecimal with 2 decimals accuracy.
+ */
+ public static String formatNumber(BigDecimal number) {
+ return number == null ? "0.00" : number.setScale(2, RoundingMode.HALF_UP).toPlainString();
+ }
+}
From 3957248e72337120e653a5953534940ad5894a0f Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:23:26 +0200
Subject: [PATCH 061/142] Updated FormatBigDecimal
Placed file in test accidentally. Moved back to correct Service package in main.
---
.../java/no/ntnu/gruppe53/service/FormatBigDecimal.java | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename millions/src/{test => main}/java/no/ntnu/gruppe53/service/FormatBigDecimal.java (100%)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java b/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
similarity index 100%
rename from millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
rename to millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
From c425ba0e01d6df14f62382554ae50366e6eb9881 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:25:05 +0200
Subject: [PATCH 062/142] Added back FileHandler
---
.../no/ntnu/gruppe53/service/FileHandler.java | 142 ++++++++++++++++++
1 file changed, 142 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java
new file mode 100644
index 0000000..2aa8262
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java
@@ -0,0 +1,142 @@
+package no.ntnu.gruppe53.service;
+
+import no.ntnu.gruppe53.model.Stock;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles reading and writing of .csv files for stocks.
+ */
+public class FileHandler {
+
+
+ /**
+ * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding.
+ *
+ * @param stockList the list of stocks to be written
+ * @param path directory path for the file
+ * @param filename name of the file (without or with .csv extension)
+ * @throws IllegalArgumentException if stockList, path, or filename is null, or if stockList is empty
+ */
+ public static void writeStocksToFile(List stockList, String path, String filename) {
+ if (stockList == null) throw new IllegalArgumentException("StockList cannot be null");
+ if (path == null || path.isBlank()) throw new IllegalArgumentException("Path cannot be null or blank");
+ if (filename == null || filename.isBlank()) throw new IllegalArgumentException(
+ "Filename cannot be null or blank");
+
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
+
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ Path dirPath = filePath.getParent();
+ if (!Files.exists(dirPath)) {
+ try {
+ Files.createDirectories(dirPath);
+ } catch (IOException e) {
+ System.out.println("ERROR: Could not create directories.");
+ e.printStackTrace();
+ return;
+ }
+ }
+
+ try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
+ writer.write("#Ticker,Name,Price");
+ writer.newLine();
+
+ for (Stock stock : stockList) {
+ if (stock == null) continue;
+ String line = String.join(",",
+ stock.getSymbol(),
+ stock.getCompany(),
+ stock.getSalesPrice().toString());
+ writer.write(line);
+ writer.newLine();
+ }
+
+ } catch (IOException e) {
+ System.out.println("ERROR: Something went wrong while writing the CSV file.");
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Reads a CSV file and converts it to a list of {@link Stock} objects.
+ *
+ * @param path directory path of the CSV file
+ * @param filename CSV file name (with or without .csv extension)
+ * @return list of Stock objects
+ * @throws IllegalArgumentException if path or filename is null or blank
+ */
+ public static List readStocksFromFile(String path, String filename) {
+ List stocks = new ArrayList<>();
+ if (path == null || path.isBlank()) {
+ throw new IllegalArgumentException("Path cannot be null or blank.");
+ }
+ if (filename == null || filename.isBlank()) {
+ throw new IllegalArgumentException("Filename cannot be null or blank.");
+ }
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
+
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ if (!Files.exists(filePath)) {
+ System.out.println("ERROR: File not found: " + filePath.toAbsolutePath());
+ return stocks;
+ }
+
+ try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+
+ if (line.isBlank() || line.startsWith("#")) continue;
+
+ String[] values = line.split(",", -1);
+ if (values.length != 3) {
+ System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line);
+ continue;
+ }
+
+ String symbol = values[0].trim();
+ String name = values[1].trim();
+ BigDecimal price;
+
+ try {
+ price = new BigDecimal(values[2].trim());
+ } catch (NumberFormatException e) {
+ System.out.println("ERROR: Price is not a valid number: " + values[2]);
+ continue;
+ }
+
+ if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) {
+ System.out.println("ERROR: Invalid stock data, skipping: " + line);
+ continue;
+ }
+
+ stocks.add(new Stock(symbol, name, price));
+ }
+
+ } catch (IOException e) {
+ System.out.println("ERROR: Something went wrong while reading the CSV file.");
+ e.printStackTrace();
+ }
+
+ return stocks;
+ }
+}
\ No newline at end of file
From 281a20e9b294c9189697fc06a6a73c1ca70346e6 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:26:14 +0200
Subject: [PATCH 063/142] Added FormatBigDecimalTest
Tests for FormatBigDecimal.
---
.../service/FormatBigDecimalTest.java | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java
new file mode 100644
index 0000000..9997a01
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/FormatBigDecimalTest.java
@@ -0,0 +1,28 @@
+package no.ntnu.gruppe53.service;
+
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class FormatBigDecimalTest {
+ @Test
+ void shouldReturnZeroIfBigDecimalIsNull() {
+ assertEquals("0.00", FormatBigDecimal.formatNumber(null),
+ "Null should be formatted as '0.00'.");
+ }
+
+ @Test
+ void numberShouldOnlyHaveTwoDecimals() {
+ assertEquals("2.20", FormatBigDecimal.formatNumber(new BigDecimal("2.2000000000001")),
+ "Number should only have two decimals.");
+ }
+
+ @Test
+ void numberShouldRoundUp() {
+ assertEquals("32.20", FormatBigDecimal.formatNumber(new BigDecimal("32.1967")),
+ "Number should round up.");
+ }
+
+}
\ No newline at end of file
From fdadcafb2420402775edd3879dfbd7d416499220 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 10:26:33 +0200
Subject: [PATCH 064/142] Moved the refreshMarketList function to
gamecontroller
---
.../gruppe53/controller/GameController.java | 53 ++++++++++++++--
.../no/ntnu/gruppe53/views/MarketView.java | 63 ++-----------------
2 files changed, 54 insertions(+), 62 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index aeb0a10..de83e3f 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -1,9 +1,11 @@
package no.ntnu.gruppe53.controller;
import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import no.ntnu.gruppe53.*;
import no.ntnu.gruppe53.views.PortfolioView;
@@ -33,11 +35,11 @@ public void startNewGame() {
marketView = new MarketView();
- marketView.setExchange(exchange);
+ refreshMarketList();
portfolioView = new PortfolioView();
-
+ refreshPortfolioList();
@@ -93,7 +95,7 @@ private void initialize() {
portfolioView.onSellButton(() -> {
-
+ //sell actions
});
@@ -151,12 +153,16 @@ private void purchaseStock(String stockSymbol, int quantity) {
try {
exchange.buy(stockSymbol, BigDecimal.valueOf(quantity), player);
- marketView.refreshStocks();
+ refreshMarketList();
} catch (RuntimeException ex) {
System.out.println("Purchase failed: " + ex.getMessage());
}
}
+ /*
+ Need to figure out how to do sales, because of how the portfolio
+ groups shasres together how to sell is a bit difficult to figure out.
+ */
private void sellShares(int quantity) {
if (exchange == null || player == null || stocks == null || quantity == 0) {
return;
@@ -186,6 +192,17 @@ private Share getSelectedStock() {
+ /*
+ Populates the portfoliolist with the player owned shares.
+ However because of how shares are handled, populating the list
+ with the raw portfolio of the player, it would in practice be
+ populated by all transactions.
+ E.g. if you bought TSL shares twice in different transactions
+ the list would be populated by two different TSL shares.
+
+ Therefore, this functions groups the shares together before displaying
+ in the for loop of the function.
+ */
private void refreshPortfolioList() {
portfolioView.getPortfolioListView().getItems().clear();
@@ -212,6 +229,20 @@ private void refreshPortfolioList() {
portfolioView.getPortfolioListView().getItems().setAll(groupedShares.values());
}
+ public void refreshMarketList() {
+ if (exchange == null) {
+ marketView.getStocksListView().getItems().clear();
+ return;
+ }
+
+ List stocks = exchange.findStocks("");
+ marketView.getStocksListView().getItems().setAll(
+ stocks.stream()
+ .map(this::formatStock)
+ .collect(Collectors.toList())
+ );
+ }
+
private void refreshNavigationView() {
BigDecimal netWorth = player.getNetWorth();
BigDecimal money = player.getMoney();
@@ -223,4 +254,18 @@ private void refreshNavigationView() {
}
+
+ private String formatStock(Stock stock) {
+ return stock.getSymbol()
+ + " - "
+ + stock.getCompany()
+ + " | Price: $"
+ + formatMoney(stock.getSalesPrice());
+ }
+
+ private String formatMoney(BigDecimal money) {
+ return money == null ? "0.00" : money.setScale(2, RoundingMode.HALF_UP).toPlainString();
+ }
+
+
}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
index f3c9fbc..95305e3 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
@@ -209,53 +209,6 @@ public void onQuantitySelect(Runnable action) {
});
}
-
-
- /*
- public void onPurchaseButton(Runnable action) {
- purchaseButton.setOnAction(event -> {
- if (purchaseHandler == null) {
- return;
- }
-
- String selectedStock = getSelectedSymbol();
- if (selectedStock == null) {
- return;
- }
-
- int quantity = quantitySpinner.getValue();
- purchaseHandler.accept(selectedStock, quantity);
- });
- }*/
-
- public void setPurchaseHandler(BiConsumer purchaseHandler) {
- this.purchaseHandler = purchaseHandler;
- }
-
-
-
-
- public void setExchange(Exchange exchange) {
- this.exchange = exchange;
- refreshStocks();
- }
-
-
-
- public void refreshStocks() {
- if (exchange == null) {
- stocksListView.getItems().clear();
- return;
- }
-
- List stocks = exchange.findStocks("");
- stocksListView.getItems().setAll(
- stocks.stream()
- .map(this::formatStock)
- .collect(Collectors.toList())
- );
- }
-
public String getSelectedSymbol() {
String selected = stocksListView.getSelectionModel().getSelectedItem();
if (selected == null || selected.isBlank()) {
@@ -272,23 +225,17 @@ public int getQuantityPurchase() {
return quantitySpinner.getValue();
}
-
+ public ListView getStocksListView() {
+ return stocksListView;
+ }
public void setCalculation() {
}
- private String formatStock(Stock stock) {
- return stock.getSymbol()
- + " - "
- + stock.getCompany()
- + " | Price: $"
- + formatMoney(stock.getSalesPrice());
- }
- private String formatMoney(BigDecimal money) {
- return money == null ? "0.00" : money.setScale(2, RoundingMode.HALF_UP).toPlainString();
- }
+
+
public NavigationView getNavigationView() {
return navigationView;
From d9f247ca4105b948d199015e7827b5373817b4d1 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:28:27 +0200
Subject: [PATCH 065/142] Deleted extra FileHandler in view.
---
.../no/ntnu/gruppe53/view/FileHandler.java | 141 ------------------
1 file changed, 141 deletions(-)
delete mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/FileHandler.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/view/FileHandler.java
deleted file mode 100644
index f1d9ea2..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/view/FileHandler.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package no.ntnu.gruppe53.view;
-
-import no.ntnu.gruppe53.model.Stock;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Handles reading and writing of .csv files for stocks.
- */
-public class FileHandler {
-
-
- /**
- * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding.
- *
- * @param stockList the list of stocks to be written
- * @param path directory path for the file
- * @param filename name of the file (without or with .csv extension)
- * @throws IllegalArgumentException if stockList, path, or filename is null, or if stockList is empty
- */
- public static void writeStocksToFile(List stockList, String path, String filename) {
- if (stockList == null) throw new IllegalArgumentException("StockList cannot be null");
- if (path == null || path.isBlank()) throw new IllegalArgumentException("Path cannot be null or blank");
- if (filename == null || filename.isBlank()) throw new IllegalArgumentException(
- "Filename cannot be null or blank");
-
-
- path = path.trim();
- filename = filename.trim();
- String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
-
- Path filePath = Paths.get(path, filenameWithExtension);
-
- Path dirPath = filePath.getParent();
- if (!Files.exists(dirPath)) {
- try {
- Files.createDirectories(dirPath);
- } catch (IOException e) {
- System.out.println("ERROR: Could not create directories.");
- e.printStackTrace();
- return;
- }
- }
-
- try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
- writer.write("#Ticker,Name,Price");
- writer.newLine();
-
- for (Stock stock : stockList) {
- if (stock == null) continue;
- String line = String.join(",",
- stock.getSymbol(),
- stock.getCompany(),
- stock.getSalesPrice().toString());
- writer.write(line);
- writer.newLine();
- }
-
- } catch (IOException e) {
- System.out.println("ERROR: Something went wrong while writing the CSV file.");
- e.printStackTrace();
- }
- }
-
- /**
- * Reads a CSV file and converts it to a list of {@link Stock} objects.
- *
- * @param path directory path of the CSV file
- * @param filename CSV file name (with or without .csv extension)
- * @return list of Stock objects
- */
- public static List readStocksFromFile(String path, String filename) {
- List stocks = new ArrayList<>();
- if (path == null || path.isBlank()) {
- throw new IllegalArgumentException("Path cannot be null or blank.");
- }
- if (filename == null || filename.isBlank()) {
- throw new IllegalArgumentException("Filename cannot be null or blank.");
- }
-
- path = path.trim();
- filename = filename.trim();
- String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
-
- Path filePath = Paths.get(path, filenameWithExtension);
-
- if (!Files.exists(filePath)) {
- System.out.println("ERROR: File not found: " + filePath.toAbsolutePath());
- return stocks;
- }
-
- try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
- String line;
-
- while ((line = reader.readLine()) != null) {
- line = line.trim();
-
- if (line.isBlank() || line.startsWith("#")) continue;
-
- String[] values = line.split(",", -1);
- if (values.length != 3) {
- System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line);
- continue;
- }
-
- String symbol = values[0].trim();
- String name = values[1].trim();
- BigDecimal price;
-
- try {
- price = new BigDecimal(values[2].trim());
- } catch (NumberFormatException e) {
- System.out.println("ERROR: Price is not a valid number: " + values[2]);
- continue;
- }
-
- if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) {
- System.out.println("ERROR: Invalid stock data, skipping: " + line);
- continue;
- }
-
- stocks.add(new Stock(symbol, name, price));
- }
-
- } catch (IOException e) {
- System.out.println("ERROR: Something went wrong while reading the CSV file.");
- e.printStackTrace();
- }
-
- return stocks;
- }
-}
\ No newline at end of file
From 8c540b037e486823bdc63708521a1cf9db7e9412 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:29:07 +0200
Subject: [PATCH 066/142] Added StockLineChartService
Added helper class for getting X and Y data for stock line chart.
---
.../service/StockLineChartService.java | 72 +++++++++++++++++++
1 file changed, 72 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java
new file mode 100644
index 0000000..28076e3
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java
@@ -0,0 +1,72 @@
+package no.ntnu.gruppe53.service;
+
+import javafx.scene.chart.XYChart;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.view.StockLineChartView;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Builds the {@link XYChart.Series} values for the {@link StockLineChartView}.
+ */
+
+public class StockLineChartService {
+
+ /**
+ * Builds the {@code XYChart.Series} for the {@code LineChart} of a single {@link Stock}.
+ *
Returns the {@code XYChart.Series}.
+ *
+ * @param stock the stock whose price values will be shown in the LineChart
+ * @param exchange the exchange containing the stock whose week number will be shown in the LineChart
+ * @return the {@code XYChart.Series} with the y-values from the price history of the stock,
+ * and the x-values from the week number of the exchange
+ * @throws IllegalArgumentException if there are more x-values than y-values or vice versa
+ */
+
+ public static XYChart.Series buildSeries(Stock stock, Exchange exchange) {
+ XYChart.Series series = new XYChart.Series<>();
+ series.setName(stock.getSymbol());
+
+ // Check for 1-1 ratio of price and week list size for plotting
+ if (stock.getHistoricalPrices().size() != exchange.getWeek()) {
+ throw new IllegalArgumentException("Invalid Data: mismatch between prices and weeks. " +
+ "Should have 1-1 ratio.");
+ }
+
+ int week = 1;
+
+ for (BigDecimal price : stock.getHistoricalPrices()) {
+ series.getData().add(
+ new XYChart.Data<>(week, price.doubleValue()));
+ week++;
+ }
+
+ return series;
+ }
+
+ /**
+ * Builds the {@code XYChart.Series} for the {@code LineChart} of a list of {@code Stock}s.
+ *
Returns a list of the {@code XYChart.Series} for each stock.
+ *
+ * @param stocks the list of stocks whose price values will be shown in the LineChart
+ * @param exchange the exchange containing the stock whose week number will be shown in the LineChart
+ * @return the list of {@code XYChart.Series} with the y-values from the price history of the stock,
+ * and the x-values from the week number of the exchange
+ */
+
+ public static List> buildSeries(List stocks, Exchange exchange) {
+ List> seriesList = new ArrayList<>();
+
+ for (Stock stock : stocks) {
+ XYChart.Series series =
+ buildSeries(stock, exchange);
+
+ seriesList.add(series);
+ }
+
+ return seriesList;
+ }
+}
\ No newline at end of file
From 8e6a304ab6f902aeff5a978c081248a308bfc3b9 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:30:17 +0200
Subject: [PATCH 067/142] Added StockLineChartServiceTest
Unit tests for StockLineChartService.
---
.../service/StockLineChartServiceTest.java | 182 ++++++++++++++++++
1 file changed, 182 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java
new file mode 100644
index 0000000..9c94dad
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/StockLineChartServiceTest.java
@@ -0,0 +1,182 @@
+package no.ntnu.gruppe53.service;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Stock;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class StockLineChartServiceTest {
+ private Stock appleStock;
+
+ private Stock microStock;
+
+ private Stock ntnuStock;
+
+ private ArrayList stocks;
+
+ private Exchange exchange;
+
+ @BeforeEach
+ void setUp() {
+ appleStock = new Stock("AAPL", "Apple", new BigDecimal("100"));
+ microStock = new Stock("MSFT", "Microsoft", new BigDecimal("50"));
+ ntnuStock = new Stock("NTNU", "Ntnu", new BigDecimal("1000"));
+
+ stocks = new ArrayList<>();
+ stocks.add(appleStock);
+ stocks.add(microStock);
+ stocks.add(ntnuStock);
+
+ exchange = new Exchange("Nasdaq", stocks);
+ }
+
+ @Test
+ void shouldThrowExceptionIfPriceAndWeekIsNotOneToOne() {
+ exchange.advance();
+
+ appleStock.addNewSalesPrice(new BigDecimal("50"));
+
+ assertThrows(IllegalArgumentException.class, () ->
+ StockLineChartService.buildSeries(appleStock, exchange),
+ "Stock should not be allowed to have more values than weeks in exchange for plotting chart.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectWeekForOneStock() {
+ for (int i = 0; i < 9; i++) {
+ exchange.advance();
+ }
+
+ var series = StockLineChartService.buildSeries(appleStock, exchange);
+ double week = series.getData().stream().mapToDouble(data -> data.getXValue()
+ .doubleValue()).max().orElse(0);
+
+
+ assertEquals(exchange.getWeek(), week, "Max week number should be correct.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectWeekForMultipleStocks() {
+ for (int i = 0; i < 4; i++) {
+ exchange.advance();
+ }
+
+ var series = StockLineChartService.buildSeries(stocks, exchange);
+ double week = exchange.getWeek();
+ double weekFirstStock = series.getFirst().getData().stream()
+ .mapToDouble(data -> data.getXValue().doubleValue())
+ .max().orElse(0);
+ double weekSecondStock = series.get(1).getData().stream()
+ .mapToDouble(data -> data.getXValue().doubleValue())
+ .max().orElse(0);
+ double weekLastStock = series.getLast().getData().stream()
+ .mapToDouble(data -> data.getXValue().doubleValue())
+ .max().orElse(0);
+
+ assertEquals(week, weekFirstStock, "Week value for first stock in series should be correct.");
+ assertEquals(week, weekSecondStock, "Week value for second stock in series should be correct.");
+ assertEquals(week, weekLastStock, "Week value for last stock in series should be correct.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectPricesForOneStock() {
+ exchange.advance();
+
+ var series = StockLineChartService.buildSeries(appleStock, exchange);
+
+ double firstPrice = appleStock.getHistoricalPrices().getFirst().doubleValue();
+ double secondPrice = exchange.getStock(appleStock.getSymbol()).getSalesPrice().doubleValue();
+ double firstPriceSeries = series.getData().stream().mapToDouble(
+ data -> data.getYValue().doubleValue()).findFirst().orElse(0);
+ double secondPriceSeries = series.getData().stream().mapToDouble(
+ data -> data.getYValue().doubleValue()).skip(1).
+ findFirst().orElse(0);
+
+ assertEquals(firstPrice, firstPriceSeries, "First price should be the same.");
+ assertEquals(secondPrice, secondPriceSeries, "Second price should be the same.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectPricesForMultipleStocks() {
+ exchange.advance();
+
+ var series = StockLineChartService.buildSeries(stocks, exchange);
+
+ // First stock prices
+ double firstPriceFirstStock = exchange.getStock(appleStock.getSymbol())
+ .getHistoricalPrices().getFirst().doubleValue();
+ double firstPriceFirstStockSeries = series.getFirst().getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .findFirst().orElse(0);
+ double lastPriceFirstStock = exchange.getStock(appleStock.getSymbol()).getSalesPrice().doubleValue();
+ double lastPriceFirstStockSeries =series.getFirst().getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .skip(1).findFirst().orElse(0);
+
+ // Second stock prices
+ double firstPriceSecondStock = exchange.getStock(microStock.getSymbol())
+ .getHistoricalPrices().getFirst().doubleValue();
+ double firstPriceSecondStockSeries = series.get(1).getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .findFirst().orElse(0);
+ double lastPriceSecondStock = exchange.getStock(microStock.getSymbol()).getSalesPrice().doubleValue();
+ double lastPriceSecondStockSeries =series.get(1).getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .skip(1).findFirst().orElse(0);
+
+ // Last stock prices
+ double firstPriceLastStock = exchange.getStock(ntnuStock.getSymbol())
+ .getHistoricalPrices().getFirst().doubleValue();
+ double firstPriceLastStockSeries = series.getLast().getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .findFirst().orElse(0);
+ double lastPriceLastStock = exchange.getStock(ntnuStock.getSymbol()).getSalesPrice().doubleValue();
+ double lastPriceLastStockSeries =series.getLast().getData().stream()
+ .mapToDouble(data -> data.getYValue().doubleValue())
+ .skip(1).findFirst().orElse(0);
+
+ assertEquals(firstPriceFirstStock, firstPriceFirstStockSeries,
+ "First price for first stock should be correct.");
+ assertEquals(lastPriceFirstStock, lastPriceFirstStockSeries,
+ "Last price for first stock should be correct.");
+ assertEquals(firstPriceSecondStock, firstPriceSecondStockSeries,
+ "First price for second stock should be correct.");
+ assertEquals(lastPriceSecondStock, lastPriceSecondStockSeries,
+ "Last price for second stock should be correct.");
+ assertEquals(firstPriceLastStock, firstPriceLastStockSeries,
+ "First price for last stock should be correct.");
+ assertEquals(lastPriceLastStock, lastPriceLastStockSeries,
+ "Last price for last stock should be correct.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectSymbolForOneStock() {
+ var series = StockLineChartService.buildSeries(appleStock, exchange);
+
+ String name = appleStock.getSymbol();
+ String nameSeries = series.getName();
+
+ assertEquals(name, nameSeries, "Series should have correct stock symbol.");
+ }
+
+ @Test
+ void buildSeriesShouldHaveCorrectSymbolForMultipleStocks() {
+ var series = StockLineChartService.buildSeries(stocks, exchange);
+
+ String firstStockName = appleStock.getSymbol();
+ String firstStockNameSeries = series.getFirst().getName();
+ String secondStockName = microStock.getSymbol();
+ String secondStockNameSeries = series.get(1).getName();
+ String lastStockName = ntnuStock.getSymbol();
+ String lastStockNameSeries = series.getLast().getName();
+
+ assertEquals(firstStockName, firstStockNameSeries, "First stock name should be correct.");
+ assertEquals(secondStockName, secondStockNameSeries, "Second stock name should be correct.");
+ assertEquals(lastStockName, lastStockNameSeries, "Last stock name should be correct.");
+ }
+}
\ No newline at end of file
From bfab3989523c45fd358d263a453f6620b9fd68cd Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:31:52 +0200
Subject: [PATCH 068/142] Added StockLineChartView
Represents a graph for stock price - time data.
---
.../gruppe53/view/StockLineChartView.java | 126 ++++++++++++++++++
1 file changed, 126 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java
new file mode 100644
index 0000000..adb5dc5
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java
@@ -0,0 +1,126 @@
+package no.ntnu.gruppe53.view;
+import javafx.geometry.Side;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart;
+import no.ntnu.gruppe53.model.Stock;
+
+import java.util.List;
+
+/**
+ * Represents a {@link LineChart} view of a {@link Stock}.
+ *
Y-axis: Price as a {@code double} value. Represented by a {@link NumberAxis}
+ *
X-axis: Week as an {@code int} value. Represented by a {@code NumberAxis}
+ */
+
+public class StockLineChartView {
+ private final NumberAxis xAxis = new NumberAxis();
+ private final NumberAxis yAxis = new NumberAxis();
+
+ private final LineChart chart;
+
+ /**
+ * Sets the settings for the {@code LineChart}.
+ *
Sets autoscale, stepsize and manual values for the range x- and y-axis, as well
+ * as the labels.
+ *
Initializes the LineChart.
+ */
+
+ public StockLineChartView() {
+ xAxis.setAutoRanging(false);
+ yAxis.setAutoRanging(true);
+ yAxis.setForceZeroInRange(false);
+
+ xAxis.setLowerBound(1);
+
+ xAxis.setLabel("Week");
+ yAxis.setLabel("Price");
+
+ chart = new LineChart<>(xAxis, yAxis);
+ }
+
+ /**
+ * Returns the constructed LineChart.
+ *
+ * @return the current LineChart
+ */
+ public LineChart getChart() {
+ return chart;
+ }
+
+ /**
+ * Calculates the max x-axis value for a {@link XYChart.Series}.
+ *
+ * @param series the XYChart series to determine max x-axis value from
+ * @return the maximum x-axis value in the XYChart.Series
+ */
+
+ private double calculateXAxisMax(XYChart.Series series) {
+ return series.getData().stream()
+ .mapToDouble(data -> data.getXValue()
+ .doubleValue())
+ .max().
+ orElse(0);
+ }
+
+ /**
+ * Calculates the max x-axis value for a {@code List}.
+ *
+ * @param seriesList the list of XYChart.Series to determine max x-axis value from
+ * @return the maximum x-axis value in the List of XYChart.Series
+ */
+
+ private double calculateXAxisMax(List> seriesList) {
+ double xMax = 0;
+
+ for (XYChart.Series s : seriesList) {
+ double tempMax = calculateXAxisMax(s);
+ if (tempMax > xMax) {
+ xMax = tempMax;
+ }
+ }
+
+ return xMax;
+ }
+
+ /**
+ * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}.
+ *
+ * @param series the XYChart.Series containing the x- and y-values for the LineChart
+ * @param title the name of the company the stock represents
+ */
+
+ public void setSeries(XYChart.Series series, String title) {
+ chart.getData().clear();
+
+ chart.setTitle(title);
+ chart.setLegendSide(Side.RIGHT);
+
+ double xMax = calculateXAxisMax(series);
+ xAxis.setUpperBound(xMax);
+
+ chart.getData().add(series);
+ }
+
+ /**
+ * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}.
+ *
Here the series values are given as a list of {@code XYChart.Series} objects.
+ *
+ * @param seriesList the list of XYChart.Series containing the x- and y-values for each stock
+ */
+
+ public void setSeries(List> seriesList) {
+ chart.getData().clear();
+
+ chart.setTitle("Price History");
+ chart.setLegendSide(Side.RIGHT);
+
+ double xMax = calculateXAxisMax(seriesList);
+
+ if (xMax != 0) {
+ xAxis.setUpperBound(xMax);
+ }
+
+ chart.getData().addAll(seriesList);
+ }
+}
\ No newline at end of file
From 02e4b1f442b359b9b2cadeff123525ea3e0f8008 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:33:22 +0200
Subject: [PATCH 069/142] Added CSVChooserView
Handles choosing a .csv file.
---
.../no/ntnu/gruppe53/view/CSVChooserView.java | 49 +++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
new file mode 100644
index 0000000..092e53f
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
@@ -0,0 +1,49 @@
+package no.ntnu.gruppe53.view;
+
+import javafx.scene.control.Alert;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+
+import java.io.File;
+
+/**
+ * Represents an {@link Alert} and {@link FileChooser} for informing user of .csv requirement and picking said file.
+ */
+public class CSVChooserView {
+ private final FileChooser fileChooser = new FileChooser();
+ Alert newGameAlert = new Alert(Alert.AlertType.INFORMATION);
+
+ /**
+ * Returns the dialog for the alert and file chooser and exposes it to controllers.
+ *
+ * @param stage the stage for the dialog to be shown
+ * @return the dialog for the alert and file chooser
+ */
+ public File getDialog(Stage stage) {
+ // Notifies the user about .csv selection
+ newGameAlert.setTitle("Please read:");
+ newGameAlert.setHeaderText("CSV File Required");
+ newGameAlert.setContentText("Please select a .csv file of the stocks to be used for the exchange " +
+ " in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used." +
+ "\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble." +
+ "\n\nExample: NVDA,Nvidia,191.27" + "\n\n'#' signifies comments, and along with lines not formatted " +
+ "correctly, will be ignored.");
+
+ // Sets the dimensions of the alert to show all text, and as a band-aid, be resizable
+ newGameAlert.setResizable(true);
+
+ newGameAlert.getDialogPane().setPrefWidth(450);
+ newGameAlert.getDialogPane().setPrefHeight(350);
+
+ newGameAlert.showAndWait();
+
+ // File chooser to choose .csv file
+ fileChooser.setTitle("Select CSV File");
+
+ // Limits to .csv file only
+ fileChooser.getExtensionFilters().add(
+ new FileChooser.ExtensionFilter("CSV Files", "*.csv")
+ );
+
+ return fileChooser.showOpenDialog(stage); }
+}
\ No newline at end of file
From f3dcc57c4d708c0a047309c2ac08edd00f410caf Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:34:17 +0200
Subject: [PATCH 070/142] Added FooterBar
Represents the footer of main view.
---
.../java/no/ntnu/gruppe53/view/FooterBar.java | 75 +++++++++++++++++++
1 file changed, 75 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
new file mode 100644
index 0000000..8c1728d
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
@@ -0,0 +1,75 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.RoundingMode;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.controller.GameController;
+import no.ntnu.gruppe53.model.Exchange;
+
+/**
+ * A {@link HBox} view representing the footer/bottom to be used in a {@link BorderPane} layout.
+ */
+public class FooterBar extends HBox {
+ private final Label netWorthLabel;
+ private final Button advanceWeekButton;
+ private Subscription netWorthSubscription;
+
+ /**
+ * Constructs the layout of the view and its components.
+ */
+ public FooterBar() {
+ this.setSpacing(10);
+ this.setPadding(new Insets(15, 12, 15, 12));
+ this.setStyle("-fx-background-color: #336699;");
+
+ netWorthLabel = new Label("Net Worth: $0.00");
+ netWorthLabel.setStyle("-fx-text-fill: #FFFFFF;");
+
+ advanceWeekButton = new Button("Advance Week");
+
+ // Spacing between buttons
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ this.getChildren().addAll(netWorthLabel, spacer, advanceWeekButton);
+ }
+
+ /**
+ * Binds a subscription/observer to a {@link Player}.
+ *
It specifically listens to the players money and net worth.
+ *
+ * @param player the player object to be observed
+ */
+ public void bindPlayer(Player player) {
+ if (netWorthSubscription != null) {
+ netWorthSubscription.unsubscribe();
+ }
+
+ if (player != null) {
+ netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ netWorthLabel.setText("Net Worth: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ netWorthLabel.setText("Net Worth: $0.00");
+ }
+ });
+ }
+ }
+
+ /**
+ * Exposes the advanceWeekButton to {@link GameController} so it can
+ * advance the week of the {@link Exchange}.
+ *
+ * @param action the action to be performed when clicking the button
+ */
+ public void onAdvanceButtonClick(Runnable action) {
+ advanceWeekButton.setOnAction(e -> action.run());
+ }
+}
From 790e77b7328beba232eb565092c548c5a7158964 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:35:14 +0200
Subject: [PATCH 071/142] Added NavigationBar
Represents the top bar of the main view.
---
.../no/ntnu/gruppe53/view/NavigationBar.java | 61 +++++++++++++++++++
1 file changed, 61 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
new file mode 100644
index 0000000..436a44b
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
@@ -0,0 +1,61 @@
+package no.ntnu.gruppe53.view;
+
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.layout.HBox;
+
+/**
+ * Represents the top bar of the views.
+ *
Provides the user with the ability to switch between views on the fly by
+ * clicking on the buttons representing the desired view.
+ *
The {@link no.ntnu.gruppe53.controller.GameController} handles
+ * the actions for the buttons.
+ */
+public class NavigationBar extends HBox {
+
+ private final Button marketButton;
+ private final Button portfolioButton;
+ private final Button historyButton;
+
+ /**
+ * Constructs the navigation bar and its components.
+ */
+ public NavigationBar() {
+ this.setSpacing(10);
+ this.setPadding(new Insets(15, 12, 15, 12));
+ this.setStyle("-fx-background-color: #336699;");
+
+ this.marketButton = new Button("Market");
+ this.portfolioButton = new Button("Portfolio");
+ this.historyButton = new Button("History");
+
+ this.getChildren().addAll(marketButton, portfolioButton, historyButton);
+ }
+
+ /**
+ * Exposes the marketButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onMarketButtonClick(Runnable action) {
+ marketButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the portfolioButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onPortfolioButtonClick(Runnable action) {
+ portfolioButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the historyButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onHistoryButtonClick(Runnable action) {
+ historyButton.setOnAction(e -> action.run());
+ }
+}
From 98fa0333d286a8b247f1d9fe21fb818135db5671 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:36:18 +0200
Subject: [PATCH 072/142] Added PlayerNameChooserView
Lets a player choose their name.
---
.../gruppe53/view/PlayerNameChooserView.java | 97 +++++++++++++++++++
1 file changed, 97 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
new file mode 100644
index 0000000..b844df9
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
@@ -0,0 +1,97 @@
+package no.ntnu.gruppe53.view;
+
+import no.ntnu.gruppe53.model.Player;
+
+import javafx.geometry.Insets;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+
+import java.util.Optional;
+
+/**
+ * Represents a prompt for the {@link Player}'s name
+ */
+public class PlayerNameChooserView {
+ Dialog dialog = new Dialog<>();
+
+ /**
+ * Returns the dialog where the player can input a name of maximum 50 characters.
+ *
A {@link TextFormatter} is used to set the maximum amount
+ * of characters for the player name.
+ *
+ * @return the dialog for player name input
+ */
+ public Optional getDialog() {
+ // Sets dialog title and header text
+ dialog.setTitle("Player Name");
+ dialog.setHeaderText(null);
+
+ // Sets the dimensions for the dialog pane
+ dialog.setResizable(true);
+
+ dialog.getDialogPane().setPrefWidth(320);
+ dialog.getDialogPane().setPrefHeight(50);
+
+ ButtonType confirmButton =
+ new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
+
+ dialog.getDialogPane().getButtonTypes().add(confirmButton);
+
+ TextField nameField = new TextField();
+
+ nameField.setEditable(true);
+
+ int maxLength = 50;
+
+ // Prevents player name over 50 characters
+
+ TextFormatter formatter = new TextFormatter<>(change -> {
+
+ String newText = change.getControlNewText();
+
+ if (newText.length() <= maxLength) {
+ return change;
+ }
+
+ return null;
+
+ });
+
+ nameField.setTextFormatter(formatter);
+
+ // Adds a counter for max characters
+ Label counterLabel = new Label("0 / 50");
+
+ nameField.textProperty().addListener((obs, oldVal, newVal) -> {
+ counterLabel.setText(newVal.length() + " / 50");
+ });
+
+ Label infoLabel = new Label(
+ "Please enter your player name:"
+ );
+
+ VBox layout = new VBox(15);
+
+ HBox hBox = new HBox(15);
+
+ layout.setPadding(new Insets(10));
+
+ hBox.getChildren().addAll(nameField, counterLabel);
+
+ layout.getChildren().addAll(infoLabel, hBox);
+
+ dialog.getDialogPane().setContent(layout);
+
+ dialog.setResultConverter(button -> {
+ if (button == confirmButton) {
+
+ return nameField.getText();
+ }
+
+ return null;
+ });
+
+ return dialog.showAndWait();
+ }
+}
\ No newline at end of file
From 9b668a74dc3c9fe2be8365ee400febba29fbad2a Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:37:17 +0200
Subject: [PATCH 073/142] Added PlayerStartingMoneyChooserView
Lets the player choose their starting money.
---
.../view/PlayerStartingMoneyChooserView.java | 105 ++++++++++++++++++
1 file changed, 105 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
new file mode 100644
index 0000000..82b0c50
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
@@ -0,0 +1,105 @@
+package no.ntnu.gruppe53.view;
+
+import no.ntnu.gruppe53.model.Player;
+
+import javafx.geometry.Insets;
+import javafx.scene.control.*;
+import javafx.scene.layout.VBox;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+/**
+ * Represents the prompt for the {@link Player}'s starting money.
+ */
+public class PlayerStartingMoneyChooserView {
+ Dialog dialog = new Dialog<>();
+
+ /**
+ * Returns the dialog for the player's starting money.
+ *
Uses a {@link TextFormatter} and listener for value integrity.
+ *
Uses a {@link SpinnerValueFactory} to set min, max, default,
+ * and step size values for the spinner.
+ * @return the dialog for player's starting money
+ */
+ public Optional getDialog() {
+
+ // Sets dialog title and header text
+ dialog.setTitle("Select starting money:");
+ dialog.setHeaderText(null);
+
+ // Sets the dimensions for the dialog pane
+ dialog.setResizable(true);
+
+ dialog.getDialogPane().setPrefWidth(320);
+ dialog.getDialogPane().setPrefHeight(50);
+
+ ButtonType confirmButton =
+ new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
+
+ dialog.getDialogPane().getButtonTypes().add(confirmButton);
+
+ Spinner spinner = new Spinner<>();
+
+ // Sets allowed values and step size for the spinner
+ SpinnerValueFactory.DoubleSpinnerValueFactory valueFactory =
+ new SpinnerValueFactory.DoubleSpinnerValueFactory(
+ 1000.0, // min
+ 1000000.0, // max
+ 5000.0, // initial value
+ 100.0 // step-size
+ );
+
+ spinner.setValueFactory(valueFactory);
+
+ spinner.setEditable(true);
+
+ // Formats the text to only allow numbers, and doubles preceded by a number
+ TextFormatter formatter = new TextFormatter<>(change -> {
+
+ String newText = change.getControlNewText();
+
+ if (newText.matches("\\d+(\\.\\d*)?")) {
+
+ return change;
+ }
+
+ return null;
+ });
+
+ spinner.focusedProperty().addListener((obs, oldVal, newVal) -> {
+
+ if (!newVal) {
+ spinner.increment(0);
+ }
+ });
+
+ spinner.getEditor().setTextFormatter(formatter);
+
+ spinner.setPrefWidth(200);
+
+ Label infoLabel = new Label(
+ "Select your starting money balance for trading:"
+ );
+
+ VBox layout = new VBox(15);
+
+ layout.setPadding(new Insets(10));
+
+ layout.getChildren().addAll(infoLabel, spinner);
+
+ dialog.getDialogPane().setContent(layout);
+
+ dialog.setResultConverter(button -> {
+
+ if (button == confirmButton) {
+
+ return BigDecimal.valueOf(spinner.getValue());
+ }
+
+ return null;
+ });
+
+ return dialog.showAndWait();
+ }
+}
\ No newline at end of file
From 0a2df656a787494eddbe25e378f97b54c6e61e0b Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 10:39:29 +0200
Subject: [PATCH 074/142] Redefined how formating money worked
---
.../gruppe53/controller/GameController.java | 28 +++++++++++--------
.../no/ntnu/gruppe53/views/MarketView.java | 12 ++------
2 files changed, 19 insertions(+), 21 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index de83e3f..dd9ffce 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -87,10 +87,10 @@ private void initialize() {
portfolioView.setSelectedShare(newValue);
portfolioView.setQuantitySpinner(newValue.getQuantity().intValue());
- portfolioView.setGross(saleCalculator.calculateGross().toString());
- portfolioView.setCommission(saleCalculator.calculateCommission().toString());
- portfolioView.setTax(saleCalculator.calculateTax().toString());
- portfolioView.setTotal(saleCalculator.calculateTotal().toString());
+ portfolioView.setGross(formatMoney(saleCalculator.calculateGross()));
+ portfolioView.setCommission(formatMoney(saleCalculator.calculateCommission()));
+ portfolioView.setTax(formatMoney(saleCalculator.calculateTax()));
+ portfolioView.setTotal(formatMoney(saleCalculator.calculateTotal()));
});
portfolioView.onSellButton(() -> {
@@ -121,9 +121,9 @@ private void initialize() {
purchaseCalculator = new PurchaseCalculator(getSelectedStock());
marketView.selectedStock.setText(newValue);
- marketView.gross.setText(purchaseCalculator.calculateGross().toString());
- marketView.commission.setText(purchaseCalculator.calculateCommission().toString());
- marketView.total.setText(purchaseCalculator.calculateTotal().toString());
+ marketView.gross.setText(formatMoney(purchaseCalculator.calculateGross()));
+ marketView.commission.setText(formatMoney(purchaseCalculator.calculateCommission()));
+ marketView.total.setText(formatMoney(purchaseCalculator.calculateTotal()));
});
marketView.onPurchaseButton(() -> {
@@ -135,9 +135,9 @@ private void initialize() {
//if no share selected need to be implemented
purchaseCalculator = new PurchaseCalculator(getSelectedStock());
- marketView.gross.setText(purchaseCalculator.calculateGross().toString());
- marketView.commission.setText(purchaseCalculator.calculateCommission().toString());
- marketView.total.setText(purchaseCalculator.calculateTotal().toString());
+ marketView.gross.setText(formatMoney(purchaseCalculator.calculateGross()));
+ marketView.commission.setText(formatMoney(purchaseCalculator.calculateCommission()));
+ marketView.total.setText(formatMoney(purchaseCalculator.calculateTotal()));
});
@@ -259,12 +259,16 @@ private String formatStock(Stock stock) {
return stock.getSymbol()
+ " - "
+ stock.getCompany()
- + " | Price: $"
+ + " | Price: "
+ formatMoney(stock.getSalesPrice());
}
private String formatMoney(BigDecimal money) {
- return money == null ? "0.00" : money.setScale(2, RoundingMode.HALF_UP).toPlainString();
+
+
+
+
+ return money == null ? "0.00" : "$" + money.abs().setScale(2, RoundingMode.HALF_UP).toPlainString();
}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
index 95305e3..3f4665e 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/views/MarketView.java
@@ -53,14 +53,6 @@ public class MarketView extends BorderPane {
//Input
private Spinner quantitySpinner;
- //Models
- private Exchange exchange;
- private Player player;
-
- // Handlers
- private BiConsumer purchaseHandler;
- private Runnable showPortfolioHandler;
-
public MarketView() {
@@ -76,7 +68,9 @@ public MarketView() {
}
-
+ /*
+ Styling of the center page
+ */
public VBox centerPane() {
VBox vBox = new VBox();
From 47311b328349bc77ebbaab296847e0251aaa657b Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:40:21 +0200
Subject: [PATCH 075/142] Updated MarketView
Added graph view to stocks.
Made the view use the observer pattern.
Added more info to formatStock.
Sorts stockList alphabetically on ticker symbol.
Added JavaDoc.
---
.../no/ntnu/gruppe53/view/MarketView.java | 316 +++++++++---------
1 file changed, 164 insertions(+), 152 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index 959ffe6..ccc7b3a 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -1,88 +1,60 @@
package no.ntnu.gruppe53.view;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.List;
-import java.util.function.BiConsumer;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import javafx.geometry.Insets;
-import javafx.scene.control.Button;
-import javafx.scene.control.Label;
-import javafx.scene.control.ListView;
-import javafx.scene.control.Spinner;
-import javafx.scene.control.SpinnerValueFactory;
+
+import javafx.collections.transformation.SortedList;
+import javafx.scene.control.*;
+import javafx.scene.chart.XYChart;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
+import javafx.scene.chart.LineChart;
+import javafx.util.Subscription;
import no.ntnu.gruppe53.model.Exchange;
import no.ntnu.gruppe53.model.Player;
import no.ntnu.gruppe53.model.Stock;
-
+import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+
+/**
+ * Represents a view of the market of an {@link Exchange}, allowing the {@link Player} to see
+ * price history statistics in the form of a {@link LineChart} through the
+ * {@link StockLineChartView} class, as well as buying {@link Stock}s.
+ *
Extends borderpane to allow easy organizing of the internal layout.
+ */
public class MarketView extends BorderPane {
- //Main pane of view
-
-
- //Buttons
private Button purchaseButton;
-
- //NavigationBar Buttons
- private Button marketButton;
- private Button portfolioButton;
-
- //Dynamic Labels/Text
private Label playerMoneyLabel;
private Label selectedStock;
-
-
- //StockListView
- private ListView stocksListView;
-
- //Input
+ private ListView stocksListView;
private Spinner quantitySpinner;
-
- //Models
private Exchange exchange;
private Player player;
- // Handlers
- private BiConsumer purchaseHandler;
- private Runnable showPortfolioHandler;
-
-
+ private final StockLineChartView stockLineChartView;
+ private Subscription playerSubscription;
+ private Subscription priceSubscription;
+ /**
+ * Initializes the StockLineChartView graph and runs the view
+ * construction method and places it in the center of the
+ * BorderPane.
+ */
public MarketView() {
-
-
-
- this.setTop(navigationBar());
+ stockLineChartView = new StockLineChartView();
this.setCenter(centerPane());
-
-
}
-
- public HBox navigationBar() {
-
- HBox hBox = new HBox();
-
- hBox.setPadding(new Insets(15, 12, 15, 12));
- hBox.setSpacing(10);
- hBox.setStyle("-fx-background-color: #336699;");
-
- marketButton = new Button("Market");
- portfolioButton = new Button("Portfolio");
-
- hBox.getChildren().addAll(marketButton, portfolioButton);
-
- return hBox;
- }
-
- public VBox centerPane() {
-
- VBox vBox = new VBox();
+ /**
+ * Constructs the view and its components.
+ *
Subscribes to a stock's sales price to update the value immediately.
+ *
Uses a cell factory to format the string found in the stocksListView.
+ *
+ * @return the constructed MarketView
+ */
+ private VBox centerPane() {
+ VBox vBox = new VBox(10);
Label titleLabel = new Label("Stock Market");
titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
@@ -90,142 +62,182 @@ public VBox centerPane() {
playerMoneyLabel = new Label("Money: $0.00");
Label stocksTitleLabel = new Label("Available stocks:");
+ stockLineChartView.getChart().setPrefHeight(150);
+
stocksListView = new ListView<>();
- stocksListView.setPrefHeight(300);
+ stocksListView.setPrefHeight(200);
+
+ stocksListView.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSubscription;
+
+ @Override
+ protected void updateItem(Stock stock, boolean empty) {
+ super.updateItem(stock, empty);
+
+ if (cellSubscription != null) {
+ cellSubscription.unsubscribe();
+ cellSubscription = null;
+ }
+
+ if (empty || stock == null) {
+ setText(null);
+ } else {
+ cellSubscription = stock.salesPriceProperty().subscribe(newPrice -> {
+ setText(stock.getSymbol() + " - " + stock.getCompany() + " | Price: $" +
+ FormatBigDecimal.formatNumber(newPrice)
+ + " | Price change: " + FormatBigDecimal.formatNumber(stock.getLatestPriceChange())
+ + " | Highest price: " + FormatBigDecimal.formatNumber(stock.getHighestPrice())
+ + " | Lowest price: " + FormatBigDecimal.formatNumber(stock.getLowestPrice()));
+ });
+ }
+ }
+ });
HBox selectedStockBox = new HBox();
-
- Label selectedStockLabel = new Label("Selected Stock:");
+ Label selectedStockLabel = new Label("Selected Stock: ");
selectedStock = new Label("");
-
selectedStockBox.getChildren().addAll(selectedStockLabel, selectedStock);
- HBox purchaseBox = new HBox();
-
-
-
- Label quantityLabel = new Label("Quantity:");
- quantitySpinner = new Spinner<>();
- quantitySpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1_000_000, 1));
+ HBox purchaseBox = new HBox(10);
+ Label quantityLabel = new Label("Quantity: ");
+ quantitySpinner = new Spinner<>(1, 1_000_000, 1);
quantitySpinner.setEditable(true);
purchaseButton = new Button("Purchase");
-
purchaseBox.getChildren().addAll(quantityLabel, quantitySpinner, purchaseButton);
- vBox.getChildren().addAll(titleLabel,
+ vBox.getChildren().addAll(
+ titleLabel,
playerMoneyLabel,
+ stockLineChartView.getChart(),
stocksTitleLabel,
stocksListView,
selectedStockBox,
- purchaseBox);
+ purchaseBox
+ );
return vBox;
}
- public void onPortfolioButton(Runnable action) {
- portfolioButton.setOnAction( e -> action.run());
- }
+ /**
+ * Sets and updates subscription to the selected stock's sales price.
+ * @param stock the selected stock
+ * @param priceChangeAction the action to run on price change (updates the sales price)
+ */
+ public void setupPriceSubscription(Stock stock, Consumer priceChangeAction) {
+ if (priceSubscription != null) {
+ priceSubscription.unsubscribe();
+ priceSubscription = null;
+ }
- public void onPurchaseButton(Runnable action) {
- purchaseButton.setOnAction(e -> action.run());
+ if (stock != null) {
+ priceSubscription = stock.salesPriceProperty().subscribe(newPrice -> {
+ priceChangeAction.accept(stock);
+ });
+ }
}
- public void onStockSelection(Consumer action) {
- stocksListView.getSelectionModel()
- .selectedItemProperty()
- .addListener((observable, oldValue, newValue) -> {
- if (newValue != null) {
- action.accept(newValue);
- }
- });
+ /**
+ * Updates chart data in the StockLineChartView.
+ * @param series the {@link javafx.scene.chart.XYChart.Series} data for the chart
+ * @param companyName the name of the company of the selected stock
+ */
+ public void updateChart(XYChart.Series series, String companyName) {
+ stockLineChartView.setSeries(series, companyName);
}
- /*
- public void onPurchaseButton(Runnable action) {
- purchaseButton.setOnAction(event -> {
- if (purchaseHandler == null) {
- return;
- }
-
- String selectedStock = getSelectedSymbol();
- if (selectedStock == null) {
- return;
- }
-
- int quantity = quantitySpinner.getValue();
- purchaseHandler.accept(selectedStock, quantity);
- });
- }*/
+ /**
+ * Unsubscribes from the previous stocks sales price and clears the LineChart.
+ */
+ public void clearChart() {
+ if (priceSubscription != null) {
+ priceSubscription.unsubscribe();
+ priceSubscription = null;
+ }
+ stockLineChartView.getChart().getData().clear();
+ }
- public void setPurchaseHandler(BiConsumer purchaseHandler) {
- this.purchaseHandler = purchaseHandler;
+ /**
+ * Sets the action to be run on selecting a stock in stockListView.
+ * @param action the action to run when selecting a stock
+ */
+ public void onStockSelection(Consumer action) {
+ stocksListView.getSelectionModel().selectedItemProperty().subscribe(action);
}
+ /**
+ * Sets the action to be run when clicking the purchase button
+ *
+ * @param action the action to run when clicking the purchase button
+ */
+ public void onPurchaseButton(Runnable action) {
+ purchaseButton.setOnAction(e -> action.run());
+ }
+ /**
+ * Sets the player to subscribe to.
+ *
Observes the player's money value and formats it to max 2 decimals.
+ *
+ * @param player the player to observe
+ */
public void setPlayer(Player player) {
+ if (playerSubscription != null) {
+ playerSubscription.unsubscribe();
+ playerSubscription = null;
+ }
this.player = player;
-
- if (player == null) {
+ if (player != null) {
+ playerSubscription = player.getMoneyProperty().subscribe(newMoney -> {
+ playerMoneyLabel.setText("Money: $" + FormatBigDecimal.formatNumber(newMoney));
+ });
+ playerMoneyLabel.setText("Money: $" + FormatBigDecimal.formatNumber(player.getMoney()));
+ } else {
playerMoneyLabel.setText("Money: $0.00");
- return;
}
-
- playerMoneyLabel.setText("Money: $" + formatMoney(player.getMoney()));
}
+ /**
+ * Sets the exchange to use for the view.
+ *
Uses a {@link SortedList} to sort the stocks in the exchange alphabetically
+ * on their symbol/ticker.
+ *
+ * @param exchange the exchange to be used for the view
+ */
public void setExchange(Exchange exchange) {
this.exchange = exchange;
- refreshStocks();
- }
-
- public void refreshPlayer() {
- setPlayer(player);
- }
-
- public void refreshStocks() {
- if (exchange == null) {
- stocksListView.getItems().clear();
- return;
+ if (exchange != null) {
+ SortedList sortedStocks = new SortedList<>(exchange.getObservableStocks());
+ sortedStocks.setComparator((s1, s2) -> s1.getSymbol().compareToIgnoreCase(s2.getSymbol()));
+ stocksListView.setItems(sortedStocks);
}
-
- List stocks = exchange.findStocks("");
- stocksListView.getItems().setAll(
- stocks.stream()
- .map(this::formatStock)
- .collect(Collectors.toList())
- );
}
- public String getSelectedSymbol() {
- String selected = stocksListView.getSelectionModel().getSelectedItem();
- if (selected == null || selected.isBlank()) {
- return null;
- }
-
- int indexEnd = selected.indexOf("-");
-
-
- return selected.substring(0,indexEnd - 1 );
+ /**
+ * Returns the selected stock in the stocksListView.
+ *
+ * @return the selected stock in the stocksListView
+ */
+ public Stock getSelectedStock() {
+ return stocksListView.getSelectionModel().getSelectedItem();
}
+ /**
+ * Returns the numeric value of the quantitySpinner for purchasing {@link Share}s in a stock.
+ *
+ * @return an integer of the amount of shares to buy
+ */
public int getQuantityPurchase() {
return quantitySpinner.getValue();
}
- public void setSelectedStock(String selectedStockString) {
+ /**
+ * Sets the company name of the selected stock in the stocksListView.
+ *
+ * @param selectedStockString the company name of the selected stock
+ */
+ public void setSelectedStockLabel(String selectedStockString) {
selectedStock.setText(selectedStockString);
}
- private String formatStock(Stock stock) {
- return stock.getSymbol()
- + " - "
- + stock.getCompany()
- + " | Price: $"
- + formatMoney(stock.getSalesPrice());
- }
- private String formatMoney(BigDecimal money) {
- return money == null ? "0.00" : money.setScale(2, RoundingMode.HALF_UP).toPlainString();
- }
-}
\ No newline at end of file
+}
From 7066d27b0d4aaae3a08f273f2dd6c2b4f405b32e Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:42:20 +0200
Subject: [PATCH 076/142] Updated PortfolioView
Added JavaDoc.
Added Observer pattern.
Added colored text whether stock price went up or down compared to purchase price.
---
.../no/ntnu/gruppe53/view/PortfolioView.java | 215 ++++++++----------
1 file changed, 100 insertions(+), 115 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
index 928a00f..ddf4bec 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -1,182 +1,167 @@
package no.ntnu.gruppe53.view;
import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.function.BiConsumer;
+import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
-import javafx.scene.control.Spinner;
-import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+import javafx.util.Subscription;
import no.ntnu.gruppe53.model.Player;
import no.ntnu.gruppe53.model.Share;
-
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+
+/**
+ * Represent a view of the {@link Player}s {@link no.ntnu.gruppe53.model.Portfolio}.
+ *
Lets the player view their current {@link Share} assets, see their
+ * current sale value, whether the value has gone up or down since they purchased,
+ * and the ability to sell the share for money.
+ *
Extends the {@link BorderPane}
for easy layout setup.
+ *
Events are handled by the {@link no.ntnu.gruppe53.controller.GameController}.
Uses a cell factory to format the displayed string representing each share.
+ *
+ * @return a VBox of the portfolio view
+ */
private VBox centerPane() {
- VBox vBox = new VBox();
+ VBox vBox = new VBox(10);
vBox.setPadding(new Insets(15, 12, 15, 12));
- vBox.setSpacing(10);
viewLabel = new Label("Portfolio");
viewLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
playerNameLabel = new Label("Player: -");
- netWorthLabel = new Label("Net worth: $0.00");
portfolioListView = new ListView<>();
portfolioListView.setPrefHeight(300);
- portfolioListView.setCellFactory(list -> new javafx.scene.control.ListCell<>() {
+
+ portfolioListView.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSubscription;
+
@Override
protected void updateItem(Share item, boolean empty) {
super.updateItem(item, empty);
- setText(empty || item == null ? null : formatShare(item));
+
+ if (cellSubscription != null) {
+ cellSubscription.unsubscribe();
+ cellSubscription = null;
+ }
+
+ if (empty || item == null || item.getStock() == null) {
+ setText(null);
+ setGraphic(null);
+ } else {
+ setText(null);
+ cellSubscription = item.getStock().salesPriceProperty().subscribe(currentPrice -> {
+ setGraphic(createColoredShareText(item, currentPrice));
+ });
+ }
}
});
HBox sellBar = new HBox(10);
- Label quantityLabel = new Label("Quantity:");
- quantitySpinner = new Spinner<>();
- quantitySpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1_000_000, 1));
- quantitySpinner.setEditable(true);
-
sellButton = new Button("Sell");
- sellBar.getChildren().addAll(quantityLabel, quantitySpinner, sellButton);
-
-
- vBox.getChildren().addAll(viewLabel,
- playerNameLabel,
- netWorthLabel,
- portfolioListView,
- sellBar);
+ sellBar.getChildren().addAll(sellButton);
+ vBox.getChildren().addAll(viewLabel, playerNameLabel, portfolioListView, sellBar);
return vBox;
}
+ /**
+ * Exposes the sell button to the GameController.
+ * @param action the action to be run when pressing the sell button
+ */
public void onSellButton(Runnable action) {
- sellButton.setOnAction(event -> {
- if (sellHandler == null) {
- return;
- }
-
- Share selectedShare = portfolioListView.getSelectionModel().getSelectedItem();
- if (selectedShare == null) {
- return;
- }
-
- int quantity = quantitySpinner.getValue();
- sellHandler.accept(selectedShare, quantity);
- });
+ sellButton.setOnAction(event -> action.run());
}
- public void onMarketButton(Runnable action) {
- marketButton.setOnAction(e -> action.run());
- }
-
- public void setShowMarketHandler(Runnable showMarketHandler) {
- this.showMarketHandler = showMarketHandler;
- }
-
- public void setSellHandler(BiConsumer sellHandler) {
- this.sellHandler = sellHandler;
+ /**
+ * Gets the selected share in the portfolioListView.
+ *
+ * @return the currently selected share
+ */
+ public Share getSelectedShare() {
+ return portfolioListView.getSelectionModel().getSelectedItem();
}
+ /**
+ * Sets the player to be observed.
+ *
The values observed are the players portfolio and name.
+ *
Uses an {@link ObservableList} to observe the shares and
+ * binds it to the portfolioListView.
+ *
+ * @param player the player to be observed
+ */
public void setPlayer(Player player) {
this.player = player;
- refresh();
- }
- public void refresh() {
- if (player == null) {
+ if (player != null && player.getPortfolio() != null) {
+ ObservableList observableShares = player.getPortfolio().getShares();
+
+ portfolioListView.setItems(observableShares);
+
+ playerNameLabel.setText("Player: " + player.getName());
+ } else {
playerNameLabel.setText("Player: -");
- netWorthLabel.setText("Net worth: $0.00");
- portfolioListView.getItems().clear();
- return;
+ portfolioListView.setItems(null);
}
-
- playerNameLabel.setText("Player: " + player.getName());
- netWorthLabel.setText("Net worth: $" + formatMoney(player.getNetWorth()));
- portfolioListView.getItems().setAll(player.getPortfolio().getShares());
}
- private String formatShare(Share share) {
- return share.getStock().getSymbol()
+ /**
+ * Formats the text representation of the share in the portfolioListView
+ * and applies a color to it based on its current sales price compared
+ * to the buy price of the share.
+ *
+ * @param share the share to be formatted
+ * @param currentPrice the current price of the share
+ * @return a {@link TextFlow} representation of the share
+ */
+ private TextFlow createColoredShareText(Share share, BigDecimal currentPrice) {
+ String text = share.getStock().getSymbol()
+ " - "
+ share.getStock().getCompany()
+ " | Qty: "
+ share.getQuantity()
+ " | Buy price: $"
- + formatMoney(share.getPurchasePrice());
- }
+ + FormatBigDecimal.formatNumber(share.getPurchasePrice())
+ + " | Current Price: $";
+
+ Text t1 = new Text(text);
+ Text tValue = new Text(FormatBigDecimal.formatNumber(currentPrice));
+
+ BigDecimal buyPrice = share.getPurchasePrice();
+ if (buyPrice != null && currentPrice != null) {
+ int compare = currentPrice.compareTo(buyPrice);
+ if (compare > 0) tValue.setStyle("-fx-fill: #2ecc71; -fx-font-weight: bold;");
+ else if (compare < 0) tValue.setStyle("-fx-fill: #e74c3c; -fx-font-weight: bold;");
+ else tValue.setStyle("-fx-fill: #000000;");
+ }
- //Formats any BigDecimal to a legible money value with a scale of - > 0.00 to a plainStrign().
- private String formatMoney(BigDecimal money) {
- return money == null ? "0.00" : money.setScale(2, RoundingMode.HALF_UP).toPlainString();
+ return new TextFlow(t1, tValue);
}
-
-
}
\ No newline at end of file
From 24638da8480a2e500eee38ca990325d964aab1d7 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:43:24 +0200
Subject: [PATCH 077/142] Added TransactionArchiveView
Lets the player view their transactions.
---
.../gruppe53/view/TransactionArchiveView.java | 103 ++++++++++++++++++
1 file changed, 103 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
new file mode 100644
index 0000000..81e501a
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
@@ -0,0 +1,103 @@
+package no.ntnu.gruppe53.view;
+
+import javafx.geometry.Insets;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.Transaction;
+import no.ntnu.gruppe53.model.Purchase;
+import no.ntnu.gruppe53.model.Sale;
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+
+/**
+ * Represents a view of all the {@link Purchase} and {@link Sale}
+ * {@link Transaction}s a {@link Player} has committed in a game.
+ *
Extends the {@link BorderPane} for easy layout setup.
+ */
+public class TransactionArchiveView extends BorderPane {
+
+ private ListView historyListView;
+ private Label titleLabel;
+
+ /**
+ * Builds the view components and places them in the center pane.
+ */
+ public TransactionArchiveView() {
+ this.setCenter(centerPane());
+ }
+
+ /**
+ * Constructs the view of the transaction archive.
+ *
Builds the necessary components and places them in a {@link VBox}.
+ *
Uses a cell factory to format the string shown
+ *
Assigns red text to purchases, and green text to sales.
+ *
Uses the formatBigDecimal method to convert the BigDecimal to a
+ * string with 2 decimals.
+ *
+ * @return a VBox of the constructed view of the transaction archive
+ */
+ private VBox centerPane() {
+ VBox vBox = new VBox(10);
+ vBox.setPadding(new Insets(15, 12, 15, 12));
+
+ titleLabel = new Label("Transaction History");
+ titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
+
+ historyListView = new ListView<>();
+ historyListView.setPrefHeight(400);
+
+ historyListView.setCellFactory(list -> new ListCell<>() {
+ @Override
+ protected void updateItem(Transaction item, boolean empty) {
+ super.updateItem(item, empty);
+
+ if (empty || item == null) {
+ setText(null);
+ setStyle("");
+ } else {
+ String type = (item instanceof Purchase) ? "PURCHASE" : "SALE";
+ String symbol = item.getShare().getStock().getSymbol();
+ BigDecimal quantity = item.getShare().getQuantity();
+ BigDecimal total = item.getCalculator().calculateTotal();
+
+ String cellText = String.format("Week %d | %s | %s | Qty: %s | Total: $%s",
+ item.getWeek(),
+ type,
+ symbol,
+ FormatBigDecimal.formatNumber(quantity),
+ FormatBigDecimal.formatNumber(total)
+ );
+
+ setText(cellText);
+
+ if (item instanceof Purchase) {
+ setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;");
+ } else if (item instanceof Sale) {
+ setStyle("-fx-text-fill: #27ae60; -fx-font-weight: bold;");
+ }
+ }
+ }
+ });
+
+ vBox.getChildren().addAll(titleLabel, historyListView);
+ return vBox;
+ }
+
+ /**
+ * Sets the player to be observed.
+ *
+ * @param player the player to be observed
+ */
+ public void setPlayer(Player player) {
+ if (player != null && player.getTransactionArchive() != null) {
+ historyListView.setItems(player.getTransactionArchive().getObservableTransactions());
+ } else {
+ historyListView.setItems(null);
+ }
+ }
+}
From 62594d51736dde4ede24cb5f3ea57723a4be169d Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:45:29 +0200
Subject: [PATCH 078/142] Added GameSetupController
Handles user input for data like name, starting money and .csv file to use.
---
.../controller/GameSetupController.java | 119 ++++++++++++++++++
1 file changed, 119 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java
new file mode 100644
index 0000000..85432b9
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java
@@ -0,0 +1,119 @@
+package no.ntnu.gruppe53.controller;
+
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.service.FileHandler;
+import no.ntnu.gruppe53.view.CSVChooserView;
+import no.ntnu.gruppe53.view.PlayerNameChooserView;
+import no.ntnu.gruppe53.view.PlayerStartingMoneyChooserView;
+import javafx.stage.Stage;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * Controller for setting up relevant game data to start a new game.
+ *
Lets the {@link Player} set their name, starting money
+ * and provide a .csv file of {@link Stock} data to populate
+ * the {@link Exchange}.
+ */
+public class GameSetupController {
+ /**
+ * Nested class to pass the player and stocks values over to the
+ * {@link GameController}.
+ */
+ public static class SetupResult {
+ private final Player player;
+ private final List stocks;
+
+ /**
+ * Initializes the player and stock variables.
+ * @param player the player that will play the stock game
+ * @param stocks the list of stocks to populate the exchange
+ */
+ public SetupResult(Player player, List stocks) {
+ this.player = player;
+ this.stocks = stocks;
+ }
+
+ /**
+ * Returns the player object containing the set name and starting money.
+ *
+ * @return the player object for the player playing the game
+ */
+ public Player getPlayer() {
+ return player;
+ }
+
+ /**
+ * A list of stocks that will be used for buying and selling on the exchange.
+ *
+ * @return a list of stock objects
+ */
+ public List getStocks() {
+ return stocks;
+ }
+ }
+
+ private final Stage stage;
+
+ /**
+ * Initializes the initial stage for the view setting up the relevant game data.
+ *
+ * @param stage the initial stage to be used
+ */
+ public GameSetupController(Stage stage) {
+ this.stage = stage;
+ }
+
+ /**
+ * Sets up the initial data for the {@code Player} and {@code Exchange}.
+ *
Uses default values if the player does not enter their desired values:
+ *
+ *
Player name: Player
+ *
Player starting money: 5000
+ *
CSV file: resources/sp500.csv
+ *
+ *
+ * @return a SetupResult object containing data for the player and the exchange
+ */
+ public SetupResult setupNewGame() {
+ PlayerNameChooserView playerNameDialog = new PlayerNameChooserView();
+ // Player name
+ String playerName =
+ playerNameDialog.getDialog()
+ .orElse("Player");
+
+ if (playerName.isBlank()) {
+ playerName = "Player";
+ }
+
+ // Starting money
+ PlayerStartingMoneyChooserView playerStartingMoneyDialog = new PlayerStartingMoneyChooserView();
+
+ BigDecimal startingMoney =
+ playerStartingMoneyDialog.getDialog()
+ .orElse(new BigDecimal("5000"));
+
+
+ // .csv File
+ CSVChooserView csvChooser = new CSVChooserView();
+ File csvFile = csvChooser.getDialog(stage);
+
+ List stocks;
+ if (csvFile == null) {
+ stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv");
+ } else {
+ stocks = FileHandler.readStocksFromFile(csvFile.getParent(), csvFile.getName());
+ if (stocks.isEmpty()) {
+ stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv");
+ }
+ }
+
+ Player player = new Player(playerName, startingMoney);
+
+ return new SetupResult(player, stocks);
+ }
+}
\ No newline at end of file
From a4318a5248d4ee2106ede864a1aa961e4735ef91 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:46:59 +0200
Subject: [PATCH 079/142] Updated ViewController
Restructured to have always on NavigationBar and FooterBar, and only swaps out center pane in the border pane layout.
---
.../gruppe53/controller/ViewController.java | 65 ++++++++++++-------
1 file changed, 42 insertions(+), 23 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
index 3c91d5a..3d7f25c 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
@@ -2,52 +2,71 @@
import javafx.scene.Parent;
import javafx.scene.Scene;
+import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
+import no.ntnu.gruppe53.view.FooterBar;
+import no.ntnu.gruppe53.view.NavigationBar;
import java.util.HashMap;
import java.util.Map;
/**
- * Manages application views and handles switching between them.
- *
- * Stores a mapping of view names to JavaFX {@link Parent} nodes and updates
- * the current Scene by replacing its root node when switching views.
- *
+ * Controller for setting and switching between views.
+ *
Uses a {@link HashMap} as an archive for registered views that
+ * are possible to switch between.
*/
public class ViewController {
- /** Primary application window used to display the Scene. */
- private Stage stage;
- /** Scene containing the currently active view. */
- private Scene scene;
- /** Map of view identifiers to their corresponding UI roots. */
- private Map views = new HashMap<>();
+ private final Stage stage;
+ private final Scene scene;
+ private final Map views = new HashMap<>();
+ private final BorderPane mainLayout;
/**
- * Constructs a ViewController and initializes a new Scene.
- *
- * @param stage the primary application stage where the scene will be set
+ * Initializes a ViewController using the {@link Stage} as the first default view.
+ *
Uses a {@link BorderPane} layout for views.
*/
public ViewController(Stage stage) {
this.stage = stage;
- this.scene = new Scene(new javafx.scene.layout.BorderPane(), 800, 600);
- stage.setScene(scene);
+ this.mainLayout = new BorderPane();
+ this.scene = new Scene(mainLayout, 800, 600);
+ this.stage.setScene(scene);
+ }
+
+ /**
+ * Returns the stage of the ViewController.
+ *
+ * @return the stage of the ViewController.
+ */
+ public Stage getStage() {
+ return this.stage;
+ }
+
+ /**
+ * Sets the top and bottom part of the BorderPane.
+ *
Uses {@link NavigationBar} as the top bar,
+ * and {@link FooterBar} as the bottom bar.
+ */
+ public void setGlobalPanels(NavigationBar navigationBar, FooterBar footerBar) {
+ this.mainLayout.setTop(navigationBar);
+ this.mainLayout.setBottom(footerBar);
}
/**
- * Registers a view in the controller.
+ * Adds a view to the stored archive of possible views.
*
- * @param name unique identifier used to reference the view later
- * @param view the JavaFX root node representing the view
+ * @param name the name/key to refer to the view when switching view
+ * @param view the view to be added
*/
public void addView(String name, Parent view) {
views.put(name, view);
}
/**
- * Switches the currently displayed view by replacing the Scene root.
+ * Switches the to the given view based on the string key
+ * stored in the view archive.
+ *
Only switches the center pane.
*
- * @param name the identifier of the view to display
- * @throws IllegalArgumentException if no view is registered under the given name
+ * @param name the name/key of the view to switch to
*/
public void switchView(String name) {
Parent view = views.get(name);
@@ -56,6 +75,6 @@ public void switchView(String name) {
throw new IllegalArgumentException("No views found matching: " + name);
}
- scene.setRoot(view);
+ mainLayout.setCenter(view);
}
}
From 9dbd38f04499f43e4e92e7c78dd8aafba75ebfdb Mon Sep 17 00:00:00 2001
From: Roar
Date: Sat, 23 May 2026 10:49:02 +0200
Subject: [PATCH 080/142] Updated GameController
Added JavaDoc.
Sets up global top and bottom bars.
Restructuring to fit changes in views.
---
.../gruppe53/controller/GameController.java | 206 +++++++++++-------
1 file changed, 130 insertions(+), 76 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index c403958..ddc86ad 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -3,134 +3,188 @@
import java.math.BigDecimal;
import java.util.List;
-import no.ntnu.gruppe53.model.Exchange;
-import no.ntnu.gruppe53.view.FileHandler;
-import no.ntnu.gruppe53.model.Player;
-import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
-import no.ntnu.gruppe53.view.PortfolioView;
-import no.ntnu.gruppe53.view.MarketView;
-
+import javafx.scene.control.Alert;
+import javafx.stage.Stage;
+import no.ntnu.gruppe53.model.*;
+import no.ntnu.gruppe53.service.StockLineChartService;
+import no.ntnu.gruppe53.view.*;
+
+/**
+ * Represents the main controller for the views of the stock game.
+ */
public class GameController {
- private final ViewController vm;
- private final FileHandler fileHandler = new FileHandler();
-
private Player player;
private Exchange exchange;
private List stocks;
+
+ private final ViewController vm;
+
private MarketView marketView;
private PortfolioView portfolioView;
+ private TransactionArchiveView transactionArchiveView;
+ /**
+ * Initializes the {@link ViewController} to be used for switching views.
+ * @param vm {@code ViewController} to be used when switching views
+ */
public GameController(ViewController vm) {
this.vm = vm;
}
+ /**
+ * Starts a new game by initializing all main views and navigation.
+ *
Creates the player and exchange to be used by the game from
+ * data from the {@link GameSetupController}.
+ */
public void startNewGame() {
- player = new Player("Player 1", new BigDecimal("100000"));
- stocks = fileHandler.readStocksFromFile("src/main/resources", "sp500.csv");
+ Stage primaryStage = vm.getStage();
+
+ GameSetupController setupController = new GameSetupController(primaryStage);
+ GameSetupController.SetupResult setupResult = setupController.setupNewGame();
+ if (setupResult == null) return;
+
+ player = setupResult.getPlayer();
+ stocks = setupResult.getStocks();
+
exchange = new Exchange("Millions Exchange", stocks);
+ NavigationBar sharedNav = new NavigationBar();
+ FooterBar sharedFooter = new FooterBar();
+ vm.setGlobalPanels(sharedNav, sharedFooter);
+
marketView = new MarketView();
marketView.setPlayer(player);
marketView.setExchange(exchange);
- //marketView.setPurchaseHandler(this::purchaseStock);
portfolioView = new PortfolioView();
portfolioView.setPlayer(player);
- //marketView.setShowPortfolioHandler(this::showPortfolio);
- portfolioView.setShowMarketHandler(this::showPurchaseView);
- portfolioView.setSellHandler(this::sellShare);
+ transactionArchiveView = new TransactionArchiveView();
+ transactionArchiveView.setPlayer(player);
+
+ sharedFooter.bindPlayer(player);
- //vm.addView("purchase", purchaseView);
vm.addView("portfolio", portfolioView);
vm.addView("market", marketView);
+ vm.addView("history", transactionArchiveView);
- initialize();
+ initialize(sharedNav, sharedFooter);
vm.switchView("market");
}
-
-
-
-
- public void showPortfolio() {
- if (portfolioView != null) {
- portfolioView.setPlayer(player);
- vm.switchView("portfolio");
- }
- }
-
- public void showPurchaseView() {
+ /**
+ * Switches the view to the {@link MarketView}.
+ *
MarketView lets the player perform {@link Purchase} transactions.
It specifically listens to the players money and net worth.
- *
- * @param player the player object to be observed
- */
- public void bindPlayer(Player player) {
- if (netWorthSubscription != null) {
- netWorthSubscription.unsubscribe();
- }
-
- if (player != null) {
- netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> {
- if (newVal != null) {
- netWorthLabel.setText("Net Worth: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
- } else {
- netWorthLabel.setText("Net Worth: $0.00");
- }
- });
- }
- }
+
/**
* Exposes the advanceWeekButton to {@link GameController} so it can
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
index 725a4d5..d6f7415 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
@@ -7,8 +7,9 @@
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Player;
-import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -31,15 +32,19 @@ public class NavigationBar extends HBox {
private final Button marketButton;
private final Button portfolioButton;
private final Button historyButton;
+ private Subscription netWorthSubscription;
+ private Subscription moneySubscription;
+ private Player player;
//Navigation Buttons
//Dynamic Labels
- public Label playerLabel;
- public Label networthLabel;
- public Label moneyLabel;
+ private Label playerLabel;
+ private Label netWorthLabel;
+ private Label moneyLabel;
+
public NavigationBar() {
@@ -64,7 +69,7 @@ public NavigationBar() {
HBox playerBox = new HBox();
Label playerLabelText = new Label("Player: ");
- playerLabel = new Label();
+ playerLabel = new Label("Player");
playerBox.setStyle("-fx-background-color: #ffe556;" +
"-fx-text-fill: #303539;" +
@@ -96,7 +101,7 @@ public NavigationBar() {
HBox networthBox = new HBox();
Label networthLabelText = new Label("Net Worth: ");
- networthLabel = new Label();
+ netWorthLabel = new Label();
networthBox.setStyle("-fx-background-color: #ffe556;" +
"-fx-text-fill: #303539;" +
@@ -106,7 +111,7 @@ public NavigationBar() {
);
networthBox.setPadding(new Insets(8, 14, 8, 14));
networthBox.setAlignment(Pos.CENTER);
- networthBox.getChildren().addAll(networthLabelText, networthLabel);
+ networthBox.getChildren().addAll(networthLabelText, netWorthLabel);
this.getChildren().addAll(marketButton, portfolioButton, historyButton, spacer, playerBox, moneyBox, networthBox);
}
@@ -136,4 +141,39 @@ public void onPortfolioButtonClick(Runnable action) {
public void onHistoryButtonClick(Runnable action) {
historyButton.setOnAction(e -> action.run());
}
+
+
+
+ /**
+ * Binds a subscription/observer to a {@link Player}.
+ *
It specifically listens to the players money, net worth and name.
+ *
+ * @param player the player object to be observed
+ */
+ public void bindPlayer(Player player) {
+ this.player = player;
+
+ if (netWorthSubscription != null) {
+ netWorthSubscription.unsubscribe();
+ }
+
+ if (player != null) {
+ playerLabel.setText(player.getName());
+ netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ netWorthLabel.setText("Net Worth: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ netWorthLabel.setText("Net Worth: $0.00");
+ }
+ });
+ moneySubscription = player.getMoneyProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ moneyLabel.setText("Money: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ moneyLabel.setText("Money: $0.00");
+ }
+ });
+
+ }
+ }
}
From 83a42bdbd0cd953c2c603a7d24f19adae00c80de Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 16:54:04 +0200
Subject: [PATCH 086/142] Fixed portfolioview
---
.../no/ntnu/gruppe53/view/MarketView.java | 31 +++++++++++++++++--
.../gruppe53/view/TransactionArchiveView.java | 9 +++++-
2 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index 8bb085d..5426f5d 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -99,12 +99,16 @@ private VBox centerPane() {
stockLineChartView.getChart().setMinHeight(0);
stockLineChartView.getChart().setMaxHeight(Double.MAX_VALUE);
+ stockLineChartView.getChart().setMinWidth(0);
+ stockLineChartView.getChart().setMaxWidth(Double.MAX_VALUE);
stocksListView = new ListView<>();
stocksListView.setMinHeight(0);
stocksListView.setMaxHeight(Double.MAX_VALUE);
+ stocksListView.setMinWidth(0);
+ stocksListView.setMaxWidth(Double.MAX_VALUE);
stocksListView.setStyle("-fx-background-color: transparent;");
stocksListView.setCellFactory(list -> new ListCell<>() {
@@ -133,20 +137,41 @@ protected void updateItem(Stock stock, boolean empty) {
}
});
- HBox stockListBox = new HBox(stocksListView, stockLineChartView.getChart());
+ HBox stockListBox = new HBox(stocksListView);
stockListBox.setStyle("-fx-background-color: #ffe556;" +
"-fx-text-fill: #303539;" +
"-fx-background-radius: 8;" +
"-fx-border-color: #303539;" +
"-fx-border-radius: 8;"
);
+
+ HBox stockChartBox = new HBox(stockLineChartView.getChart());
+ stockChartBox.setStyle("-fx-background-color: #ffe556;" +
+ "-fx-text-fill: #303539;" +
+ "-fx-background-radius: 8;" +
+ "-fx-border-color: #303539;" +
+ "-fx-border-radius: 8;"
+ );
+
+ HBox stockBox = new HBox(stockListBox, stockChartBox);
+ stockBox.setSpacing(10);
+
stockListBox.setPadding(new Insets(4));
stockListBox.setMinHeight(0);
stockListBox.setMaxHeight(Double.MAX_VALUE);
+ stockListBox.setMinWidth(0);
+ stockListBox.setMaxWidth(Double.MAX_VALUE);
VBox.setVgrow(stocksListView, Priority.ALWAYS);
+ HBox.setHgrow(stocksListView, Priority.ALWAYS);
VBox.setVgrow(stockLineChartView.getChart(), Priority.ALWAYS);
- HBox.setHgrow(stockListBox, Priority.ALWAYS);
+ HBox.setHgrow(stockLineChartView.getChart(), Priority.ALWAYS);
+ VBox.setVgrow(stockListBox, Priority.ALWAYS);
+ HBox.setHgrow(stockListBox, Priority.ALWAYS);
+ VBox.setVgrow(stockChartBox, Priority.ALWAYS);
+ HBox.setHgrow(stockChartBox, Priority.ALWAYS);
+ VBox.setVgrow(stockBox, Priority.ALWAYS);
+ HBox.setHgrow(stockBox, Priority.ALWAYS);
HBox selectedStockBox = new HBox();
@@ -204,7 +229,7 @@ protected void updateItem(Stock stock, boolean empty) {
vBox.getChildren().addAll(titleBox,
- stockListBox,
+ stockBox,
purchaseBox);
return vBox;
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
index 81e501a..08ee07e 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
@@ -14,6 +14,12 @@
import no.ntnu.gruppe53.model.Sale;
import no.ntnu.gruppe53.service.FormatBigDecimal;
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
/**
* Represents a view of all the {@link Purchase} and {@link Sale}
* {@link Transaction}s a {@link Player} has committed in a game.
@@ -44,9 +50,10 @@ public TransactionArchiveView() {
private VBox centerPane() {
VBox vBox = new VBox(10);
vBox.setPadding(new Insets(15, 12, 15, 12));
+ vBox.setStyle("-fx-background-color: #00bcf0; ");
titleLabel = new Label("Transaction History");
- titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
+ titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;");
historyListView = new ListView<>();
historyListView.setPrefHeight(400);
From d389a93015d7846c4b5346f5da037d63475382ae Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 17:03:35 +0200
Subject: [PATCH 087/142] Added styling to History
---
.../gruppe53/controller/GameController.java | 2 ++
.../no/ntnu/gruppe53/view/PortfolioView.java | 9 +++++
.../gruppe53/view/TransactionArchiveView.java | 33 +++++++++++++++++--
3 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index b8606df..aadc8ea 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -147,6 +147,8 @@ public void initialize(NavigationBar nav, FooterBar footer) {
});
portfolioView.onSellButton(this::handleSell);
+
+
}
/**
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
index 82373b0..b1c87cb 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -22,6 +22,7 @@
import javafx.util.Subscription;
import no.ntnu.gruppe53.model.Player;
import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.model.Stock;
import no.ntnu.gruppe53.service.FormatBigDecimal;
/*
@@ -205,6 +206,14 @@ public void onSellButton(Runnable action) {
sellButton.setOnAction(e -> action.run());
}
+ /**
+ * Sets the action to be run on selecting a share in portfolioListView.
+ * @param action the action to run when selecting a share
+ */
+ public void onShareSelection(Consumer action) {
+ portfolioListView.getSelectionModel().selectedItemProperty().subscribe(action);
+ }
+
/**
* Gets the selected share in the portfolioListView.
*
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
index 08ee07e..630adc8 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
@@ -1,10 +1,12 @@
package no.ntnu.gruppe53.view;
import javafx.geometry.Insets;
+import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -55,8 +57,20 @@ private VBox centerPane() {
titleLabel = new Label("Transaction History");
titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;");
+ VBox titleBox = new VBox(titleLabel);
+ titleBox.setStyle("-fx-background-color: #ffe556;" +
+ "-fx-text-fill: #303539;" +
+ "-fx-background-radius: 8;" +
+ "-fx-border-color: #303539;" +
+ "-fx-border-radius: 8;"
+ );
+ titleBox.setAlignment(Pos.CENTER);
+ titleBox.setPadding(new Insets(4));
+ titleBox.setMaxWidth(300);
+
historyListView = new ListView<>();
- historyListView.setPrefHeight(400);
+ historyListView.setMinHeight(0);
+ historyListView.setMaxHeight(Double.MAX_VALUE);
historyListView.setCellFactory(list -> new ListCell<>() {
@Override
@@ -91,7 +105,22 @@ protected void updateItem(Transaction item, boolean empty) {
}
});
- vBox.getChildren().addAll(titleLabel, historyListView);
+ VBox historyListBox = new VBox(historyListView);
+ historyListBox.setStyle("-fx-background-color: #ffe556;" +
+ "-fx-text-fill: #303539;" +
+ "-fx-background-radius: 8;" +
+ "-fx-border-color: #303539;" +
+ "-fx-border-radius: 8;"
+ );
+
+ historyListBox.setPadding(new Insets(4));
+ historyListBox.setMinHeight(0);
+ historyListBox.setMaxHeight(Double.MAX_VALUE);
+
+ VBox.setVgrow(historyListView, Priority.ALWAYS);
+ VBox.setVgrow(historyListBox, Priority.ALWAYS);
+
+ vBox.getChildren().addAll(titleBox, historyListBox);
return vBox;
}
From 1f781c8937b1998ab349492bb242b514deec7d40 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 17:08:54 +0200
Subject: [PATCH 088/142] added share selection label in portfolioview
---
.../no/ntnu/gruppe53/controller/GameController.java | 9 +++++++++
.../main/java/no/ntnu/gruppe53/view/PortfolioView.java | 10 ++++++++++
2 files changed, 19 insertions(+)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index aadc8ea..8c9b59e 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -146,6 +146,15 @@ public void initialize(NavigationBar nav, FooterBar footer) {
//implement the quantity select which updates gross, commision and total
});
+ portfolioView.onShareSelection(share -> {
+ if (share != null) {
+ portfolioView.setSelectedShareLabel(share.getStock().getSymbol() + " - " + share.getStock().getCompany());
+
+ } else {
+ portfolioView.setSelectedShareLabel("");
+ }
+ });
+
portfolioView.onSellButton(this::handleSell);
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
index b1c87cb..5fb1b3a 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -246,6 +246,16 @@ public void setPlayer(Player player) {
}
}
+ /**
+ * Sets the share name of the selected share in the portfolioListView.
+ *
+ * @param selectedShareString the share name of the selected share
+ */
+ public void setSelectedShareLabel(String selectedShareString) {
+ selectedShare.setText(selectedShareString);
+ }
+
+
/**
* Formats the text representation of the share in the portfolioListView
* and applies a color to it based on its current sales price compared
From 7513bad88549e459e0e63de837313fb1eb25300f Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 18:06:14 +0200
Subject: [PATCH 089/142] Added a week counter to footer
---
.../gruppe53/controller/GameController.java | 1 +
.../java/no/ntnu/gruppe53/model/Exchange.java | 13 ++++++++
.../java/no/ntnu/gruppe53/view/FooterBar.java | 33 ++++++++++++++++++-
.../no/ntnu/gruppe53/view/MarketView.java | 2 +-
4 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index 8c9b59e..dad6f25 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -69,6 +69,7 @@ public void startNewGame() {
transactionArchiveView.setPlayer(player);
sharedNav.bindPlayer(player);
+ sharedFooter.setExchange(exchange);
vm.addView("portfolio", portfolioView);
vm.addView("market", marketView);
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
index 8a5684a..cb106b6 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
@@ -1,5 +1,7 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -19,6 +21,7 @@ public class Exchange {
private final Map stockMap;
private final Random random;
private final ObservableList stocks = FXCollections.observableArrayList();
+ private final ObjectProperty currentWeekProperty = new SimpleObjectProperty<>();
/**
* Sets the name of the exchange and the list of stocks to populate it.
@@ -40,6 +43,7 @@ public Exchange(String name, List stocks) {
this.name = name;
this.week = 1;
+ this.currentWeekProperty.set(week);
this.stockMap = new HashMap<>();
this.random = new Random();
@@ -173,6 +177,7 @@ public Transaction sell(Share share, Player player) {
*/
public void advance() {
this.week++;
+ this.currentWeekProperty.set(week);
double negativePriceChange = -0.05;
double positivePriceChange = 0.1;
@@ -230,4 +235,12 @@ public List getGainers(int entries) {
public List getLosers(int entries) {
return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange));
}
+
+ /**
+ * Returns an observable of the current week
+ * @return an observable of the current week
+ */
+ public ObjectProperty getCurrentWeekProperty() {
+ return currentWeekProperty;
+ }
}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
index 8307751..efea8a3 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
@@ -2,6 +2,7 @@
import java.math.RoundingMode;
import javafx.geometry.Insets;
+import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
@@ -26,7 +27,10 @@
public class FooterBar extends HBox {
private final Button advanceWeekButton;
+ private Label currentWeekLabel;
+ private Subscription currentWeekSubscription;
+ private Exchange exchange;
/**
* Constructs the layout of the view and its components.
@@ -40,17 +44,44 @@ public FooterBar() {
+
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
+ HBox currentWeekBox = new HBox();
+ currentWeekBox.setSpacing(10);
+
+ currentWeekBox.setStyle("-fx-background-color: #ffe556;" +
+ "-fx-text-fill: #303539;" +
+ "-fx-background-radius: 8;" +
+ "-fx-border-color: #303539;" +
+ "-fx-border-radius: 8;"
+ );
+ currentWeekBox.setPadding(new Insets(8, 14, 8, 14));
+ currentWeekBox.setAlignment(Pos.CENTER);
+
+ currentWeekLabel = new Label();
+ currentWeekBox.getChildren().add(currentWeekLabel);
+
advanceWeekButton = new Button("Advance Week");
advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
- this.getChildren().addAll(spacer, advanceWeekButton);
+ this.getChildren().addAll(spacer, currentWeekBox, advanceWeekButton);
}
+ public void setExchange(Exchange exchange) {
+ this.exchange = exchange;
+ currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> {
+ if (exchange != null) {
+ currentWeekLabel.setText("Current Week: " + newVal);
+ } else {
+ currentWeekLabel.setText("Current Week: None");
+ }
+ });
+
+ }
/**
* Exposes the advanceWeekButton to {@link GameController} so it can
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index 5426f5d..8a2cf80 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -175,7 +175,7 @@ protected void updateItem(Stock stock, boolean empty) {
HBox selectedStockBox = new HBox();
- Label selectedStockLabel = new Label("Selected Stock:");
+ Label selectedStockLabel = new Label("Selected Stock: ");
selectedStock = new Label("");
selectedStockBox.getChildren().addAll(selectedStockLabel, selectedStock);
From 8b16010bf57bd6bfd389fbbe3cab66a6c66bbc12 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 18:10:46 +0200
Subject: [PATCH 090/142] Added test to exchange for the currentweekproperty
---
.../java/no/ntnu/gruppe53/model/ExchangeTest.java | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
index 128badd..6e28192 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/ExchangeTest.java
@@ -1,6 +1,7 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -343,4 +344,14 @@ void sellWithNullShareShouldThrowIllegalArgumentException() {
"Should throw if share is null.");
}
+ @Test
+ void getCurrentWeekPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, exchange.getCurrentWeekProperty(),
+ "Should be Observable.");
+ assertEquals(exchange.getWeek(), exchange.getCurrentWeekProperty().getValue(),
+ "Should have correct week value.");
+ }
+
}
From c1dd2786ee368486c2a31b71bf7a175bd3d2ef07 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 19:46:34 +0200
Subject: [PATCH 091/142] Set up proper display of purchasecalculator values in
the marketView
---
.../gruppe53/controller/GameController.java | 18 +++++++-
.../gruppe53/model/PurchaseCalculator.java | 44 ++++++++++++++++++-
.../no/ntnu/gruppe53/view/MarketView.java | 36 ++++++++++++---
3 files changed, 91 insertions(+), 7 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index dad6f25..1caaaf8 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -132,6 +132,8 @@ public void initialize(NavigationBar nav, FooterBar footer) {
var series = StockLineChartService.buildSeries(currentStock, exchange);
marketView.updateChart(series, currentStock.getCompany());
});
+ marketView.setPurchaseCalculator(calculatePurchase(marketView.getSelectedStock(),
+ marketView.getQuantityPurchase()));
} else {
marketView.setSelectedStockLabel("");
marketView.clearChart();
@@ -144,7 +146,10 @@ public void initialize(NavigationBar nav, FooterBar footer) {
});
marketView.onQuantitySelect(() -> {
- //implement the quantity select which updates gross, commision and total
+ if (marketView.getSelectedStock() != null) {
+ marketView.setPurchaseCalculator(calculatePurchase(marketView.getSelectedStock(),
+ marketView.getQuantityPurchase()));
+ }
});
portfolioView.onShareSelection(share -> {
@@ -197,6 +202,17 @@ private void sellShare(Share selectedShare) {
}
}
+ private PurchaseCalculator calculatePurchase(Stock stock, int quantity) {
+ stock = marketView.getSelectedStock();
+ quantity = marketView.getQuantityPurchase();
+
+ Share share = new Share(stock, BigDecimal.valueOf(quantity), stock.getSalesPrice());
+
+
+
+ return purchaseCalculator = new PurchaseCalculator(share);
+ }
+
/**
* Creates a default error alert to be used for notifying the player of errors.
* @param title title of the error alert
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
index 37fe66a..a98e809 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
@@ -1,5 +1,8 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
import java.math.BigDecimal;
/**
@@ -10,9 +13,16 @@ public class PurchaseCalculator implements TransactionCalculator{
private final BigDecimal purchasePrice;
private final BigDecimal quantity;
+ //Objectproperties
+ private final ObjectProperty grossProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty commissionProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty totalProperty = new SimpleObjectProperty<>();
+
+
/**
* Sets the purchase price and quantity based on data from the given share
- *
+ *
Also sets up the ObjectProperties of the different calculations such
+ * that Subscriptions can listen to the calculations
* @param share the share to calculate purchase costs for
*/
public PurchaseCalculator(Share share) {
@@ -20,6 +30,10 @@ public PurchaseCalculator(Share share) {
this.purchasePrice = share.getPurchasePrice();
this.quantity = share.getQuantity();
+ this.grossProperty.set(calculateGross());
+ this.commissionProperty.set(calculateCommission());
+ this.totalProperty.set(calculateTotal());
+
}
/**
@@ -29,6 +43,7 @@ public PurchaseCalculator(Share share) {
*/
@Override
public BigDecimal calculateGross(){
+
return this.quantity.multiply(this.purchasePrice);
}
@@ -42,6 +57,8 @@ public BigDecimal calculateGross(){
public BigDecimal calculateCommission() {
BigDecimal commission = new BigDecimal("0.005");
+
+
return calculateGross().multiply(commission);
}
@@ -64,9 +81,34 @@ public BigDecimal calculateTax() {
@Override
public BigDecimal calculateTotal() {
+
return calculateGross().add(calculateCommission()).add(calculateTax());
}
+ /**
+ * Returns an observable of the calculated gross
+ * @return an observable of the calculated gross
+ */
+ public ObjectProperty getGrossProperty() {
+ return grossProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated commission
+ * @return an observable of the calculated commission
+ */
+ public ObjectProperty getCommissionProperty() {
+ return commissionProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated total
+ * @return an observable of the calculated total
+ */
+ public ObjectProperty getTotalProperty() {
+ return totalProperty;
+ }
+
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index 8a2cf80..dcdcf0c 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -1,5 +1,6 @@
package no.ntnu.gruppe53.view;
+import java.math.RoundingMode;
import java.util.function.Consumer;
import javafx.collections.transformation.SortedList;
@@ -13,10 +14,7 @@
import javafx.scene.layout.VBox;
import javafx.scene.chart.LineChart;
import javafx.util.Subscription;
-import no.ntnu.gruppe53.model.Exchange;
-import no.ntnu.gruppe53.model.Player;
-import no.ntnu.gruppe53.model.Stock;
-import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.model.*;
import no.ntnu.gruppe53.service.FormatBigDecimal;
/*
@@ -51,11 +49,14 @@ public class MarketView extends BorderPane {
//Input
private Spinner quantitySpinner;
private Exchange exchange;
- private Player player;
+ private PurchaseCalculator purchaseCalculator;
private final StockLineChartView stockLineChartView;
private Subscription playerSubscription;
private Subscription priceSubscription;
+ private Subscription grossSubscription;
+ private Subscription commissionSubscription;
+ private Subscription totalSubscription;
/**
* Initializes the StockLineChartView graph and runs the view
@@ -314,6 +315,31 @@ public void setExchange(Exchange exchange) {
}
}
+ public void setPurchaseCalculator(PurchaseCalculator purchaseCalculator) {
+ this.purchaseCalculator = purchaseCalculator;
+ grossSubscription = purchaseCalculator.getGrossProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ gross.setText("");
+ }
+ });
+ commissionSubscription = purchaseCalculator.getCommissionProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ commission.setText("");
+ }
+ });
+ totalSubscription = purchaseCalculator.getTotalProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ total.setText("");
+ }
+ });
+ }
+
/**
* Returns the selected stock in the stocksListView.
*
From b7cc682ea7f18359b58b87292692529df6678369 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 19:50:08 +0200
Subject: [PATCH 092/142] Added tests for the Objectproperties added to
Purchase Calculators
---
.../model/PurchaseCalculatorTest.java | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
index 4eb3102..4935a80 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseCalculatorTest.java
@@ -1,5 +1,6 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
import no.ntnu.gruppe53.model.PurchaseCalculator;
import no.ntnu.gruppe53.model.Share;
import no.ntnu.gruppe53.model.Stock;
@@ -9,6 +10,7 @@
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
//The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works
@@ -55,6 +57,36 @@ void testCalculateTotal() {
}
+ @Test
+ void getGrossPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, purchaseCalculator.getGrossProperty(),
+ "Should be Observable.");
+ assertEquals(purchaseCalculator.calculateGross(), purchaseCalculator.getGrossProperty().getValue(),
+ "Should have the correct gross calculation.");
+ }
+
+ @Test
+ void getCommissionPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, purchaseCalculator.getCommissionProperty(),
+ "Should be Observable.");
+ assertEquals(purchaseCalculator.calculateCommission(), purchaseCalculator.getCommissionProperty().getValue(),
+ "Should have the correct commission calculation.");
+ }
+
+ @Test
+ void getTotalPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, purchaseCalculator.getTotalProperty(),
+ "Should be Observable.");
+ assertEquals(purchaseCalculator.calculateTotal(), purchaseCalculator.getTotalProperty().getValue(),
+ "Should have the correct total calculation.");
+ }
+
}
From fec97744494deeea418c7e44852c247c7cd7d160 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 20:11:10 +0200
Subject: [PATCH 093/142] Added the same to PortfolioView
---
.../gruppe53/controller/GameController.java | 24 ++++++++-
.../ntnu/gruppe53/model/SaleCalculator.java | 46 +++++++++++++++++
.../no/ntnu/gruppe53/view/MarketView.java | 7 +++
.../no/ntnu/gruppe53/view/PortfolioView.java | 49 ++++++++++++++++++-
.../gruppe53/model/SaleCalculatorTest.java | 40 +++++++++++++++
5 files changed, 164 insertions(+), 2 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
index 1caaaf8..6bc40d8 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -155,7 +155,7 @@ public void initialize(NavigationBar nav, FooterBar footer) {
portfolioView.onShareSelection(share -> {
if (share != null) {
portfolioView.setSelectedShareLabel(share.getStock().getSymbol() + " - " + share.getStock().getCompany());
-
+ portfolioView.setSaleCalculator(calculateSale(portfolioView.getSelectedShare()));
} else {
portfolioView.setSelectedShareLabel("");
}
@@ -202,6 +202,14 @@ private void sellShare(Share selectedShare) {
}
}
+
+ /**
+ * Creates a {@link PurchaseCalculator} that can be used for the
+ * for the setPurchaseCalculator() function in the {@link MarketView}.
+ * @param stock the currently selected stock
+ * @param quantity the currently selected quantity
+ * @return {@link PurchaseCalculator}
+ */
private PurchaseCalculator calculatePurchase(Stock stock, int quantity) {
stock = marketView.getSelectedStock();
quantity = marketView.getQuantityPurchase();
@@ -213,6 +221,20 @@ private PurchaseCalculator calculatePurchase(Stock stock, int quantity) {
return purchaseCalculator = new PurchaseCalculator(share);
}
+ /**
+ * Creates a {@link SaleCalculator} that can be used for the
+ * for the setSaleCalculator() function in the {@link PortfolioView}.
+ * @param share
+ * @return {@link SaleCalculator}
+ */
+ private SaleCalculator calculateSale(Share share) {
+
+ share = portfolioView.getSelectedShare();
+
+ return saleCalculator = new SaleCalculator(share);
+
+ }
+
/**
* Creates a default error alert to be used for notifying the player of errors.
* @param title title of the error alert
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
index 872aa07..e96015a 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
@@ -1,5 +1,8 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -13,6 +16,12 @@ public class SaleCalculator implements TransactionCalculator {
private final BigDecimal salesPrice;
private final BigDecimal quantity;
+ //Objectproperties
+ private final ObjectProperty grossProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty commissionProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty taxProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty totalProperty = new SimpleObjectProperty<>();
+
/**
* Sets the quantity, purchase and sales price based on the given share.
*
@@ -22,6 +31,11 @@ public SaleCalculator(Share share) {
this.purchasePrice = share.getPurchasePrice();
this.quantity = share.getQuantity();
this.salesPrice = share.getStock().getSalesPrice();
+
+ this.grossProperty.set(calculateGross());
+ this.commissionProperty.set(calculateCommission());
+ this.taxProperty.set(calculateTax());
+ this.totalProperty.set(calculateTotal());
}
/**
@@ -89,4 +103,36 @@ public BigDecimal calculateTotal() {
.subtract(calculateTax())
.setScale(2, RoundingMode.HALF_UP);
}
+
+ /**
+ * Returns an observable of the calculated gross
+ * @return an observable of the calculated gross
+ */
+ public ObjectProperty getGrossProperty() {
+ return grossProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated commission
+ * @return an observable of the calculated commission
+ */
+ public ObjectProperty getCommissionProperty() {
+ return commissionProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated tax
+ * @return an observable of the calculated tax
+ */
+ public ObjectProperty getTaxProperty() {
+ return taxProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated total
+ * @return an observable of the calculated total
+ */
+ public ObjectProperty getTotalProperty() {
+ return totalProperty;
+ }
}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index dcdcf0c..380ef7b 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -315,6 +315,13 @@ public void setExchange(Exchange exchange) {
}
}
+ /**
+ * Sets the PurchaseCalculator to use for the view.
+ *
Uses a {@link Subscription} to observe the calculations made by the current
+ * Stock and Quantity selected.
+ *
+ * @param purchaseCalculator the purchaseCalculator to be used for the view
+ */
public void setPurchaseCalculator(PurchaseCalculator purchaseCalculator) {
this.purchaseCalculator = purchaseCalculator;
grossSubscription = purchaseCalculator.getGrossProperty().subscribe(newVal -> {
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
index 5fb1b3a..7388c16 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -21,6 +21,7 @@
import javafx.scene.text.TextFlow;
import javafx.util.Subscription;
import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.SaleCalculator;
import no.ntnu.gruppe53.model.Share;
import no.ntnu.gruppe53.model.Stock;
import no.ntnu.gruppe53.service.FormatBigDecimal;
@@ -61,6 +62,12 @@ public class PortfolioView extends BorderPane {
private ListView portfolioListView;
private Player player;
+ private SaleCalculator saleCalculator;
+
+ private Subscription grossSubscription;
+ private Subscription commissionSubscription;
+ private Subscription taxSubscription;
+ private Subscription totalSubscription;
/**
* Builds the view components and places them in the center pane.
@@ -255,6 +262,45 @@ public void setSelectedShareLabel(String selectedShareString) {
selectedShare.setText(selectedShareString);
}
+ /**
+ * Sets the SaleCalculator to use for the view.
+ *
Uses a {@link Subscription} to observe the calculations made by the current
+ * Share selected.
+ *
+ * @param saleCalculator the SaleCalculator to be used for the view
+ */
+ public void setSaleCalculator(SaleCalculator saleCalculator) {
+ this.saleCalculator = saleCalculator;
+ grossSubscription = saleCalculator.getGrossProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ gross.setText("");
+ }
+ });
+ commissionSubscription = saleCalculator.getCommissionProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ commission.setText("");
+ }
+ });
+ taxSubscription = saleCalculator.getTaxProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ tax.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN).abs());
+ } else {
+ tax.setText("");
+ }
+ });
+ totalSubscription = saleCalculator.getTotalProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ total.setText("");
+ }
+ });
+ }
+
/**
* Formats the text representation of the share in the portfolioListView
@@ -262,7 +308,8 @@ public void setSelectedShareLabel(String selectedShareString) {
* to the buy price of the share.
*
* @param share the share to be formatted
- * @param currentPrice the current price of the share
+ * @param currentPrice the cusort the stocks in the exchange alphabetically
+ * on their symbol/tickerrrent price of the share
* @return a {@link TextFlow} representation of the share
*/
private TextFlow createColoredShareText(Share share, BigDecimal currentPrice) {
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
index 0848f1e..fc0be79 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleCalculatorTest.java
@@ -1,5 +1,6 @@
package no.ntnu.gruppe53.model;
+import javafx.beans.property.ObjectProperty;
import no.ntnu.gruppe53.model.SaleCalculator;
import no.ntnu.gruppe53.model.Share;
import no.ntnu.gruppe53.model.Stock;
@@ -9,6 +10,7 @@
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
//The need to setScale(2) at all the BigDecimals is to ensure that assertEquals works
@@ -67,4 +69,42 @@ void testCalculateTotal() {
}
+ @Test
+ void getGrossPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, saleCalculator.getGrossProperty(),
+ "Should be Observable.");
+ assertEquals(saleCalculator.calculateGross(), saleCalculator.getGrossProperty().getValue(),
+ "Should have the correct gross calculation.");
+ }
+
+ @Test
+ void getCommissionPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, saleCalculator.getCommissionProperty(),
+ "Should be Observable.");
+ assertEquals(saleCalculator.calculateCommission(), saleCalculator.getCommissionProperty().getValue(),
+ "Should have the correct commission calculation.");
+ }
+
+ @Test
+ void getTaxPropertyShouldReturnObservable() {
+ assertInstanceOf(ObjectProperty.class, saleCalculator.getTaxProperty(),
+ "Should be Observable.");
+ assertEquals(saleCalculator.calculateTax(), saleCalculator.getTaxProperty().getValue(),
+ "Should have the correct tax calculation.");
+ }
+
+ @Test
+ void getTotalPropertyShouldReturnObservable() {
+
+
+ assertInstanceOf(ObjectProperty.class, saleCalculator.getTotalProperty(),
+ "Should be Observable.");
+ assertEquals(saleCalculator.calculateTotal(), saleCalculator.getTotalProperty().getValue(),
+ "Should have the correct total calculation.");
+ }
+
}
From c2cdc27c79ae70c932ec1868ce3fe1fc545ab437 Mon Sep 17 00:00:00 2001
From: Arelodgaard
Date: Sat, 23 May 2026 20:28:09 +0200
Subject: [PATCH 094/142] idk
---
.../java/no/ntnu/gruppe53/view/PlayerNameChooserView.java | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
index b844df9..669c5f2 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
@@ -9,6 +9,12 @@
import java.util.Optional;
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
/**
* Represents a prompt for the {@link Player}'s name
*/
From 860eee197b08f5a8e668b088035a5caaef424961 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:15:01 +0200
Subject: [PATCH 095/142] Updated FileHandlerTest
Fixed an error where it tried to look for FileHandler in view instead of service package.
---
.../src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
index 55e6133..b0a09f1 100644
--- a/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/FileHandlerTest.java
@@ -1,7 +1,6 @@
package no.ntnu.gruppe53.service;
import no.ntnu.gruppe53.model.Stock;
-import no.ntnu.gruppe53.view.FileHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
From 357fd60ceb1aa6d0c5a3402e4175f9c1e2939d53 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:15:29 +0200
Subject: [PATCH 096/142] Added TransactionFactory
Abstract class for creating transactions.
---
.../gruppe53/model/TransactionFactory.java | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java
new file mode 100644
index 0000000..9ddb766
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java
@@ -0,0 +1,33 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Factory for creating a {@link Transaction} for a {@link Share} in an {@link Exchange}.
+ *
Subclasses implement the specific type of transaction to be used.
+ */
+public abstract class TransactionFactory {
+
+ /**
+ * Creates a transaction.
+ *
+ * @param share the share involved
+ * @param week the transaction week
+ * @return a transaction
+ */
+ public Transaction create(Share share, int week) {
+
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null.");
+ }
+
+ if (week <= 0) {
+ throw new IllegalArgumentException("Week must be positive whole number.");
+ }
+
+ return createTransaction(share, week);
+ }
+
+ protected abstract Transaction createTransaction(
+ Share share, int week
+ );
+
+}
From b27f41987fdb34821eda33b1f9406d17a62d75b4 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:16:02 +0200
Subject: [PATCH 097/142] Added PurchaseFactory
Creates Purchase transactions.
---
.../ntnu/gruppe53/model/PurchaseFactory.java | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java
new file mode 100644
index 0000000..ffce90a
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java
@@ -0,0 +1,19 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Creates a {@link Purchase} {@link Transaction} of a {@link Share}.
+ */
+public class PurchaseFactory extends TransactionFactory{
+
+ /**
+ * Returns the purchase transaction for the given share and week.
+ *
+ * @param share the share to be purchased
+ * @param week the week of purchase
+ * @return the purchase transaction
+ */
+ @Override
+ protected Transaction createTransaction(Share share, int week) {
+ return new Purchase(share, week);
+ }
+}
From c033ef15abbeebba2c30589bc678ca177a60dbbc Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:18:10 +0200
Subject: [PATCH 098/142] Added SaleFactory
Creates Sale transactions.
---
.../no/ntnu/gruppe53/model/SaleFactory.java | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java
new file mode 100644
index 0000000..2340e5c
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java
@@ -0,0 +1,19 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Creates a {@link Sale} {@link Transaction} of a {@link Share}.
+ */
+public class SaleFactory extends TransactionFactory {
+
+ /**
+ * Returns the sale transaction of the given share and week.
+ *
+ * @param share the share to be sold
+ * @param week the week of the sale
+ * @return the sale transaction
+ */
+ @Override
+ protected Transaction createTransaction(Share share, int week) {
+ return new Sale(share, week);
+ }
+}
From a66d146a7b301c957f146fb04f85f4a2770ff673 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:18:38 +0200
Subject: [PATCH 099/142] Added PurchaseFactoryTest
Test class for PurchaseFactory.
---
.../gruppe53/model/PurchaseFactoryTest.java | 64 +++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java
new file mode 100644
index 0000000..cfab01a
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/PurchaseFactoryTest.java
@@ -0,0 +1,64 @@
+package no.ntnu.gruppe53.model;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PurchaseFactoryTest {
+ private int week;
+ private Share share;
+ private TransactionFactory purchaseFactory;
+ private Transaction transaction;
+
+ @BeforeEach
+ void setUp() {
+ String symbol = "AAPL";
+ String company = "Apple Inc.";
+ var price = new BigDecimal("100");
+
+ var testStock = new Stock(symbol,company, price);
+ var quantity = new BigDecimal("10");
+ var purchasePrice = testStock.getSalesPrice();
+
+ this.week = 1;
+ this.share = new Share(testStock, quantity, purchasePrice);
+ this.purchaseFactory = new PurchaseFactory();
+ this.transaction = purchaseFactory.create(share, week);
+ }
+
+ @Test
+ void createShouldMakeACorrectTransactionType() {
+ assertInstanceOf(Transaction.class, transaction,
+ "The returned value should be a transaction.");
+ assertInstanceOf(Purchase.class, transaction,
+ "Should be a purchase transaction.");
+ assertFalse(transaction instanceof Sale,
+ "Should not be a sale transaction.");
+ }
+
+ @Test
+ void shouldHaveCorrectShareAndWeek() {
+ assertEquals(share, transaction.getShare(),
+ "Should contain the correct share.");
+ assertEquals(week, transaction.getWeek(),
+ "Should contain the correct week.");
+ }
+
+ @Test
+ void shouldThrowIfShareIsNull() {
+ assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(null, week),
+ "Share should not be allowed to be null.");
+ }
+
+ @Test
+ void shouldThrowIfWeekIsZeroOrBelow() {
+ assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(share, 0),
+ "Week should not be allowed to be zero");
+ assertThrows(IllegalArgumentException.class, () -> purchaseFactory.create(share, -1),
+ "Week should not be allowed to be negative");
+ }
+
+}
\ No newline at end of file
From 48f8a56fa7836f9f4ae0c2aad9c9714ababde949 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:19:01 +0200
Subject: [PATCH 100/142] Added SaleFactoryTest
Test class for SaleFactory.
---
.../ntnu/gruppe53/model/SaleFactoryTest.java | 64 +++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java b/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java
new file mode 100644
index 0000000..3bfed18
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/model/SaleFactoryTest.java
@@ -0,0 +1,64 @@
+package no.ntnu.gruppe53.model;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SaleFactoryTest {
+ private int week;
+ private Share share;
+ private TransactionFactory saleFactory;
+ private Transaction transaction;
+
+ @BeforeEach
+ void setUp() {
+ String symbol = "AAPL";
+ String company = "Apple Inc.";
+ var price = new BigDecimal("100");
+
+ var testStock = new Stock(symbol,company, price);
+ var quantity = new BigDecimal("10");
+ var purchasePrice = testStock.getSalesPrice();
+
+ this.week = 1;
+ this.share = new Share(testStock, quantity, purchasePrice);
+ this.saleFactory = new SaleFactory();
+ this.transaction = saleFactory.create(share, week);
+ }
+
+ @Test
+ void createShouldMakeACorrectTransactionType() {
+ assertInstanceOf(Transaction.class, transaction,
+ "The returned value should be a transaction.");
+ assertInstanceOf(Sale.class, transaction,
+ "Should be a sale transaction.");
+ assertFalse(transaction instanceof Purchase,
+ "Should not be a purchase transaction.");
+ }
+
+ @Test
+ void shouldHaveCorrectShareAndWeek() {
+ assertEquals(share, transaction.getShare(),
+ "Should contain the correct share.");
+ assertEquals(week, transaction.getWeek(),
+ "Should contain the correct week.");
+ }
+
+ @Test
+ void shouldThrowIfShareIsNull() {
+ assertThrows(IllegalArgumentException.class, () -> saleFactory.create(null, week),
+ "Share should not be allowed to be null.");
+ }
+
+ @Test
+ void shouldThrowIfWeekIsZeroOrBelow() {
+ assertThrows(IllegalArgumentException.class, () -> saleFactory.create(share, 0),
+ "Week should not be allowed to be zero");
+ assertThrows(IllegalArgumentException.class, () -> saleFactory.create(share, -1),
+ "Week should not be allowed to be negative");
+ }
+
+}
\ No newline at end of file
From 6eb274fea72763f6c4e826c97840e1ccff9b024f Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 11:19:27 +0200
Subject: [PATCH 101/142] Updated Exchange
Exchange now uses factories for making transactions.
---
.../main/java/no/ntnu/gruppe53/model/Exchange.java | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
index cb106b6..f20919c 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
@@ -22,10 +22,13 @@ public class Exchange {
private final Random random;
private final ObservableList stocks = FXCollections.observableArrayList();
private final ObjectProperty currentWeekProperty = new SimpleObjectProperty<>();
+ private final TransactionFactory purchaseFactory;
+ private final TransactionFactory saleFactory;
/**
* Sets the name of the exchange and the list of stocks to populate it.
*
Validates that the values are proper before creating the exchange.
+ *
Initializes factories for creating a {@link Transaction}.
*
* @param name the name of the exchange
* @param stocks the list of stocks to be tradeable on the exchange
@@ -59,6 +62,9 @@ public Exchange(String name, List stocks) {
stockMap.put(stock.getSymbol(), stock);
this.stocks.add(stock);
}
+
+ this.purchaseFactory = new PurchaseFactory();
+ this.saleFactory = new SaleFactory();
}
/**
@@ -130,7 +136,7 @@ public List findStocks(String searchTerm) {
/**
* Represents a player buying a share on the exchange.
- *
Creates a {@link Purchase} {@link Transaction} for the
+ *
Creates a {@link Purchase} transaction for the
* purchase.
*
* @param symbol the symbol/ticker of the stock
@@ -146,7 +152,7 @@ public Transaction buy(String symbol, BigDecimal quantity, Player player) {
Stock stock = getStock(symbol);
Share share = new Share(stock, quantity, stock.getSalesPrice());
- Transaction transaction = new Purchase(share, week);
+ Transaction transaction = purchaseFactory.create(share, week);
transaction.commit(player);
return transaction;
@@ -165,7 +171,7 @@ public Transaction sell(Share share, Player player) {
if (player == null) throw new IllegalArgumentException("Player cannot be null");
if (share == null) throw new IllegalArgumentException("Share cannot be null");
- Transaction transaction = new Sale(share, week);
+ Transaction transaction = saleFactory.create(share, week);
transaction.commit(player);
return transaction;
}
From 17c04d92a76587a664becf6e17da62352f217286 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:48:07 +0200
Subject: [PATCH 102/142] Added LanguageManager
Acts as a manager that sets the language to be used by JavaFX, swaps the strings associated with UI elements and returns observables to watch for language changes.
---
.../gruppe53/service/LanguageManager.java | 96 +++++++++++++++++++
1 file changed, 96 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
new file mode 100644
index 0000000..09b9494
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
@@ -0,0 +1,96 @@
+package no.ntnu.gruppe53.service;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+/**
+ * Manages the language used in the user-interface.
+ */
+public class LanguageManager {
+
+ /**
+ * Sets a default language and creates an observable {@link ResourceBundle}.
+ */
+ private final ObjectProperty bundleProperty =
+ new SimpleObjectProperty<>();
+
+ private LanguageManager() {
+ setLocale(LanguageRegistry.getDefaultLocale());
+ }
+
+ /**
+ * Static Inner Class to lazily create the instance
+ */
+ private static class LanguageManagerInner {
+ private static final LanguageManager INSTANCE =
+ new LanguageManager();
+ }
+
+ /**
+ * Returns the instance of the language manager
+ *
+ * @return the instance of the language manager
+ */
+ public static LanguageManager getInstance() {
+ return LanguageManagerInner.INSTANCE;
+ }
+
+ /**
+ * Returns the observable resource bundle.
+ *
+ * @return an observable resource bundle
+ */
+ public ObjectProperty getBundleProperty() {
+ return bundleProperty;
+ }
+
+ /**
+ * Sets the locale of the instance to a given locale which determines the properties file to load
+ * values from.
+ *
+ * @param locale the locale to change to
+ */
+ public void setLocale(Locale locale) {
+ ResourceBundle bundle =
+ ResourceBundle.getBundle("i18n.lang", locale);
+
+ bundleProperty.set(bundle);
+ }
+
+ /**
+ * Returns the string value of the given key in a .properties file.
+ *
+ * @param key the key to look for in the .properties file
+ * @return the value of the key
+ */
+ public String getString(String key) {
+ return bundleProperty.get().getString(key);
+ }
+
+ /**
+ * Returns the current locale.
+ *
+ * @return the current locale
+ */
+ public Locale getLocale() {
+ return bundleProperty.get().getLocale();
+ }
+
+ /**
+ * Creates a {@link StringBinding} to a given string and associates it with a given key in a .properties file
+ *
+ * @param key the key to associate the string binding with
+ * @return a string binding observable
+ */
+ public StringBinding bindString(String key) {
+ return Bindings.createStringBinding(
+ () -> getString(key),
+ getBundleProperty()
+ );
+ }
+}
\ No newline at end of file
From d987e024af2d0ae055c4659e371a1cfc9085e5de Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:48:54 +0200
Subject: [PATCH 103/142] Added LanguageManagerTest
Test class for LanguageManager.
---
.../gruppe53/service/LanguageManagerTest.java | 68 +++++++++++++++++++
1 file changed, 68 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java
new file mode 100644
index 0000000..4f04ae2
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageManagerTest.java
@@ -0,0 +1,68 @@
+package no.ntnu.gruppe53.service;
+
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Constructor;
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class LanguageManagerTest {
+
+ @Test
+ void constructorShouldBePrivate() throws Exception {
+ Constructor constructor =
+ LanguageManager.class.getDeclaredConstructor();
+
+ assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers()));
+ }
+
+ @Test
+ void instanceShouldBeUniform() {
+ var lm1 = LanguageManager.getInstance();
+ var lm2 = LanguageManager.getInstance();
+
+ assertEquals(lm1, lm2, "The instance should be unchanged.");
+ }
+
+ @Test
+ void shouldLoadStringsCorrectly() {
+ LanguageManager manager = LanguageManager.getInstance();
+
+ manager.setLocale(new Locale("en"));
+
+ String value = manager.getString("hello");
+
+ assertEquals("Hello world", value);
+ }
+
+ @Test
+ void shouldReturnCorrectLocale() {
+ LanguageManager manager = LanguageManager.getInstance();
+
+ manager.setLocale(new Locale("en"));
+
+ assertEquals(new Locale("en"), manager.getLocale());
+ }
+
+ @Test
+ void getBundlePropertyShouldBeObservable() {
+ assertInstanceOf(ObjectProperty.class, LanguageManager.getInstance().getBundleProperty());
+ }
+
+ @Test
+ void bindStringShouldBeReactiveOnLocaleChange() {
+ LanguageManager lm = LanguageManager.getInstance();
+
+ lm.setLocale(new Locale("en"));
+ StringBinding binding = lm.bindString("hello");
+
+ assertEquals("Hello world", binding.get(), "Should return english version of key.");
+
+ lm.setLocale(new java.util.Locale("no"));
+
+ assertEquals("Hallo verden", binding.get(), "Should return norwegian version of key.");
+ }
+}
\ No newline at end of file
From 4c8fd8849003d95d6168deb04d5914d9daf36ed3 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:50:00 +0200
Subject: [PATCH 104/142] Added lang_en.properties
Properties test file for the english language.
---
millions/src/test/resources/i18n/lang_en.properties | 1 +
1 file changed, 1 insertion(+)
create mode 100644 millions/src/test/resources/i18n/lang_en.properties
diff --git a/millions/src/test/resources/i18n/lang_en.properties b/millions/src/test/resources/i18n/lang_en.properties
new file mode 100644
index 0000000..6d5d7ab
--- /dev/null
+++ b/millions/src/test/resources/i18n/lang_en.properties
@@ -0,0 +1 @@
+hello = Hello world
\ No newline at end of file
From 83de3476b51fa80c1db3deb01ebc34eae6509f65 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:50:24 +0200
Subject: [PATCH 105/142] Added lang_no.properties
Properties test file for the norwegian language.
---
millions/src/test/resources/i18n/lang_no.properties | 1 +
1 file changed, 1 insertion(+)
create mode 100644 millions/src/test/resources/i18n/lang_no.properties
diff --git a/millions/src/test/resources/i18n/lang_no.properties b/millions/src/test/resources/i18n/lang_no.properties
new file mode 100644
index 0000000..05bdb92
--- /dev/null
+++ b/millions/src/test/resources/i18n/lang_no.properties
@@ -0,0 +1 @@
+hello = Hallo verden
\ No newline at end of file
From 61b0244595e1860e2361bd9569f55e94241a1ecb Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:51:43 +0200
Subject: [PATCH 106/142] Added lang.properties
Default properties file that loads if target locale was not found.
---
.../src/main/resources/i18n/lang.properties | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 millions/src/main/resources/i18n/lang.properties
diff --git a/millions/src/main/resources/i18n/lang.properties b/millions/src/main/resources/i18n/lang.properties
new file mode 100644
index 0000000..ac7a7dd
--- /dev/null
+++ b/millions/src/main/resources/i18n/lang.properties
@@ -0,0 +1,62 @@
+# Default file to use if there is an error with language locale
+
+# StartView
+newGame = New Game
+languageButtonLabel = Toggle Language
+quitGame = Quit Game
+
+# PlayerNameChooserView
+playerNameConfirmButton = Confirm
+playerNameInfo = Please enter your player name:
+
+# PlayerStartingMoneyChooserView
+playerStartingMoneyConfirmButton = Confirm
+playerStartingMoneyInfoLabel = Select your starting money balance for trading:
+
+# CSVChooserView
+csvHeader = CSV File Required
+csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored.
+csvFileChooserTitle = Select CSV File
+csvFileFilterLabel = CSV Files
+
+# NavigationBar
+navMoneyLabelText = Money:
+navNetWorthLabelText = Net Worth:
+navPlayerLabelText = Player:
+marketButton = Market
+portfolioButton = Portfolio
+historyButton = History
+
+# FooterBar
+currentWeekLabelText = Current Week:
+advanceWeekButton = Advance Week
+
+# MarketView
+marketTitle = Stock Market
+xAxisLabel = Week
+yAxisLabel = Price
+selectedStock = Selected Stock:
+purchaseQuantity = Quantity:
+purchaseGross = Gross:
+purchaseCommission = Commission:
+purchaseTotal = Total:
+purchaseButton = Purchase
+stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s
+
+# PortfolioView
+portfolioTitle = Portfolio
+selectedShare = Selected Share:
+portfolioGross = Gross:
+portfolioCommission = Commission:
+portfolioTax = Tax:
+portfolioTotal = Total:
+sellButton = Sell
+portfolioShareQuantity = Qty:
+portfolioBuyPrice = Buy price:
+portfolioCurrentPrice = Current Price:
+
+# TransactionArchiveView
+historyTitle=Transaction History
+historyTypePurchase=Purchase
+historyTypeSale=Sale
+historyCellFormat=Week %d | %s | %s | Qty: %s | Total: $%s
From e5927c979cb8b86765943b5d5b136e19e4645f93 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:52:09 +0200
Subject: [PATCH 107/142] Added lang_en.properties
Properties file for the english language.
---
.../main/resources/i18n/lang_en.properties | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 millions/src/main/resources/i18n/lang_en.properties
diff --git a/millions/src/main/resources/i18n/lang_en.properties b/millions/src/main/resources/i18n/lang_en.properties
new file mode 100644
index 0000000..3e162bc
--- /dev/null
+++ b/millions/src/main/resources/i18n/lang_en.properties
@@ -0,0 +1,62 @@
+# File for the english language
+
+# StartView
+newGame = New Game
+languageButtonLabel = Toggle Language
+quitGame = Quit Game
+
+# PlayerNameChooserView
+playerNameConfirmButton = Confirm
+playerNameInfo = Please enter your player name:
+
+# PlayerStartingMoneyChooserView
+playerStartingMoneyConfirmButton = Confirm
+playerStartingMoneyInfoLabel = Select your starting money balance for trading:
+
+# CSVChooserView
+csvHeader = CSV File Required
+csvContent = Please select a .csv file of the stocks to be used for the exchange in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used.\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble.\n\nExample: NVDA,Nvidia,191.27\n\n'#' signifies comments, and along with lines not formatted correctly, will be ignored.
+csvFileChooserTitle = Select CSV File
+csvFileFilterLabel = CSV Files
+
+# NavigationBar
+navMoneyLabelText = Money:
+navNetWorthLabelText = Net Worth:
+navPlayerLabelText = Player:
+marketButton = Market
+portfolioButton = Portfolio
+historyButton = History
+
+# FooterBar
+currentWeekLabelText = Current Week:
+advanceWeekButton = Advance Week
+
+# MarketView
+marketTitle = Stock Market
+xAxisLabel = Week
+yAxisLabel = Price
+selectedStock = Selected Stock:
+purchaseQuantity = Quantity:
+purchaseGross = Gross:
+purchaseCommission = Commission:
+purchaseTotal = Total:
+purchaseButton = Purchase
+stockCellFormat=%s - %s | Price: $%s | Price change: %s | Highest price: %s | Lowest price: %s
+
+# PortfolioView
+portfolioTitle = Portfolio
+selectedShare = Selected Share:
+portfolioGross = Gross:
+portfolioCommission = Commission:
+portfolioTax = Tax:
+portfolioTotal = Total:
+sellButton = Sell
+portfolioShareQuantity = Qty:
+portfolioBuyPrice = Buy price:
+portfolioCurrentPrice = Current Price:
+
+# TransactionArchiveView
+historyTitle=Transaction History
+historyTypePurchase=Purchase
+historyTypeSale=Sale
+historyCellFormat=Week %d | %s | %s | Qty: %s | Total: $%s
From bc20399ff73a2c2f6af03e9a90abd92334e1e642 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:52:26 +0200
Subject: [PATCH 108/142] Added lang_en.properties
Properties file for the norwegian language.
---
.../main/resources/i18n/lang_no.properties | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 millions/src/main/resources/i18n/lang_no.properties
diff --git a/millions/src/main/resources/i18n/lang_no.properties b/millions/src/main/resources/i18n/lang_no.properties
new file mode 100644
index 0000000..63261ec
--- /dev/null
+++ b/millions/src/main/resources/i18n/lang_no.properties
@@ -0,0 +1,62 @@
+# File for the norwegian language
+
+# StartView
+newGame = Nytt Spill
+languageButtonLabel = Skift Språk
+quitGame = Avslutt
+
+# PlayerNameChooserView
+playerNameConfirmButton = Bekreft
+playerNameInfo = Vennligst skriv spillernavnet ditt:
+
+# PlayerStartingMoneyChooserView
+playerStartingMoneyConfirmButton = Bekreft
+playerStartingMoneyInfoLabel = Velg startsaldo for aksjehandel:
+
+# CSVChooserView
+csvHeader = CSV Fil Påkrevd
+csvContent = Vennligst velg en .csv fil som inneholder aksjene som skal brukes på børsen i neste vindu. \n\nDersom ingen fil velges (eller .csv filen er tom eller har feil struktur), vil en standard fil bli brukt. \n\nFilen må formates slik: Aksjesymbol,FirmaNavn,PrisTypeDobbel. \n\nEksempel: NVDA,Nvidia,191.27\n\n'#' brukes for kommentarer, og som med feilformaterte linjer, vil slike linjer bli ignorert.
+csvFileChooserTitle = Velg CSV fil
+csvFileFilterLabel = CSV Filer
+
+# NavigationBar
+navMoneyLabelText = Saldo:
+navNetWorthLabelText = Nettoformue:
+navPlayerLabelText = Spiller:
+marketButton = Marked
+portfolioButton = Portefølje
+historyButton = Historikk
+
+# FooterBar
+currentWeekLabelText = Gjeldende Uke:
+advanceWeekButton = Neste Uke
+
+# MarketView
+marketTitle = Aksjemarked
+xAxisLabel = Uke
+yAxisLabel = Pris
+selectedStock = Valgt aksje:
+purchaseQuantity = Antall:
+purchaseGross = Brutto:
+purchaseCommission = Kurtasje:
+purchaseTotal = Total:
+purchaseButton = Kjøp
+stockCellFormat=%s - %s | Pris: $%s | Prisendring: %s | Høyeste pris: %s | Laveste pris: %s
+
+# PortfolioView
+portfolioTitle = Portefølje
+selectedShare = Valgt andel:
+portfolioGross = Brutto:
+portfolioCommission = Kurtasje:
+portfolioTax = Skatt:
+portfolioTotal = Total:
+sellButton = Selg
+portfolioShareQuantity = Ant:
+portfolioBuyPrice = Kjøpspris:
+portfolioCurrentPrice = Gjeldende pris:
+
+# TransactionArchiveView
+historyTitle=Transaksjonshistorikk
+historyTypePurchase=Kjøp
+historyTypeSale=Salg
+historyCellFormat=Uke %d | %s | %s | Ant: %s | Total: $%s
\ No newline at end of file
From 6618ef3c72b311180fd9dc94bcc390c250f6710c Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:53:03 +0200
Subject: [PATCH 109/142] Added LanguageOption
Act as a datacarrier for language.
---
.../no/ntnu/gruppe53/service/LanguageOption.java | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java
new file mode 100644
index 0000000..cd607b1
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java
@@ -0,0 +1,12 @@
+package no.ntnu.gruppe53.service;
+
+import java.util.Locale;
+
+/**
+ * Acts as a data-carrier representing a language with a string name and a {@link Locale}.
+ *
+ * @param name the name of the language
+ * @param locale the locale of the language
+ */
+public record LanguageOption(String name, Locale locale) {}
+
From 77a243bdee455198d2c72b21ea49ebf802c42471 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 20:53:39 +0200
Subject: [PATCH 110/142] Added LanguageRegistry
A registry of all possible languages that can be selected by LanguageManager.
---
.../gruppe53/service/LanguageRegistry.java | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java
new file mode 100644
index 0000000..1122d2a
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java
@@ -0,0 +1,54 @@
+package no.ntnu.gruppe53.service;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Represents a registry of allowed {@link LanguageOption}s.
+ */
+public class LanguageRegistry {
+
+ /**
+ * Sets all possible languages that can be used.
+ */
+ private static final List languages = List.of(
+ new LanguageOption("English", new Locale("en")),
+ new LanguageOption("Norsk", new Locale("no"))
+ );
+
+ /**
+ * Sets the default language to be used.
+ *
Searches through the registry to find english and sets it as default.
+ */
+ private static final LanguageOption defaultLang = languages.stream()
+ .filter(lang -> lang.locale().getLanguage().equals("en"))
+ .findFirst()
+ .orElse(languages.getFirst());
+
+ /**
+ * Returns all registered languages
+ *
+ * @return a list of registered languages
+ */
+ public static List getLanguages() {
+ return languages;
+ }
+
+ /**
+ * Returns the default language.
+ *
+ * @return the default language
+ */
+ public static LanguageOption getDefaultLanguage() {
+ return defaultLang;
+ }
+
+ /**
+ * Returns the default locale of the default language.
+ *
+ * @return the default locale
+ */
+ public static Locale getDefaultLocale() {
+ return defaultLang.locale();
+ }
+}
From 52ccd01b70b19215d0784245d059be753edf0963 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:14:11 +0200
Subject: [PATCH 111/142] Added LanguageRegistryTest
Test file for LanguageRegistry.
---
.../service/LanguageRegistryTest.java | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java
diff --git a/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java
new file mode 100644
index 0000000..ab72057
--- /dev/null
+++ b/millions/src/test/java/no/ntnu/gruppe53/service/LanguageRegistryTest.java
@@ -0,0 +1,37 @@
+package no.ntnu.gruppe53.service;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class LanguageRegistryTest {
+ @Test
+ void getLanguageShouldReturnCorrectAmountAndCorrectLanguages() {
+ var languages = LanguageRegistry.getLanguages();
+ boolean hasEnglish = languages.stream()
+ .anyMatch(lang -> lang.locale().equals(new Locale("en")));
+ boolean hasNorwegian = languages.stream()
+ .anyMatch(lang -> lang.locale().equals(new Locale("no")));
+
+ assertEquals(2, languages.size(), "Should be 2 languages.");
+ assertTrue(hasEnglish, "English should be a supported language.");
+ assertTrue(hasNorwegian, "Norwegian should be a supported language.");
+ }
+
+ @Test
+ void getDefaultLanguageShouldEqualEnglish() {
+ assertEquals("English", LanguageRegistry.getDefaultLanguage().name(),
+ "Default language name should be 'English'.");
+ assertEquals(new Locale("en"), LanguageRegistry.getDefaultLanguage().locale(),
+ "Default language locale should be 'en'.");
+ }
+
+ @Test
+ void getDefaultLocaleShouldReturnEnglish() {
+ assertEquals(new Locale("en"), LanguageRegistry.getDefaultLocale(),
+ "Default language locale should be 'en'.");
+ }
+
+}
\ No newline at end of file
From 492f7cf25eb267eb09c4c14e61b41720268e43a6 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:29:31 +0200
Subject: [PATCH 112/142] Updated StartView
Changed buttons and labels to use LanguageManager for string generation.
---
.../java/no/ntnu/gruppe53/view/StartView.java | 39 ++++++++++++++++---
1 file changed, 33 insertions(+), 6 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java
index 5756028..a49d52b 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java
@@ -5,16 +5,23 @@
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
+
+import javafx.util.Subscription;
import no.ntnu.gruppe53.controller.StartViewController;
import no.ntnu.gruppe53.controller.ViewController;
+import no.ntnu.gruppe53.service.LanguageManager;
/**
* Represents the first page seen by the user of the application.
+ *
Uses a {@link LanguageManager} for dynamically setting button and label text.
+ * Subscribes to changes in language set by the manager.
*/
public class StartView extends VBox {
private final Button newGameButton;
+ private final Button languageButton;
private final Button quitButton;
private final Image logo;
+ private Subscription languageSubscription;
/**
* Constructs the view and sets parameters to be exposed for the {@link StartViewController}.
@@ -22,6 +29,8 @@ public class StartView extends VBox {
* @param vm the {@link ViewController} for the view
*/
public StartView(ViewController vm) {
+ LanguageManager lm = LanguageManager.getInstance();
+
this.setAlignment(Pos.CENTER);
this.setSpacing(20);
@@ -32,29 +41,47 @@ public StartView(ViewController vm) {
logoView.setFitWidth(300);
logoView.setPreserveRatio(true);
- newGameButton = new Button("New Game");
- quitButton = new Button("Quit");
+ newGameButton = new Button();
+ newGameButton.textProperty().bind(lm.bindString("newGame"));
+
+ languageButton = new Button();
+ languageButton.setPrefWidth(200);
+ languageButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+ languageSubscription = lm.getBundleProperty().subscribe(bundle -> {
+ languageButton.setText(lm.getString("languageButtonLabel"));
+ });
+
+ quitButton = new Button();
+ quitButton.textProperty().bind(lm.bindString("quitGame"));
newGameButton.setPrefWidth(200);
newGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
quitButton.setPrefWidth(200);
quitButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
- this.getChildren().addAll(logoView, newGameButton, quitButton);
+ this.getChildren().addAll(logoView, newGameButton, languageButton, quitButton);
}
/**
- * Sets the method to be run when
+ * Sets the method to be run when the new game button is pressed
*
- * @param action
+ * @param action the method to execute on button press
*/
public void setOnNewGame(Runnable action) {
newGameButton.setOnAction(e -> action.run());
}
/**
+ * Sets the method to be run when the languageButton is pressed
+ *
+ * @param action the method to execute on button press
+ */
+ public void setOnLanguage(Runnable action) {languageButton.setOnAction(e -> action.run());}
+
+ /**
+ * Sets the method to be run when the quit game button is pressed
*
- * @param action
+ * @param action the method to execute on button press
*/
public void setOnQuit(Runnable action) {
quitButton.setOnAction(e -> action.run());
From 0423e585ca5ce9f820ea2c477b0d1c3020bc3a18 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:33:13 +0200
Subject: [PATCH 113/142] Updated StartViewController
Added method to change language when clicking the language button in StartView. Updated JavaDoc.
---
.../controller/StartViewController.java | 28 ++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java
index de9e1ae..82d8fde 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java
@@ -1,16 +1,23 @@
package no.ntnu.gruppe53.controller;
+import no.ntnu.gruppe53.service.LanguageManager;
+import no.ntnu.gruppe53.service.LanguageOption;
+import no.ntnu.gruppe53.service.LanguageRegistry;
import no.ntnu.gruppe53.view.StartView;
import javafx.application.Platform;
+import java.util.List;
+
/**
* The controller for {@link StartView},
* the first view for the player upon launching the app.
- *
Contains two choices:
+ *
Contains three choices:
*
*
New Game
+ *
Language Select
*
Quit
*
+ *
Uses a {@link LanguageManager} to change the UI language.
*/
public class StartViewController {
/**
@@ -25,6 +32,25 @@ public StartViewController(StartView view, ViewController vm) {
gameController.startNewGame();
});
+ view.setOnLanguage(() -> {
+ LanguageManager lm = LanguageManager.getInstance();
+ List availableLanguages = LanguageRegistry.getLanguages();
+
+ int currentIndex = 0;
+ for (int i = 0; i < availableLanguages.size(); i++) {
+ if (availableLanguages.get(i).locale().equals(lm.getLocale())) {
+ currentIndex = i;
+ break;
+ }
+ }
+
+ int nextIndex = (currentIndex + 1) % availableLanguages.size();
+ LanguageOption nextLanguage = availableLanguages.get(nextIndex);
+
+ lm.setLocale(nextLanguage.locale());
+ });
+
+
view.setOnQuit(Platform::exit);
}
}
\ No newline at end of file
From afea12c4e362d1992c52cf51660f295777a9cb37 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:35:18 +0200
Subject: [PATCH 114/142] Updated PlayerNameChooserView
Updated to use a LanguageManager to insert text in given language. Updated JavaDoc.
---
.../gruppe53/view/PlayerNameChooserView.java | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
index 669c5f2..0711ae2 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
@@ -6,6 +6,7 @@
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
+import no.ntnu.gruppe53.service.LanguageManager;
import java.util.Optional;
@@ -19,18 +20,22 @@
* Represents a prompt for the {@link Player}'s name
*/
public class PlayerNameChooserView {
- Dialog dialog = new Dialog<>();
+ private final LanguageManager lm = LanguageManager.getInstance();
+ Dialog dialog;
/**
* Returns the dialog where the player can input a name of maximum 50 characters.
*
A {@link TextFormatter} is used to set the maximum amount
* of characters for the player name.
+ *
Uses a {@link LanguageManager} to determine text language.
*
* @return the dialog for player name input
*/
public Optional getDialog() {
+ dialog = new Dialog<>();
+
// Sets dialog title and header text
- dialog.setTitle("Player Name");
+ dialog.setTitle(null);
dialog.setHeaderText(null);
// Sets the dimensions for the dialog pane
@@ -41,8 +46,10 @@ public Optional getDialog() {
ButtonType confirmButton =
new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
-
dialog.getDialogPane().getButtonTypes().add(confirmButton);
+ Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton);
+
+ actualConfirmButton.textProperty().bind(lm.bindString("playerNameConfirmButton"));
TextField nameField = new TextField();
@@ -73,9 +80,8 @@ public Optional getDialog() {
counterLabel.setText(newVal.length() + " / 50");
});
- Label infoLabel = new Label(
- "Please enter your player name:"
- );
+ Label infoLabel = new Label();
+ infoLabel.textProperty().bind(lm.bindString("playerNameInfo"));
VBox layout = new VBox(15);
From 00d63a8e27471e3b52516726b5ccb2bb6c3b85c4 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:36:29 +0200
Subject: [PATCH 115/142] Updated CSVChooserView
Updated to use LanguageManager to set text to given language. Updated JavaDoc.
---
.../no/ntnu/gruppe53/view/CSVChooserView.java | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
index 092e53f..a60d295 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/CSVChooserView.java
@@ -3,15 +3,18 @@
import javafx.scene.control.Alert;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
+import no.ntnu.gruppe53.service.LanguageManager;
import java.io.File;
/**
* Represents an {@link Alert} and {@link FileChooser} for informing user of .csv requirement and picking said file.
+ *
Uses a {@link LanguageManager} to insert set text to target language.
*/
public class CSVChooserView {
private final FileChooser fileChooser = new FileChooser();
Alert newGameAlert = new Alert(Alert.AlertType.INFORMATION);
+ private final LanguageManager lm = LanguageManager.getInstance();
/**
* Returns the dialog for the alert and file chooser and exposes it to controllers.
@@ -21,13 +24,9 @@ public class CSVChooserView {
*/
public File getDialog(Stage stage) {
// Notifies the user about .csv selection
- newGameAlert.setTitle("Please read:");
- newGameAlert.setHeaderText("CSV File Required");
- newGameAlert.setContentText("Please select a .csv file of the stocks to be used for the exchange " +
- " in the next window.\n\nIf none is selected (or if .csv file is empty/malformed), a default file will be used." +
- "\n\nThe file needs to be formated as: StockTicker,CorporationName,PriceTypeDouble." +
- "\n\nExample: NVDA,Nvidia,191.27" + "\n\n'#' signifies comments, and along with lines not formatted " +
- "correctly, will be ignored.");
+ newGameAlert.setTitle(null);
+ newGameAlert.headerTextProperty().bind(lm.bindString("csvHeader"));
+ newGameAlert.contentTextProperty().bind(lm.bindString("csvContent"));
// Sets the dimensions of the alert to show all text, and as a band-aid, be resizable
newGameAlert.setResizable(true);
@@ -38,11 +37,12 @@ public File getDialog(Stage stage) {
newGameAlert.showAndWait();
// File chooser to choose .csv file
- fileChooser.setTitle("Select CSV File");
+ fileChooser.setTitle(lm.getString("csvFileChooserTitle"));
// Limits to .csv file only
+ fileChooser.getExtensionFilters().clear();
fileChooser.getExtensionFilters().add(
- new FileChooser.ExtensionFilter("CSV Files", "*.csv")
+ new FileChooser.ExtensionFilter(lm.getString("csvFileFilterLabel"), "*.csv")
);
return fileChooser.showOpenDialog(stage); }
From 00f6e802d52c145e2268b04a2edc9b371ebcee3a Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:38:48 +0200
Subject: [PATCH 116/142] Updated PlayerStartingMoneyChooserView
Updated to set text of UI using language manager. Updated JavaDoc.
---
.../view/PlayerStartingMoneyChooserView.java | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
index 82b0c50..7dd6d65 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
@@ -5,15 +5,18 @@
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
+import no.ntnu.gruppe53.service.LanguageManager;
import java.math.BigDecimal;
import java.util.Optional;
/**
* Represents the prompt for the {@link Player}'s starting money.
+ *
Uses a {@link LanguageManager} to set text to target language.
*/
public class PlayerStartingMoneyChooserView {
Dialog dialog = new Dialog<>();
+ private final LanguageManager lm = LanguageManager.getInstance();
/**
* Returns the dialog for the player's starting money.
@@ -25,7 +28,7 @@ public class PlayerStartingMoneyChooserView {
public Optional getDialog() {
// Sets dialog title and header text
- dialog.setTitle("Select starting money:");
+ dialog.setTitle(null);
dialog.setHeaderText(null);
// Sets the dimensions for the dialog pane
@@ -36,9 +39,12 @@ public Optional getDialog() {
ButtonType confirmButton =
new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
-
dialog.getDialogPane().getButtonTypes().add(confirmButton);
+ Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton);
+
+ actualConfirmButton.textProperty().bind(lm.bindString("playerStartingMoneyConfirmButton"));
+
Spinner spinner = new Spinner<>();
// Sets allowed values and step size for the spinner
@@ -78,9 +84,8 @@ public Optional getDialog() {
spinner.setPrefWidth(200);
- Label infoLabel = new Label(
- "Select your starting money balance for trading:"
- );
+ Label infoLabel = new Label();
+ infoLabel.textProperty().bind(lm.bindString("playerStartingMoneyInfoLabel"));
VBox layout = new VBox(15);
From 3126b4049e8b66ee750b486ba9494c2c19a412cf Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:42:50 +0200
Subject: [PATCH 117/142] Updated NavigationBar
Updated to use a LanguageManager to set text dynamically. Updated JavaDoc.
---
.../no/ntnu/gruppe53/view/NavigationBar.java | 34 +++++++++++++------
1 file changed, 23 insertions(+), 11 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
index d6f7415..e494941 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
@@ -9,6 +9,7 @@
import javafx.scene.layout.Region;
import javafx.util.Subscription;
import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.service.LanguageManager;
import java.math.RoundingMode;
@@ -26,8 +27,10 @@
* clicking on the buttons representing the desired view.
*
The {@link no.ntnu.gruppe53.controller.GameController} handles
* the actions for the buttons.
+ *
Uses a {@link LanguageManager} to set text of UI elements dynamically.
*/
public class NavigationBar extends HBox {
+ private final LanguageManager lm = LanguageManager.getInstance();
private final Button marketButton;
private final Button portfolioButton;
@@ -45,7 +48,10 @@ public class NavigationBar extends HBox {
private Label netWorthLabel;
private Label moneyLabel;
-
+ /**
+ * Constructs the UI elements of the navigationBar.
+ *
Binds the language manager to the textProperty of the UI elements to dynamically change the text.
+ */
public NavigationBar() {
this.setPadding(new Insets(15, 12, 15, 12));
@@ -56,11 +62,14 @@ public NavigationBar() {
"-fx-border-width: 0 0 5 0;");
- marketButton = new Button("Market");
+ marketButton = new Button();
+ marketButton.textProperty().bind(lm.bindString("marketButton"));
marketButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
- portfolioButton = new Button("Portfolio");
+ portfolioButton = new Button();
+ portfolioButton.textProperty().bind(lm.bindString("portfolioButton"));
portfolioButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
- historyButton = new Button("History");
+ historyButton = new Button();
+ historyButton.textProperty().bind(lm.bindString("historyButton"));
historyButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
Region spacer = new Region();
@@ -68,7 +77,8 @@ public NavigationBar() {
HBox playerBox = new HBox();
- Label playerLabelText = new Label("Player: ");
+ Label playerLabelText = new Label();
+ playerLabelText.textProperty().bind(lm.bindString("navPlayerLabelText"));
playerLabel = new Label("Player");
playerBox.setStyle("-fx-background-color: #ffe556;" +
@@ -85,7 +95,8 @@ public NavigationBar() {
HBox moneyBox = new HBox();
- Label moneyLabelText = new Label("Money: ");
+ Label moneyLabelText = new Label();
+ moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText"));
moneyLabel = new Label();
moneyBox.setStyle("-fx-background-color: #ffe556;" +
@@ -100,7 +111,8 @@ public NavigationBar() {
HBox networthBox = new HBox();
- Label networthLabelText = new Label("Net Worth: ");
+ Label networthLabelText = new Label();
+ networthLabelText.textProperty().bind(lm.bindString("navNetWorthLabelText"));
netWorthLabel = new Label();
networthBox.setStyle("-fx-background-color: #ffe556;" +
@@ -161,16 +173,16 @@ public void bindPlayer(Player player) {
playerLabel.setText(player.getName());
netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> {
if (newVal != null) {
- netWorthLabel.setText("Net Worth: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ netWorthLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN));
} else {
- netWorthLabel.setText("Net Worth: $0.00");
+ netWorthLabel.setText("$0.00");
}
});
moneySubscription = player.getMoneyProperty().subscribe(newVal -> {
if (newVal != null) {
- moneyLabel.setText("Money: $" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ moneyLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN));
} else {
- moneyLabel.setText("Money: $0.00");
+ moneyLabel.setText("$0.00");
}
});
From 41ad7cbbb9300cfb16f04585379caef38f7a0f0e Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:47:26 +0200
Subject: [PATCH 118/142] Updated FooterBar
Updated to use LanguageManager to set text dynamically and uses a ComboBox to change language quickly. Updated JavaDoc.
---
.../java/no/ntnu/gruppe53/view/FooterBar.java | 171 +++++++++++++-----
1 file changed, 122 insertions(+), 49 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
index efea8a3..58cf825 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
@@ -1,18 +1,25 @@
package no.ntnu.gruppe53.view;
-import java.math.RoundingMode;
+import java.util.Locale;
+import java.util.function.Consumer;
+
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.util.Subscription;
-import no.ntnu.gruppe53.model.Player;
+
import no.ntnu.gruppe53.controller.GameController;
import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.service.LanguageManager;
+import no.ntnu.gruppe53.service.LanguageOption;
+import no.ntnu.gruppe53.service.LanguageRegistry;
/*
Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
@@ -23,65 +30,117 @@
/**
* A {@link HBox} view representing the footer/bottom to be used in a {@link BorderPane} layout.
+ *
Uses a {@link LanguageManager} to change text to target language dynamically.
*/
public class FooterBar extends HBox {
private final Button advanceWeekButton;
- private Label currentWeekLabel;
- private Subscription currentWeekSubscription;
+ private Label currentWeekLabel;
+ private Subscription currentWeekSubscription;
- private Exchange exchange;
+ private Exchange exchange;
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private final ComboBox languageBox;
/**
* Constructs the layout of the view and its components.
+ *
Constructs a {@link ComboBox} to let the user switch language on the fly.
+ *
Sets the text of the UI elements dynamically.
*/
public FooterBar() {
this.setSpacing(10);
this.setPadding(new Insets(15, 12, 15, 12));
- this.setStyle("-fx-background-color: #00bcf0;" +
- "-fx-border-color: #303539 transparent transparent transparent;" +
- "-fx-border-width: 5 0 0 0;");
-
-
-
-
- Region spacer = new Region();
- HBox.setHgrow(spacer, Priority.ALWAYS);
-
- HBox currentWeekBox = new HBox();
- currentWeekBox.setSpacing(10);
-
- currentWeekBox.setStyle("-fx-background-color: #ffe556;" +
- "-fx-text-fill: #303539;" +
- "-fx-background-radius: 8;" +
- "-fx-border-color: #303539;" +
- "-fx-border-radius: 8;"
- );
- currentWeekBox.setPadding(new Insets(8, 14, 8, 14));
- currentWeekBox.setAlignment(Pos.CENTER);
-
- currentWeekLabel = new Label();
- currentWeekBox.getChildren().add(currentWeekLabel);
-
-
-
- advanceWeekButton = new Button("Advance Week");
- advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
-
- this.getChildren().addAll(spacer, currentWeekBox, advanceWeekButton);
- }
-
- public void setExchange(Exchange exchange) {
- this.exchange = exchange;
- currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> {
- if (exchange != null) {
- currentWeekLabel.setText("Current Week: " + newVal);
- } else {
- currentWeekLabel.setText("Current Week: None");
- }
- });
+ this.setStyle("-fx-background-color: #00bcf0;" +
+ "-fx-border-color: #303539 transparent transparent transparent;" +
+ "-fx-border-width: 5 0 0 0;");
+
+ languageBox = new ComboBox<>();
+ languageBox.getItems().addAll(LanguageRegistry.getLanguages());
+
+ Locale activeLocale = lm.getLocale();
+ LanguageOption activeOption = LanguageRegistry.getLanguages().stream()
+ .filter(lang -> lang.locale().equals(activeLocale))
+ .findFirst()
+ .orElse(LanguageRegistry.getDefaultLanguage());
+
+ languageBox.setValue(activeOption);
+
+ // Listens for changes in language from other sources (e.g. startView)
+ lm.getBundleProperty().subscribe(bundle -> {
+ java.util.Locale currentLocale = lm.getLocale();
+
+ LanguageOption matchingOption = LanguageRegistry.getLanguages().stream()
+ .filter(lang -> lang.locale().equals(currentLocale))
+ .findFirst()
+ .orElse(null);
+
+ if (matchingOption != null && !matchingOption.equals(languageBox.getValue())) {
+ languageBox.setValue(matchingOption);
+ }
+ });
+
+ languageBox.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSub;
+ @Override
+ protected void updateItem(LanguageOption item, boolean empty) {
+ super.updateItem(item, empty);
+ if (cellSub != null) cellSub.unsubscribe();
+
+ if (empty || item == null) {
+ setText(null);
+ } else {
+ cellSub = lm.getBundleProperty().subscribe(bundle -> setText(item.name()));
+ }
+ }
+ });
+
+ languageBox.setButtonCell(languageBox.getCellFactory().call(null));
+
+
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ HBox currentWeekBox = new HBox();
+ currentWeekBox.setSpacing(10);
+
+ currentWeekBox.setStyle("-fx-background-color: #ffe556;" +
+ "-fx-text-fill: #303539;" +
+ "-fx-background-radius: 8;" +
+ "-fx-border-color: #303539;" +
+ "-fx-border-radius: 8;"
+ );
+ currentWeekBox.setPadding(new Insets(8, 14, 8, 14));
+ currentWeekBox.setAlignment(Pos.CENTER);
+
+ Label currentWeekLabelText = new Label();
+ currentWeekLabelText.textProperty().bind(lm.bindString("currentWeekLabelText"));
+ currentWeekLabel = new Label();
+ currentWeekBox.getChildren().addAll(currentWeekLabelText, currentWeekLabel);
+
+ advanceWeekButton = new Button();
+ advanceWeekButton.textProperty().bind(lm.bindString("advanceWeekButton"));
+ advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+
+ this.getChildren().addAll(languageBox, spacer, currentWeekBox, advanceWeekButton);
+ }
- }
+ /**
+ * Sets the exchange and subscribes to week updates.
+ *
+ * @param exchange the {@link Exchange} used in the game
+ */
+ public void setExchange(Exchange exchange) {
+ this.exchange = exchange;
+ currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> {
+ if (exchange != null) {
+ currentWeekLabel.setText(newVal.toString());
+ } else {
+ currentWeekLabel.setText("None");
+ }
+ });
+ }
/**
* Exposes the advanceWeekButton to {@link GameController} so it can
@@ -92,4 +151,18 @@ public void setExchange(Exchange exchange) {
public void onAdvanceButtonClick(Runnable action) {
advanceWeekButton.setOnAction(e -> action.run());
}
-}
+
+ /**
+ * Exoposes the LanguageOption ComboButton to the controller.
+ *
+ * @param action the action to be performed when selecting the language
+ */
+ public void onLanguageChange(Consumer action) {
+ languageBox.setOnAction(e -> {
+ LanguageOption selected = languageBox.getValue();
+ if (selected != null) {
+ action.accept(selected);
+ }
+ });
+ }
+}
\ No newline at end of file
From 6868f1ed5b29221ba611118b62410ab30e582d3e Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:47:42 +0200
Subject: [PATCH 119/142] Updated LanguageManager
Updated JavaDoc.
---
.../src/main/java/no/ntnu/gruppe53/service/LanguageManager.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
index 09b9494..776f15a 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
@@ -10,6 +10,7 @@
/**
* Manages the language used in the user-interface.
+ *
The possible languages to be used are set by {@link LanguageRegistry}.
*/
public class LanguageManager {
From b3e25938e973a159a981f4551f4a3c6a484f30a7 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 22:54:56 +0200
Subject: [PATCH 120/142] Updated MarketView
Updated to use a LanguageManager for dynamically setting text to the target language. Updated JavaDoc. Added some comments.
---
.../no/ntnu/gruppe53/view/MarketView.java | 69 +++++++++++++------
1 file changed, 47 insertions(+), 22 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
index 380ef7b..bab3197 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -16,6 +16,7 @@
import javafx.util.Subscription;
import no.ntnu.gruppe53.model.*;
import no.ntnu.gruppe53.service.FormatBigDecimal;
+import no.ntnu.gruppe53.service.LanguageManager;
/*
Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
@@ -29,15 +30,14 @@
* price history statistics in the form of a {@link LineChart} through the
* {@link StockLineChartView} class, as well as buying {@link Stock}s.
*
Extends borderpane to allow easy organizing of the internal layout.
+ *
Uses a {@link LanguageManager} to set text to the targer language dynamically.
*/
public class MarketView extends BorderPane {
+ private final LanguageManager lm = LanguageManager.getInstance();
private Button purchaseButton;
private Label selectedStock;
-
-
-
//Dynamic Labels/Text
public TextField gross;
public TextField commission;
@@ -52,7 +52,6 @@ public class MarketView extends BorderPane {
private PurchaseCalculator purchaseCalculator;
private final StockLineChartView stockLineChartView;
- private Subscription playerSubscription;
private Subscription priceSubscription;
private Subscription grossSubscription;
private Subscription commissionSubscription;
@@ -71,6 +70,7 @@ public MarketView() {
/**
* Constructs the view and its components.
*
Subscribes to a stock's sales price to update the value immediately.
+ *
Subscribes to the current language to change text immediately.
*
Uses a cell factory to format the string found in the stocksListView.
Uses a {@link LanguageManager} for methods to change text dynamically.
*/
public class GameController {
private Player player;
private Exchange exchange;
private List stocks;
+ private final LanguageManager lm = LanguageManager.getInstance();
private final ViewController vm;
@@ -111,6 +110,7 @@ public void showTransactionHistory() {
* Initializes the {@link NavigationBar} and {@link FooterBar} for the controller.
*
Binds functionality to exposed buttons and lists in views.
*
Sets subscriptions for observable values for views.
+ *
Sets the method for setting the selected language locale for the footer.
* @param nav the NavigationBar view
* @param footer the FooterBar view
*/
@@ -119,6 +119,10 @@ public void initialize(NavigationBar nav, FooterBar footer) {
nav.onPortfolioButtonClick(() -> vm.switchView("portfolio"));
nav.onHistoryButtonClick(() -> vm.switchView("history"));
+ footer.onLanguageChange(selected -> {
+ lm.setLocale(selected.locale());
+ });
+
footer.onAdvanceButtonClick(() -> {
if (exchange != null) {
exchange.advance();
@@ -223,8 +227,8 @@ private PurchaseCalculator calculatePurchase(Stock stock, int quantity) {
/**
* Creates a {@link SaleCalculator} that can be used for the
- * for the setSaleCalculator() function in the {@link PortfolioView}.
- * @param share
+ * setSaleCalculator() function in the {@link PortfolioView}.
+ * @param share the share to use the calculator on
* @return {@link SaleCalculator}
*/
private SaleCalculator calculateSale(Share share) {
From 7fc4129e77c90d4e23017cdd6e505cd88cc48e61 Mon Sep 17 00:00:00 2001
From: Roar
Date: Sun, 24 May 2026 23:13:24 +0200
Subject: [PATCH 122/142] Updated PortfolioView
Updated UI to use language manager to dynamically set text. Updated JavaDoc.
---
.../no/ntnu/gruppe53/view/PortfolioView.java | 65 ++++++++++++++-----
1 file changed, 47 insertions(+), 18 deletions(-)
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
index 7388c16..290d375 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -2,7 +2,6 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
-import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javafx.collections.ObservableList;
@@ -23,8 +22,8 @@
import no.ntnu.gruppe53.model.Player;
import no.ntnu.gruppe53.model.SaleCalculator;
import no.ntnu.gruppe53.model.Share;
-import no.ntnu.gruppe53.model.Stock;
import no.ntnu.gruppe53.service.FormatBigDecimal;
+import no.ntnu.gruppe53.service.LanguageManager;
/*
Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
@@ -40,13 +39,12 @@
* and the ability to sell the share for money.
*
Extends the {@link BorderPane}
for easy layout setup.
*
Events are handled by the {@link no.ntnu.gruppe53.controller.GameController}.
+ *
Uses a {@link LanguageManager} to dynamically set text based on target language.
*/
public class PortfolioView extends BorderPane {
- private Button sellButton;
+ private final LanguageManager lm = LanguageManager.getInstance();
- //NavigationBar Buttons
- private Button marketButton;
- private Button portfolioButton;
+ private Button sellButton;
//Static Labels
private Label titleLabel;
@@ -79,7 +77,8 @@ public PortfolioView() {
/**
* Constructs the view of the portfolio.
*
Subscribes to shares for sales price updates.
- *
Uses a cell factory to format the displayed string representing each share.
+ *
Uses a cell factory to format the displayed string representing each share.
+ * The sell factory has the text dynamically set by the language manager.